Skip to content

Latest commit

 

History

237 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lucida

Lucida is a Python toolkit for precision camera geometry, single-view calibration, and multi-view rig calibration. It features a backend-agnostic geometry kernel (NumPy/JAX) and supports robust Bundle Adjustment (JAX only).

Ultra basic usage examples

1. Camera models and geometry

Create a camera model from specifications, project 3D points, and visualise rays.

import numpy as np
from lucida import CameraModel, Intrinsics, Extrinsics

# Define Intrinsics (from physical specs)
intrinsics = Intrinsics.from_specs(
    image_size=(1920, 1080),
    focal=35.0,             # 35mm lens
    sensor='Full frame',    # 36x24mm sensor
    distortion_model='standard'
)

# Define Extrinsics (Camera at origin, looking forward)
extrinsics = Extrinsics(
    tvec=[0, 0, 0],
    rvec=[0, 0, 0], 
    convention='c2w' # Camera-to-World
)

# Create camera model
cam = CameraModel(intrinsics, extrinsics, name="cam_01")

# Project 3D points
points_3d = np.array([
    [0, 0, 1000],  # 1m in front
    [500, 200, 2000]
])

pixels, valid_mask = cam.project(points_3d)
print(f"Projected Pixels:\n{pixels}")

# Raycast (back-projection)
origins, directions = cam.raycast(pixels)

2. Creating calibration boards

Generate SVG files for Charuco or Chessboards to print.

from lucida.calibration import CharucoBoard

# Define a 5x7 Charuco board with 30mm squares
board = CharucoBoard(
    rows=5, 
    cols=7, 
    square_length=30.0, # in millimetres
    marker_size=4       # Aruco marker size (in squares)
)

# Generate SVG for printing
svg_content = board.to_svg()
with open("calibration_target.svg", "w") as f:
    f.write(svg_content)

3. Monocular calibration

Calibrate a single camera using the calibration board. Note that detection and calibration logic are decoupled.

import cv2
from lucida import CameraModel, Intrinsics, Extrinsics
from lucida.calibration import MonocularCalibrationTool, CharucoBoard, CharucoDetector

# Setup camera (with a guess), and the board
cam = CameraModel(
    Intrinsics.from_specs((1920, 1080), focal=50, sensor='APS-C'),
    Extrinsics()
)

board = CharucoBoard(rows=10, cols=7, square_length=25.0)

# Init detection and solver separately
detector = CharucoDetector(board)
tool = MonocularCalibrationTool(cam, board)

# Feed it frames
# (mock loop, in reality, read from cv2.VideoCapture)
for i in range(50):
    frame = cv2.imread(f"data/calib_img_{i}.jpg")
    
    # Detect (stateless)
    detection = detector.detect(frame, K=cam.K, D=cam.D)
    
    if detection.valid:
        # Register (stateful)
        # Returns True only if the frame improved calibration coverage
        accepted = tool.register_detection(detection.image_points)
        
        if accepted:
            print(f"Frame {i} accepted. Current coverage: {tool.current_coverage:.1f}%")

# Run calibration
if tool.compute_intrinsics():
    print(f"Calibration successful! RMS: {cam.intrinsics.rms:.4f}")
    cam.save("calibrated_camera.toml")

4. Multi-camera calibration

Calibrate relative poses between multiple cameras.

from lucida import CameraRig
from lucida.calibration import MultiviewCalibrationTool, CharucoDetector

# Load a rig with rough initial guesses
rig = CameraRig.load("initial_rig.toml")
board = CharucoBoard(rows=5, cols=7, square_length=30.0)

# Init tools
detector = CharucoDetector(board)
tool = MultiviewCalibrationTool(rig, board, anchor_cam="cam_01")

# Feed synchronized frames
# (mock example with a frame_dict = { 'cam_01': img1, 'cam_02': img2 ... } )
for idx, frame_dict in enumerate(synchronized_stream):
    for cam_name, img in frame_dict.items():

        # Detect (stateless)
        cam_idx = rig.get_index(cam_name)
        detection = detector.detect(img, K=rig[cam_idx].K, D=rig[cam_idx].D)

        if detection.valid:
            # Buffer for stereo matching
            # Returns True if PnP solved and frame buffered
            tool.register_detection(cam_idx, idx, detection.image_points)

# Optimize geometry
success = tool.refine()

if success:
    print("Bundle Adjustment complete.")
    rig.save("optimized_rig.toml")

5. Threaded pipeline example

Since the Detector classes are stateless, you can offload image processing to worker threads while keeping the Calibration Tool in the main thread.

import queue
import threading

def worker(input_queue, output_queue, detector):
    """Consumes images, produces detections."""
    while True:
        frame_data = input_queue.get()
        if frame_data is None: break
        
        img = frame_data['image']
        # Heavy image processing happens here, releasing the GIL (well, mostly)
        det = detector.detect(img)
        
        output_queue.put({'det': det, 'orig': frame_data})

######

# Main thread
detector = CharucoDetector(board)
tool = MonocularCalibrationTool(cam, board)
# setup queues, start worker, etc ...

while True:
    try:
        result = out_queue.get_nowait()
        detection = result['det']
        
        # Visualisation (always draw, even if rejected by tool)
        if detection.valid:
            draw_points(result['orig']['image'], detection.image_points)
            
        # Calibration Logic (lightweight)
        if detection.valid:
            if tool.register_detection(detection.image_points):
                print("Added sample!")
                
    except queue.Empty:
        pass

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages