From 28d52a6d2e2e4c5d25d7d59ed7fbda4b2c4ce17e Mon Sep 17 00:00:00 2001 From: David Plowman Date: Wed, 8 Nov 2023 14:25:24 +0000 Subject: [PATCH 1/2] Allow requests to be captured with a "no earlier than" timestamp This means that no pixel in the returned request started being exposed before the given timestamp. Any such requests are flushed out and not returned to the caller. The "flush" parameter may be an explicit timestamp (in nanoseconds, for example obtained from time.monotonic_ns()), or just True, meaning "now". Signed-off-by: David Plowman --- picamera2/picamera2.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/picamera2/picamera2.py b/picamera2/picamera2.py index c60d59d1..5f768a21 100644 --- a/picamera2/picamera2.py +++ b/picamera2/picamera2.py @@ -1309,12 +1309,24 @@ def set_frame_drops_(self, num_frames): return (True, None) def drop_frames_(self): - if not self.completed_requests: - return (False, None) - if self._frame_drops == 0: + while self.completed_requests: + if self._frame_drops == 0: + return (True, None) + self.completed_requests.pop(0).release() + self._frame_drops -= 1 + return (False, None) + + def wait_for_timestamp_(self, timestamp_ns): + # No wait requested. This function in the job is done. + if not timestamp_ns: return (True, None) - self.completed_requests.pop(0).release() - self._frame_drops -= 1 + while self.completed_requests: + # Check if frame started being exposed after the timestamp. + md = self.completed_requests[0].get_metadata() + frame_timestamp_ns = md['SensorTimestamp'] - 1000 * md['ExposureTime'] + if frame_timestamp_ns >= timestamp_ns: + return (True, None) + self.completed_requests.pop(0).release() return (False, None) def drop_frames(self, num_frames, wait=None, signal_function=None): @@ -1413,13 +1425,17 @@ def capture_request_(self): return (False, None) return (True, self.completed_requests.pop(0)) - def capture_request(self, wait=None, signal_function=None): + def capture_request(self, wait=None, signal_function=None, flush=None): """Fetch the next completed request from the camera system. You will be holding a reference to this request so you must release it again to return it to the camera system. """ - functions = [self.capture_request_] + # flush will be the timestamp in ns that we wait for (if any) + if flush is True: + flush = time.monotonic_ns() + functions = [partial(self.wait_for_timestamp_, flush), + self.capture_request_] return self.dispatch_functions(functions, wait, signal_function) def switch_mode_capture_request_and_stop(self, camera_config, wait=None, signal_function=None): From 7ac199f69fd992aa234f7dbf5f0d4b6b48f03a45 Mon Sep 17 00:00:00 2001 From: David Plowman Date: Wed, 8 Nov 2023 14:39:12 +0000 Subject: [PATCH 2/2] Add an example/test for capturing requests with a "not earlier than" timestamp Signed-off-by: David Plowman --- examples/timestamp_capture.py | 49 +++++++++++++++++++++++++++++++++++ tests/test_list.txt | 1 + 2 files changed, 50 insertions(+) create mode 100644 examples/timestamp_capture.py diff --git a/examples/timestamp_capture.py b/examples/timestamp_capture.py new file mode 100644 index 00000000..13da96bc --- /dev/null +++ b/examples/timestamp_capture.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 + +import time + +from picamera2 import Picamera2 + + +def check_request_timestamp(request, check_time): + md = request.get_metadata() + # 'SensorTimestamp' is when the first pixel was read out, so it started being + # exposed 'ExposureTime' earlier. + exposure_start_time = md['SensorTimestamp'] - 1000 * md['ExposureTime'] + if exposure_start_time < check_time: + print("ERROR: request captured too early by", check_time - exposure_start_time, "nanoseconds") + + +picam2 = Picamera2() +picam2.start() + +time.sleep(1) + +# Capture a request that is guaranteed not to have started being exposed before +# we make the request. Do it 30 times as a test. Note that this kind of thing is +# likely cut the apparent framerate because of all the stopping and waiting for sync. + +for _ in range(30): + check_time = time.monotonic_ns() + request = picam2.capture_request(flush=True) + check_request_timestamp(request, check_time) + request.release() + +# Capture a request where nothing started being exposed until 1/2s in the +# future. Do this 10 times as a test. + +for _ in range(10): + check_time = time.monotonic_ns() + 5e8 + request = picam2.capture_request(flush=check_time) + check_request_timestamp(request, check_time) + request.release() + +# Let's do this last test again, but using asynchronous operation. + +for _ in range(10): + check_time = time.monotonic_ns() + 5e8 + job = picam2.capture_request(flush=check_time, wait=False) + # Now we can do other stuff while waiting for the capture. + request = picam2.wait(job) + check_request_timestamp(request, check_time) + request.release() diff --git a/tests/test_list.txt b/tests/test_list.txt index 5dfda496..06b39af0 100644 --- a/tests/test_list.txt +++ b/tests/test_list.txt @@ -37,6 +37,7 @@ examples/still_capture_with_config.py examples/still_during_video.py examples/switch_mode.py examples/switch_mode_2.py +examples/timestamp_capture.py examples/title_bar.py examples/tuning_file.py examples/video_with_config.py