From 21bd9507c5b7ee60f94eaa73d382cc480277db15 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 16:20:59 +0000 Subject: [PATCH] CameraBase: keep the newest frames when the camera kept several, instead of adding an axis When the camera had kept more frames than the ones asked for (nobody reads while it acquires, so the second snap finds several), the frames were wrapped in an extra axis and the viewer switched to a DataND. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .../hardware/camera_base.py | 16 ++++++------- tests/test_camera_base.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/pymodaq_plugins_utils/hardware/camera_base.py b/src/pymodaq_plugins_utils/hardware/camera_base.py index 0a0981d..0cd2610 100644 --- a/src/pymodaq_plugins_utils/hardware/camera_base.py +++ b/src/pymodaq_plugins_utils/hardware/camera_base.py @@ -107,17 +107,17 @@ def wait_for_acquisition(self, mode: Grab): return continue - # in case hardware has a random buffer size (ex: Andor CCD cameras) - if new_frames.shape[0] != mode.nframes: - new_frames = np.expand_dims(new_frames[-mode.nframes:], axis=0) + # the camera may have kept more frames than the ones still missing (ex: nobody read while it + # was acquiring, or Andor CCD cameras with a random buffer size): keep the newest ones + n_missing = mode.nframes - ind_frames + new_frames = new_frames[-n_missing:] + n_new = new_frames.shape[0] if ind_average == 0 and ind_frames == 0: - shape = list(new_frames.shape[1:]) - shape = [mode.n_average, mode.nframes] + shape + shape = [mode.n_average, mode.nframes] + list(new_frames.shape[1:]) frames = np.zeros(shape, dtype=new_frames.dtype) - nacq = rng[1] - rng[0] - frames[ind_average, ind_frames:nacq, ...] = new_frames - ind_frames += nacq + frames[ind_average, ind_frames:ind_frames + n_new, ...] = new_frames + ind_frames += n_new ind_average += 1 self.data_sig.emit(frames) QtCore.QThread.msleep(10) diff --git a/tests/test_camera_base.py b/tests/test_camera_base.py index eef18e3..92f117f 100644 --- a/tests/test_camera_base.py +++ b/tests/test_camera_base.py @@ -93,6 +93,15 @@ def get_pixel_size(self): return 5.0, 10.0 +class FakeCameraSeveralFrames(FakeCamera): + """A camera that kept several frames since the last read (as when nobody reads while it acquires)""" + n_unread = 3 + + def read_multiple_images(self, rng=None, peek=False, missing_frame='skip', return_rng=False): + frames = np.repeat(self.read_newest_image()[None, ...], self.n_unread, axis=0) + return (frames, (0, self.n_unread)) if return_rng else frames + + class DAQ_2DViewer_Fake(CameraBase): driver_class = FakeCamera @@ -100,6 +109,10 @@ def ini_detector_custom(self, controller=None): self.controller = self.driver_class() if self.is_master else controller +class DAQ_2DViewer_FakeSeveralFrames(DAQ_2DViewer_Fake): + driver_class = FakeCameraSeveralFrames + + class DAQ_2DViewer_FakeWithPixelSize(DAQ_2DViewer_Fake): driver_class = FakeCameraWithPixelSize @@ -143,6 +156,17 @@ def set_binning(plugin, xbin: int, ybin: int): plugin.commit_settings(param) +class TestSeveralUnreadFrames: + def test_a_snap_gives_one_image_when_the_camera_kept_several_frames(self, qapp): + plugin = make_detector(DAQ_2DViewer_FakeSeveralFrames) + try: + dte = grab_and_wait(plugin) + assert dte[0].dim.name == 'Data2D' + assert dte[0].shape == (HEIGHT, WIDTH) + finally: + plugin.close() + + class TestDriverContract: @pytest.mark.parametrize('method, args', [ ('get_detector_size', ()), ('get_data_dimensions', ()), ('get_roi', ()), ('set_roi', ()),