Move framerate to basecamera, using data_dir
This commit is contained in:
parent
5a8e8f745a
commit
ab25705edc
4 changed files with 155 additions and 156 deletions
|
|
@ -8,6 +8,7 @@ See repository root for licensing information.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
@ -198,6 +199,8 @@ class BaseCamera(OFMThing):
|
||||||
|
|
||||||
def __enter__(self) -> Self:
|
def __enter__(self) -> Self:
|
||||||
"""Open hardware connection when the Thing context manager is opened."""
|
"""Open hardware connection when the Thing context manager is opened."""
|
||||||
|
super().__enter__()
|
||||||
|
|
||||||
self._background_detector_name = coerce_thing_selector(
|
self._background_detector_name = coerce_thing_selector(
|
||||||
thing_mapping=self._all_background_detectors,
|
thing_mapping=self._all_background_detectors,
|
||||||
selected=self._background_detector_name,
|
selected=self._background_detector_name,
|
||||||
|
|
@ -264,6 +267,157 @@ class BaseCamera(OFMThing):
|
||||||
self.mjpeg_stream._streaming = False
|
self.mjpeg_stream._streaming = False
|
||||||
self.lores_mjpeg_stream._streaming = False
|
self.lores_mjpeg_stream._streaming = False
|
||||||
|
|
||||||
|
async def _monitor_framerate(
|
||||||
|
self,
|
||||||
|
duration: float,
|
||||||
|
output_dir: str,
|
||||||
|
sample_interval: float = 0.1,
|
||||||
|
) -> None:
|
||||||
|
"""Monitor the delivered MJPEG frame rate asynchronously.
|
||||||
|
|
||||||
|
This method listens to frames arriving on self.mjpeg_stream and records
|
||||||
|
timing information without directly interacting with the underlying
|
||||||
|
``Picamera2`` instance.
|
||||||
|
|
||||||
|
Writes a CSV log file with timestamp, frame count, and size of the most
|
||||||
|
recent MJPEG frame in bytes
|
||||||
|
|
||||||
|
Monitoring stops automatically after ``duration`` seconds, or earlier if
|
||||||
|
``self._framerate_monitor_running`` is set to ``False``.
|
||||||
|
|
||||||
|
:param duration: Total monitoring duration in seconds.
|
||||||
|
:param output_dir: Directory in which the CSV log file will be written.
|
||||||
|
:param sample_interval: Time interval in seconds between FPS samples.
|
||||||
|
Default = 0.1s.
|
||||||
|
|
||||||
|
:raises Exception: Any unexpected exception is logged before the monitor
|
||||||
|
exits.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||||
|
log_path = os.path.join(
|
||||||
|
output_dir,
|
||||||
|
f"framerate_{timestamp}.csv",
|
||||||
|
)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
last_sample_time = start_time
|
||||||
|
last_sample_frames = 0
|
||||||
|
frames = 0
|
||||||
|
|
||||||
|
self._framerate_monitor_running = True
|
||||||
|
|
||||||
|
# Store rows in memory and write once at the end
|
||||||
|
sample_rows = []
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
"Starting framerate monitor for %.2fs -> %s",
|
||||||
|
duration,
|
||||||
|
log_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
async for frame in self.mjpeg_stream.frame_async_generator():
|
||||||
|
if not self._framerate_monitor_running:
|
||||||
|
self.logger.info("Framerate monitor stopped externally")
|
||||||
|
break
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
frames += 1
|
||||||
|
frame_size = len(frame)
|
||||||
|
|
||||||
|
if now - last_sample_time >= sample_interval:
|
||||||
|
interval = now - last_sample_time
|
||||||
|
|
||||||
|
instant_fps = (frames - last_sample_frames) / interval
|
||||||
|
|
||||||
|
sample_rows.append(
|
||||||
|
[
|
||||||
|
now,
|
||||||
|
frames,
|
||||||
|
frame_size,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
"[Framerate test] FPS=%.2f, size=%d B",
|
||||||
|
instant_fps,
|
||||||
|
frame_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
last_sample_time = now
|
||||||
|
last_sample_frames = frames
|
||||||
|
|
||||||
|
if now - start_time >= duration:
|
||||||
|
self.logger.info("Framerate monitor duration reached")
|
||||||
|
break
|
||||||
|
|
||||||
|
total_time = time.time() - start_time
|
||||||
|
avg_fps = frames / total_time if total_time > 0 else 0
|
||||||
|
|
||||||
|
# Write CSV once at completion
|
||||||
|
with open(log_path, "w", newline="") as csv_file:
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"timestamp",
|
||||||
|
"frame_count",
|
||||||
|
"frame_size_bytes",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.writerows(sample_rows)
|
||||||
|
|
||||||
|
writer.writerow([])
|
||||||
|
writer.writerow(["TOTAL_DURATION", total_time])
|
||||||
|
writer.writerow(["TOTAL_FRAMES", frames])
|
||||||
|
writer.writerow(["AVG_FPS", avg_fps])
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
"[Framerate test complete] Frames=%d, Avg FPS=%.2f",
|
||||||
|
frames,
|
||||||
|
avg_fps,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Framerate monitor crashed")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._framerate_monitor_running = False
|
||||||
|
|
||||||
|
@lt.action
|
||||||
|
def record_framerate_async(
|
||||||
|
self,
|
||||||
|
duration: float = 5.0,
|
||||||
|
) -> str:
|
||||||
|
"""Start asynchronous MJPEG framerate monitoring.
|
||||||
|
|
||||||
|
The monitor observes frames delivered through ``self.mjpeg_stream`` rather
|
||||||
|
than directly querying the camera hardware. This makes it suitable for
|
||||||
|
measuring effective stream throughput under real operating conditions.
|
||||||
|
|
||||||
|
:param duration: Monitoring duration in seconds. Default = 5.0s.
|
||||||
|
|
||||||
|
:returns: The output directory containing generated log files.
|
||||||
|
"""
|
||||||
|
output_dir = os.path.join(self.data_dir, "characterisation", "framerate")
|
||||||
|
self._thing_server_interface.start_async_task_soon(
|
||||||
|
self._monitor_framerate,
|
||||||
|
duration,
|
||||||
|
output_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
"Started async framerate monitor for %.2fs -> %s",
|
||||||
|
duration,
|
||||||
|
output_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output_dir
|
||||||
|
|
||||||
@lt.property
|
@lt.property
|
||||||
def stream_active(self) -> bool:
|
def stream_active(self) -> bool:
|
||||||
"""Whether the MJPEG stream is active."""
|
"""Whether the MJPEG stream is active."""
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import csv
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -550,159 +549,6 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
with self._streaming_picamera() as cam:
|
with self._streaming_picamera() as cam:
|
||||||
cam.capture_metadata()
|
cam.capture_metadata()
|
||||||
|
|
||||||
async def _monitor_framerate(
|
|
||||||
self,
|
|
||||||
duration: float,
|
|
||||||
output_dir: str,
|
|
||||||
sample_interval: float = 0.1,
|
|
||||||
) -> None:
|
|
||||||
"""Monitor the delivered MJPEG frame rate asynchronously.
|
|
||||||
|
|
||||||
This method listens to frames arriving on self.mjpeg_stream and records
|
|
||||||
timing information without directly interacting with the underlying
|
|
||||||
``Picamera2`` instance.
|
|
||||||
|
|
||||||
Writes a CSV log file with timestamp, frame count, and size of the most
|
|
||||||
recent MJPEG frame in bytes
|
|
||||||
|
|
||||||
Monitoring stops automatically after ``duration`` seconds, or earlier if
|
|
||||||
``self._framerate_monitor_running`` is set to ``False``.
|
|
||||||
|
|
||||||
:param duration: Total monitoring duration in seconds.
|
|
||||||
:param output_dir: Directory in which the CSV log file will be written.
|
|
||||||
:param sample_interval: Time interval in seconds between FPS samples.
|
|
||||||
Default = 0.1s.
|
|
||||||
|
|
||||||
:raises Exception: Any unexpected exception is logged before the monitor
|
|
||||||
exits.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
|
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
||||||
log_path = os.path.join(
|
|
||||||
output_dir,
|
|
||||||
f"framerate_log_{timestamp}.csv",
|
|
||||||
)
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
last_sample_time = start_time
|
|
||||||
last_sample_frames = 0
|
|
||||||
frames = 0
|
|
||||||
|
|
||||||
self._framerate_monitor_running = True
|
|
||||||
|
|
||||||
LOGGER.info(
|
|
||||||
"Starting framerate monitor for %.2fs -> %s",
|
|
||||||
duration,
|
|
||||||
log_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(log_path, "w", newline="") as csv_file:
|
|
||||||
writer = csv.writer(csv_file)
|
|
||||||
|
|
||||||
writer.writerow(
|
|
||||||
[
|
|
||||||
"timestamp",
|
|
||||||
"frame_count",
|
|
||||||
"frame_size_bytes",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
async for frame in self.mjpeg_stream.frame_async_generator():
|
|
||||||
if not self._framerate_monitor_running:
|
|
||||||
LOGGER.info("Framerate monitor stopped externally")
|
|
||||||
break
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
|
|
||||||
frames += 1
|
|
||||||
frame_size = len(frame)
|
|
||||||
|
|
||||||
if now - last_sample_time >= sample_interval:
|
|
||||||
interval = now - last_sample_time
|
|
||||||
|
|
||||||
instant_fps = (frames - last_sample_frames) / interval
|
|
||||||
|
|
||||||
writer.writerow(
|
|
||||||
[
|
|
||||||
now,
|
|
||||||
frames,
|
|
||||||
frame_size,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
csv_file.flush()
|
|
||||||
|
|
||||||
LOGGER.info(
|
|
||||||
"[Framerate test] FPS=%.2f, size=%d B",
|
|
||||||
instant_fps,
|
|
||||||
frame_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
last_sample_time = now
|
|
||||||
last_sample_frames = frames
|
|
||||||
|
|
||||||
if now - start_time >= duration:
|
|
||||||
LOGGER.info("Framerate monitor duration reached")
|
|
||||||
break
|
|
||||||
|
|
||||||
total_time = time.time() - start_time
|
|
||||||
avg_fps = frames / total_time if total_time > 0 else 0
|
|
||||||
|
|
||||||
writer.writerow([])
|
|
||||||
writer.writerow(["TOTAL_DURATION", total_time])
|
|
||||||
writer.writerow(["TOTAL_FRAMES", frames])
|
|
||||||
writer.writerow(["AVG_FPS", avg_fps])
|
|
||||||
|
|
||||||
LOGGER.info(
|
|
||||||
"[Framerate test complete] Frames=%d, Avg FPS=%.2f",
|
|
||||||
frames,
|
|
||||||
avg_fps,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
LOGGER.exception("Framerate monitor crashed")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self._framerate_monitor_running = False
|
|
||||||
|
|
||||||
@lt.action
|
|
||||||
def record_framerate_async(
|
|
||||||
self,
|
|
||||||
duration: float = 5.0,
|
|
||||||
output_dir: str = "framerate",
|
|
||||||
) -> str:
|
|
||||||
"""Start asynchronous MJPEG framerate monitoring.
|
|
||||||
|
|
||||||
This action schedules ``_monitor_framerate`` on the Thing server event loop
|
|
||||||
and immediately returns. Monitoring occurs in the background and does not
|
|
||||||
block the calling thread.
|
|
||||||
|
|
||||||
The monitor observes frames delivered through ``self.mjpeg_stream`` rather
|
|
||||||
than directly querying the camera hardware. This makes it suitable for
|
|
||||||
measuring effective stream throughput under real operating conditions.
|
|
||||||
|
|
||||||
:param duration: Monitoring duration in seconds. Default = 5.0s.
|
|
||||||
:param output_dir: Directory where CSV log files will be written.
|
|
||||||
Default = ``"framerate"``.
|
|
||||||
|
|
||||||
:returns: The output directory containing generated log files.
|
|
||||||
"""
|
|
||||||
self._thing_server_interface.start_async_task_soon(
|
|
||||||
self._monitor_framerate,
|
|
||||||
duration,
|
|
||||||
output_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
LOGGER.info(
|
|
||||||
"Started async framerate monitor for %.2fs -> %s",
|
|
||||||
duration,
|
|
||||||
output_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
return output_dir
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _switch_to_still_capture_mode(self) -> Iterator[Picamera2]:
|
def _switch_to_still_capture_mode(self) -> Iterator[Picamera2]:
|
||||||
"""Get the picamera lock, pause stream and switch into still capture config.
|
"""Get the picamera lock, pause stream and switch into still capture config.
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ def test_record_framerate_async(picamera_client, tmp_path):
|
||||||
# Wait slightly longer than recording duration for async task completion
|
# Wait slightly longer than recording duration for async task completion
|
||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
|
|
||||||
log_files = list(output_dir.glob("framerate_log_*.csv"))
|
log_files = list(output_dir.glob("framerate_*.csv"))
|
||||||
|
|
||||||
# Ensure exactly one log file was created
|
# Ensure exactly one log file was created
|
||||||
assert len(log_files) == 1
|
assert len(log_files) == 1
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,6 @@ def test_thing_description_equivalence(mock_picam_thing):
|
||||||
"set_static_green_equalisation",
|
"set_static_green_equalisation",
|
||||||
"set_ce_enable_to_off",
|
"set_ce_enable_to_off",
|
||||||
"stop_streaming",
|
"stop_streaming",
|
||||||
"record_framerate_async",
|
|
||||||
"reset_ccm",
|
"reset_ccm",
|
||||||
}
|
}
|
||||||
picamera_extra_props = {
|
picamera_extra_props = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue