From 1ef11c4f530ab08d7d23fc5f398da7e7ff827a35 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 1 May 2025 14:50:50 +0100 Subject: [PATCH 01/13] Timeout in capture_array, capture_highres_array added --- src/labthings_picamera2/thing.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 965ea07..82ac556 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -737,8 +737,9 @@ def capture_jpeg( logging.info("Reconfiguring camera for full resolution capture") cam.configure(cam.create_still_configuration()) cam.start() + cam.options["quality"] = 95 logging.info("capturing") - cam.capture_file(path, name="main", format="jpeg") + cam.capture_file(path, name="main", format="jpeg", wait=5) logging.info("done") # After the file is written, add metadata about the current Things exif_dict = piexif.load(path) @@ -773,6 +774,15 @@ def grab_jpeg( ) return JPEGBlob.from_bytes(frame) + @thing_action + def capture_highres_array( + self, + ): + with self.picamera(pause_stream=True) as picam2: + capture_config = picam2.create_still_configuration() + array = picam2.switch_mode_and_capture_array(capture_config) + return array + @thing_action def grab_jpeg_size( self, From 1c91e59a1c7b2cee772af1a09679f342886540d7 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 1 May 2025 16:05:13 +0100 Subject: [PATCH 02/13] Add full res array capture to capture_array --- src/labthings_picamera2/thing.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 82ac556..7ecbc30 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -528,7 +528,7 @@ def snap_image(self) -> ArrayModel: @thing_action def capture_array( self, - stream_name: Literal["main", "lores", "raw"] = "main", + stream_name: Literal["main", "lores", "raw", "full"] = "main", wait: Optional[float] = None, ) -> ArrayModel: """Acquire one image from the camera and return as an array @@ -537,9 +537,13 @@ def capture_array( It's likely to be highly inefficient - raw and/or uncompressed captures using binary image formats will be added in due course. - stream_name: (Optional) The PiCamera2 stream to use, should be one of ["main", "lores", "raw"]. Default = "main" + stream_name: (Optional) The PiCamera2 stream to use, should be one of ["main", "lores", "raw", "full"]. Default = "main" wait: (Optional, float) Set a timeout in seconds. A TimeoutError is raised if this time is exceeded during capture. Default = None """ + if stream_name == "full": + with self.picamera(pause_stream=True) as picam2: + capture_config = picam2.create_still_configuration() + return picam2.switch_mode_and_capture_array(capture_config) with self.picamera() as cam: return cam.capture_array(stream_name, wait = wait) @@ -774,15 +778,6 @@ def grab_jpeg( ) return JPEGBlob.from_bytes(frame) - @thing_action - def capture_highres_array( - self, - ): - with self.picamera(pause_stream=True) as picam2: - capture_config = picam2.create_still_configuration() - array = picam2.switch_mode_and_capture_array(capture_config) - return array - @thing_action def grab_jpeg_size( self, From ad283bce409720871eb7aa983e2faeec9f870282 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 2 May 2025 15:58:22 +0100 Subject: [PATCH 03/13] Wait param in highres array --- src/labthings_picamera2/thing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 7ecbc30..61500fd 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -543,7 +543,7 @@ def capture_array( if stream_name == "full": with self.picamera(pause_stream=True) as picam2: capture_config = picam2.create_still_configuration() - return picam2.switch_mode_and_capture_array(capture_config) + return picam2.switch_mode_and_capture_array(capture_config, wait=wait) with self.picamera() as cam: return cam.capture_array(stream_name, wait = wait) From 7d3b70e83bfb154a50adcf4928329dda23a2261b Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 2 May 2025 16:46:36 +0100 Subject: [PATCH 04/13] Remove spaces --- src/labthings_picamera2/thing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 61500fd..e4f4872 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -545,7 +545,7 @@ def capture_array( capture_config = picam2.create_still_configuration() return picam2.switch_mode_and_capture_array(capture_config, wait=wait) with self.picamera() as cam: - return cam.capture_array(stream_name, wait = wait) + return cam.capture_array(stream_name, wait=wait) @thing_action def capture_raw( From bea34132dedcf1c44426887aa152dfde6b1bc53d Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 7 May 2025 10:26:52 +0100 Subject: [PATCH 05/13] Make starting the stream in different resolutions Things --- src/labthings_picamera2/thing.py | 52 +++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index e4f4872..4acbf6f 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -448,6 +448,7 @@ def __exit__(self, exc_type, exc_value, traceback): cam.close() del self._picamera + @thing_action def start_streaming(self) -> None: """ Start the MJPEG stream @@ -463,7 +464,7 @@ def start_streaming(self) -> None: picam.stop() picam.stop_encoder() # make sure there are no other encoders going stream_config = picam.create_video_configuration( - main={"size": self.stream_resolution}, + main={"size": (832, 624)}, lores={"size": (320, 240), "format": "YUV420"}, sensor=self.thing_settings.get("sensor_mode", None), controls=self.persistent_controls, @@ -494,6 +495,55 @@ def start_streaming(self) -> None: "Started MJPEG stream at %s on port %s", self.stream_resolution, 1 ) + @thing_action + def start_highres_streaming(self) -> None: + """ + Start the MJPEG stream + + Sets the camera resolution to the video/stream resolution, and starts recording + if the stream should be active. + """ + with self.picamera() as picam: + # TODO: Filip: can we use the lores output to keep preview stream going + # while recording? According to picamera2 docs 4.2.1.6 this should work + try: + if picam.started: + picam.stop() + picam.stop_encoder() # make sure there are no other encoders going + stream_config = picam.create_video_configuration( + main={"size": (3280, 2464)}, + lores={"size": (320, 240), "format": "YUV420"}, + sensor=self.thing_settings.get("sensor_mode", None), + controls=self.persistent_controls, + ) + picam.configure(stream_config) + logging.info("Starting picamera MJPEG stream...") + picam.start_recording( + MJPEGEncoder(self.mjpeg_bitrate), + PicameraStreamOutput( + self.mjpeg_stream, + get_blocking_portal(self), + ), + name="lores", + ) + picam.start_encoder( + MJPEGEncoder(100000000), + PicameraStreamOutput( + self.lores_mjpeg_stream, + get_blocking_portal(self), + ), + name="lores", + ) + except Exception as e: + logging.exception("Error while starting preview: {e}") + logging.exception(e) + else: + self.stream_active = True + logging.debug( + "Started MJPEG stream at %s on port %s", self.stream_resolution, 1 + ) + + @thing_action def stop_streaming(self, stop_web_stream=True) -> None: """ Stop the MJPEG stream From c57119dc53c9aa035c962cb40702d38cf743c3fd Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 7 May 2025 14:36:57 +0100 Subject: [PATCH 06/13] Start stream with main resolution parameter --- src/labthings_picamera2/thing.py | 54 +++----------------------------- 1 file changed, 4 insertions(+), 50 deletions(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 51c2a7b..45ea416 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -448,7 +448,7 @@ def __exit__(self, exc_type, exc_value, traceback): del self._picamera @thing_action - def start_streaming(self) -> None: + def start_streaming(self, main_resolution: tuple[int, int] = (832, 624)) -> None: """ Start the MJPEG stream @@ -463,67 +463,21 @@ def start_streaming(self) -> None: picam.stop() picam.stop_encoder() # make sure there are no other encoders going stream_config = picam.create_video_configuration( - main={"size": (832, 624)}, + main={"size": main_resolution}, lores={"size": (320, 240), "format": "YUV420"}, sensor=self.thing_settings.get("sensor_mode", None), controls=self.persistent_controls, ) picam.configure(stream_config) logging.info("Starting picamera MJPEG stream...") + stream_name='lores' if main_resolution != (832, 624) else 'main' picam.start_recording( MJPEGEncoder(self.mjpeg_bitrate), PicameraStreamOutput( self.mjpeg_stream, get_blocking_portal(self), ), - ) - picam.start_encoder( - MJPEGEncoder(100000000), - PicameraStreamOutput( - self.lores_mjpeg_stream, - get_blocking_portal(self), - ), - name="lores", - ) - except Exception as e: - logging.exception("Error while starting preview: {e}") - logging.exception(e) - else: - self.stream_active = True - logging.debug( - "Started MJPEG stream at %s on port %s", self.stream_resolution, 1 - ) - - @thing_action - def start_highres_streaming(self) -> None: - """ - Start the MJPEG stream - - Sets the camera resolution to the video/stream resolution, and starts recording - if the stream should be active. - """ - with self.picamera() as picam: - # TODO: Filip: can we use the lores output to keep preview stream going - # while recording? According to picamera2 docs 4.2.1.6 this should work - try: - if picam.started: - picam.stop() - picam.stop_encoder() # make sure there are no other encoders going - stream_config = picam.create_video_configuration( - main={"size": (3280, 2464)}, - lores={"size": (320, 240), "format": "YUV420"}, - sensor=self.thing_settings.get("sensor_mode", None), - controls=self.persistent_controls, - ) - picam.configure(stream_config) - logging.info("Starting picamera MJPEG stream...") - picam.start_recording( - MJPEGEncoder(self.mjpeg_bitrate), - PicameraStreamOutput( - self.mjpeg_stream, - get_blocking_portal(self), - ), - name="lores", + name=stream_name, ) picam.start_encoder( MJPEGEncoder(100000000), From e6a2b78d0e25fe42902c8c1a44ae603e2d7ae936 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 7 May 2025 22:19:23 +0100 Subject: [PATCH 07/13] Capture image returns PIL Image --- src/labthings_picamera2/thing.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 45ea416..da63cb8 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -528,6 +528,24 @@ def snap_image(self) -> ArrayModel: """ return self.capture_array() + @thing_action + def capture_image( + self, + stream_name: Literal["main", "lores", "raw", "full"] = "main", + wait: Optional[float] = 0.9, + ): + """Acquire one image from the camera. + + Return it as a PIL Image + + stream_name: (Optional) The PiCamera2 stream to use, should be one of ["main", "lores", "raw", "full"]. Default = "main" + wait: (Optional, float) Set a timeout in seconds. + A TimeoutError is raised if this time is exceeded during capture. + Default = 0.9s, lower than the 1s timeout default in picamera yaml settings + """ + with self.picamera() as cam: + return cam.capture_image(stream_name, wait=wait) + @thing_action def capture_array( self, From d1bb9a9c57a3ab9da2152b5c131f9a3459b692ec Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 9 May 2025 11:56:02 +0100 Subject: [PATCH 08/13] Correct default main resolution to 1/4 sensor size --- src/labthings_picamera2/thing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index da63cb8..1e5fb46 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -448,7 +448,7 @@ def __exit__(self, exc_type, exc_value, traceback): del self._picamera @thing_action - def start_streaming(self, main_resolution: tuple[int, int] = (832, 624)) -> None: + def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None: """ Start the MJPEG stream @@ -470,7 +470,7 @@ def start_streaming(self, main_resolution: tuple[int, int] = (832, 624)) -> None ) picam.configure(stream_config) logging.info("Starting picamera MJPEG stream...") - stream_name='lores' if main_resolution != (832, 624) else 'main' + stream_name='lores' if main_resolution != (820, 616) else 'main' picam.start_recording( MJPEGEncoder(self.mjpeg_bitrate), PicameraStreamOutput( From 0b04db8b7b08028baa1074ff205b61ee1f8790cb Mon Sep 17 00:00:00 2001 From: jaknapper Date: Mon, 12 May 2025 12:26:26 +0100 Subject: [PATCH 09/13] Lower buffer count, changed threshold for using lores stream --- src/labthings_picamera2/thing.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 1e5fb46..57e6fc2 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -452,8 +452,14 @@ def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None """ Start the MJPEG stream - Sets the camera resolution to the video/stream resolution, and starts recording - if the stream should be active. + Sets the camera resolutions based on input parameters, and sets the low-res resolution to (320, 240) + ((320, 240) is a standard from the Pi Camera manual) + + Create two streams, `lores_mjpeg_stream` for autofocus at low-res resolution, and `mjpeg_stream` for preview. + This is the `main_resolution` if this is less than (1280, 960), or the low-res resolution if above. + This allows for high resolution capture without streaming high resolution video. + + main_resolution: the resolution for the main configuration. Defaults to (820, 616), 1/4 sensor size """ with self.picamera() as picam: # TODO: Filip: can we use the lores output to keep preview stream going @@ -468,9 +474,11 @@ def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None sensor=self.thing_settings.get("sensor_mode", None), controls=self.persistent_controls, ) + # Reducing buffer_count from video standard 6 to 4 to save memory + stream_config["buffer_count"] = 4 picam.configure(stream_config) logging.info("Starting picamera MJPEG stream...") - stream_name='lores' if main_resolution != (820, 616) else 'main' + stream_name='lores' if main_resolution[0] > 1280 else 'main' picam.start_recording( MJPEGEncoder(self.mjpeg_bitrate), PicameraStreamOutput( From a3fa0c4df351b4501a7ce66c7906963f9ade6622 Mon Sep 17 00:00:00 2001 From: JohemianKnapsody <43779362+JohemianKnapsody@users.noreply.github.com> Date: Mon, 12 May 2025 13:31:02 +0100 Subject: [PATCH 10/13] Update start_streaming docstring Co-authored-by: Julian Stirling --- src/labthings_picamera2/thing.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 57e6fc2..05fbcb9 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -452,14 +452,18 @@ def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None """ Start the MJPEG stream - Sets the camera resolutions based on input parameters, and sets the low-res resolution to (320, 240) - ((320, 240) is a standard from the Pi Camera manual) - - Create two streams, `lores_mjpeg_stream` for autofocus at low-res resolution, and `mjpeg_stream` for preview. - This is the `main_resolution` if this is less than (1280, 960), or the low-res resolution if above. - This allows for high resolution capture without streaming high resolution video. - - main_resolution: the resolution for the main configuration. Defaults to (820, 616), 1/4 sensor size + Sets the camera resolutions based on input parameters, and sets the low-res + resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera + manual. + + Create two streams: + - `lores_mjpeg_stream` for autofocus at low-res resolution + - `mjpeg_stream` for preview. This is the `main_resolution` if this is less + than (1280, 960), or the low-res resolution if above. This allows for + high resolution capture without streaming high resolution video. + + main_resolution: the resolution for the main configuration. Defaults to + (820, 616), 1/4 sensor size. """ with self.picamera() as picam: # TODO: Filip: can we use the lores output to keep preview stream going From 40f29c6e04d2de3564e41afbc70a2604c9c817a5 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Mon, 12 May 2025 13:35:47 +0100 Subject: [PATCH 11/13] Updated comment on capture_array --- src/labthings_picamera2/thing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index 05fbcb9..b9ec860 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -575,6 +575,9 @@ def capture_array( A TimeoutError is raised if this time is exceeded during capture. Default = 0.9s, lower than the 1s timeout default in picamera yaml settings """ + + # This was slower than capture_image for our use case, but directly returning + # an image as an array is still a useful feature if stream_name == "full": with self.picamera(pause_stream=True) as picam2: capture_config = picam2.create_still_configuration() From 139b9fa14afb62bc5751bacadd98b1d31945fa11 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Mon, 12 May 2025 13:37:33 +0100 Subject: [PATCH 12/13] Remove commented out code, ruff format thing file only --- src/labthings_picamera2/thing.py | 92 +++++++++++++++----------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index b9ec860..f7e9ed8 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -56,6 +56,7 @@ class RawImageModel(BaseModel): stride: int format: str + class PicameraControl(PropertyDescriptor): def __init__( self, control_name: str, model: type = float, description: Optional[str] = None @@ -91,7 +92,9 @@ def __init__(self, stream: MJPEGStream, portal: BlockingPortal): self.stream = stream self.portal = portal - def outputframe(self, frame, _keyframe=True, _timestamp=None, _packet=None, _audio=False): + def outputframe( + self, frame, _keyframe=True, _timestamp=None, _packet=None, _audio=False + ): """Add a frame to the stream's ringbuffer""" self.stream.add_frame(frame, self.portal) @@ -126,7 +129,9 @@ class ImageProcessingInputs(BaseModel): colour_gains: tuple[float, float] white_norm_lores: NDArray raw_size: tuple[int, int] - colour_correction_matrix: tuple[float, float, float, float, float, float, float, float, float] + colour_correction_matrix: tuple[ + float, float, float, float, float, float, float, float, float + ] gamma: NDArray @@ -137,7 +142,7 @@ class ImageProcessingCache: ccm: np.ndarray -class BlobNumpyDict(BlobBytes): +class BlobNumpyDict(BlobBytes): def __init__(self, arrays: Mapping[str, np.ndarray]): self._arrays = arrays self._bytesio: Optional[io.BytesIO] = None @@ -148,7 +153,7 @@ def arrays(self) -> Mapping[str, np.ndarray]: return self._arrays @property - def _bytes(self) -> bytes: #noqa mypy: override + def _bytes(self) -> bytes: # noqa mypy: override """Generate binary content on-the-fly from numpy data""" if not self._bytesio: out = io.BytesIO() @@ -164,18 +169,14 @@ class NumpyBlob(Blob): def from_arrays(cls, arrays: Mapping[str, np.ndarray]) -> Self: return cls.model_construct( # type: ignore[return-value] href="blob://local", - _data=BlobNumpyDict( - arrays, - media_type=cls.default_media_type() - ), + _data=BlobNumpyDict(arrays, media_type=cls.default_media_type()), ) - def raw2rggb(raw: np.ndarray, size: tuple[int, int]) -> np.ndarray: """Convert packed 10 bit raw to RGGB 8 bit""" raw = np.asarray(raw) # ensure it's an array - output_shape = (size[1]//2, size[0]//2, 4) + output_shape = (size[1] // 2, size[0] // 2, 4) rggb = np.empty(output_shape, dtype=np.uint8) raw_w = rggb.shape[1] // 2 * 5 for plane, offset in enumerate([(1, 1), (0, 1), (1, 0), (0, 0)]): @@ -482,7 +483,7 @@ def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None stream_config["buffer_count"] = 4 picam.configure(stream_config) logging.info("Starting picamera MJPEG stream...") - stream_name='lores' if main_resolution[0] > 1280 else 'main' + stream_name = "lores" if main_resolution[0] > 1280 else "main" picam.start_recording( MJPEGEncoder(self.mjpeg_bitrate), PicameraStreamOutput( @@ -507,7 +508,7 @@ def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None logging.debug( "Started MJPEG stream at %s on port %s", self.stream_resolution, 1 ) - + @thing_action def stop_streaming(self, stop_web_stream=True) -> None: """ @@ -589,15 +590,15 @@ def capture_array( def capture_raw( self, states_getter: GetThingStates, - get_states: bool=True, - get_processing_inputs: bool=True, + get_states: bool = True, + get_processing_inputs: bool = True, wait: Optional[float] = 0.9, ) -> RawImageModel: """Capture a raw image - + This function is intended to be as fast as possible, and will return as soon as an image has been captured. The output format is not intended - to be useful, except as input to `raw_to_png`. + to be useful, except as input to `raw_to_png`. wait: (Optional, float) Set a timeout in seconds. A TimeoutError is raised if this time is exceeded during capture. @@ -608,18 +609,22 @@ def capture_raw( transferring it over the network. """ with self.picamera() as cam: - (buffer, ), parameters = cam.capture_buffers(["raw"], wait=wait) + (buffer,), parameters = cam.capture_buffers(["raw"], wait=wait) configuration = cam.camera_configuration() return RawImageModel( - image_data = RawBlob.from_bytes(buffer.tobytes()), - thing_states = states_getter() if get_states else None, - metadata = { "parameters": parameters, "sensor": configuration["sensor"], "tuning": self.tuning }, - processing_inputs = ( + image_data=RawBlob.from_bytes(buffer.tobytes()), + thing_states=states_getter() if get_states else None, + metadata={ + "parameters": parameters, + "sensor": configuration["sensor"], + "tuning": self.tuning, + }, + processing_inputs=( self.image_processing_inputs if get_processing_inputs else None ), - size = configuration["raw"]["size"], - format = configuration["raw"]["format"], - stride = configuration["raw"]["stride"], + size=configuration["raw"]["size"], + format=configuration["raw"]["format"], + stride=configuration["raw"]["stride"], ) @thing_property @@ -657,32 +662,32 @@ def generate_image_processing_cache( p: ImageProcessingInputs, ) -> ImageProcessingCache: """Prepare to process raw images - + This is a static method to ensure its outputs depend only on its inputs.""" zoom_factors = [ i / 2 / n for i, n in zip(p.raw_size[::-1], p.white_norm_lores.shape[:2]) ] + [1] white_norm = zoom(p.white_norm_lores, zoom_factors, order=1)[ - : (p.raw_size[1]//2), : (p.raw_size[0]//2), : + : (p.raw_size[1] // 2), : (p.raw_size[0] // 2), : ] - ccm = np.array(p.colour_correction_matrix).reshape((3,3)) + ccm = np.array(p.colour_correction_matrix).reshape((3, 3)) gamma = interp1d(p.gamma[:, 0] / 255, p.gamma[:, 1] / 255) return ImageProcessingCache( white_norm=white_norm, - ccm = ccm, - gamma = gamma, + ccm=ccm, + gamma=gamma, ) _image_processing_cache: ImageProcessingCache | None = None + @thing_action def prepare_image_normalisation( - self, - inputs: ImageProcessingInputs | None = None + self, inputs: ImageProcessingInputs | None = None ) -> ImageProcessingInputs: """The parameters used to convert raw image data into processed images - - NB this method uses only information from `inputs` or + + NB this method uses only information from `inputs` or `self.image_processing_inputs`, to ensure repeatability """ p = inputs or self.image_processing_inputs @@ -694,7 +699,7 @@ def process_raw_array( self, raw: RawImageModel, use_cache: bool = False, - )->NDArray: + ) -> NDArray: """Convert a raw image to a processed array""" if not use_cache: if raw.processing_inputs is None: @@ -703,9 +708,7 @@ def process_raw_array( "and we are not using the cache. This may be solved by " "capturing with `get_processing_inputs=True`." ) - self.prepare_image_normalisation( - raw.processing_inputs - ) + self.prepare_image_normalisation(raw.processing_inputs) p = self._image_processing_cache assert p is not None assert raw.format == "SBGGR10_CSI2P" @@ -713,16 +716,14 @@ def process_raw_array( packed = buffer.reshape((-1, raw.stride)) rgb = rggb2rgb(raw2rggb(packed, raw.size)) normed = rgb / p.white_norm - corrected = np.dot( - p.ccm, normed.reshape((-1, 3)).T - ).T.reshape(normed.shape) + corrected = np.dot(p.ccm, normed.reshape((-1, 3)).T).T.reshape(normed.shape) corrected[corrected < 0] = 0 corrected[corrected > 255] = 255 processed_image = p.gamma(corrected) return processed_image.astype(np.uint8) @thing_action - def raw_to_png(self, raw: RawImageModel, use_cache: bool = False)->PNGBlob: + def raw_to_png(self, raw: RawImageModel, use_cache: bool = False) -> PNGBlob: """Process a raw image to a PNG""" arr = self.process_raw_array(raw=raw, use_cache=use_cache) image = Image.fromarray(arr.astype(np.uint8), mode="RGB") @@ -838,15 +839,6 @@ def grab_jpeg_size( ) return portal.call(stream.next_frame_size) - # @thing_action - # def capture_to_scan( - # self, - # scan_manager: ScanManager, - # format: Literal["jpeg"] = "jpeg", - # ) -> None: - # with scan_manager.new_jpeg() as output, self.picamera() as cam: - # cam.capture_file(output, format="jpeg") - @thing_property def exposure(self) -> float: """An alias for `exposure_time` to fit the micromanager API""" From 9cccfe5a2ef7c67b5361d19d174fd8683734a642 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 15 May 2025 16:27:52 +0100 Subject: [PATCH 13/13] Buffer count as argument for start streaming --- src/labthings_picamera2/thing.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/labthings_picamera2/thing.py b/src/labthings_picamera2/thing.py index f7e9ed8..ac23ddf 100644 --- a/src/labthings_picamera2/thing.py +++ b/src/labthings_picamera2/thing.py @@ -449,7 +449,9 @@ def __exit__(self, exc_type, exc_value, traceback): del self._picamera @thing_action - def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None: + def start_streaming( + self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6 + ) -> None: """ Start the MJPEG stream @@ -465,6 +467,8 @@ def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None main_resolution: the resolution for the main configuration. Defaults to (820, 616), 1/4 sensor size. + buffer_count: the number of frames to hold in the buffer. Higher uses more memory, + lower may cause dropped frames. Defaults to 6. """ with self.picamera() as picam: # TODO: Filip: can we use the lores output to keep preview stream going @@ -479,8 +483,8 @@ def start_streaming(self, main_resolution: tuple[int, int] = (820, 616)) -> None sensor=self.thing_settings.get("sensor_mode", None), controls=self.persistent_controls, ) - # Reducing buffer_count from video standard 6 to 4 to save memory - stream_config["buffer_count"] = 4 + # Set buffer count - can't be negative + stream_config["buffer_count"] = buffer_count picam.configure(stream_config) logging.info("Starting picamera MJPEG stream...") stream_name = "lores" if main_resolution[0] > 1280 else "main"