Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6d24bfe
feat(camera): extend CameraDefinition with type, hardware_trigger, pe…
hongquanli Aug 9, 2026
1387eb5
feat(camera): build per-camera CameraConfig from cameras.yaml definit…
hongquanli Aug 9, 2026
762e518
fix(config-editor): store camera id (int), not display name, in chann…
hongquanli Aug 9, 2026
e29e163
feat(camera): ActiveCameraFacade delegating to the active concrete ca…
hongquanli Aug 9, 2026
238f5e7
feat(microscope): build all cameras.yaml cameras, facade + set_active…
hongquanli Aug 9, 2026
b8b3c1d
fix(microscope): roll back failed camera switch; lock trigger-memory …
hongquanli Aug 9, 2026
8a2b736
feat(live): set_microscope_mode switches active camera per channel bi…
hongquanli Aug 9, 2026
082753e
feat(sim): SimulatedCamera serves RGB24/RGB48 frames for color-camera…
hongquanli Aug 9, 2026
0c6000c
fix(sim): invalidate cached raw frame on set_pixel_format; cover RGB …
hongquanli Aug 9, 2026
e45c930
feat(cache): per-serial camera settings cache with legacy migration
hongquanli Aug 9, 2026
8f23575
fix(cache): isolate per-camera read failures when saving all camera s…
hongquanli Aug 9, 2026
99b3995
feat(gui): camera dot + suffix labels in channel lists, name identity…
hongquanli Aug 9, 2026
d078e3b
fix(gui): tooltip on disabled dropdown camera entries; pin bare-name …
hongquanli Aug 9, 2026
7066ff2
test(gui): pin the real dropdown readers; extract activated handler t…
hongquanli Aug 9, 2026
b994af2
feat(gui): per-camera trigger options, settings tabs, and camera-chan…
hongquanli Aug 9, 2026
9a80543
fix(gui): sync trigger options when the startup channel lives on anot…
hongquanli Aug 9, 2026
3b0bf74
fix(gui): deliver camera-change notifications cross-thread; clamp uno…
hongquanli Aug 9, 2026
6b645c1
feat(acquisition): Zarr mixed-geometry guard (Start disable + backsto…
hongquanli Aug 9, 2026
6234031
fix(acquisition): add dtype axis, separate Start-button vetoes, re-ch…
hongquanli Aug 9, 2026
a7d1ade
feat(acquisition): per-camera warm-up and per-channel pixel sizes in …
hongquanli Aug 9, 2026
32271bd
fix(acquisition): quiesce live before the warm-up switches cameras; v…
hongquanli Aug 9, 2026
db33d7d
test(dual-camera): end-to-end mixed-camera sim acquisition; tracking/…
hongquanli Aug 9, 2026
2acdccd
docs(dual-camera): cameras.yaml.example fields and user guide
hongquanli Aug 9, 2026
7ef2304
docs(dual-camera): correct startup-crash and OME-TIFF claims
hongquanli Aug 9, 2026
c9803ea
fix(dual-camera): final review fixes — guard message, warm-up flag le…
hongquanli Aug 9, 2026
9bb18d3
feat(gui): channel-list icon encodes sensor type — grey dot mono, RGB…
hongquanli Aug 10, 2026
5c419a0
feat(toupcam): open camera by serial number, fix auto white balance s…
hongquanli Aug 10, 2026
c6130cc
feat(qt): add binding selector preferring PyQt6 when installed
Alpaca233 Aug 10, 2026
eaca176
feat(qt): entry points select Qt binding via squid.qt_binding
Alpaca233 Aug 10, 2026
b9634c8
fix(qt6): use binding-agnostic matplotlib backend_qtagg
Alpaca233 Aug 10, 2026
7cd5701
fix(qt6): replace removed QDesktopWidget with QScreen API
Alpaca233 Aug 10, 2026
1363dae
fix(napari): guard layerButtons for napari versions that removed it
Alpaca233 Aug 10, 2026
be66789
feat(toupcam): colour capture, and pixel formats that follow the sensor
cephlainc Aug 10, 2026
0040970
fix(dual-camera): apply live exposure/gain to the active camera
hongquanli Aug 12, 2026
8954e96
fix(dual-camera): keep per-channel napari layers across mixed dtypes
hongquanli Aug 12, 2026
d94b203
test: isolate tests from the machine's user_profiles
hongquanli Aug 12, 2026
2ecb197
fix(dual-camera): finish removing widget-global state from the displa…
hongquanli Aug 12, 2026
505c7e7
test: make the profile fixture non-fatal and per-test
hongquanli Aug 12, 2026
e92b314
fix(dual-camera): remember contrast limits per channel in their own d…
hongquanli Aug 12, 2026
3790abd
docs+test: correct the serial-number limits and cover the mosaic writ…
hongquanli Aug 12, 2026
94ba41f
fix(toupcam): make white balance work on colour cameras
cephlainc Aug 13, 2026
12c850d
fix(widgets): stop the auto white balance button raising
cephlainc Aug 13, 2026
1f029b2
feat(camera): remember white balance gains across restarts
cephlainc Aug 13, 2026
77b0111
feat(objectives): let a machine override objectives.csv from cache
cephlainc Aug 13, 2026
1471576
fix(click-to-move): undo the display rotation before moving the stage
cephlainc Aug 13, 2026
b14ee54
feat(mosaic): save colour layers as PNG instead of dropping them
cephlainc Aug 13, 2026
5be0774
Revert "fix(click-to-move): undo the display rotation before moving t…
cephlainc Aug 13, 2026
98a7910
revert(qt): drop the PyQt6 compatibility changes
Alpaca233 Aug 15, 2026
2e6a2ee
feat(photometrics): open by device_index; refcount PVCAM init for dua…
Alpaca233 Aug 15, 2026
d851f92
fix(camera): raise a clear error when a camera driver fails to import
Alpaca233 Aug 15, 2026
439aab8
fix(gui): camera settings tabs derive options from each camera's driv…
Alpaca233 Aug 15, 2026
4a7206d
feat(camera): per-camera default_roi override in cameras.yaml
Alpaca233 Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ def convert_to_var(option: Union[str, "TriggerMode"]) -> "TriggerMode":
raise ValueError(f"Invalid trigger mode: {option}")


# The camera id (cameras.yaml) that is the default/primary imaging camera.
# A channel with camera=None uses this camera.
PRIMARY_CAMERA_ID = 1


class Acquisition:
NUMBER_OF_FOVS_PER_AF = 3
IMAGE_FORMAT = "tiff"
Expand Down Expand Up @@ -1163,12 +1168,23 @@ def read_sample_formats_csv(file_path):


def load_formats():
"""Load formats, prioritizing cache for sample formats."""
"""Load formats, prioritizing cache for objectives and sample formats."""
cache_path = "cache"
default_path = "objective_and_sample_formats"

# Load objectives (from default location)
objectives = read_objectives_csv(os.path.join(default_path, "objectives.csv"))
# Try cache first for objectives, fall back to default if not found. tube_lens_f_mm
# is a property of the objectives actually fitted - Nikon are designed for a 200mm
# tube lens, Olympus for 180mm - so a machine whose optics differ from the checked-in
# default needs its own copy rather than a change every other machine inherits.
cached_objectives_path = os.path.join(cache_path, "objectives.csv")
default_objectives_path = os.path.join(default_path, "objectives.csv")

if os.path.exists(cached_objectives_path):
print("Using cached objectives")
objectives = read_objectives_csv(cached_objectives_path)
else:
print("Using default objectives")
objectives = read_objectives_csv(default_objectives_path)

# Try cache first for sample formats, fall back to default if not found
cached_formats_path = os.path.join(cache_path, "sample_formats.csv")
Expand Down
59 changes: 43 additions & 16 deletions software/control/camera_photometrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,60 @@
)
from control._def import *

# PVCAM is a process-global library: init/uninit must bracket ALL open cameras,
# so a refcount (not per-camera init/uninit) lets two PhotometricsCamera
# instances coexist and close in any order.
_pvcam_lock = threading.Lock()
_pvcam_user_count = 0


def _acquire_pvcam():
global _pvcam_user_count
with _pvcam_lock:
if _pvcam_user_count == 0:
pvc.init_pvcam()
_pvcam_user_count += 1


def _release_pvcam():
global _pvcam_user_count
with _pvcam_lock:
if _pvcam_user_count == 0:
return
_pvcam_user_count -= 1
if _pvcam_user_count == 0:
pvc.uninit_pvcam()


class PhotometricsCamera(AbstractCamera):
PIXEL_SIZE_UM = 6.5 # Kinetix camera

@staticmethod
def _open(index: Optional[int] = None) -> PVCam:
"""Open a Photometrics camera and return the camera object."""
"""Open the Photometrics camera at the given PVCAM enumeration index.

None opens the first detected camera. PVCAM has no open-by-serial, so
cameras.yaml `device_index` is how a multi-Photometrics system picks a
specific device (enumeration order can change if USB topology changes).
"""
log = squid.logging.get_logger("PhotometricsCamera._open")

pvc.init_pvcam()
_acquire_pvcam()

try:
if index is not None:
# Open by index (not commonly used for Photometrics)
cameras = list(PVCam.detect_camera())
if index >= len(cameras):
raise CameraError(f"Camera index {index} out of range. Found {len(cameras)} cameras.")
cam = cameras[index]
else:
# Open first available camera
cam = next(PVCam.detect_camera())

cameras = list(PVCam.detect_camera())
effective_index = 0 if index is None else index
if effective_index >= len(cameras):
raise CameraError(
f"Camera index {effective_index} out of range. Found {len(cameras)} Photometrics cameras."
)
cam = cameras[effective_index]
cam.open()
log.info("Photometrics camera opened successfully")
log.info(f"Photometrics camera opened successfully (index {effective_index})")
return cam

except Exception as e:
pvc.uninit_pvcam()
_release_pvcam()
raise CameraError(f"Failed to open Photometrics camera: {e}")

def __init__(
Expand Down Expand Up @@ -72,7 +98,7 @@ def __init__(
self._is_streaming = threading.Event()

# Open camera
self._camera = PhotometricsCamera._open()
self._camera = PhotometricsCamera._open(camera_config.device_index)

# Camera configuration
self._exposure_time_ms = 20 # set it to some default value
Expand Down Expand Up @@ -141,7 +167,8 @@ def close(self):
self._camera.close()
except Exception as e:
raise CameraError(f"Failed to close camera: {e}")
pvc.uninit_pvcam()
finally:
_release_pvcam()

def _ensure_read_thread_running(self):
with self._read_thread_lock:
Expand Down
Loading
Loading