Renaming picamera context manager to make it clear it is different from using the picamera itself as a context manager

This commit is contained in:
Julian Stirling 2025-07-04 12:29:53 +01:00
parent 52a80ef66b
commit df8a9d0e93

View file

@ -158,7 +158,7 @@ class StreamingPiCamera2(BaseCamera):
and only update `self.persistent_controls` if the change is greater than and only update `self.persistent_controls` if the change is greater than
this tolerance. this tolerance.
""" """
with self.picamera() as cam: with self._streaming_picamera() as cam:
for i in range(discard_frames): for i in range(discard_frames):
# Discard frames, so data is fresh # Discard frames, so data is fresh
cam.capture_metadata() cam.capture_metadata()
@ -242,7 +242,7 @@ class StreamingPiCamera2(BaseCamera):
See comment within the function for more detail. See comment within the function for more detail.
""" """
with self.picamera() as cam: with self._streaming_picamera() as cam:
# Note: This set a value 1 higher than requested as picamera2 always sets # Note: This set a value 1 higher than requested as picamera2 always sets
# a lower value than requested, even if the requested is allowed # a lower value than requested, even if the requested is allowed
cam.set_controls({"ExposureTime": value + 1}) cam.set_controls({"ExposureTime": value + 1})
@ -253,46 +253,42 @@ class StreamingPiCamera2(BaseCamera):
def sensor_modes(self) -> list[SensorMode]: def sensor_modes(self) -> list[SensorMode]:
"""All the available modes the current sensor supports""" """All the available modes the current sensor supports"""
if not self._sensor_modes: if not self._sensor_modes:
with self.picamera() as cam: with self._streaming_picamera() as cam:
self._sensor_modes = cam.sensor_modes self._sensor_modes = cam.sensor_modes
return self._sensor_modes return self._sensor_modes
@lt.thing_property _sensor_mode: Optional[dict] = None
@lt.thing_setting
def sensor_mode(self) -> Optional[SensorModeSelector]: def sensor_mode(self) -> Optional[SensorModeSelector]:
"""The intended sensor mode of the camera""" """The intended sensor mode of the camera"""
return self.thing_settings["sensor_mode"] if self._sensor_mode is None:
return None
return SensorModeSelector(**self._sensor_mode)
@sensor_mode.setter @sensor_mode.setter
def sensor_mode(self, new_mode: Optional[SensorModeSelector]): def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]):
"""Change the sensor mode used""" """Change the sensor mode used"""
if new_mode is None:
self._sensor_mode = None
elif isinstance(new_mode, SensorModeSelector):
self._sensor_mode = new_mode.model_dump()
elif isinstance(new_mode, dict):
self._sensor_mode = new_mode
if isinstance(new_mode, SensorModeSelector): if isinstance(new_mode, SensorModeSelector):
new_mode = new_mode.model_dump() new_mode = new_mode.model_dump()
with self.picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
self.thing_settings["sensor_mode"] = new_mode self.thing_settings["sensor_mode"] = new_mode
@lt.thing_property @lt.thing_setting
def sensor_resolution(self) -> tuple[int, int]: def sensor_resolution(self) -> tuple[int, int]:
"""The native resolution of the camera's sensor""" """The native resolution of the camera's sensor"""
with self.picamera() as cam: with self._streaming_picamera() as cam:
return cam.sensor_resolution return cam.sensor_resolution
tuning = lt.ThingProperty(Optional[dict], None, readonly=True) tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
def settings_to_properties(self):
"""Set the values of properties based on the settings dict"""
try:
props = self.thing_settings["properties"]
except KeyError:
return
for k, v in props.items():
setattr(self, k, v)
def properties_to_settings(self):
"""Save certain properties to the settings dictionary"""
props = {}
for k in ["mjpeg_bitrate", "stream_resolution"]:
props[k] = getattr(self, k)
self.thing_settings["properties"] = props
def initialise_tuning(self): def initialise_tuning(self):
"""Read the tuning from the settings, or load default tuning """Read the tuning from the settings, or load default tuning
@ -328,10 +324,10 @@ class StreamingPiCamera2(BaseCamera):
"Camera object already exists, closing for reinitialisation" "Camera object already exists, closing for reinitialisation"
) )
self._picamera.close() self._picamera.close()
print("closed, deleting picamera") print("Picamera closed, deleting picamera")
del self._picamera del self._picamera
recalibrate_utils.recreate_camera_manager() recalibrate_utils.recreate_camera_manager()
print("[re]creating Picamera2 object") print("Creating new Picamera2 object")
self._picamera = picamera2.Picamera2( self._picamera = picamera2.Picamera2(
camera_num=self.camera_num, camera_num=self.camera_num,
tuning=self.tuning, tuning=self.tuning,
@ -350,14 +346,19 @@ class StreamingPiCamera2(BaseCamera):
return self return self
@contextmanager @contextmanager
def picamera(self, pause_stream=False) -> Iterator[Picamera2]: def _streaming_picamera(self, pause_stream=False) -> Iterator[Picamera2]:
"""Return the underlying `Picamera2` instance, optionally pausing the stream. """Lock access to picamera and return the underlying `Picamera2` instance.
If pause_stream is True (default is False), we will stop the MJPEG stream Optionally the stream can be paused to allow updating the camera settings.
before yielding control of the camera, and restart afterwards. If you make
changes to the camera settings, these may be ignored when the stream is :param pause_stream: If False the `Picamera2` instance is simply yielded.
restarted: you may nened to call `update_persistent_controls()` to ensure If True:
your changes persist after the stream restarts. * Updated persistent controls
* Stop the MJPEG Stream
* Yield the `Picamera2` instance for function calling the context manager to
make changes. If seetings are changed run `update_persistent_controls()` before
closing the context manager.
* On closing of the context manager the stream will restart.
""" """
already_streaming = self.stream_active already_streaming = self.stream_active
with self._picamera_lock: with self._picamera_lock:
@ -390,7 +391,7 @@ class StreamingPiCamera2(BaseCamera):
self.thing_settings.write_to_file() self.thing_settings.write_to_file()
# Shut down the camera # Shut down the camera
self.stop_streaming() self.stop_streaming()
with self.picamera() as cam: with self._streaming_picamera() as cam:
cam.close() cam.close()
del self._picamera del self._picamera
@ -426,7 +427,7 @@ class StreamingPiCamera2(BaseCamera):
f"Can't set a buffer count of {buffer_count}. " f"Can't set a buffer count of {buffer_count}. "
"Buffer count must be an integer from 1-8" "Buffer count must be an integer from 1-8"
) )
with self.picamera() as picam: with self._streaming_picamera() as picam:
try: try:
if picam.started: if picam.started:
picam.stop() picam.stop()
@ -471,7 +472,7 @@ class StreamingPiCamera2(BaseCamera):
""" """
Stop the MJPEG stream Stop the MJPEG stream
""" """
with self.picamera() as picam: with self._streaming_picamera() as picam:
try: try:
picam.stop_recording() # This should also stop the extra lores encoder picam.stop_recording() # This should also stop the extra lores encoder
except Exception as e: except Exception as e:
@ -502,7 +503,7 @@ class StreamingPiCamera2(BaseCamera):
A TimeoutError is raised if this time is exceeded during capture. A TimeoutError is raised if this time is exceeded during capture.
Default = 0.9s, lower than the 1s timeout default in picamera yaml settings Default = 0.9s, lower than the 1s timeout default in picamera yaml settings
""" """
with self.picamera() as cam: with self._streaming_picamera() as cam:
return cam.capture_image(stream_name, wait=wait) return cam.capture_image(stream_name, wait=wait)
@lt.thing_action @lt.thing_action
@ -526,10 +527,10 @@ class StreamingPiCamera2(BaseCamera):
# This was slower than capture_image for our use case, but directly returning # This was slower than capture_image for our use case, but directly returning
# an image as an array is still a useful feature # an image as an array is still a useful feature
if stream_name == "full": if stream_name == "full":
with self.picamera(pause_stream=True) as picam2: with self._streaming_picamera(pause_stream=True) as picam2:
capture_config = picam2.create_still_configuration() capture_config = picam2.create_still_configuration()
return picam2.switch_mode_and_capture_array(capture_config, wait=wait) return picam2.switch_mode_and_capture_array(capture_config, wait=wait)
with self.picamera() as cam: with self._streaming_picamera() as cam:
return cam.capture_array(stream_name, wait=wait) return cam.capture_array(stream_name, wait=wait)
@lt.thing_property @lt.thing_property
@ -544,7 +545,7 @@ class StreamingPiCamera2(BaseCamera):
this property refers to whatever configuration is currently in force - this property refers to whatever configuration is currently in force -
usually the one used for the preview stream. usually the one used for the preview stream.
""" """
with self.picamera() as cam: with self._streaming_picamera() as cam:
return cam.camera_configuration() return cam.camera_configuration()
@lt.thing_action @lt.thing_action
@ -580,14 +581,14 @@ class StreamingPiCamera2(BaseCamera):
# Low-res and main streams are running already - so we don't need # Low-res and main streams are running already - so we don't need
# to reconfigure for these # to reconfigure for these
if resolution in ("lores", "main") and config[resolution]: if resolution in ("lores", "main") and config[resolution]:
with self.picamera() as cam: with self._streaming_picamera() as cam:
cam.capture_file(path, name=resolution, format="jpeg", wait=wait) cam.capture_file(path, name=resolution, format="jpeg", wait=wait)
else: else:
if resolution != "full": if resolution != "full":
logging.warning( logging.warning(
f"There was no {resolution} stream, capturing full resolution" f"There was no {resolution} stream, capturing full resolution"
) )
with self.picamera(pause_stream=True) as cam: with self._streaming_picamera(pause_stream=True) as cam:
logging.info("Reconfiguring camera for full resolution capture") logging.info("Reconfiguring camera for full resolution capture")
cam.configure(cam.create_still_configuration()) cam.configure(cam.create_still_configuration())
cam.start() cam.start()
@ -606,7 +607,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property @lt.thing_property
def capture_metadata(self) -> dict: def capture_metadata(self) -> dict:
"""Return the metadata from the camera""" """Return the metadata from the camera"""
with self.picamera() as cam: with self._streaming_picamera() as cam:
return cam.capture_metadata() return cam.capture_metadata()
@lt.thing_action @lt.thing_action
@ -627,7 +628,7 @@ class StreamingPiCamera2(BaseCamera):
calculating the brightest pixel, a percentile is used rather than the calculating the brightest pixel, a percentile is used rather than the
maximum in order to be robust to a small number of noisy/bright pixels. maximum in order to be robust to a small number of noisy/bright pixels.
""" """
with self.picamera(pause_stream=True) as cam: with self._streaming_picamera(pause_stream=True) as cam:
recalibrate_utils.adjust_shutter_and_gain_from_raw( recalibrate_utils.adjust_shutter_and_gain_from_raw(
cam, cam,
target_white_level=target_white_level, target_white_level=target_white_level,
@ -652,7 +653,7 @@ class StreamingPiCamera2(BaseCamera):
If `method` is `"centre"`, we will correct the mean of the central 10% If `method` is `"centre"`, we will correct the mean of the central 10%
of the image. of the image.
""" """
with self.picamera(pause_stream=True) as cam: with self._streaming_picamera(pause_stream=True) as cam:
if self.lens_shading_is_static: if self.lens_shading_is_static:
lst: LensShading = self.lens_shading_tables lst: LensShading = self.lens_shading_tables
recalibrate_utils.adjust_white_balance_from_raw( recalibrate_utils.adjust_white_balance_from_raw(
@ -679,7 +680,7 @@ class StreamingPiCamera2(BaseCamera):
one. This uses the camera's "tuning" file to correct the preview and one. This uses the camera's "tuning" file to correct the preview and
the processed images. It should not affect raw images. the processed images. It should not affect raw images.
""" """
with self.picamera(pause_stream=True) as cam: with self._streaming_picamera(pause_stream=True) as cam:
# Suppress lint warning that L, Cr, and Cb are not lowercase, as these are # Suppress lint warning that L, Cr, and Cb are not lowercase, as these are
# the standard mathematical terms for: # the standard mathematical terms for:
# luminance (L), red-difference chroma (Cr), and blue-difference chroma # luminance (L), red-difference chroma (Cr), and blue-difference chroma
@ -740,7 +741,7 @@ class StreamingPiCamera2(BaseCamera):
See page Raspberry Pi Camera Algorithm and Tuning Guide, page 45. See page Raspberry Pi Camera Algorithm and Tuning Guide, page 45.
""" """
with self.picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
recalibrate_utils.set_static_ccm(self.tuning, col_corr_matrix) recalibrate_utils.set_static_ccm(self.tuning, col_corr_matrix)
self.initialise_picamera() self.initialise_picamera()
@ -755,7 +756,7 @@ class StreamingPiCamera2(BaseCamera):
A value of 0 here does nothing, a value of 65535 is maximum correction. A value of 0 here does nothing, a value of 65535 is maximum correction.
""" """
with self.picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
recalibrate_utils.set_static_geq(self.tuning, offset) recalibrate_utils.set_static_geq(self.tuning, offset)
self.initialise_picamera() self.initialise_picamera()
@ -788,7 +789,7 @@ class StreamingPiCamera2(BaseCamera):
This flat table is used to take an image wth no lens shading so that the This flat table is used to take an image wth no lens shading so that the
correct lens shading table can be calibrated. correct lens shading table can be calibrated.
""" """
with self.picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
# Generate and array of ones of the correct size for each channel # Generate and array of ones of the correct size for each channel
flat_array = np.ones((12, 16)) flat_array = np.ones((12, 16))
recalibrate_utils.set_static_lst( recalibrate_utils.set_static_lst(
@ -833,7 +834,7 @@ class StreamingPiCamera2(BaseCamera):
@lens_shading_tables.setter @lens_shading_tables.setter
def lens_shading_tables(self, lst: LensShading) -> None: def lens_shading_tables(self, lst: LensShading) -> None:
"""Set the lens shading tables""" """Set the lens shading tables"""
with self.picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
recalibrate_utils.set_static_lst( recalibrate_utils.set_static_lst(
self.tuning, self.tuning,
luminance=lst.luminance, luminance=lst.luminance,
@ -881,7 +882,7 @@ class StreamingPiCamera2(BaseCamera):
flat, i.e. we'll correct vignetting of intensity, but not any change in flat, i.e. we'll correct vignetting of intensity, but not any change in
colour across the image. colour across the image.
""" """
with self.picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
alsc = Picamera2.find_tuning_algo(self.tuning, "rpi.alsc") alsc = Picamera2.find_tuning_algo(self.tuning, "rpi.alsc")
luminance = alsc["luminance_lut"] luminance = alsc["luminance_lut"]
flat = np.ones((12, 16)) flat = np.ones((12, 16))
@ -895,7 +896,7 @@ class StreamingPiCamera2(BaseCamera):
This method will restore the default "adaptive" lens shading method used This method will restore the default "adaptive" lens shading method used
by the Raspberry Pi camera. by the Raspberry Pi camera.
""" """
with self.picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
recalibrate_utils.copy_alsc_section(self.default_tuning, self.tuning) recalibrate_utils.copy_alsc_section(self.default_tuning, self.tuning)
self.initialise_picamera() self.initialise_picamera()