diff --git a/.flake8 b/.flake8 deleted file mode 100644 index fc125833..00000000 --- a/.flake8 +++ /dev/null @@ -1,17 +0,0 @@ -[flake8] -docstring_style=sphinx -max-line-length = 127 -ignore = D10, D400, D401, E800, Q000, S311, PLW, PLC, PLR, PLE -extend-ignore = W503, W504 -per-file-ignores = __init__.py:F401 - qt.py:F401 - gl_helpers.py:F401,F403,F405 - q_gl_picamera2.py:E402,F403,F405 - v4l2_encoder.py:F403,F405 - encoder.py:F403,F405 - drm_preview.py:F403,F405 - capture_video_raw.py:F403,F405 - picamera2.py:B006,B008 - configuration.py:B006 - metadata.py:B006 - controls.py:B006 diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml deleted file mode 100644 index d8e09f6c..00000000 --- a/.github/workflows/flake8.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: flake8 -on: - pull_request: - branches: [ main, next ] -jobs: - compile: - name: compile libcamera - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: '3.9.19' - - name: Update - run: sudo apt update - - name: Install apt dependencies - run: sudo apt install -y build-essential qtbase5-dev libqt5core5a libqt5gui5 libqt5widgets5 libgnutls28-dev openssl libtiff5-dev libcap-dev libfmt-dev libdrm-dev git python3-sphinx libyaml-dev python3-yaml python3-ply python3-jinja2 libgnutls28-dev openssl libboost-dev libgles2-mesa-dev libegl1-mesa-dev - - name: Install dependencies - run: | - python -m pip install --upgrade pip - - name: Install dependencies for testing - run: | - if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi - - name: Set libcamera hash - run: echo "LIBCAMERA-HASH=$(git ls-remote https://github.com/raspberrypi/libcamera.git main | cut -c1-40)" >> $GITHUB_ENV - - name: Cache libcamera - id: cache-libcamera - uses: actions/cache@v3 - with: - path: /home/runner/work/libcamera - key: ${{ runner.os }}-libcamera2-${{env.LIBCAMERA-HASH}} - - name: Generate libcamera - if: steps.cache-libcamera.outputs.cache-hit != 'true' - run: cd /home/runner/work; git clone https://github.com/raspberrypi/libcamera.git; cd libcamera; meson build --buildtype=release -Dpipelines=rpi/vc4 -Dipas=rpi/vc4 -Dv4l2=true -Dtest=false -Dlc-compliance=disabled -Dcam=disabled -Dqcam=disabled -Dpycamera=enabled -Ddocumentation=disabled ; ninja -C build - lint: - name: lint code - needs: compile - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: '3.9.19' - - name: Display python version - run: python -c "import sys; print(sys.version)" - - name: Update - run: sudo apt update - - name: Install apt dependencies - run: sudo apt install -y build-essential libcap-dev libfmt-dev libdrm-dev git python3-sphinx libyaml-dev python3-yaml python3-ply python3-jinja2 libgnutls28-dev openssl libboost-dev - - name: Display python version - run: python -c "import sys; print(sys.version)" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - - name: Install dependencies for testing - run: | - if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi - - name: Set libcamera hash - run: echo "LIBCAMERA-HASH=$(git ls-remote https://github.com/raspberrypi/libcamera.git main | cut -c1-40)" >> $GITHUB_ENV - - name: Cache libcamera - id: cache-libcamera - uses: actions/cache@v3 - with: - path: /home/runner/work/libcamera - key: ${{ runner.os }}-libcamera2-${{env.LIBCAMERA-HASH}} - - name: Generate libcamera - if: steps.cache-libcamera.outputs.cache-hit != 'true' - run: exit 1 - - name: kmsxx - run: cd /home/runner/work; git clone https://github.com/tomba/kmsxx.git; cd kmsxx; meson build; ninja -C build - - name: set pythonpath - run: | - echo "PYTHONPATH=.:/home/runner/work/kmsxx/build/py/:/home/runner/work/libcamera/build/src/py/" >> $GITHUB_ENV - - name: test cv2 - run: python -c 'import cv2; print(cv2.__version__)' - - name: test pykms - run: python -c 'import pykms' - - name: Lint with flake8 - run: | - flake8 . --count --show-source --statistics diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..a8cb1d54 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,18 @@ +name: Ruff + +on: + pull_request: + branches: [main, next] + +jobs: + lint: + name: Lint and format check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + with: + args: check --output-format=github + - uses: astral-sh/ruff-action@v3 + with: + args: format --check --diff diff --git a/.gitignore b/.gitignore index 10034c26..3a97fbb7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ docs/_build/ /.spyproject .spyproject hailort.log +.ruff_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eed0b689..ea551466 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,6 @@ repos: -- repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - additional_dependencies: [ - 'astroid==2.12.14', - 'pylint==2.15.10', - 'flake8-bugbear>=22.10.27', - 'flake8-comprehensions>=3.10', - 'flake8-debugger', - 'flake8-docstrings>=1.6.0', - 'flake8-isort>=5.0', - 'flake8-pylint', - 'flake8-rst-docstrings', - 'flake8-string-format' - ] +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.4 + hooks: + - id: ruff + - id: ruff-format diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index bd3ccd7e..00000000 --- a/.pylintrc +++ /dev/null @@ -1,5 +0,0 @@ -[MASTER] -extension-pkg-allow-list=libcamera, PyQt5, pykms, cv2 - -[TYPECHECK] -generated-members=cv2.* diff --git a/apps/app_capture2.py b/apps/app_capture2.py index 08346316..22b55424 100755 --- a/apps/app_capture2.py +++ b/apps/app_capture2.py @@ -5,8 +5,7 @@ # when the capture, that is running asynchronously, is finished. from PyQt5 import QtCore -from PyQt5.QtWidgets import (QApplication, QHBoxLayout, QLabel, QPushButton, - QVBoxLayout, QWidget) +from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QGlPicamera2 diff --git a/apps/app_capture_af.py b/apps/app_capture_af.py index 5051d8ac..42c6dd18 100755 --- a/apps/app_capture_af.py +++ b/apps/app_capture_af.py @@ -3,8 +3,7 @@ from libcamera import controls from PyQt5 import QtCore from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import (QApplication, QCheckBox, QHBoxLayout, QLabel, - QPushButton, QVBoxLayout, QWidget) +from PyQt5.QtWidgets import QApplication, QCheckBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QGlPicamera2 diff --git a/apps/app_capture_overlay.py b/apps/app_capture_overlay.py index 1f8a8f6c..ada125af 100755 --- a/apps/app_capture_overlay.py +++ b/apps/app_capture_overlay.py @@ -2,8 +2,7 @@ import numpy as np from PyQt5 import QtCore -from PyQt5.QtWidgets import (QApplication, QCheckBox, QHBoxLayout, QLabel, - QPushButton, QVBoxLayout, QWidget) +from PyQt5.QtWidgets import QApplication, QCheckBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QGlPicamera2 diff --git a/apps/app_capture_request.py b/apps/app_capture_request.py index 8a45fe2e..5b55afb0 100755 --- a/apps/app_capture_request.py +++ b/apps/app_capture_request.py @@ -5,8 +5,7 @@ # async_result field. from PyQt5 import QtCore -from PyQt5.QtWidgets import (QApplication, QHBoxLayout, QLabel, QPushButton, - QVBoxLayout, QWidget) +from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QPicamera2 diff --git a/apps/app_full.py b/apps/app_full.py index 603886b3..7b5896a5 100755 --- a/apps/app_full.py +++ b/apps/app_full.py @@ -2,10 +2,22 @@ from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QPainter, QPalette -from PyQt5.QtWidgets import (QApplication, QCheckBox, QComboBox, - QDoubleSpinBox, QFormLayout, QHBoxLayout, QLabel, - QLineEdit, QPushButton, QSlider, QSpinBox, - QTabWidget, QVBoxLayout, QWidget) +from PyQt5.QtWidgets import ( + QApplication, + QCheckBox, + QComboBox, + QDoubleSpinBox, + QFormLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QSlider, + QSpinBox, + QTabWidget, + QVBoxLayout, + QWidget, +) from picamera2 import Picamera2 from picamera2.encoders import H264Encoder, Quality @@ -14,6 +26,7 @@ try: import cv2 + cv_present = True except ImportError: cv_present = False @@ -67,9 +80,7 @@ def post_callback(request): while lores_size[0] > 1600: lores_size = (lores_size[0] // 2 & ~1, lores_size[1] // 2 & ~1) still_kwargs = {"lores": {"size": lores_size}, "display": "lores", "encode": "lores", "buffer_count": 1} -picam2.still_configuration = picam2.create_still_configuration( - **still_kwargs, -) +picam2.still_configuration = picam2.create_still_configuration(**still_kwargs) picam2.configure("still") # Read the sensor modes _ = picam2.sensor_modes @@ -193,9 +204,7 @@ def capture_done(job): # Save the normal image request = picam2.wait(job) if pic_tab.filetype.currentText() == "raw": - request.save_dng( - f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}.dng" - ) + request.save_dng(f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}.dng") else: request.save( "main", f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}.{pic_tab.filetype.currentText()}" @@ -225,10 +234,11 @@ def capture_done(job): above = max_e - e_log print("Desired exposure too long, reducing", e_log + 1, max_e, above) # list(set()) to ensure uniqueness - hdr_imgs["exposures"] = {"all": list(set(np.logspace( - e_log - below, e_log + above, pic_tab.num_hdr.value(), - base=2.0, dtype=np.integer - )))} + hdr_imgs["exposures"] = { + "all": list( + set(np.logspace(e_log - below, e_log + above, pic_tab.num_hdr.value(), base=2.0, dtype=np.integer)) + ) + } # Remove any 0 exposures if 0 in hdr_imgs["exposures"]["all"]: i = hdr_imgs["exposures"]["all"].index(0) @@ -242,7 +252,7 @@ def capture_done(job): # Save first image cv2.imwrite( f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}_base.{pic_tab.filetype.currentText()}", - new_cv_img + new_cv_img, ) else: # Find which exposure time has been captured @@ -295,8 +305,7 @@ def process_hdr(): mean_image = np.average(np.array(img_list), axis=0) mean_8bit = mean_image.astype('uint8') cv2.imwrite( - f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}_mean.{pic_tab.filetype.currentText()}", - mean_8bit + f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}_mean.{pic_tab.filetype.currentText()}", mean_8bit ) del mean_image, mean_8bit print("Mean Done") @@ -307,7 +316,7 @@ def process_hdr(): res_debevec_8bit = np.clip(res_debevec * 255, 0, 255).astype('uint8') cv2.imwrite( f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}_debevec.{pic_tab.filetype.currentText()}", - res_debevec_8bit + res_debevec_8bit, ) del merge_debevec, hdr_debevec, res_debevec, res_debevec_8bit print("Debevec Done") @@ -318,7 +327,7 @@ def process_hdr(): res_robertson_8bit = np.clip(res_robertson * 255, 0, 255).astype('uint8') cv2.imwrite( f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}_robertson.{pic_tab.filetype.currentText()}", - res_robertson_8bit + res_robertson_8bit, ) del merge_robertson, hdr_robertson, res_robertson, res_robertson_8bit print("Robertson Done") @@ -328,7 +337,7 @@ def process_hdr(): res_mertens_8bit = np.clip(res_mertens * 255, 0, 255).astype('uint8') cv2.imwrite( f"{pic_tab.filename.text() if pic_tab.filename.text() else 'test'}_mertens.{pic_tab.filetype.currentText()}", - res_mertens_8bit + res_mertens_8bit, ) del merge_mertens, res_mertens, res_mertens_8bit print("Mertens Done") @@ -383,7 +392,7 @@ def sliderToBox(self, val=None): else: center = self.points // 2 scaling = center / np.log2(self.maximum) - return round(2**((val - center) / scaling), int(-np.log10(self.precision))) + return round(2 ** ((val - center) / scaling), int(-np.log10(self.precision))) def updateValue(self): self.blockAllSignals(True) @@ -435,7 +444,7 @@ def __init__(self, box_type=float): self.setLayout(self.layout) self.slider = QSlider(Qt.Horizontal) - if box_type == float: + if box_type is float: self.box = QDoubleSpinBox() else: self.box = QSpinBox() @@ -494,15 +503,19 @@ def __init__(self): self.layout = QFormLayout() self.setLayout(self.layout) - self.label = QLabel(( - "Pan and Zoom Controls\n" - "To zoom in/out, scroll up/down in the display below\n" - "To pan, click and drag in the display below"), - alignment=Qt.AlignCenter) + self.label = QLabel( + ( + "Pan and Zoom Controls\n" + "To zoom in/out, scroll up/down in the display below\n" + "To pan, click and drag in the display below" + ), + alignment=Qt.AlignCenter, + ) self.zoom_text = QLabel("Current Zoom Level: 1.0", alignment=Qt.AlignCenter) self.pan_display = panZoomDisplay() - self.pan_display.updated.connect(lambda: self.zoom_text.setText( - f"Current Zoom Level: {self.pan_display.zoom_level:.1f}x")) + self.pan_display.updated.connect( + lambda: self.zoom_text.setText(f"Current Zoom Level: {self.pan_display.zoom_level:.1f}x") + ) self.layout.addRow(self.label) self.layout.addRow(self.zoom_text) @@ -643,10 +656,7 @@ def __init__(self): self.awb_check.setChecked(True) self.awb_check.stateChanged.connect(self.awb_update) self.awb_mode = QComboBox() - self.awb_mode.addItems([ - "Auto", "Incandescent", "Tungsten", "Fluorescent", - "Indoor", "Daylight", "Cloudy" - ]) + self.awb_mode.addItems(["Auto", "Incandescent", "Tungsten", "Fluorescent", "Indoor", "Daylight", "Cloudy"]) self.awb_mode.currentIndexChanged.connect(self.awb_update) self.colour_gain_r = QDoubleSpinBox() self.colour_gain_r.setSingleStep(0.1) @@ -692,7 +702,7 @@ def aec_dict(self): "AeExposureMode": self.aec_exposure.currentIndex(), "ExposureValue": self.exposure_val.value(), "ExposureTime": self.exposure_time.value(), - "AnalogueGain": self.analogue_gain.value() + "AnalogueGain": self.analogue_gain.value(), } if self.aec_check.isChecked(): del ret["ExposureTime"] @@ -728,7 +738,7 @@ def awb_dict(self): ret = { "AwbEnable": self.awb_check.isChecked(), "AwbMode": self.awb_mode.currentIndex(), - "ColourGains": [self.colour_gain_r.value(), self.colour_gain_b.value()] + "ColourGains": [self.colour_gain_r.value(), self.colour_gain_b.value()], } if self.awb_check.isChecked(): del ret["ColourGains"] @@ -911,7 +921,7 @@ def quality(self): "Low": Quality.LOW, "Medium": Quality.MEDIUM, "High": Quality.HIGH, - "Very High": Quality.VERY_HIGH + "Very High": Quality.VERY_HIGH, } return qualities[self.quality_box.currentText()] @@ -933,9 +943,7 @@ def frametime(self, value): @property def vid_dict(self): - return { - "FrameRate": self.framerate.value() - } + return {"FrameRate": self.framerate.value()} def vid_update(self): if self.isVisible(): @@ -949,14 +957,12 @@ def reset(self): self.resolution_h.setValue(720) self.resolution_w.setValue(1280) picam2.video_configuration = picam2.create_video_configuration( - main={"size": (self.resolution_w.value(), self.resolution_h.value())}, - raw=self.sensor_mode + main={"size": (self.resolution_w.value(), self.resolution_h.value())}, raw=self.sensor_mode ) def apply_settings(self): picam2.video_configuration = picam2.create_video_configuration( - main={"size": (self.resolution_w.value(), self.resolution_h.value())}, - raw=self.sensor_mode + main={"size": (self.resolution_w.value(), self.resolution_h.value())}, raw=self.sensor_mode ) switch_config("video") @@ -1053,9 +1059,7 @@ def preview_mode(self): @property def pic_dict(self): - return { - "FrameDurationLimits": picam2.camera_controls["FrameDurationLimits"][0:2] - } + return {"FrameDurationLimits": picam2.camera_controls["FrameDurationLimits"][0:2]} def pic_update(self): if cv_present: @@ -1074,9 +1078,7 @@ def reset(self): if cv_present: self.hdr_gamma.setValue(2.2) picam2.still_configuration = picam2.create_still_configuration( - main={"size": (self.resolution_w.value(), self.resolution_h.value())}, - **still_kwargs, - raw=self.sensor_mode, + main={"size": (self.resolution_w.value(), self.resolution_h.value())}, **still_kwargs, raw=self.sensor_mode ) def update_options(self): @@ -1114,15 +1116,13 @@ def apply_settings(self): # Set configurations picam2.still_configuration = picam2.create_still_configuration( - main={"size": (self.resolution_w.value(), self.resolution_h.value())}, - **still_kwargs, - raw=self.sensor_mode, + main={"size": (self.resolution_w.value(), self.resolution_h.value())}, **still_kwargs, raw=self.sensor_mode ) picam2.preview_configuration = picam2.create_preview_configuration( - main={"size": ( - qpicamera2.width(), int(qpicamera2.width() * (self.resolution_h.value() / self.resolution_w.value())) - )}, - raw=self.preview_mode + main={ + "size": (qpicamera2.width(), int(qpicamera2.width() * (self.resolution_h.value() / self.resolution_w.value()))) + }, + raw=self.preview_mode, ) self.preview_format.setEnabled(self.preview_check.isChecked()) @@ -1168,7 +1168,7 @@ def toggle_hidden_controls(): "AnalogueGain", "ColourGains", "ScalerCrop", - "FrameDurationLimits" + "FrameDurationLimits", ] ignore_controls = { @@ -1180,7 +1180,7 @@ def toggle_hidden_controls(): "AfWindows", "AfPause", "AfMetering", - "ScalerCrops" + "ScalerCrops", } # Main widgets diff --git a/apps/app_recording.py b/apps/app_recording.py index 6cd5bcb3..3130cecb 100755 --- a/apps/app_recording.py +++ b/apps/app_recording.py @@ -1,8 +1,7 @@ #!/usr/bin/python3 from PyQt5 import QtCore -from PyQt5.QtWidgets import (QApplication, QHBoxLayout, QLabel, QPushButton, - QVBoxLayout, QWidget) +from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.encoders import H264Encoder diff --git a/examples/capture_circular_improved.py b/examples/capture_circular_improved.py index ae859095..a7c893e9 100755 --- a/examples/capture_circular_improved.py +++ b/examples/capture_circular_improved.py @@ -31,7 +31,6 @@ ltime = 0 try: - while True: # Just get the greyscale part of the YUV420 image. cur = picam2.capture_array("lores")[:h, :w] diff --git a/examples/capture_timelapse_video.py b/examples/capture_timelapse_video.py index 329c7edd..18e995b8 100755 --- a/examples/capture_timelapse_video.py +++ b/examples/capture_timelapse_video.py @@ -51,9 +51,5 @@ def outputtimestamp(self, timestamp): picam2.stop() # Create the output mp4 video -merge = subprocess.Popen([ - "mkvmerge", "-o", "timelapse.mkv", - "--timestamps", "0:timestamps.txt", - "test.h264" -]) +merge = subprocess.Popen(["mkvmerge", "-o", "timelapse.mkv", "--timestamps", "0:timestamps.txt", "test.h264"]) merge.wait() diff --git a/examples/capture_video_multiple.py b/examples/capture_video_multiple.py index ea87dd5a..a59463d0 100755 --- a/examples/capture_video_multiple.py +++ b/examples/capture_video_multiple.py @@ -5,8 +5,10 @@ from picamera2.encoders import H264Encoder, MJPEGEncoder picam2 = Picamera2() +# fmt: off video_config = picam2.create_video_configuration(main={"size": (1280, 720), "format": "RGB888"}, lores={"size": (640, 480), "format": "YUV420"}) +# fmt: on picam2.configure(video_config) diff --git a/examples/capture_video_multiple_2.py b/examples/capture_video_multiple_2.py index df6cf4d4..476e2ba0 100755 --- a/examples/capture_video_multiple_2.py +++ b/examples/capture_video_multiple_2.py @@ -5,8 +5,9 @@ from picamera2.encoders import H264Encoder, MJPEGEncoder picam2 = Picamera2() -video_config = picam2.create_video_configuration(main={"size": (1280, 720), "format": "RGB888"}, - lores={"size": (640, 480), "format": "YUV420"}) +video_config = picam2.create_video_configuration( + main={"size": (1280, 720), "format": "RGB888"}, lores={"size": (640, 480), "format": "YUV420"} +) picam2.configure(video_config) diff --git a/examples/capture_video_raw.py b/examples/capture_video_raw.py index e8371b93..f3dbfcce 100755 --- a/examples/capture_video_raw.py +++ b/examples/capture_video_raw.py @@ -27,7 +27,7 @@ buf = open("/dev/shm/test.raw", "rb").read(stride * size[1]) arr = np.frombuffer(buf, dtype=np.uint8).reshape((size[1], stride)) -arr = arr[:, :2 * size[0]].view(np.uint16) +arr = arr[:, : 2 * size[0]].view(np.uint16) # Scale 10 bit / channel to 8 bit per channel im = Image.fromarray((arr * ((2**8 - 1) / (2**10 - 1))).astype(np.uint8)) diff --git a/examples/ffmpeg_audio_filter.py b/examples/ffmpeg_audio_filter.py index e1106915..b323f007 100755 --- a/examples/ffmpeg_audio_filter.py +++ b/examples/ffmpeg_audio_filter.py @@ -15,11 +15,7 @@ # or add audio delay on left channel like this: audio_filter="pan=stereo|adelay=1500|0" # source for more examples: https://ffmpeg.org/ffmpeg-filters.html#Examples-2 -output = FfmpegOutput( - 'ffmpeg_audio_filter_test.mp4', - audio=True, - audio_filter="pan=stereo|c0=c0|c1=c0" -) +output = FfmpegOutput('ffmpeg_audio_filter_test.mp4', audio=True, audio_filter="pan=stereo|c0=c0|c1=c0") picam2.start_recording(encoder, output) time.sleep(10) diff --git a/examples/frame_server.py b/examples/frame_server.py index a1e23d10..77a46924 100755 --- a/examples/frame_server.py +++ b/examples/frame_server.py @@ -67,6 +67,7 @@ def wait_for_frame(self, previous=None): # Below here is just demo code that uses the class: + def thread1_func(): global thread1_count while not thread_abort: diff --git a/examples/hailo/detect.py b/examples/hailo/detect.py index e26ea635..67bb0908 100755 --- a/examples/hailo/detect.py +++ b/examples/hailo/detect.py @@ -31,21 +31,24 @@ def draw_objects(request): x0, y0, x1, y1 = bbox label = f"{class_name} %{int(score * 100)}" cv2.rectangle(m.array, (x0, y0), (x1, y1), (0, 255, 0, 0), 2) - cv2.putText(m.array, label, (x0 + 5, y0 + 15), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0, 0), 1, cv2.LINE_AA) + cv2.putText(m.array, label, (x0 + 5, y0 + 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0, 0), 1, cv2.LINE_AA) if __name__ == "__main__": # Parse command-line arguments. parser = argparse.ArgumentParser(description="Detection Example") - parser.add_argument("-m", "--model", help="Path for the HEF model. " - "Defaults to /usr/share/hailo-models/yolov8s_h8l.hef for H8 devices, " - "and /usr/share/hailo-models/yolov8m_h10.hef for H10 devices.", - default=None) - parser.add_argument("-l", "--labels", default="coco.txt", - help="Path to a text file containing labels.") - parser.add_argument("-s", "--score_thresh", type=float, default=0.5, - help="Score threshold, must be a float between 0 and 1.") + parser.add_argument( + "-m", + "--model", + help="Path for the HEF model. " + "Defaults to /usr/share/hailo-models/yolov8s_h8l.hef for H8 devices, " + "and /usr/share/hailo-models/yolov8m_h10.hef for H10 devices.", + default=None, + ) + parser.add_argument("-l", "--labels", default="coco.txt", help="Path to a text file containing labels.") + parser.add_argument( + "-s", "--score_thresh", type=float, default=0.5, help="Score threshold, must be a float between 0 and 1." + ) args = parser.parse_args() if args.model is None: diff --git a/examples/hailo/pose.py b/examples/hailo/pose.py index 016e7152..c148de87 100755 --- a/examples/hailo/pose.py +++ b/examples/hailo/pose.py @@ -9,10 +9,14 @@ from picamera2.devices import Hailo, hailo_architecture parser = argparse.ArgumentParser(description='Pose estimation using Hailo') -parser.add_argument('-m', '--model', help="HEF file path" - "Defaults to /usr/share/hailo-models/yolov8s_pose_h8l_pi.hef for H8 devices, " - "and /usr/share/hailo-models/yolov8s_pose_h10.hef for H10 devices.", - default=None) +parser.add_argument( + '-m', + '--model', + help="HEF file path" + "Defaults to /usr/share/hailo-models/yolov8s_pose_h8l_pi.hef for H8 devices, " + "and /usr/share/hailo-models/yolov8s_pose_h10.hef for H10 devices.", + default=None, +) args = parser.parse_args() if args.model is None: @@ -21,14 +25,44 @@ else: args.model = '/usr/share/hailo-models/yolov8s_pose_h8l_pi.hef' -NOSE, L_EYE, R_EYE, L_EAR, R_EAR, L_SHOULDER, R_SHOULDER, L_ELBOW, R_ELBOW, \ - L_WRIST, R_WRIST, L_HIP, R_HIP, L_KNEE, R_KNEE, L_ANKLE, R_ANKLE = range(17) - -JOINT_PAIRS = [[NOSE, L_EYE], [L_EYE, L_EAR], [NOSE, R_EYE], [R_EYE, R_EAR], - [L_SHOULDER, R_SHOULDER], - [L_SHOULDER, L_ELBOW], [L_ELBOW, L_WRIST], [R_SHOULDER, R_ELBOW], [R_ELBOW, R_WRIST], - [L_SHOULDER, L_HIP], [R_SHOULDER, R_HIP], [L_HIP, R_HIP], - [L_HIP, L_KNEE], [R_HIP, R_KNEE], [L_KNEE, L_ANKLE], [R_KNEE, R_ANKLE]] +( + NOSE, + L_EYE, + R_EYE, + L_EAR, + R_EAR, + L_SHOULDER, + R_SHOULDER, + L_ELBOW, + R_ELBOW, + L_WRIST, + R_WRIST, + L_HIP, + R_HIP, + L_KNEE, + R_KNEE, + L_ANKLE, + R_ANKLE, +) = range(17) + +JOINT_PAIRS = [ + [NOSE, L_EYE], + [L_EYE, L_EAR], + [NOSE, R_EYE], + [R_EYE, R_EAR], + [L_SHOULDER, R_SHOULDER], + [L_SHOULDER, L_ELBOW], + [L_ELBOW, L_WRIST], + [R_SHOULDER, R_ELBOW], + [R_ELBOW, R_WRIST], + [L_SHOULDER, L_HIP], + [R_SHOULDER, R_HIP], + [L_HIP, R_HIP], + [L_HIP, L_KNEE], + [R_HIP, R_KNEE], + [L_KNEE, L_ANKLE], + [R_KNEE, R_ANKLE], +] def visualize_pose_estimation_result(results, image, model_size, detection_threshold=0.5, joint_threshold=0.5): @@ -38,11 +72,16 @@ def scale_coord(coord): return tuple([int(c * t / f) for c, f, t in zip(coord, model_size, image_size)]) bboxes, scores, keypoints, joint_scores = ( - results['bboxes'], results['scores'], results['keypoints'], results['joint_scores']) + results['bboxes'], + results['scores'], + results['keypoints'], + results['joint_scores'], + ) box, score, keypoint, keypoint_score = bboxes[0], scores[0], keypoints[0], joint_scores[0] - for detection_box, detection_score, detection_keypoints, detection_keypoints_score in ( - zip(box, score, keypoint, keypoint_score)): + for detection_box, detection_score, detection_keypoints, detection_keypoints_score in zip( + box, score, keypoint, keypoint_score + ): if detection_score < detection_threshold: continue @@ -60,8 +99,9 @@ def scale_coord(coord): for joint0, joint1 in JOINT_PAIRS: if joint_visible[joint0] and joint_visible[joint1]: - cv2.line(image, scale_coord(detection_keypoints[joint0]), - scale_coord(detection_keypoints[joint1]), (255, 0, 255), 3) + cv2.line( + image, scale_coord(detection_keypoints[joint0]), scale_coord(detection_keypoints[joint1]), (255, 0, 255), 3 + ) def draw_predictions(request): diff --git a/examples/hailo/pose_utils.py b/examples/hailo/pose_utils.py index 39b52f98..43090ed2 100644 --- a/examples/hailo/pose_utils.py +++ b/examples/hailo/pose_utils.py @@ -7,7 +7,7 @@ 'score_threshold': 0.001, 'nms_iou_thresh': 0.7, 'meta_arch': 'nanodet_v8', - 'device_pre_post_layers': None + 'device_pre_post_layers': None, } @@ -32,15 +32,17 @@ def postproc_yolov8_pose(num_of_classes, raw_detections, img_size): keypoints = 51 # The following assumes that the batch size is 1: - endnodes = [raw_detections[layer_from_shape[1, 20, 20, detection_output_channels]], - raw_detections[layer_from_shape[1, 20, 20, num_of_classes]], - raw_detections[layer_from_shape[1, 20, 20, keypoints]], - raw_detections[layer_from_shape[1, 40, 40, detection_output_channels]], - raw_detections[layer_from_shape[1, 40, 40, num_of_classes]], - raw_detections[layer_from_shape[1, 40, 40, keypoints]], - raw_detections[layer_from_shape[1, 80, 80, detection_output_channels]], - raw_detections[layer_from_shape[1, 80, 80, num_of_classes]], - raw_detections[layer_from_shape[1, 80, 80, keypoints]]] + endnodes = [ + raw_detections[layer_from_shape[1, 20, 20, detection_output_channels]], + raw_detections[layer_from_shape[1, 20, 20, num_of_classes]], + raw_detections[layer_from_shape[1, 20, 20, keypoints]], + raw_detections[layer_from_shape[1, 40, 40, detection_output_channels]], + raw_detections[layer_from_shape[1, 40, 40, num_of_classes]], + raw_detections[layer_from_shape[1, 40, 40, keypoints]], + raw_detections[layer_from_shape[1, 80, 80, detection_output_channels]], + raw_detections[layer_from_shape[1, 80, 80, num_of_classes]], + raw_detections[layer_from_shape[1, 80, 80, keypoints]], + ] predictions_dict = yolov8_pose_estimation_postprocess(endnodes, **kwargs) @@ -49,6 +51,7 @@ def postproc_yolov8_pose(num_of_classes, raw_detections, img_size): # ---------------- Architecture functions ----------------- # + def _sigmoid(x): return 1 / (1 + np.exp(-x)) @@ -121,8 +124,7 @@ def _yolov8_decoding(raw_boxes, raw_kpts, strides, image_dims, reg_max): # box distribution to distance reg_range = np.arange(reg_max + 1) - box_distribute = np.reshape( - box_distribute, (-1, box_distribute.shape[1] * box_distribute.shape[2], 4, reg_max + 1)) + box_distribute = np.reshape(box_distribute, (-1, box_distribute.shape[1] * box_distribute.shape[2], 4, reg_max + 1)) box_distance = _softmax(box_distribute) box_distance = box_distance * np.reshape(reg_range, (1, 1, 1, -1)) box_distance = np.sum(box_distance, axis=-1) @@ -159,8 +161,7 @@ def xywh2xyxy(x): return y -def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.45, - max_det=100, n_kpts=17): +def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.45, max_det=100, n_kpts=17): """Non-Maximum Suppression (NMS) on inference results to reject overlapping detections. Args: @@ -180,10 +181,8 @@ def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.45, 'num_detections': int } """ - assert 0 <= conf_thres <= 1, \ - f'Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0' - assert 0 <= iou_thres <= 1, \ - f'Invalid IoU threshold {iou_thres}, valid values are between 0.0 and 1.0' + assert 0 <= conf_thres <= 1, f'Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0' + assert 0 <= iou_thres <= 1, f'Invalid IoU threshold {iou_thres}, valid values are between 0.0 and 1.0' nc = prediction.shape[2] - n_kpts * 3 - 4 # number of classes xc = prediction[..., 4] > conf_thres # candidates @@ -195,10 +194,14 @@ def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.45, x = x[xc[xi]] # If none remain process next image if not x.shape[0]: - output.append({'bboxes': np.zeros((0, 4)), - 'keypoints': np.zeros((0, n_kpts, 3)), - 'scores': np.zeros((0)), - 'num_detections': 0}) + output.append( + { + 'bboxes': np.zeros((0, 4)), + 'keypoints': np.zeros((0, n_kpts, 3)), + 'scores': np.zeros((0)), + 'num_detections': 0, + } + ) continue # (center_x, center_y, width, height) to (x1, y1, x2, y2) @@ -228,10 +231,7 @@ def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.45, kpts = out[:, 6:] kpts = np.reshape(kpts, (-1, n_kpts, 3)) - out = {'bboxes': boxes, - 'keypoints': kpts, - 'scores': scores, - 'num_detections': int(scores.shape[0])} + out = {'bboxes': boxes, 'keypoints': kpts, 'scores': scores, 'num_detections': int(scores.shape[0])} output.append(out) return output @@ -283,8 +283,8 @@ def yolov8_pose_estimation_postprocess(endnodes, **kwargs): output['joint_scores'] = np.zeros((batch_size, max_detections, 17, 1)) output['scores'] = np.zeros((batch_size, max_detections, 1)) for b in range(batch_size): - output['bboxes'][b, :nms_res[b]['num_detections']] = nms_res[b]['bboxes'] - output['keypoints'][b, :nms_res[b]['num_detections']] = nms_res[b]['keypoints'][..., :2] - output['joint_scores'][b, :nms_res[b]['num_detections'], ..., 0] = _sigmoid(nms_res[b]['keypoints'][..., 2]) - output['scores'][b, :nms_res[b]['num_detections'], ..., 0] = nms_res[b]['scores'] + output['bboxes'][b, : nms_res[b]['num_detections']] = nms_res[b]['bboxes'] + output['keypoints'][b, : nms_res[b]['num_detections']] = nms_res[b]['keypoints'][..., :2] + output['joint_scores'][b, : nms_res[b]['num_detections'], ..., 0] = _sigmoid(nms_res[b]['keypoints'][..., 2]) + output['scores'][b, : nms_res[b]['num_detections'], ..., 0] = nms_res[b]['scores'] return output diff --git a/examples/imx500/imx500_classification_demo.py b/examples/imx500/imx500_classification_demo.py index 1742471a..9ef7ef0d 100755 --- a/examples/imx500/imx500_classification_demo.py +++ b/examples/imx500/imx500_classification_demo.py @@ -81,33 +81,37 @@ def draw_classification_results(request: CompletedRequest, results: List[Classif overlay = m.array.copy() # Draw the background rectangle on the overlay - cv2.rectangle(overlay, - (text_x, text_y - text_height), - (text_x + text_width, text_y + baseline), - (255, 255, 255), # Background color (white) - cv2.FILLED) + cv2.rectangle( + overlay, + (text_x, text_y - text_height), + (text_x + text_width, text_y + baseline), + (255, 255, 255), # Background color (white) + cv2.FILLED, + ) alpha = 0.3 cv2.addWeighted(overlay, alpha, m.array, 1 - alpha, 0, m.array) # Draw text on top of the background - cv2.putText(m.array, text, (text_x, text_y), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) + cv2.putText(m.array, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) def get_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser() - parser.add_argument("--model", type=str, help="Path of the model", - default="/usr/share/imx500-models/imx500_network_mobilenet_v2.rpk") + parser.add_argument( + "--model", type=str, help="Path of the model", default="/usr/share/imx500-models/imx500_network_mobilenet_v2.rpk" + ) parser.add_argument("--fps", type=int, help="Frames per second") parser.add_argument("-s", "--softmax", action=argparse.BooleanOptionalAction, help="Add post-process softmax") - parser.add_argument("-r", "--preserve-aspect-ratio", action=argparse.BooleanOptionalAction, - help="preprocess the image with preserve aspect ratio") - parser.add_argument("--labels", type=str, - help="Path to the labels file") - parser.add_argument("--print-intrinsics", action="store_true", - help="Print JSON network_intrinsics then exit") + parser.add_argument( + "-r", + "--preserve-aspect-ratio", + action=argparse.BooleanOptionalAction, + help="preprocess the image with preserve aspect ratio", + ) + parser.add_argument("--labels", type=str, help="Path to the labels file") + parser.add_argument("--print-intrinsics", action="store_true", help="Print JSON network_intrinsics then exit") return parser.parse_args() diff --git a/examples/imx500/imx500_object_detection_demo.py b/examples/imx500/imx500_object_detection_demo.py index 6377abaa..d6253682 100755 --- a/examples/imx500/imx500_object_detection_demo.py +++ b/examples/imx500/imx500_object_detection_demo.py @@ -6,8 +6,7 @@ from picamera2 import MappedArray, Picamera2 from picamera2.devices import IMX500 -from picamera2.devices.imx500 import (NetworkIntrinsics, - postprocess_nanodet_detection) +from picamera2.devices.imx500 import NetworkIntrinsics, postprocess_nanodet_detection last_detections = [] @@ -34,10 +33,11 @@ def parse_detections(metadata: dict): if np_outputs is None: return last_detections if intrinsics.postprocess == "nanodet": - boxes, scores, classes = \ - postprocess_nanodet_detection(outputs=np_outputs[0], conf=threshold, iou_thres=iou, - max_out_dets=max_detections)[0] + boxes, scores, classes = postprocess_nanodet_detection( + outputs=np_outputs[0], conf=threshold, iou_thres=iou, max_out_dets=max_detections + )[0] from picamera2.devices.imx500.postprocess import scale_boxes + boxes = scale_boxes(boxes, 1, 1, input_h, input_w, False, False) else: boxes, scores, classes = np_outputs[0][0], np_outputs[1][0], np_outputs[2][0] @@ -48,9 +48,7 @@ def parse_detections(metadata: dict): boxes = boxes[:, [1, 0, 3, 2]] last_detections = [ - Detection(box, category, score, metadata) - for box, score, category in zip(boxes, scores, classes) - if score > threshold + Detection(box, category, score, metadata) for box, score, category in zip(boxes, scores, classes) if score > threshold ] return last_detections @@ -84,18 +82,19 @@ def draw_detections(request, stream="main"): overlay = m.array.copy() # Draw the background rectangle on the overlay - cv2.rectangle(overlay, - (text_x, text_y - text_height), - (text_x + text_width, text_y + baseline), - (255, 255, 255), # Background color (white) - cv2.FILLED) + cv2.rectangle( + overlay, + (text_x, text_y - text_height), + (text_x + text_width, text_y + baseline), + (255, 255, 255), # Background color (white) + cv2.FILLED, + ) alpha = 0.30 cv2.addWeighted(overlay, alpha, m.array, 1 - alpha, 0, m.array) # Draw text on top of the background - cv2.putText(m.array, label, (text_x, text_y), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) + cv2.putText(m.array, label, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) # Draw detection box cv2.rectangle(m.array, (x, y), (x + w, y + h), (0, 255, 0, 0), thickness=2) @@ -109,24 +108,30 @@ def draw_detections(request, stream="main"): def get_args(): parser = argparse.ArgumentParser() - parser.add_argument("--model", type=str, help="Path of the model", - default="/usr/share/imx500-models/imx500_network_ssd_mobilenetv2_fpnlite_320x320_pp.rpk") + parser.add_argument( + "--model", + type=str, + help="Path of the model", + default="/usr/share/imx500-models/imx500_network_ssd_mobilenetv2_fpnlite_320x320_pp.rpk", + ) parser.add_argument("--fps", type=int, help="Frames per second") parser.add_argument("--bbox-normalization", action=argparse.BooleanOptionalAction, help="Normalize bbox") - parser.add_argument("--bbox-order", choices=["yx", "xy"], default="yx", - help="Set bbox order yx -> (y0, x0, y1, x1) xy -> (x0, y0, x1, y1)") + parser.add_argument( + "--bbox-order", choices=["yx", "xy"], default="yx", help="Set bbox order yx -> (y0, x0, y1, x1) xy -> (x0, y0, x1, y1)" + ) parser.add_argument("--threshold", type=float, default=0.55, help="Detection threshold") parser.add_argument("--iou", type=float, default=0.65, help="Set iou threshold") parser.add_argument("--max-detections", type=int, default=10, help="Set max detections") parser.add_argument("--ignore-dash-labels", action=argparse.BooleanOptionalAction, help="Remove '-' labels ") - parser.add_argument("--postprocess", choices=["", "nanodet"], - default=None, help="Run post process of type") - parser.add_argument("-r", "--preserve-aspect-ratio", action=argparse.BooleanOptionalAction, - help="preserve the pixel aspect ratio of the input tensor") - parser.add_argument("--labels", type=str, - help="Path to the labels file") - parser.add_argument("--print-intrinsics", action="store_true", - help="Print JSON network_intrinsics then exit") + parser.add_argument("--postprocess", choices=["", "nanodet"], default=None, help="Run post process of type") + parser.add_argument( + "-r", + "--preserve-aspect-ratio", + action=argparse.BooleanOptionalAction, + help="preserve the pixel aspect ratio of the input tensor", + ) + parser.add_argument("--labels", type=str, help="Path to the labels file") + parser.add_argument("--print-intrinsics", action="store_true", help="Print JSON network_intrinsics then exit") return parser.parse_args() diff --git a/examples/imx500/imx500_object_detection_demo_mp.py b/examples/imx500/imx500_object_detection_demo_mp.py index cc9871a6..cc60fe33 100755 --- a/examples/imx500/imx500_object_detection_demo_mp.py +++ b/examples/imx500/imx500_object_detection_demo_mp.py @@ -9,8 +9,7 @@ from picamera2 import MappedArray, Picamera2 from picamera2.devices import IMX500 -from picamera2.devices.imx500 import (NetworkIntrinsics, - postprocess_nanodet_detection) +from picamera2.devices.imx500 import NetworkIntrinsics, postprocess_nanodet_detection class Detection: @@ -33,10 +32,11 @@ def parse_detections(metadata: dict): if np_outputs is None: return None if intrinsics.postprocess == "nanodet": - boxes, scores, classes = \ - postprocess_nanodet_detection(outputs=np_outputs[0], conf=threshold, iou_thres=iou, - max_out_dets=max_detections)[0] + boxes, scores, classes = postprocess_nanodet_detection( + outputs=np_outputs[0], conf=threshold, iou_thres=iou, max_out_dets=max_detections + )[0] from picamera2.devices.imx500.postprocess import scale_boxes + boxes = scale_boxes(boxes, 1, 1, input_h, input_w, False, False) else: boxes, scores, classes = np_outputs[0][0], np_outputs[1][0], np_outputs[2][0] @@ -44,9 +44,7 @@ def parse_detections(metadata: dict): boxes = boxes / input_h detections = [ - Detection(box, category, score, metadata) - for box, score, category in zip(boxes, scores, classes) - if score > threshold + Detection(box, category, score, metadata) for box, score, category in zip(boxes, scores, classes) if score > threshold ] return detections @@ -85,18 +83,19 @@ def draw_detections(jobs): overlay = m.array.copy() # Draw the background rectangle on the overlay - cv2.rectangle(overlay, - (text_x, text_y - text_height), - (text_x + text_width, text_y + baseline), - (255, 255, 255), # Background color (white) - cv2.FILLED) + cv2.rectangle( + overlay, + (text_x, text_y - text_height), + (text_x + text_width, text_y + baseline), + (255, 255, 255), # Background color (white) + cv2.FILLED, + ) alpha = 0.3 cv2.addWeighted(overlay, alpha, m.array, 1 - alpha, 0, m.array) # Draw text on top of the background - cv2.putText(m.array, label, (text_x, text_y), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) + cv2.putText(m.array, label, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) # Draw detection box cv2.rectangle(m.array, (x, y), (x + w, y + h), (0, 255, 0), thickness=2) @@ -114,22 +113,27 @@ def draw_detections(jobs): def get_args(): parser = argparse.ArgumentParser() - parser.add_argument("--model", type=str, help="Path of the model", - default="/usr/share/imx500-models/imx500_network_ssd_mobilenetv2_fpnlite_320x320_pp.rpk") + parser.add_argument( + "--model", + type=str, + help="Path of the model", + default="/usr/share/imx500-models/imx500_network_ssd_mobilenetv2_fpnlite_320x320_pp.rpk", + ) parser.add_argument("--fps", type=int, help="Frames per second") parser.add_argument("--bbox-normalization", action=argparse.BooleanOptionalAction, help="Normalize bbox") parser.add_argument("--threshold", type=float, default=0.55, help="Detection threshold") parser.add_argument("--iou", type=float, default=0.65, help="Set iou threshold") parser.add_argument("--max-detections", type=int, default=10, help="Set max detections") parser.add_argument("--ignore-dash-labels", action=argparse.BooleanOptionalAction, help="Remove '-' labels ") - parser.add_argument("--postprocess", choices=["", "nanodet"], - default=None, help="Run post process of type") - parser.add_argument("-r", "--preserve-aspect-ratio", action=argparse.BooleanOptionalAction, - help="preserve the pixel aspect ratio of the input tensor") - parser.add_argument("--labels", type=str, - help="Path to the labels file") - parser.add_argument("--print-intrinsics", action="store_true", - help="Print JSON network_intrinsics then exit") + parser.add_argument("--postprocess", choices=["", "nanodet"], default=None, help="Run post process of type") + parser.add_argument( + "-r", + "--preserve-aspect-ratio", + action=argparse.BooleanOptionalAction, + help="preserve the pixel aspect ratio of the input tensor", + ) + parser.add_argument("--labels", type=str, help="Path to the labels file") + parser.add_argument("--print-intrinsics", action="store_true", help="Print JSON network_intrinsics then exit") return parser.parse_args() diff --git a/examples/imx500/imx500_object_detection_injection_demo.py b/examples/imx500/imx500_object_detection_injection_demo.py index 05d21c8b..e4147d24 100755 --- a/examples/imx500/imx500_object_detection_injection_demo.py +++ b/examples/imx500/imx500_object_detection_injection_demo.py @@ -16,8 +16,7 @@ from picamera2 import Picamera2 from picamera2.devices import IMX500 -from picamera2.devices.imx500 import (NetworkIntrinsics, - postprocess_nanodet_detection) +from picamera2.devices.imx500 import NetworkIntrinsics, postprocess_nanodet_detection tensor_files = [] current_tensor_index = 0 @@ -40,11 +39,7 @@ def process_and_display_tensor(input_tensor, frame_count, metadata): scale_factor = window.scale_factor # Scale up the image using OpenCV for better quality - scaled_image = cv2.resize( - tensor_image, - (w * scale_factor, h * scale_factor), - interpolation=cv2.INTER_LINEAR, - ) + scaled_image = cv2.resize(tensor_image, (w * scale_factor, h * scale_factor), interpolation=cv2.INTER_LINEAR) # Draw detections on the scaled image at the proper scale draw_detections(scaled_image, detections, scale_factor=scale_factor) @@ -99,9 +94,7 @@ def load_specific_tensor(target_index): return False exr_path = tensor_files[target_index] - print( - f"Loading tensor {target_index + 1}/{len(tensor_files)}: {os.path.basename(exr_path)}" - ) + print(f"Loading tensor {target_index + 1}/{len(tensor_files)}: {os.path.basename(exr_path)}") try: injection_cmp_frm = convert_and_inject_tensor(exr_path) @@ -195,9 +188,7 @@ def __init__(self, scale_factor=3): self.label = QLabel() self.setCentralWidget(self.label) self.scale_factor = scale_factor - self.resize( - 320 * scale_factor, 320 * scale_factor - ) # Default size, will be updated based on tensor size + self.resize(320 * scale_factor, 320 * scale_factor) # Default size, will be updated based on tensor size # Set up timer for tensor cycling with configurable interval self.tensor_timer = QTimer() @@ -219,9 +210,7 @@ def update_display_immediate(self, rgb_image): bytes_per_line = ch * scaled_w # Create QImage (only remaining operation) - qt_image = QImage( - rgb_image.data, scaled_w, scaled_h, bytes_per_line, QImage.Format_RGB888 - ) + qt_image = QImage(rgb_image.data, scaled_w, scaled_h, bytes_per_line, QImage.Format_RGB888) # Convert to QPixmap pixmap = QPixmap.fromImage(qt_image) @@ -258,12 +247,7 @@ def __init__(self, coords, category, conf, metadata): scale_x = input_w / isp_w scale_y = input_h / isp_h - self.box = ( - int(x * scale_x), - int(y * scale_y), - int(w * scale_x), - int(h * scale_y), - ) + self.box = (int(x * scale_x), int(y * scale_y), int(w * scale_x), int(h * scale_y)) def parse_detections(metadata: dict): @@ -280,10 +264,7 @@ def parse_detections(metadata: dict): return [] if intrinsics.postprocess == "nanodet": boxes, scores, classes = postprocess_nanodet_detection( - outputs=np_outputs[0], - conf=threshold, - iou_thres=iou, - max_out_dets=max_detections, + outputs=np_outputs[0], conf=threshold, iou_thres=iou, max_out_dets=max_detections )[0] from picamera2.devices.imx500.postprocess import scale_boxes @@ -334,9 +315,7 @@ def draw_detections(image, detections, scale_factor=1): # Calculate text size and position (scale font size with scale_factor) font_scale = 0.5 * scale_factor thickness = max(1, int(scale_factor)) - (text_width, text_height), baseline = cv2.getTextSize( - label, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness - ) + (text_width, text_height), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness) text_x = x + int(5 * scale_factor) text_y = y + int(15 * scale_factor) @@ -356,24 +335,10 @@ def draw_detections(image, detections, scale_factor=1): cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0, image) # Draw text on top of the background - cv2.putText( - image, - label, - (text_x, text_y), - cv2.FONT_HERSHEY_SIMPLEX, - font_scale, - (0, 0, 255), - thickness, - ) + cv2.putText(image, label, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 0, 255), thickness) # Draw detection box - cv2.rectangle( - image, - (x, y), - (x + w, y + h), - (0, 255, 0, 0), - thickness=max(2, int(2 * scale_factor)), - ) + cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0, 0), thickness=max(2, int(2 * scale_factor))) def get_args(): @@ -385,35 +350,15 @@ def get_args(): default="/usr/share/imx500-models/imx500_network_ssd_mobilenetv2_fpnlite_320x320_pp.rpk", ) parser.add_argument("--fps", type=int, help="Frames per second") + parser.add_argument("--bbox-normalization", action=argparse.BooleanOptionalAction, help="Normalize bbox") parser.add_argument( - "--bbox-normalization", - action=argparse.BooleanOptionalAction, - help="Normalize bbox", - ) - parser.add_argument( - "--bbox-order", - choices=["yx", "xy"], - default="yx", - help="Set bbox order yx -> (y0, x0, y1, x1) xy -> (x0, y0, x1, y1)", - ) - parser.add_argument( - "--threshold", type=float, default=0.55, help="Detection threshold" + "--bbox-order", choices=["yx", "xy"], default="yx", help="Set bbox order yx -> (y0, x0, y1, x1) xy -> (x0, y0, x1, y1)" ) + parser.add_argument("--threshold", type=float, default=0.55, help="Detection threshold") parser.add_argument("--iou", type=float, default=0.65, help="Set iou threshold") - parser.add_argument( - "--max-detections", type=int, default=10, help="Set max detections" - ) - parser.add_argument( - "--ignore-dash-labels", - action=argparse.BooleanOptionalAction, - help="Remove '-' labels ", - ) - parser.add_argument( - "--postprocess", - choices=["", "nanodet"], - default=None, - help="Run post process of type", - ) + parser.add_argument("--max-detections", type=int, default=10, help="Set max detections") + parser.add_argument("--ignore-dash-labels", action=argparse.BooleanOptionalAction, help="Remove '-' labels ") + parser.add_argument("--postprocess", choices=["", "nanodet"], default=None, help="Run post process of type") parser.add_argument( "-r", "--preserve-aspect-ratio", @@ -421,17 +366,8 @@ def get_args(): help="preserve the pixel aspect ratio of the input tensor", ) parser.add_argument("--labels", type=str, help="Path to the labels file") - parser.add_argument( - "--print-intrinsics", - action="store_true", - help="Print JSON network_intrinsics then exit", - ) - parser.add_argument( - "--tensor-dir", - type=str, - required=True, - help="Directory containing EXR tensor files to step through", - ) + parser.add_argument("--print-intrinsics", action="store_true", help="Print JSON network_intrinsics then exit") + parser.add_argument("--tensor-dir", type=str, required=True, help="Directory containing EXR tensor files to step through") return parser.parse_args() @@ -467,9 +403,7 @@ def get_args(): exit() picam2 = Picamera2(imx500.camera_num) - config = picam2.create_preview_configuration( - controls={"FrameRate": 12, "CnnEnableInputTensor": True}, buffer_count=30 - ) + config = picam2.create_preview_configuration(controls={"FrameRate": 12, "CnnEnableInputTensor": True}, buffer_count=30) imx500.show_network_fw_progress_bar() picam2.start(config, show_preview=False) diff --git a/examples/imx500/imx500_pose_estimation_higherhrnet_demo.py b/examples/imx500/imx500_pose_estimation_higherhrnet_demo.py index 2b754e46..45d59426 100755 --- a/examples/imx500/imx500_pose_estimation_higherhrnet_demo.py +++ b/examples/imx500/imx500_pose_estimation_higherhrnet_demo.py @@ -7,8 +7,7 @@ from picamera2 import CompletedRequest, MappedArray, Picamera2 from picamera2.devices.imx500 import IMX500, NetworkIntrinsics from picamera2.devices.imx500.postprocess import COCODrawer -from picamera2.devices.imx500.postprocess_highernet import \ - postprocess_higherhrnet +from picamera2.devices.imx500.postprocess_highernet import postprocess_higherhrnet last_boxes = None last_scores = None @@ -21,12 +20,14 @@ def ai_output_tensor_parse(metadata: dict): global last_boxes, last_scores, last_keypoints np_outputs = imx500.get_outputs(metadata=metadata, add_batch=True) if np_outputs is not None: - keypoints, scores, boxes = postprocess_higherhrnet(outputs=np_outputs, - img_size=WINDOW_SIZE_H_W, - img_w_pad=(0, 0), - img_h_pad=(0, 0), - detection_threshold=args.detection_threshold, - network_postprocess=True) + keypoints, scores, boxes = postprocess_higherhrnet( + outputs=np_outputs, + img_size=WINDOW_SIZE_H_W, + img_w_pad=(0, 0), + img_h_pad=(0, 0), + detection_threshold=args.detection_threshold, + network_postprocess=True, + ) if scores is not None and len(scores) > 0: last_keypoints = np.reshape(np.stack(keypoints, axis=0), (len(scores), 17, 3)) @@ -39,9 +40,18 @@ def ai_output_tensor_draw(request: CompletedRequest, boxes, scores, keypoints, s """Draw the detections for this request onto the ISP output.""" with MappedArray(request, stream) as m: if boxes is not None and len(boxes) > 0: - drawer.annotate_image(m.array, boxes, scores, - np.zeros(scores.shape), keypoints, args.detection_threshold, - args.detection_threshold, request.get_metadata(), picam2, stream) + drawer.annotate_image( + m.array, + boxes, + scores, + np.zeros(scores.shape), + keypoints, + args.detection_threshold, + args.detection_threshold, + request.get_metadata(), + picam2, + stream, + ) def picamera2_pre_callback(request: CompletedRequest): @@ -52,15 +62,13 @@ def picamera2_pre_callback(request: CompletedRequest): def get_args(): parser = argparse.ArgumentParser() - parser.add_argument("--model", type=str, help="Path of the model", - default="/usr/share/imx500-models/imx500_network_higherhrnet_coco.rpk") + parser.add_argument( + "--model", type=str, help="Path of the model", default="/usr/share/imx500-models/imx500_network_higherhrnet_coco.rpk" + ) parser.add_argument("--fps", type=int, help="Frames per second") - parser.add_argument("--detection-threshold", type=float, default=0.3, - help="Post-process detection threshold") - parser.add_argument("--labels", type=str, - help="Path to the labels file") - parser.add_argument("--print-intrinsics", action="store_true", - help="Print JSON network_intrinsics then exit") + parser.add_argument("--detection-threshold", type=float, default=0.3, help="Post-process detection threshold") + parser.add_argument("--labels", type=str, help="Path to the labels file") + parser.add_argument("--print-intrinsics", action="store_true", help="Print JSON network_intrinsics then exit") return parser.parse_args() diff --git a/examples/imx500/imx500_segmentation_demo.py b/examples/imx500/imx500_segmentation_demo.py index 64263508..e7a9058b 100755 --- a/examples/imx500/imx500_segmentation_demo.py +++ b/examples/imx500/imx500_segmentation_demo.py @@ -58,11 +58,11 @@ def draw_masks(masks: Dict[int, np.ndarray]): def get_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser() - parser.add_argument("--model", type=str, help="Path of the model", - default="/usr/share/imx500-models/imx500_network_deeplabv3plus.rpk") + parser.add_argument( + "--model", type=str, help="Path of the model", default="/usr/share/imx500-models/imx500_network_deeplabv3plus.rpk" + ) parser.add_argument("--fps", type=int, help="Frames per second") - parser.add_argument("--print-intrinsics", action="store_true", - help="Print JSON network_intrinsics then exit") + parser.add_argument("--print-intrinsics", action="store_true", help="Print JSON network_intrinsics then exit") return parser.parse_args() diff --git a/examples/mjpeg_server.py b/examples/mjpeg_server.py index 68bc280e..9fa859cc 100755 --- a/examples/mjpeg_server.py +++ b/examples/mjpeg_server.py @@ -73,9 +73,7 @@ def do_GET(self): self.wfile.write(frame) self.wfile.write(b'\r\n') except Exception as e: - logging.warning( - 'Removed streaming client %s: %s', - self.client_address, str(e)) + logging.warning('Removed streaming client %s: %s', self.client_address, str(e)) else: self.send_error(404) self.end_headers() diff --git a/examples/mjpeg_server_2.py b/examples/mjpeg_server_2.py index 00080b15..72af5df9 100755 --- a/examples/mjpeg_server_2.py +++ b/examples/mjpeg_server_2.py @@ -68,9 +68,7 @@ def do_GET(self): self.wfile.write(frame) self.wfile.write(b'\r\n') except Exception as e: - logging.warning( - 'Removed streaming client %s: %s', - self.client_address, str(e)) + logging.warning('Removed streaming client %s: %s', self.client_address, str(e)) else: self.send_error(404) self.end_headers() diff --git a/examples/mjpeg_server_with_rotation.py b/examples/mjpeg_server_with_rotation.py index d7618797..7c5f0866 100755 --- a/examples/mjpeg_server_with_rotation.py +++ b/examples/mjpeg_server_with_rotation.py @@ -83,9 +83,7 @@ def do_GET(self): self.wfile.write(frame) self.wfile.write(b'\r\n') except Exception as e: - logging.warning( - 'Removed streaming client %s: %s', - self.client_address, str(e)) + logging.warning('Removed streaming client %s: %s', self.client_address, str(e)) else: self.send_error(404) self.end_headers() diff --git a/examples/opencv_face_detect.py b/examples/opencv_face_detect.py index a609baf3..ea1d7a9f 100755 --- a/examples/opencv_face_detect.py +++ b/examples/opencv_face_detect.py @@ -22,7 +22,7 @@ faces = face_detector.detectMultiScale(grey, 1.1, 5) with MappedArray(request, 'main') as m: - for (x, y, w, h) in faces: + for x, y, w, h in faces: cv2.rectangle(m.array, (x, y), (x + w, y + h), (0, 255, 0)) cv2.imshow("Camera", m.array) diff --git a/examples/opencv_face_detect_2.py b/examples/opencv_face_detect_2.py index 6ae6879f..efc10f4e 100755 --- a/examples/opencv_face_detect_2.py +++ b/examples/opencv_face_detect_2.py @@ -22,8 +22,7 @@ def draw_faces(request): picam2 = Picamera2() picam2.start_preview(Preview.QTGL) -config = picam2.create_preview_configuration(main={"size": (640, 480)}, - lores={"size": (320, 240), "format": "YUV420"}) +config = picam2.create_preview_configuration(main={"size": (640, 480)}, lores={"size": (320, 240), "format": "YUV420"}) picam2.configure(config) (w0, h0) = picam2.stream_configuration("main")["size"] diff --git a/examples/opencv_face_detect_3.py b/examples/opencv_face_detect_3.py index b027bd80..d1695a7f 100755 --- a/examples/opencv_face_detect_3.py +++ b/examples/opencv_face_detect_3.py @@ -23,8 +23,7 @@ def draw_faces(request): picam2 = Picamera2() picam2.start_preview(Preview.QTGL) -config = picam2.create_preview_configuration(main={"size": (640, 480)}, - lores={"size": (320, 240), "format": "YUV420"}) +config = picam2.create_preview_configuration(main={"size": (640, 480)}, lores={"size": (320, 240), "format": "YUV420"}) picam2.configure(config) (w0, h0) = picam2.stream_configuration("main")["size"] diff --git a/examples/opencv_mertens_merge.py b/examples/opencv_mertens_merge.py index 1861d5df..4e3784ef 100755 --- a/examples/opencv_mertens_merge.py +++ b/examples/opencv_mertens_merge.py @@ -21,9 +21,7 @@ gain = metadata["AnalogueGain"] * metadata["DigitalGain"] picam2.stop() controls = {"ExposureTime": exposure_normal, "AnalogueGain": gain} -capture_config = picam2.create_preview_configuration(main={"size": (1024, 768), - "format": "RGB888"}, - controls=controls) +capture_config = picam2.create_preview_configuration(main={"size": (1024, 768), "format": "RGB888"}, controls=controls) picam2.configure(capture_config) picam2.start() normal = picam2.capture_array() diff --git a/examples/picamera2_multiprocessing.py b/examples/picamera2_multiprocessing.py index 9b00fe36..6057094f 100755 --- a/examples/picamera2_multiprocessing.py +++ b/examples/picamera2_multiprocessing.py @@ -82,9 +82,9 @@ def _format_array(self, mem): return array.reshape((height + height // 2, stride)) array = array.reshape((height, stride)) if format in ('RGB888', 'BGR888'): - return array[:, :width * 3].reshape((height, width, 3)) + return array[:, : width * 3].reshape((height, width, 3)) elif format in ("XBGR8888", "XRGB8888"): - return array[:, :width * 4].reshape((height, width, 4)) + return array[:, : width * 4].reshape((height, width, 4)) return array def _map_fd(self, picam2_fd): @@ -151,6 +151,7 @@ def close(self): # own derived Process instances. Maybe I've missed something. Anyhow, here follows a # simple-minded implementation thereof. + class Pool: """A pool of Picamera2 child processes to which tasks can be sent.""" diff --git a/examples/pick_mode.py b/examples/pick_mode.py index 2a1594eb..54c4ce16 100755 --- a/examples/pick_mode.py +++ b/examples/pick_mode.py @@ -13,7 +13,7 @@ available_modes = picam2.sensor_modes min_bit_depth = 10 -available_modes = list(filter(lambda x: (x["bit_depth"] >= min_bit_depth), available_modes)) +available_modes = list(filter(lambda x: x["bit_depth"] >= min_bit_depth, available_modes)) available_modes.sort(key=lambda x: x["fps"], reverse=True) [print(i) for i in available_modes] chosen_mode = available_modes[0] diff --git a/examples/remote_motion_detection.py b/examples/remote_motion_detection.py index 3c1a8e95..df4108ed 100755 --- a/examples/remote_motion_detection.py +++ b/examples/remote_motion_detection.py @@ -48,7 +48,7 @@ def calculate_motion(frame1, frame2): motion_map = np.zeros((frame1.shape[0] // BLOCK_SIZE, frame1.shape[1] // BLOCK_SIZE, 2), dtype=np.int8) for block_x in range(0, frame1.shape[1], BLOCK_SIZE): for block_y in range(0, frame1.shape[0], BLOCK_SIZE): - block = frame1[block_y:block_y + BLOCK_SIZE, block_x:block_x + BLOCK_SIZE] + block = frame1[block_y : block_y + BLOCK_SIZE, block_x : block_x + BLOCK_SIZE] min_diff = np.inf max_diff = 0 for offset_x in range(-SEARCH_SIZE, SEARCH_SIZE + 1, STEP_SIZE): @@ -57,9 +57,11 @@ def calculate_motion(frame1, frame2): for offset_y in range(-SEARCH_SIZE, SEARCH_SIZE + 1, STEP_SIZE): if block_y + offset_y < 0 or block_y + offset_y + BLOCK_SIZE >= frame2.shape[0]: continue - block2 = frame2[block_y + offset_y:block_y + offset_y + BLOCK_SIZE, - block_x + offset_x:block_x + offset_x + BLOCK_SIZE] - diff = np.sum((block - block2)**2) + block2 = frame2[ + block_y + offset_y : block_y + offset_y + BLOCK_SIZE, + block_x + offset_x : block_x + offset_x + BLOCK_SIZE, + ] + diff = np.sum((block - block2) ** 2) if diff < min_diff: min_diff = diff min_offset = (offset_x, offset_y) diff --git a/examples/stack_raw.py b/examples/stack_raw.py index 87e574d7..b6797f8b 100755 --- a/examples/stack_raw.py +++ b/examples/stack_raw.py @@ -33,9 +33,9 @@ accumulated += image # Fix the black level, and convert back to uint8 form for saving as a DNG. -black_level = metadata["SensorBlackLevels"][0] / 2**(16 - raw_format.bit_depth) +black_level = metadata["SensorBlackLevels"][0] / 2 ** (16 - raw_format.bit_depth) accumulated -= (num_frames - 1) * int(black_level) -accumulated = accumulated.clip(0, 2 ** raw_format.bit_depth - 1).astype(np.uint16) +accumulated = accumulated.clip(0, 2**raw_format.bit_depth - 1).astype(np.uint16) accumulated = accumulated.view(np.uint8) metadata["ExposureTime"] = exposure_time picam2.helpers.save_dng(accumulated, metadata, config["raw"], "accumulated.dng") diff --git a/examples/stereo_preview.py b/examples/stereo_preview.py index 0a9ac70f..901b8988 100755 --- a/examples/stereo_preview.py +++ b/examples/stereo_preview.py @@ -26,7 +26,7 @@ def copy_image(request): with MappedArray(request, "main") as m1, MappedArray(request_2, "main") as m2: a1 = m1.array a2 = m2.array - a1[:, -a2.shape[1]:] = a2 + a1[:, -a2.shape[1] :] = a2 request_2.release() @@ -54,7 +54,7 @@ def save_request(request): picam2a.post_callback = copy_image main_config = picam2a.create_preview_configuration( main={"size": half_size, "stride": stride}, - controls={"ScalerCrop": (0, 0, picam2a.sensor_resolution[0], picam2a.sensor_resolution[1])} + controls={"ScalerCrop": (0, 0, picam2a.sensor_resolution[0], picam2a.sensor_resolution[1])}, ) picam2a.configure(main_config) picam2a.start_preview(True) @@ -63,8 +63,7 @@ def save_request(request): picam2b = Picamera2(1) picam2b.pre_callback = save_request half_config = picam2a.create_preview_configuration( - main={"size": half_size}, - controls={"ScalerCrop": (0, 0, picam2a.sensor_resolution[0], picam2a.sensor_resolution[1])} + main={"size": half_size}, controls={"ScalerCrop": (0, 0, picam2a.sensor_resolution[0], picam2a.sensor_resolution[1])} ) picam2b.configure(half_config) diff --git a/examples/tensorflow/compositing.py b/examples/tensorflow/compositing.py index eb6d11d9..6e675a12 100755 --- a/examples/tensorflow/compositing.py +++ b/examples/tensorflow/compositing.py @@ -40,8 +40,9 @@ def DrawRectangles(request): if len(rect) == 5: text = rect[4] font = cv2.FONT_HERSHEY_SIMPLEX - cv2.putText(m.array, text, (int(rect[0] * 2) + 10, int(rect[1] * 2) + 10), - font, 1, (255, 255, 255), 2, cv2.LINE_AA) + cv2.putText( + m.array, text, (int(rect[0] * 2) + 10, int(rect[1] * 2) + 10), font, 1, (255, 255, 255), 2, cv2.LINE_AA + ) def InferenceTensorFlow(image, model, label=None): @@ -110,7 +111,7 @@ def capture_image_and_masks(picam2: Picamera2, model, label_file): image = request.make_image("main") lores = request.make_buffer("lores") stride = picam2.stream_configuration("lores")["stride"] - grey = lores[:stride * lowresSize[1]].reshape((lowresSize[1], stride)) + grey = lores[: stride * lowresSize[1]].reshape((lowresSize[1], stride)) InferenceTensorFlow(grey, model, label_file) for rect in rectangles: print(image.size) @@ -136,21 +137,19 @@ def main(): parser.add_argument('--output', help='File path of the output image.') args = parser.parse_args() - if (args.output): + if args.output: output_file = args.output else: output_file = 'out.png' - if (args.label): + if args.label: label_file = args.label else: label_file = None picam2 = Picamera2() picam2.start_preview(Preview.QTGL) - config = picam2.create_preview_configuration( - main={"size": normalSize}, - lores={"size": lowresSize, "format": "YUV420"}) + config = picam2.create_preview_configuration(main={"size": normalSize}, lores={"size": lowresSize, "format": "YUV420"}) picam2.configure(config) stride = picam2.stream_configuration("lores")["stride"] @@ -162,7 +161,7 @@ def main(): print("Starting capture, press enter to capture objects") while True: buffer = picam2.capture_buffer("lores") - grey = buffer[:stride * lowresSize[1]].reshape((lowresSize[1], stride)) + grey = buffer[: stride * lowresSize[1]].reshape((lowresSize[1], stride)) InferenceTensorFlow(grey, args.model, label_file) # Check if enter has been pressed i, o, e = select.select([sys.stdin], [], [], 0.1) diff --git a/examples/tensorflow/real_time.py b/examples/tensorflow/real_time.py index 367e7b7d..b61cf010 100755 --- a/examples/tensorflow/real_time.py +++ b/examples/tensorflow/real_time.py @@ -112,20 +112,19 @@ def main(): parser.add_argument('--output', help='File path of the output image.') args = parser.parse_args() - if (args.output): + if args.output: output_file = args.output else: output_file = 'out.jpg' - if (args.label): + if args.label: label_file = args.label else: label_file = None picam2 = Picamera2() picam2.start_preview(Preview.QTGL) - config = picam2.create_preview_configuration(main={"size": normalSize}, - lores={"size": lowresSize, "format": "YUV420"}) + config = picam2.create_preview_configuration(main={"size": normalSize}, lores={"size": lowresSize, "format": "YUV420"}) picam2.configure(config) stride = picam2.stream_configuration("lores")["stride"] @@ -135,7 +134,7 @@ def main(): while True: buffer = picam2.capture_buffer("lores") - grey = buffer[:stride * lowresSize[1]].reshape((lowresSize[1], stride)) + grey = buffer[: stride * lowresSize[1]].reshape((lowresSize[1], stride)) _ = InferenceTensorFlow(grey, args.model, output_file, label_file) diff --git a/examples/tensorflow/real_time_with_labels.py b/examples/tensorflow/real_time_with_labels.py index c1938ed2..78d96b89 100755 --- a/examples/tensorflow/real_time_with_labels.py +++ b/examples/tensorflow/real_time_with_labels.py @@ -52,8 +52,9 @@ def DrawRectangles(request): if len(rect) == 5: text = rect[4] font = cv2.FONT_HERSHEY_SIMPLEX - cv2.putText(m.array, text, (int(rect[0] * 2) + 10, int(rect[1] * 2) + 10), - font, 1, (255, 255, 255), 2, cv2.LINE_AA) + cv2.putText( + m.array, text, (int(rect[0] * 2) + 10, int(rect[1] * 2) + 10), font, 1, (255, 255, 255), 2, cv2.LINE_AA + ) def InferenceTensorFlow(image, model, output, label=None): @@ -119,20 +120,19 @@ def main(): parser.add_argument('--output', help='File path of the output image.') args = parser.parse_args() - if (args.output): + if args.output: output_file = args.output else: output_file = 'out.jpg' - if (args.label): + if args.label: label_file = args.label else: label_file = None picam2 = Picamera2() picam2.start_preview(Preview.QTGL) - config = picam2.create_preview_configuration(main={"size": normalSize}, - lores={"size": lowresSize, "format": "YUV420"}) + config = picam2.create_preview_configuration(main={"size": normalSize}, lores={"size": lowresSize, "format": "YUV420"}) picam2.configure(config) stride = picam2.stream_configuration("lores")["stride"] @@ -142,7 +142,7 @@ def main(): while True: buffer = picam2.capture_buffer("lores") - grey = buffer[:stride * lowresSize[1]].reshape((lowresSize[1], stride)) + grey = buffer[: stride * lowresSize[1]].reshape((lowresSize[1], stride)) _ = InferenceTensorFlow(grey, args.model, output_file, label_file) diff --git a/examples/tensorflow/remove_background.py b/examples/tensorflow/remove_background.py index 723519f7..fcb3ce71 100755 --- a/examples/tensorflow/remove_background.py +++ b/examples/tensorflow/remove_background.py @@ -51,8 +51,7 @@ def InferenceTensorFlow(image, model): mask = np.argmax(output, axis=-1) output_shape = (o_width, o_height) overlay = (mask == 0).astype(np.uint8) - overlay = np.array([0, 255])[overlay].reshape( - output_shape).astype(np.uint8) + overlay = np.array([0, 255])[overlay].reshape(output_shape).astype(np.uint8) overlay = cv2.resize(overlay, normalSize) background_mask = Image.fromarray(overlay) @@ -66,8 +65,10 @@ def main(): picam2 = Picamera2() picam2.start_preview(Preview.QTGL) + # fmt: off config = picam2.create_preview_configuration(main={"size": normalSize}, lores={"size": lowresSize, "format": "YUV420"}) + # fmt: on picam2.configure(config) stride = picam2.stream_configuration("lores")["stride"] @@ -83,7 +84,7 @@ def main(): while True: buffer = picam2.capture_buffer("lores") - grey = buffer[:stride * lowresSize[1]].reshape((lowresSize[1], stride)) + grey = buffer[: stride * lowresSize[1]].reshape((lowresSize[1], stride)) InferenceTensorFlow(grey, args.model) base_img = np.zeros((normalSize[1], normalSize[0], 3), dtype=np.uint8) base_img = Image.fromarray(base_img) diff --git a/examples/tensorflow/segmentation.py b/examples/tensorflow/segmentation.py index 3ec419df..f58a28e3 100755 --- a/examples/tensorflow/segmentation.py +++ b/examples/tensorflow/segmentation.py @@ -50,7 +50,7 @@ def run_inference(self, image): if len(image.shape) == 2: # Image is YUV420. Must convert and trim off any padding. image = cv2.cvtColor(image, cv2.COLOR_YUV420p2RGB) - image = image[:self.height, :self.width] + image = image[: self.height, : self.width] input_data = np.expand_dims(image, axis=0) if self.floating_model: input_data = np.float32(input_data / 255) @@ -116,8 +116,9 @@ def main(): captured = [] picam2 = Picamera2() - config = picam2.create_preview_configuration(main={"size": NORMAL_SIZE}, - lores={"size": LOWRES_SIZE, "format": lowres_format}) + config = picam2.create_preview_configuration( + main={"size": NORMAL_SIZE}, lores={"size": LOWRES_SIZE, "format": lowres_format} + ) picam2.configure(config) picam2.start(show_preview=True) diff --git a/examples/tensorflow/yolo_v5_real_time_with_labels.py b/examples/tensorflow/yolo_v5_real_time_with_labels.py index 12d2248b..b7edf072 100644 --- a/examples/tensorflow/yolo_v5_real_time_with_labels.py +++ b/examples/tensorflow/yolo_v5_real_time_with_labels.py @@ -58,8 +58,7 @@ def DrawRectangles(request): if len(rect) == 5: text = rect[4] font = cv2.FONT_HERSHEY_SIMPLEX - cv2.putText(m.array, text, (xmin, ymin - 10), - font, 1, (255, 255, 255), 2, cv2.LINE_AA) + cv2.putText(m.array, text, (xmin, ymin - 10), font, 1, (255, 255, 255), 2, cv2.LINE_AA) def classFilter(classdata): @@ -68,8 +67,8 @@ def classFilter(classdata): def YOLOdetect(output_data): # input = interpreter, output is boxes(xyxy), classes, scores - output_data = output_data[0] # x(1, 25200, 7) to x(25200, 7) - boxes = np.squeeze(output_data[..., :4]) # boxes [25200, 4] + output_data = output_data[0] # x(1, 25200, 7) to x(25200, 7) + boxes = np.squeeze(output_data[..., :4]) # boxes [25200, 4] scores = np.squeeze(output_data[..., 4:5]) # confidences [25200, 1] classes = classFilter(output_data[..., 5:]) # get classes # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right @@ -97,8 +96,9 @@ def main(): if Picamera2.platform == Platform.PISP: stream_format = "RGB888" - config = picam2.create_preview_configuration(main={"size": normalSize}, - lores={"size": lowresSize, "format": stream_format}) + config = picam2.create_preview_configuration( + main={"size": normalSize}, lores={"size": lowresSize, "format": stream_format} + ) picam2.configure(config) picam2.post_callback = DrawRectangles @@ -136,7 +136,7 @@ def main(): rectangles = [] for i in range(len(scores)): - if ((scores[i] > 0.4) and (scores[i] <= 1.0)): + if (scores[i] > 0.4) and (scores[i] <= 1.0): xmin = int(max(1, (xyxy[0][i] * normalSize[0]))) ymin = int(max(1, (xyxy[1][i] * normalSize[1]))) xmax = int(min(normalSize[0], (xyxy[2][i] * normalSize[0]))) diff --git a/picamera2/__init__.py b/picamera2/__init__.py index 85418341..dac6ae41 100644 --- a/picamera2/__init__.py +++ b/picamera2/__init__.py @@ -26,11 +26,10 @@ def _set_configuration_file(filename): platform_dir = "vc4" if get_platform() == Platform.VC4 else "pisp" dirs = [ - os.path.expanduser( - "~/libcamera/src/libcamera/pipeline/rpi/" + platform_dir + "/data" - ), + os.path.expanduser("~/libcamera/src/libcamera/pipeline/rpi/" + platform_dir + "/data"), "/usr/local/share/libcamera/pipeline/rpi/" + platform_dir, - "/usr/share/libcamera/pipeline/rpi/" + platform_dir] + "/usr/share/libcamera/pipeline/rpi/" + platform_dir, + ] for directory in dirs: file = os.path.join(directory, filename) @@ -48,8 +47,12 @@ def libcamera_transforms_eq(t1, t2): def libcamera_colour_spaces_eq(c1, c2): - return c1.primaries == c2.primaries and c1.transferFunction == c2.transferFunction and \ - c1.ycbcrEncoding == c2.ycbcrEncoding and c1.range == c2.range + return ( + c1.primaries == c2.primaries + and c1.transferFunction == c2.transferFunction + and c1.ycbcrEncoding == c2.ycbcrEncoding + and c1.range == c2.range + ) libcamera.Transform.__repr__ = libcamera.Transform.__str__ diff --git a/picamera2/allocators/dmaallocator.py b/picamera2/allocators/dmaallocator.py index fdbaafae..e50a6aa2 100644 --- a/picamera2/allocators/dmaallocator.py +++ b/picamera2/allocators/dmaallocator.py @@ -6,9 +6,15 @@ import libcamera from picamera2.allocators.allocator import Allocator, Sync -from picamera2.dma_heap import (DMA_BUF_IOCTL_SYNC, DMA_BUF_SYNC_END, - DMA_BUF_SYNC_READ, DMA_BUF_SYNC_RW, - DMA_BUF_SYNC_START, DmaHeap, dma_buf_sync) +from picamera2.dma_heap import ( + DMA_BUF_IOCTL_SYNC, + DMA_BUF_SYNC_END, + DMA_BUF_SYNC_READ, + DMA_BUF_SYNC_RW, + DMA_BUF_SYNC_START, + DmaHeap, + dma_buf_sync, +) _log = logging.getLogger("picamera2") diff --git a/picamera2/allocators/persistent_allocator.py b/picamera2/allocators/persistent_allocator.py index b2dc216f..3945cab1 100644 --- a/picamera2/allocators/persistent_allocator.py +++ b/picamera2/allocators/persistent_allocator.py @@ -35,8 +35,7 @@ def allocate(self, libcamera_config, use_case): self.mapped_buffers_used, ) else: - (self.open_fds, self.libcamera_fds, self.frame_buffers, - self.mapped_buffers, self.mapped_buffers_used) = buffers + (self.open_fds, self.libcamera_fds, self.frame_buffers, self.mapped_buffers, self.mapped_buffers_used) = buffers def cleanup(self): pass @@ -49,8 +48,9 @@ def deallocate(self, buffer_key=None): tmp = super().__new__(DmaAllocator) tmp.dmaHeap = None - (tmp.open_fds, tmp.libcamera_fds, tmp.frame_buffers, - tmp.mapped_buffers, tmp.mapped_buffers_used) = self.buffer_dict[buffer_key] + (tmp.open_fds, tmp.libcamera_fds, tmp.frame_buffers, tmp.mapped_buffers, tmp.mapped_buffers_used) = self.buffer_dict[ + buffer_key + ] tmp.close() del self.buffer_dict[buffer_key] diff --git a/picamera2/configuration.py b/picamera2/configuration.py index 9c01794b..4ed55bf1 100644 --- a/picamera2/configuration.py +++ b/picamera2/configuration.py @@ -95,10 +95,26 @@ class SensorConfiguration(Configuration): class CameraConfiguration(Configuration): - _ALLOWED_FIELDS = ("use_case", "buffer_count", "transform", "display", "encode", "colour_space", - "controls", "main", "lores", "raw", "queue", "sensor") - _FIELD_CLASS_MAP = {"main": StreamConfiguration, "lores": StreamConfiguration, "raw": StreamConfiguration, - "sensor": SensorConfiguration} + _ALLOWED_FIELDS = ( + "use_case", + "buffer_count", + "transform", + "display", + "encode", + "colour_space", + "controls", + "main", + "lores", + "raw", + "queue", + "sensor", + ) + _FIELD_CLASS_MAP = { + "main": StreamConfiguration, + "lores": StreamConfiguration, + "raw": StreamConfiguration, + "sensor": SensorConfiguration, + } _FORWARD_FIELDS = {"size": "main", "format": "main"} def __init__(self, d={}, picam2=None): diff --git a/picamera2/controls.py b/picamera2/controls.py index cf203006..26a99b11 100644 --- a/picamera2/controls.py +++ b/picamera2/controls.py @@ -4,7 +4,7 @@ from libcamera import ControlType, Rectangle, Size -class Controls(): +class Controls: def _framerates_to_durations_(framerates): if not isinstance(framerates, (tuple, list)): framerates = (framerates, framerates) diff --git a/picamera2/converters.py b/picamera2/converters.py index 29623253..0c338964 100644 --- a/picamera2/converters.py +++ b/picamera2/converters.py @@ -1,8 +1,10 @@ import numpy as np -YUV2RGB_JPEG = np.array([[1.0, 1.0, 1.0 ], [0.0, -0.344, 1.772], [1.402, -0.714, 0.0]]) # noqa -YUV2RGB_SMPTE170M = np.array([[1.164, 1.164, 1.164], [0.0, -0.392, 2.017], [1.596, -0.813, 0.0]]) # noqa -YUV2RGB_REC709 = np.array([[1.164, 1.164, 1.164], [0.0, -0.213, 2.112], [1.793, -0.533, 0.0]]) # noqa +# fmt: off +YUV2RGB_JPEG = np.array([[1.0, 1.0, 1.0 ], [0.0, -0.344, 1.772], [1.402, -0.714, 0.0]]) # noqa: E501 +YUV2RGB_SMPTE170M = np.array([[1.164, 1.164, 1.164], [0.0, -0.392, 2.017], [1.596, -0.813, 0.0]]) # noqa: E501 +YUV2RGB_REC709 = np.array([[1.164, 1.164, 1.164], [0.0, -0.213, 2.112], [1.793, -0.533, 0.0]]) # noqa: E501 +# fmt: on def YUV420_to_RGB(YUV_in, size, matrix=YUV2RGB_JPEG, rb_swap=True, final_width=0): @@ -20,8 +22,8 @@ def YUV420_to_RGB(YUV_in, size, matrix=YUV2RGB_JPEG, rb_swap=True, final_width=0 YUV = np.empty((h2, w2, 3), dtype=int) YUV[:, :, 0] = YUV_in[:n].reshape(h, w)[0::2, 0::2] - YUV[:, :, 1] = YUV_in[n:n + n4].reshape(h2, w2) - 128.0 - YUV[:, :, 2] = YUV_in[n + n4:n + n2].reshape(h2, w2) - 128.0 + YUV[:, :, 1] = YUV_in[n : n + n4].reshape(h2, w2) - 128.0 + YUV[:, :, 2] = YUV_in[n + n4 : n + n2].reshape(h2, w2) - 128.0 if rb_swap: matrix = matrix[:, [2, 1, 0]] diff --git a/picamera2/devices/hailo/hailo.py b/picamera2/devices/hailo/hailo.py index 7bbf0bbd..55ab18e1 100644 --- a/picamera2/devices/hailo/hailo.py +++ b/picamera2/devices/hailo/hailo.py @@ -171,8 +171,9 @@ def run_async(self, input_data): bindings = self._create_bindings() bindings.input().set_buffer(frame) self.configured_infer_model.wait_for_async_ready(timeout_ms=10000) - self.configured_infer_model.run_async([bindings], - partial(self.callback, bindings=bindings, future=future, last=last)) + self.configured_infer_model.run_async( + [bindings], partial(self.callback, bindings=bindings, future=future, last=last) + ) return future @@ -196,8 +197,9 @@ def _create_bindings(self): Returns: bindings: Bindings object with input and output buffers. """ - output_buffers = {name: np.empty(self.infer_model.output(name).shape, dtype=np.float32) - for name in self.infer_model.output_names} + output_buffers = { + name: np.empty(self.infer_model.output(name).shape, dtype=np.float32) for name in self.infer_model.output_names + } return self.configured_infer_model.create_bindings(output_buffers=output_buffers) def close(self): diff --git a/picamera2/devices/imx500/__init__.py b/picamera2/devices/imx500/__init__.py index 2cabd5ee..26c4553e 100644 --- a/picamera2/devices/imx500/__init__.py +++ b/picamera2/devices/imx500/__init__.py @@ -1,6 +1,5 @@ from .imx500 import IMX500, NetworkIntrinsics -from .postprocess_efficientdet_lite0 import \ - postprocess_efficientdet_lite0_detection +from .postprocess_efficientdet_lite0 import postprocess_efficientdet_lite0_detection from .postprocess_nanodet import postprocess_nanodet_detection from .postprocess_yolov5 import postprocess_yolov5_detection from .postprocess_yolov8 import postprocess_yolov8_detection diff --git a/picamera2/devices/imx500/imx500.py b/picamera2/devices/imx500/imx500.py index 8c2ba3c8..7c3e865d 100644 --- a/picamera2/devices/imx500/imx500.py +++ b/picamera2/devices/imx500/imx500.py @@ -15,9 +15,15 @@ from libarchive.read import fd_reader from libcamera import Rectangle, Size from tqdm import tqdm -from videodev2 import (VIDIOC_G_CTRL, VIDIOC_G_EXT_CTRLS, VIDIOC_S_CTRL, - VIDIOC_S_EXT_CTRLS, v4l2_control, v4l2_ext_control, - v4l2_ext_controls) +from videodev2 import ( + VIDIOC_G_CTRL, + VIDIOC_G_EXT_CTRLS, + VIDIOC_S_CTRL, + VIDIOC_S_EXT_CTRLS, + v4l2_control, + v4l2_ext_control, + v4l2_ext_controls, +) from picamera2 import CompletedRequest, Picamera2 @@ -304,15 +310,20 @@ def __init__(self, network_file: str, camera_id: str = '', tensor_injection: boo test_dir = f'/sys/class/video4linux/v4l-subdev{i}/device' module_dir = f'{test_dir}/driver/module' id_dir = f'{test_dir}/of_node' - if os.path.exists(module_dir) and os.path.islink(module_dir) and os.path.islink(id_dir) \ - and 'imx500' in os.readlink(module_dir): + if ( + os.path.exists(module_dir) + and os.path.islink(module_dir) + and os.path.islink(id_dir) + and 'imx500' in os.readlink(module_dir) + ): if camera_id == '' or (camera_id in os.readlink(id_dir)): self.device_fd = open(f'/dev/v4l-subdev{i}', 'rb+', buffering=0) imx500_device_id = os.readlink(test_dir).split('/')[-1] spi_device_id = imx500_device_id.replace('001a', '0040') camera_info = Picamera2.global_camera_info() - self.__camera_num = next((c['Num'] for c in camera_info if c['Model'] == 'imx500' - and c['Id'] in os.readlink(id_dir))) + self.__camera_num = next( + (c['Num'] for c in camera_info if c['Model'] == 'imx500' and c['Id'] in os.readlink(id_dir)) + ) break if self.device_fd is None: @@ -382,10 +393,7 @@ def convert_inference_coords(self, coords: tuple, metadata: dict, picam2: Picame full_sensor = self.__get_full_sensor_resolution() width, height = full_sensor.size.to_tuple() obj = Rectangle( - *np.maximum( - np.array([x0 * width, y0 * height, (x1 - x0) * width, (y1 - y0) * height]), - 0, - ).astype(np.int32) + *np.maximum(np.array([x0 * width, y0 * height, (x1 - x0) * width, (y1 - y0) * height]), 0).astype(np.int32) ) out = self.__get_obj_scaled(obj, isp_output_size, scaler_crop, sensor_output_size) return out.to_tuple() @@ -461,8 +469,7 @@ def show_network_fw_progress_bar(self): if not self.fw_progress and not self.fw_progress_chunk: # No readable progress source: skip the bar so the worker does not spin on (0, 0). return - p = multiprocessing.Process(target=self.__do_progress_bar, - args=(FW_NETWORK_STAGE, 'Network Firmware Upload')) + p = multiprocessing.Process(target=self.__do_progress_bar, args=(FW_NETWORK_STAGE, 'Network Firmware Upload')) p.start() p.join(0) @@ -533,7 +540,7 @@ def input_tensor_image(self, input_tensor): div_val = self.config['input_tensor']['div_val'] div_shift = self.config['input_tensor']['div_shift'] for i in [0, 1, 2]: - r1[i] = ((((r1[i] << norm_shift[i]) - norm_val[i]) << div_shift) // div_val[i]) & 0xff + r1[i] = ((((r1[i] << norm_shift[i]) - norm_val[i]) << div_shift) // div_val[i]) & 0xFF return np.transpose(r1, (1, 2, 0)).astype(np.uint8) @@ -555,19 +562,19 @@ def prepare_tensor_for_injection(self, exr_input: OpenEXR.InputFile) -> bytes: try: if channels[ch] != Imath_FLOAT_Channel: raise ValueError(f"Channel '{ch}' is not of type FLOAT (found: {channels[ch]})") - except KeyError: - raise ValueError(f"EXR input missing required channel '{ch}'") + except KeyError as e: + raise ValueError(f"EXR input missing required channel '{ch}'") from e np_float_channels = { - 'R': np.frombuffer( - exr_input.channel('R', Imath_FLOAT_Type), dtype=np.float32 - ).reshape(tuple(reversed(tensor_size))), - 'G': np.frombuffer( - exr_input.channel('G', Imath_FLOAT_Type), dtype=np.float32 - ).reshape(tuple(reversed(tensor_size))), - 'B': np.frombuffer( - exr_input.channel('B', Imath_FLOAT_Type), dtype=np.float32 - ).reshape(tuple(reversed(tensor_size))), + 'R': np.frombuffer(exr_input.channel('R', Imath_FLOAT_Type), dtype=np.float32).reshape( + tuple(reversed(tensor_size)) + ), + 'G': np.frombuffer(exr_input.channel('G', Imath_FLOAT_Type), dtype=np.float32).reshape( + tuple(reversed(tensor_size)) + ), + 'B': np.frombuffer(exr_input.channel('B', Imath_FLOAT_Type), dtype=np.float32).reshape( + tuple(reversed(tensor_size)) + ), } # Verify that all channels are in the range 0.0 to 1.0 @@ -641,7 +648,7 @@ def get_outputs(self, metadata: dict, add_batch=False) -> Optional[list[np.ndarr outputs = [] for tensor_shape in output_shapes: size = np.prod(tensor_shape) - reshaped_tensor = np_output[offset:offset + size].reshape(tensor_shape, order='F') + reshaped_tensor = np_output[offset : offset + size].reshape(tensor_shape, order='F') if add_batch: reshaped_tensor = np.expand_dims(reshaped_tensor, 0) outputs.append(reshaped_tensor) @@ -720,14 +727,14 @@ def __get_output_tensor_info(self, tensor_info) -> dict: 'network_name': parsed.network_name.decode('utf-8').strip('\x00'), 'num_tensors': parsed.num_tensors, 'frameCount': parsed.frameCount, - 'info': [] + 'info': [], } - for t in parsed.info[0:parsed.num_tensors]: + for t in parsed.info[0 : parsed.num_tensors]: info = { 'tensor_data_num': t.tensor_data_num, 'num_dimensions': t.num_dimensions, - 'size': list(t.size)[0:t.num_dimensions], + 'size': list(t.size)[0 : t.num_dimensions], } result['info'].append(info) @@ -762,7 +769,7 @@ def __enable_injection(self): try: fcntl.ioctl(self.device_fd, VIDIOC_S_CTRL, ctrl) except OSError as err: - raise RuntimeError(f'IMX500: Unable to enable input tensor injection: {err}') + raise RuntimeError(f'IMX500: Unable to enable input tensor injection: {err}') from err def __set_input_tensor(self, input_tensor_fd: int): ctrl = v4l2_control() @@ -772,7 +779,7 @@ def __set_input_tensor(self, input_tensor_fd: int): try: fcntl.ioctl(self.device_fd, VIDIOC_S_CTRL, ctrl) except OSError as err: - raise RuntimeError(f'IMX500: Unable to set input tensor fd: {err}') + raise RuntimeError(f'IMX500: Unable to set input tensor fd: {err}') from err def __set_network_firmware(self, network_filename: str): """Provides a firmware rpk file to upload to the IMX500. This must be called before Picamera2 is configured.""" @@ -788,7 +795,7 @@ def __set_network_firmware(self, network_filename: str): try: fcntl.ioctl(self.device_fd, VIDIOC_S_CTRL, ctrl) except OSError as err: - raise RuntimeError(f'IMX500: Unable to set network firmware {network_filename}: {err}') + raise RuntimeError(f'IMX500: Unable to set network firmware {network_filename}: {err}') from err finally: os.close(fd) @@ -809,7 +816,7 @@ def __ni_from_network(self, network_filename: str): fw = fw[8:] flags = struct.unpack('8B', fw[:8]) device_lock_flag = flags[6] - fw = fw[(size + 60 - 8):] # jump to footer + fw = fw[(size + 60 - 8) :] # jump to footer # Ensure footer is as expected (magic,) = struct.unpack('4s', fw[:4]) @@ -818,7 +825,7 @@ def __ni_from_network(self, network_filename: str): fw = fw[4:] cpio_offset += size + 64 - if ((device_lock_flag & 0x01) == 1): + if (device_lock_flag & 0x01) == 1: # skip forward 32 bytes if device_lock_flag.bit0 == 1 fw = fw[32:] cpio_offset += 32 @@ -871,8 +878,7 @@ def __ni_from_network(self, network_filename: str): # Extract some input tensor config params self.__cfg['input_tensor']['width'] = int(res['network'][0]['inputTensorWidth']) self.__cfg['input_tensor']['height'] = int(res['network'][0]['inputTensorHeight']) - self.__cfg['input_tensor_size'] = (self.config['input_tensor']['width'], - self.config['input_tensor']['height']) + self.__cfg['input_tensor_size'] = (self.config['input_tensor']['width'], self.config['input_tensor']['height']) input_format = self.__cfg['network_info']['network'][0]['inputTensorFormat'] inputTensorNorm_K03 = int(self.__cfg['network_info']['network'][0]['inputTensorNorm_K03'], 0) @@ -887,12 +893,15 @@ def __ni_from_network(self, network_filename: str): self.__cfg['input_tensor']['input_format'] = input_format if input_format == 'RGB' or input_format == 'BGR': - norm_val_0 = \ - inputTensorNorm_K03 if ((inputTensorNorm_K03 >> 12) & 1) == 0 else -((~inputTensorNorm_K03 + 1) & 0x1fff) - norm_val_1 = \ - inputTensorNorm_K13 if ((inputTensorNorm_K13 >> 12) & 1) == 0 else -((~inputTensorNorm_K13 + 1) & 0x1fff) - norm_val_2 = \ - inputTensorNorm_K23 if ((inputTensorNorm_K23 >> 12) & 1) == 0 else -((~inputTensorNorm_K23 + 1) & 0x1fff) + norm_val_0 = ( + inputTensorNorm_K03 if ((inputTensorNorm_K03 >> 12) & 1) == 0 else -((~inputTensorNorm_K03 + 1) & 0x1FFF) + ) + norm_val_1 = ( + inputTensorNorm_K13 if ((inputTensorNorm_K13 >> 12) & 1) == 0 else -((~inputTensorNorm_K13 + 1) & 0x1FFF) + ) + norm_val_2 = ( + inputTensorNorm_K23 if ((inputTensorNorm_K23 >> 12) & 1) == 0 else -((~inputTensorNorm_K23 + 1) & 0x1FFF) + ) norm_val = [norm_val_0, norm_val_1, norm_val_2] self.__cfg['input_tensor']['norm_val'] = norm_val norm_shift = [4, 4, 4] @@ -900,16 +909,21 @@ def __ni_from_network(self, network_filename: str): dtype = 'unsigned' if ((inputTensorNorm_K03 >> 12) & 1) == 0 else 'signed' self.__cfg['input_tensor']['dtype'] = dtype if input_format == 'RGB': - div_val_0 = \ - inputTensorNorm_K00 if ((inputTensorNorm_K00 >> 11) & 1) == 0 else -((~inputTensorNorm_K00 + 1) & 0x0fff) - div_val_2 =\ - inputTensorNorm_K22 if ((inputTensorNorm_K22 >> 11) & 1) == 0 else -((~inputTensorNorm_K22 + 1) & 0x0fff) + div_val_0 = ( + inputTensorNorm_K00 if ((inputTensorNorm_K00 >> 11) & 1) == 0 else -((~inputTensorNorm_K00 + 1) & 0x0FFF) + ) + div_val_2 = ( + inputTensorNorm_K22 if ((inputTensorNorm_K22 >> 11) & 1) == 0 else -((~inputTensorNorm_K22 + 1) & 0x0FFF) + ) else: - div_val_0 = \ - inputTensorNorm_K02 if ((inputTensorNorm_K02 >> 11) & 1) == 0 else -((~inputTensorNorm_K02 + 1) & 0x0fff) - div_val_2 = \ - inputTensorNorm_K20 if ((inputTensorNorm_K20 >> 11) & 1) == 0 else -((~inputTensorNorm_K20 + 1) & 0x0fff) - div_val_1 = \ - inputTensorNorm_K11 if ((inputTensorNorm_K11 >> 11) & 1) == 0 else -((~inputTensorNorm_K11 + 1) & 0x0fff) + div_val_0 = ( + inputTensorNorm_K02 if ((inputTensorNorm_K02 >> 11) & 1) == 0 else -((~inputTensorNorm_K02 + 1) & 0x0FFF) + ) + div_val_2 = ( + inputTensorNorm_K20 if ((inputTensorNorm_K20 >> 11) & 1) == 0 else -((~inputTensorNorm_K20 + 1) & 0x0FFF) + ) + div_val_1 = ( + inputTensorNorm_K11 if ((inputTensorNorm_K11 >> 11) & 1) == 0 else -((~inputTensorNorm_K11 + 1) & 0x0FFF) + ) self.__cfg['input_tensor']['div_val'] = [div_val_0, div_val_1, div_val_2] self.__cfg['input_tensor']['div_shift'] = 6 diff --git a/picamera2/devices/imx500/postprocess.py b/picamera2/devices/imx500/postprocess.py index ee1c825e..bf813e9d 100644 --- a/picamera2/devices/imx500/postprocess.py +++ b/picamera2/devices/imx500/postprocess.py @@ -83,15 +83,15 @@ def combined_nms(batch_boxes, batch_scores, iou_thres: float = 0.65, conf: float return nms_results -def combined_nms_seg(batch_boxes, batch_scores, batch_masks, iou_thres: float = 0.5, conf: float = 0.001, - max_out_dets: int = 300): +def combined_nms_seg( + batch_boxes, batch_scores, batch_masks, iou_thres: float = 0.5, conf: float = 0.001, max_out_dets: int = 300 +): nms_results = [] for boxes, scores, masks in zip(batch_boxes, batch_scores, batch_masks): # Compute maximum scores and corresponding class indices class_indices = np.argmax(scores, axis=1) max_scores = np.amax(scores, axis=1) - detections = np.concatenate([boxes, np.expand_dims(max_scores, axis=1), np.expand_dims(class_indices, axis=1)], - axis=1) + detections = np.concatenate([boxes, np.expand_dims(max_scores, axis=1), np.expand_dims(class_indices, axis=1)], axis=1) # Swap the position of the two dimensions (32, 8400) to (8400, 32) masks = np.transpose(masks, (1, 0)) @@ -101,7 +101,6 @@ def combined_nms_seg(batch_boxes, batch_scores, batch_masks, iou_thres: float = if np.all(valid_detections is False): nms_results.append((np.ndarray(0), np.ndarray(0), np.ndarray(0), np.ndarray(0))) else: - detections = detections[valid_detections] masks = masks[valid_detections] @@ -198,8 +197,15 @@ def clip_boxes(boxes: np.ndarray, h: int, w: int) -> np.ndarray: return boxes -def scale_boxes(boxes: np.ndarray, h_image: int, w_image: int, h_model: int, w_model: int, preserve_aspect_ratio: bool, - normalized: bool = True) -> np.ndarray: +def scale_boxes( + boxes: np.ndarray, + h_image: int, + w_image: int, + h_model: int, + w_model: int, + preserve_aspect_ratio: bool, + normalized: bool = True, +) -> np.ndarray: """ Scale and offset bounding boxes based on model output size and original image size. @@ -239,8 +245,9 @@ def scale_boxes(boxes: np.ndarray, h_image: int, w_image: int, h_model: int, w_m return boxes -def scale_coords(kpts: np.ndarray, h_image: int, w_image: int, h_model: int, w_model: int, - preserve_aspect_ratio: bool) -> np.ndarray: +def scale_coords( + kpts: np.ndarray, h_image: int, w_image: int, h_model: int, w_model: int, preserve_aspect_ratio: bool +) -> np.ndarray: """ Scale and offset keypoints based on model output size and original image size. @@ -309,7 +316,7 @@ def clip_coords(kpts: np.ndarray, h: int, w: int) -> np.ndarray: 13: 'KneeL', 14: 'KneeR', 15: 'AnkleL', - 16: 'AnkleR' + 16: 'AnkleR', } @@ -344,12 +351,14 @@ def get_point(index): y0, x0, _, _ = self.get_coords((y0, x0, y0 + 1, x0 + 1), metadata, picam2, stream) return x0, y0 + # fmt: off skeleton = [ [0, 1], [0, 2], [1, 3], [2, 4], # Head [5, 6], [5, 7], [7, 9], [6, 8], # Arms [8, 10], [5, 11], [6, 12], [11, 12], # Body - [11, 13], [12, 14], [13, 15], [14, 16] # Legs + [11, 13], [12, 14], [13, 15], [14, 16], # Legs ] + # fmt: on # Draw skeleton lines for connection in skeleton: diff --git a/picamera2/devices/imx500/postprocess_efficientdet_lite0.py b/picamera2/devices/imx500/postprocess_efficientdet_lite0.py index 3ffd7fd4..d301e37c 100644 --- a/picamera2/devices/imx500/postprocess_efficientdet_lite0.py +++ b/picamera2/devices/imx500/postprocess_efficientdet_lite0.py @@ -9,37 +9,40 @@ import numpy as np -from picamera2.devices.imx500.postprocess import ( - BoxFormat, convert_to_ymin_xmin_ymax_xmax_format, nms) +from picamera2.devices.imx500.postprocess import BoxFormat, convert_to_ymin_xmin_ymax_xmax_format, nms from picamera2.devices.imx500.postprocess_yolov5 import coco80_to_coco91 default_box_variance = [1.0, 1.0, 1.0, 1.0] default_aspect_ratios = [1.0, 2.0, 0.5] -def postprocess_efficientdet_lite0_detection(outputs: Tuple[np.ndarray, np.ndarray, np.ndarray], - anchor_scale=3, - min_level=3, - max_level=7, - box_variance=default_box_variance, - model_input_shape=(320, 320), - min_wh=2, - max_wh=7680, - conf_thres: float = 0.001, - iou_thres: float = 0.65, - max_nms_dets: int = 5000, - max_out_dets: int = 1000): +def postprocess_efficientdet_lite0_detection( + outputs: Tuple[np.ndarray, np.ndarray, np.ndarray], + anchor_scale=3, + min_level=3, + max_level=7, + box_variance=default_box_variance, + model_input_shape=(320, 320), + min_wh=2, + max_wh=7680, + conf_thres: float = 0.001, + iou_thres: float = 0.65, + max_nms_dets: int = 5000, + max_out_dets: int = 1000, +): H, W = model_input_shape ############################################################ # Box decoding ############################################################ - outputs_decoded = box_decoding_edetlite(output_annotations=outputs, - H=H, - W=W, - anchor_scale=anchor_scale, - min_level=min_level, - max_level=max_level, - box_variance=box_variance) + outputs_decoded = box_decoding_edetlite( + output_annotations=outputs, + H=H, + W=W, + anchor_scale=anchor_scale, + min_level=min_level, + max_level=max_level, + box_variance=box_variance, + ) classes = outputs[0] num_categories = classes.shape[-1] @@ -98,13 +101,9 @@ def postprocess_efficientdet_lite0_detection(outputs: Tuple[np.ndarray, np.ndarr return post_processed_outputs[0]['boxes'], post_processed_outputs[0]['scores'], post_processed_outputs[0]['classes'] -def box_decoding_edetlite(output_annotations, - H=320, - W=320, - anchor_scale=3, - min_level=3, - max_level=7, - box_variance=default_box_variance): +def box_decoding_edetlite( + output_annotations, H=320, W=320, anchor_scale=3, min_level=3, max_level=7, box_variance=default_box_variance +): # ----------------------------------------------- # EfficientDetLite detection post processing # ----------------------------------------------- @@ -123,18 +122,20 @@ def box_decoding_edetlite(output_annotations, # Anchor boxes format: [y_min, x_min, y_max, x_max] normalized # Extract feature map sizes - strides = [2 ** i for i in range(max_level + 1)] + strides = [2**i for i in range(max_level + 1)] featmap_sizes = [(np.ceil(H / stride), np.ceil(W / stride)) for stride in strides] # Generate priors batch_size = outputs.shape[0] - anchors = generate_anchors_EDETLITE(batch_size=batch_size, - featmap_sizes=featmap_sizes, - H=H, - W=W, - anchor_scale=anchor_scale, - min_level=min_level, - max_level=max_level) + anchors = generate_anchors_EDETLITE( + batch_size=batch_size, + featmap_sizes=featmap_sizes, + H=H, + W=W, + anchor_scale=anchor_scale, + min_level=min_level, + max_level=max_level, + ) # Decode bboxes y_c_anchors = (anchors[..., 0:1] + anchors[..., 2:3]) / 2 @@ -155,14 +156,9 @@ def box_decoding_edetlite(output_annotations, return outputs -def generate_anchors_EDETLITE(batch_size, - featmap_sizes, - H=320, - W=320, - anchor_scale=3, - min_level=3, - max_level=7, - aspect_ratios=default_aspect_ratios): +def generate_anchors_EDETLITE( + batch_size, featmap_sizes, H=320, W=320, anchor_scale=3, min_level=3, max_level=7, aspect_ratios=default_aspect_ratios +): """Generate configurations of anchor boxes.""" anchor_scales = [anchor_scale] * (max_level - min_level + 1) num_scales = len(aspect_ratios) @@ -172,10 +168,16 @@ def generate_anchors_EDETLITE(batch_size, for scale_octave in range(num_scales): for aspect in aspect_ratios: anchor_configs[level].append( - ((featmap_sizes[0][0] / float(featmap_sizes[level][0]), - featmap_sizes[0][1] / float(featmap_sizes[level][1])), - scale_octave / float(num_scales), aspect, - anchor_scales[level - min_level])) + ( + ( + featmap_sizes[0][0] / float(featmap_sizes[level][0]), + featmap_sizes[0][1] / float(featmap_sizes[level][1]), + ), + scale_octave / float(num_scales), + aspect, + anchor_scales[level - min_level], + ) + ) """Generates multiscale anchor boxes.""" boxes_all = [] @@ -183,8 +185,8 @@ def generate_anchors_EDETLITE(batch_size, boxes_level = [] for config in configs: stride, octave_scale, aspect, anchor_scale = config - base_anchor_size_x = anchor_scale * stride[1] * 2 ** octave_scale - base_anchor_size_y = anchor_scale * stride[0] * 2 ** octave_scale + base_anchor_size_x = anchor_scale * stride[1] * 2**octave_scale + base_anchor_size_y = anchor_scale * stride[0] * 2**octave_scale if isinstance(aspect, list): aspect_x, aspect_y = aspect else: @@ -199,8 +201,7 @@ def generate_anchors_EDETLITE(batch_size, xv = xv.reshape(-1) yv = yv.reshape(-1) - boxes = np.vstack((yv - anchor_size_y_2, xv - anchor_size_x_2, - yv + anchor_size_y_2, xv + anchor_size_x_2)) + boxes = np.vstack((yv - anchor_size_y_2, xv - anchor_size_x_2, yv + anchor_size_y_2, xv + anchor_size_x_2)) boxes = np.swapaxes(boxes, 0, 1) boxes_level.append(np.expand_dims(boxes, axis=1)) diff --git a/picamera2/devices/imx500/postprocess_highernet.py b/picamera2/devices/imx500/postprocess_highernet.py index 6cd97539..3965acb5 100644 --- a/picamera2/devices/imx500/postprocess_highernet.py +++ b/picamera2/devices/imx500/postprocess_highernet.py @@ -14,52 +14,54 @@ try: from munkres import Munkres -except ImportError: - raise ImportError("Please install munkres first. `pip3 install --break-system-packages munkres`") +except ImportError as e: + raise ImportError("Please install munkres first. `pip3 install --break-system-packages munkres`") from e default_joint_order = [0, 1, 2, 3, 4, 5, 6, 11, 12, 7, 8, 9, 10, 13, 14, 15, 16] -def postprocess_higherhrnet(outputs: list[np.ndarray, np.ndarray], - img_size, - img_w_pad, - img_h_pad, - network_postprocess, - num_joints=17, - tag_per_joint=True, - joint_order=default_joint_order, - detection_threshold=0.3, - max_num_people=30, - nms_kernel=5, - nms_padding=2, - ignore_too_much=False, - use_detection_val=True, - tag_threshold=1.0, - adjust=False, - refine=False, - input_image_size=(288, 384), - output_shape=(144, 192)) -> Tuple[list[list], list, list[list]]: +def postprocess_higherhrnet( + outputs: list[np.ndarray, np.ndarray], + img_size, + img_w_pad, + img_h_pad, + network_postprocess, + num_joints=17, + tag_per_joint=True, + joint_order=default_joint_order, + detection_threshold=0.3, + max_num_people=30, + nms_kernel=5, + nms_padding=2, + ignore_too_much=False, + use_detection_val=True, + tag_threshold=1.0, + adjust=False, + refine=False, + input_image_size=(288, 384), + output_shape=(144, 192), +) -> Tuple[list[list], list, list[list]]: all_preds = [] all_scores = [] if network_postprocess: # outputs [[B, max_num_people, num_joints], [B, max_num_people, num_joints], [B, max_num_people, num_joints]] - grouped, scores = parse(network_outputs=[outputs[0][0, ...], - outputs[1][0, ...], - outputs[2][0, ...]], - output_shape=output_shape, - adjust=adjust, - refine=refine, - network_postprocess=network_postprocess, - tag_per_joint=tag_per_joint, - max_num_people=max_num_people, - nms_kernel=nms_kernel, - nms_padding=nms_padding, - num_joints=num_joints, - joint_order=joint_order, - detection_threshold=detection_threshold, - ignore_too_much=ignore_too_much, - use_detection_val=use_detection_val, - tag_threshold=tag_threshold) + grouped, scores = parse( + network_outputs=[outputs[0][0, ...], outputs[1][0, ...], outputs[2][0, ...]], + output_shape=output_shape, + adjust=adjust, + refine=refine, + network_postprocess=network_postprocess, + tag_per_joint=tag_per_joint, + max_num_people=max_num_people, + nms_kernel=nms_kernel, + nms_padding=nms_padding, + num_joints=num_joints, + joint_order=joint_order, + detection_threshold=detection_threshold, + ignore_too_much=ignore_too_much, + use_detection_val=use_detection_val, + tag_threshold=tag_threshold, + ) else: out0 = outputs[0][0] out1 = outputs[1][0] @@ -70,21 +72,23 @@ def postprocess_higherhrnet(outputs: list[np.ndarray, np.ndarray], # average heatmaps from both outputs heatmaps = (out0[..., :17] + out1) / 2 tags = out0[..., 17:] - grouped, scores = parse(network_outputs=[heatmaps, tags], - output_shape=output_shape, - adjust=adjust, - refine=refine, - network_postprocess=network_postprocess, - tag_per_joint=tag_per_joint, - max_num_people=max_num_people, - nms_kernel=nms_kernel, - nms_padding=nms_padding, - num_joints=num_joints, - joint_order=joint_order, - detection_threshold=detection_threshold, - ignore_too_much=ignore_too_much, - use_detection_val=use_detection_val, - tag_threshold=tag_threshold) + grouped, scores = parse( + network_outputs=[heatmaps, tags], + output_shape=output_shape, + adjust=adjust, + refine=refine, + network_postprocess=network_postprocess, + tag_per_joint=tag_per_joint, + max_num_people=max_num_people, + nms_kernel=nms_kernel, + nms_padding=nms_padding, + num_joints=num_joints, + joint_order=joint_order, + detection_threshold=detection_threshold, + ignore_too_much=ignore_too_much, + use_detection_val=use_detection_val, + tag_threshold=tag_threshold, + ) # scale keypoints coordinates to input image size scale_factor = (np.array(input_image_size) / output_shape).reshape((1, 1, 2)) @@ -96,17 +100,13 @@ def postprocess_higherhrnet(outputs: list[np.ndarray, np.ndarray], grouped[img_index][:, :, 0] = grouped[img_index][:, :, 0] - img_w_pad[0] grouped[img_index][:, :, 1] = grouped[img_index][:, :, 1] - img_h_pad[0] # rescale to original image size - resized_input_image = np.array(input_image_size) - np.array( - (sum(img_h_pad), - sum(img_w_pad))) + resized_input_image = np.array(input_image_size) - np.array((sum(img_h_pad), sum(img_w_pad))) s = (np.array(img_size) / resized_input_image).reshape((1, 1, 2)) grouped[img_index][:, :, :2] = grouped[img_index][:, :, :2] * s # Calculate zero keypoint zero_kpt = np.zeros((1, 4)) - resized_input_image = np.array(input_image_size) - np.array( - (sum(img_h_pad), - sum(img_w_pad))) + resized_input_image = np.array(input_image_size) - np.array((sum(img_h_pad), sum(img_w_pad))) s = (np.array(img_size) / resized_input_image).reshape((1, 1, 2)) zero_kpt[:, 0] = zero_kpt[:, 0] - img_w_pad[0] zero_kpt[:, 1] = zero_kpt[:, 1] - img_h_pad[0] @@ -122,14 +122,7 @@ def postprocess_higherhrnet(outputs: list[np.ndarray, np.ndarray], area = (np.max(kpt[:, 0]) - np.min(kpt[:, 0])) * (np.max(kpt[:, 1]) - np.min(kpt[:, 1])) # kpt [17, 4] kpt = processKeypoints(kpt) - kpts.append( - { - 'keypoints': kpt[:, 0:3], - 'score': all_scores[idx][idx_kpt], - 'tags': kpt[:, 3], - 'area': area - } - ) + kpts.append({'keypoints': kpt[:, 0:3], 'score': all_scores[idx][idx_kpt], 'tags': kpt[:, 3], 'area': area}) # _coco_keypoint_results_one_category_kernel out_keypoints = [] out_scores = [] @@ -140,13 +133,8 @@ def postprocess_higherhrnet(outputs: list[np.ndarray, np.ndarray], if len(img_kpts) == 0: return [], [], [] - _key_points = np.array( - [img_kpts[k]['keypoints'] for k in range(len(img_kpts))] - ) - key_points = np.zeros( - (_key_points.shape[0], num_joints * 3), - dtype=np.float32 - ) + _key_points = np.array([img_kpts[k]['keypoints'] for k in range(len(img_kpts))]) + key_points = np.zeros((_key_points.shape[0], num_joints * 3), dtype=np.float32) for ipt in range(num_joints): key_points[:, ipt * 3 + 0] = _key_points[:, ipt, 0] @@ -167,55 +155,57 @@ def postprocess_higherhrnet(outputs: list[np.ndarray, np.ndarray], return out_keypoints, out_scores, out_bbox -def parse(network_outputs, - output_shape, - adjust=False, - refine=False, - network_postprocess=False, - tag_per_joint=17, - max_num_people=30, - nms_kernel=5, - nms_padding=2, - num_joints=17, - joint_order=default_joint_order, - detection_threshold=0.1, - ignore_too_much=False, - use_detection_val=True, - tag_threshold=1.0 - ): +def parse( + network_outputs, + output_shape, + adjust=False, + refine=False, + network_postprocess=False, + tag_per_joint=17, + max_num_people=30, + nms_kernel=5, + nms_padding=2, + num_joints=17, + joint_order=default_joint_order, + detection_threshold=0.1, + ignore_too_much=False, + use_detection_val=True, + tag_threshold=1.0, +): if network_postprocess: tag_k, ind_k, val_k = network_outputs x = ind_k % output_shape[1] y = (ind_k / output_shape[1]).astype(ind_k.dtype) ind_k = np.stack([x, y], axis=2) - topk_output_dict = {'tag_k': tag_k[np.newaxis, ...], - 'loc_k': ind_k[np.newaxis, ...], - 'val_k': val_k[np.newaxis, ...], - } + topk_output_dict = {'tag_k': tag_k[np.newaxis, ...], 'loc_k': ind_k[np.newaxis, ...], 'val_k': val_k[np.newaxis, ...]} else: det, tag = network_outputs # topk_output_dict # {'tag_k': [num_images, max_num_people, num_joints], # 'loc_k': [num_images, max_num_people, num_joints, 2], # 'val_k': [num_images, max_num_people, num_joints]} - topk_output_dict = top_k(det=det, - tag=tag, - tag_per_joint=tag_per_joint, - max_num_people=max_num_people, - nms_kernel=nms_kernel, - nms_padding=nms_padding) + topk_output_dict = top_k( + det=det, + tag=tag, + tag_per_joint=tag_per_joint, + max_num_people=max_num_people, + nms_kernel=nms_kernel, + nms_padding=nms_padding, + ) # ans [num_joints_detected, num_joints, 4] - ans = match(tag_k=topk_output_dict['tag_k'], - loc_k=topk_output_dict['loc_k'], - val_k=topk_output_dict['val_k'], - num_joints=num_joints, - joint_order=joint_order, - detection_threshold=detection_threshold, - max_num_people=max_num_people, - ignore_too_much=ignore_too_much, - use_detection_val=use_detection_val, - tag_threshold=tag_threshold) + ans = match( + tag_k=topk_output_dict['tag_k'], + loc_k=topk_output_dict['loc_k'], + val_k=topk_output_dict['val_k'], + num_joints=num_joints, + joint_order=joint_order, + detection_threshold=detection_threshold, + max_num_people=max_num_people, + ignore_too_much=ignore_too_much, + use_detection_val=use_detection_val, + tag_threshold=tag_threshold, + ) if adjust: # ans [[num_joints_detected, num_joints, 4]] ans = adjust_func(ans, det[np.newaxis, ...]) # TODO support batch size > 1 @@ -239,19 +229,12 @@ def ResizeBilinear(img, new_height, new_width): return cv2.resize(img, (new_width, new_height)) -def top_k(det, - tag, - tag_per_joint=17, - max_num_people=30, - nms_kernel=5, - nms_padding=2): +def top_k(det, tag, tag_per_joint=17, max_num_people=30, nms_kernel=5, nms_padding=2): # det [144, 192, 17] # tag [144, 192, 17] # det [144, 192, 17] - det = nms(det, - nms_kernel=nms_kernel, - nms_padding=nms_padding) + det = nms(det, nms_kernel=nms_kernel, nms_padding=nms_padding) # num_images 1 # h 144 # w 192 @@ -286,15 +269,10 @@ def top_k(det, # {'tag_k': [num_images, max_num_people, num_joints], # 'loc_k': [num_images, max_num_people, num_joints, 2], # 'val_k': [num_images, max_num_people, num_joints]} - return {'tag_k': tag_k, - 'loc_k': ind_k, - 'val_k': val_k, - } + return {'tag_k': tag_k, 'loc_k': ind_k, 'val_k': val_k} -def nms(det, - nms_kernel=5, - nms_padding=2): +def nms(det, nms_kernel=5, nms_padding=2): # det [144, 192, 17] # maxm [144, 192, 17] maxm = np_max_pool(det, k=nms_kernel, p=nms_padding) @@ -303,10 +281,7 @@ def nms(det, return det -def np_max_pool(x, - k=5, - p=2, - p_value=0): +def np_max_pool(x, k=5, p=2, p_value=0): # x [144, 192, 17] # k - kernel size (h, w) # p - padding size (top, bottom, left, right) @@ -320,9 +295,23 @@ def np_max_pool(x, # y [148, 196, 17 y = np.pad(x, p) out = np.concatenate( - [np.max(np.concatenate([y[ky:ky + y.shape[0] - k[0] + 1, kx:kx + y.shape[1] - k[1] + 1, c:c + 1] - for ky in range(k[0]) - for kx in range(k[1])], 2), axis=2, keepdims=True) for c in range(y.shape[2])], 2) + [ + np.max( + np.concatenate( + [ + y[ky : ky + y.shape[0] - k[0] + 1, kx : kx + y.shape[1] - k[1] + 1, c : c + 1] + for ky in range(k[0]) + for kx in range(k[1]) + ], + 2, + ), + axis=2, + keepdims=True, + ) + for c in range(y.shape[2]) + ], + 2, + ) # out [144, 192, 17] return out @@ -340,42 +329,49 @@ def np_topk(x, k): for kp in range(n_keypoints): # _inds [k] _inds = np.argpartition(x[img, :, kp], -k)[-k:] - _inds = _inds[np.argsort(x[img, _inds, kp], )][::-1] + _inds = _inds[np.argsort(x[img, _inds, kp])][::-1] inds[img, :, kp] = _inds vals[img, :, kp] = x[img, _inds, kp] return vals, inds -def match(tag_k, - loc_k, - val_k, - num_joints=17, - joint_order=default_joint_order, - detection_threshold=0.1, - max_num_people=30, - ignore_too_much=False, - use_detection_val=True, - tag_threshold=1.0): +def match( + tag_k, + loc_k, + val_k, + num_joints=17, + joint_order=default_joint_order, + detection_threshold=0.1, + max_num_people=30, + ignore_too_much=False, + use_detection_val=True, + tag_threshold=1.0, +): def m(x): - return match_by_tag(inp=x, - num_joints=num_joints, - joint_order=joint_order, - detection_threshold=detection_threshold, - max_num_people=max_num_people, - ignore_too_much=ignore_too_much, - use_detection_val=use_detection_val, - tag_threshold=tag_threshold) + return match_by_tag( + inp=x, + num_joints=num_joints, + joint_order=joint_order, + detection_threshold=detection_threshold, + max_num_people=max_num_people, + ignore_too_much=ignore_too_much, + use_detection_val=use_detection_val, + tag_threshold=tag_threshold, + ) + return list(map(m, zip(tag_k, loc_k, val_k))) -def match_by_tag(inp, - num_joints=17, - joint_order=default_joint_order, - detection_threshold=0.1, - max_num_people=30, - ignore_too_much=False, - use_detection_val=True, - tag_threshold=1.0): +def match_by_tag( + inp, + num_joints=17, + joint_order=default_joint_order, + detection_threshold=0.1, + max_num_people=30, + ignore_too_much=False, + use_detection_val=True, + tag_threshold=1.0, +): # tag_k [num_images, max_num_people, num_joints] # loc_k [num_images, max_num_people, num_joints, 2] # val_k [num_images, max_num_people, num_joints] @@ -389,9 +385,9 @@ def match_by_tag(inp, idx = joint_order[i] # tags [max_num_people, 1] - tags = tag_k[:, idx:idx + 1] + tags = tag_k[:, idx : idx + 1] # joints [max_num_people, 4] - joints = np.concatenate((loc_k[:, idx, :], val_k[:, idx:idx + 1], tags), 1) + joints = np.concatenate((loc_k[:, idx, :], val_k[:, idx : idx + 1], tags), 1) # mask [max_num_people] mask = joints[:, 2] > detection_threshold tags = tags[mask] @@ -409,8 +405,7 @@ def match_by_tag(inp, grouped_keys = list(joint_dict.keys())[:max_num_people] grouped_tags = [np.mean(tag_dict[i], axis=0) for i in grouped_keys] - if ignore_too_much \ - and len(grouped_keys) == max_num_people: + if ignore_too_much and len(grouped_keys) == max_num_people: continue diff = joints[:, None, 3:] - np.array(grouped_tags)[None, :, :] @@ -424,28 +419,17 @@ def match_by_tag(inp, num_grouped = diff.shape[1] if num_added > num_grouped: - diff_normed = np.concatenate( - ( - diff_normed, - np.zeros((num_added, num_added - num_grouped)) + 1e10 - ), - axis=1 - ) + diff_normed = np.concatenate((diff_normed, np.zeros((num_added, num_added - num_grouped)) + 1e10), axis=1) pairs = py_max_match(diff_normed) for row, col in pairs: - if ( - row < num_added - and col < num_grouped - and diff_saved[row][col] < tag_threshold - ): + if row < num_added and col < num_grouped and diff_saved[row][col] < tag_threshold: key = grouped_keys[col] joint_dict[key][idx] = joints[row] tag_dict[key].append(tags[row]) else: key = tags[row][0] - joint_dict.setdefault(key, np.copy(default_))[idx] = \ - joints[row] + joint_dict.setdefault(key, np.copy(default_))[idx] = joints[row] tag_dict[key] = [tags[row]] # ans [len(joint_dict), num_joints, 4] @@ -509,7 +493,7 @@ def refine_func(det, tag, keypoints): # score of joints i at all position tmp = det[:, :, i] # distance of all tag values with mean tag of current detected people - tt = (((tag[:, :, i] - prev_tag[None, None, :]) ** 2).sum(axis=2) ** 0.5) + tt = ((tag[:, :, i] - prev_tag[None, None, :]) ** 2).sum(axis=2) ** 0.5 tmp2 = tmp - np.round(tt) # find maximum position @@ -553,10 +537,6 @@ def processKeypoints(keypoints): if keypoints[:, 2].max() > 0: num_keypoints = keypoints.shape[0] for i in range(num_keypoints): - tmp[i][0:3] = [ - float(keypoints[i][0]), - float(keypoints[i][1]), - float(keypoints[i][2]) - ] + tmp[i][0:3] = [float(keypoints[i][0]), float(keypoints[i][1]), float(keypoints[i][2])] return tmp diff --git a/picamera2/devices/imx500/postprocess_nanodet.py b/picamera2/devices/imx500/postprocess_nanodet.py index b7f1d0b8..7548c0ec 100644 --- a/picamera2/devices/imx500/postprocess_nanodet.py +++ b/picamera2/devices/imx500/postprocess_nanodet.py @@ -12,10 +12,9 @@ from picamera2.devices.imx500.postprocess import combined_nms, softmax -def postprocess_nanodet_detection(outputs, - conf: float = 0.0, - iou_thres: float = 0.65, - max_out_dets: int = 300) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +def postprocess_nanodet_detection( + outputs, conf: float = 0.0, iou_thres: float = 0.65, max_out_dets: int = 300 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: reg_max = 7 num_categories = 80 classes = outputs[..., :num_categories] diff --git a/picamera2/devices/imx500/postprocess_yolov5.py b/picamera2/devices/imx500/postprocess_yolov5.py index d9c5600f..e218a6a7 100644 --- a/picamera2/devices/imx500/postprocess_yolov5.py +++ b/picamera2/devices/imx500/postprocess_yolov5.py @@ -4,29 +4,29 @@ This code is based on: https://github.com/ultralytics/ultralytics """ + from typing import List import cv2 import numpy as np -from picamera2.devices.imx500.postprocess import ( - BoxFormat, convert_to_ymin_xmin_ymax_xmax_format, nms) +from picamera2.devices.imx500.postprocess import BoxFormat, convert_to_ymin_xmin_ymax_xmax_format, nms -default_anchors = [[10, 13, 16, 30, 33, 23], - [30, 61, 62, 45, 59, 119], - [116, 90, 156, 198, 373, 326]] +default_anchors = [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]] default_strides = [8, 16, 32] -def postprocess_yolov5_detection(outputs: List[np.ndarray], - model_input_shape=(640, 640), - num_categories=80, - min_wh=2, - max_wh=7680, - conf_thres: float = 0.001, - iou_thres: float = 0.65, - max_nms_dets: int = 5000, - max_out_dets: int = 1000): +def postprocess_yolov5_detection( + outputs: List[np.ndarray], + model_input_shape=(640, 640), + num_categories=80, + min_wh=2, + max_wh=7680, + conf_thres: float = 0.001, + iou_thres: float = 0.65, + max_nms_dets: int = 5000, + max_out_dets: int = 1000, +): H, W = model_input_shape ############################################################ # Box decoding @@ -86,12 +86,7 @@ def postprocess_yolov5_detection(outputs: List[np.ndarray], return post_processed_outputs[0]['boxes'], post_processed_outputs[0]['scores'], post_processed_outputs[0]['classes'] -def box_decoding_yolov5n(tensors, - num_categories=80, - H=640, - W=640, - anchors=default_anchors, - strides=default_strides): +def box_decoding_yolov5n(tensors, num_categories=80, H=640, W=640, anchors=default_anchors, strides=default_strides): # Tensors box format: [x_c, y_c, w, h] no = num_categories + 5 # number of outputs per anchor nl = len(anchors) # number of detection layers @@ -117,8 +112,9 @@ def box_decoding_yolov5n(tensors, # same as in preprocess but differs in h/w location -def scale_boxes(boxes: np.ndarray, h_image: int, w_image: int, h_model: int, w_model: int, - preserve_aspect_ratio: bool) -> np.ndarray: +def scale_boxes( + boxes: np.ndarray, h_image: int, w_image: int, h_model: int, w_model: int, preserve_aspect_ratio: bool +) -> np.ndarray: """ Scale and offset bounding boxes based on model output size and original image size. @@ -212,10 +208,12 @@ def apply_normalization(boxes, orig_width, orig_height, boxes_format): # Locate at tutorials def coco80_to_coco91(x): # converts 80-index to 91-index + # fmt: off coco91Indexs = np.array( [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90]) + # fmt: on return coco91Indexs[x.astype(np.int32)] @@ -229,9 +227,11 @@ def yolov5n_preprocess(img): resize_ratio = max(img.shape[0] / new_height, img.shape[1] / new_width) height_tag = int(np.round(img.shape[0] / resize_ratio)) width_tag = int(np.round(img.shape[1] / resize_ratio)) - pad_values = ((int((new_height - height_tag) / 2), int((new_height - height_tag) / 2 + 0.5)), - (int((new_width - width_tag) / 2), int((new_width - width_tag) / 2 + 0.5)), - (0, 0)) + pad_values = ( + (int((new_height - height_tag) / 2), int((new_height - height_tag) / 2 + 0.5)), + (int((new_width - width_tag) / 2), int((new_width - width_tag) / 2 + 0.5)), + (0, 0), + ) resized_img = cv2.resize(img, (width_tag, height_tag), interpolation=resize_method) padded_img = np.pad(resized_img, pad_values, constant_values=pad_value) diff --git a/picamera2/devices/imx500/postprocess_yolov8.py b/picamera2/devices/imx500/postprocess_yolov8.py index 91a1d63a..0ab75636 100644 --- a/picamera2/devices/imx500/postprocess_yolov8.py +++ b/picamera2/devices/imx500/postprocess_yolov8.py @@ -4,20 +4,25 @@ This code is based on: https://github.com/ultralytics/ultralytics """ + from typing import Tuple import cv2 import numpy as np from picamera2.devices.imx500.postprocess import ( - BoxFormat, combined_nms, combined_nms_seg, - convert_to_ymin_xmin_ymax_xmax_format, crop_mask, nms) - - -def postprocess_yolov8_detection(outputs: Tuple[np.ndarray, np.ndarray], - conf: float = 0.3, - iou_thres: float = 0.7, - max_out_dets: int = 50) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + BoxFormat, + combined_nms, + combined_nms_seg, + convert_to_ymin_xmin_ymax_xmax_format, + crop_mask, + nms, +) + + +def postprocess_yolov8_detection( + outputs: Tuple[np.ndarray, np.ndarray], conf: float = 0.3, iou_thres: float = 0.7, max_out_dets: int = 50 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Postprocess the outputs of a YOLOv8 model for object detection @@ -44,10 +49,9 @@ def postprocess_yolov8_detection(outputs: Tuple[np.ndarray, np.ndarray], return combined_nms(xd[..., :4], xd[..., 4:84], iou_thres, conf, max_out_dets) -def postprocess_yolov8_keypoints(outputs: Tuple[np.ndarray, np.ndarray, np.ndarray], - conf: float = 0.3, - iou_thres: float = 0.7, - max_out_dets: int = 300) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +def postprocess_yolov8_keypoints( + outputs: Tuple[np.ndarray, np.ndarray, np.ndarray], conf: float = 0.3, iou_thres: float = 0.7, max_out_dets: int = 300 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Postprocess the outputs of a YOLOv8 model for object detection and pose estimation. @@ -96,10 +100,9 @@ class predictions, and keypoint predictions. return nms_bbox, nms_scores, nms_kpts -def postprocess_yolov8_inst_seg(outputs: Tuple[np.ndarray, np.ndarray, np.ndarray], - conf: float = 0.001, - iou_thres: float = 0.7, - max_out_dets: int = 300) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +def postprocess_yolov8_inst_seg( + outputs: Tuple[np.ndarray, np.ndarray, np.ndarray], conf: float = 0.001, iou_thres: float = 0.7, max_out_dets: int = 300 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: feat_sizes = np.array([80, 40, 20]) stride_sizes = np.array([8, 16, 32]) a, s = (x.transpose() for x in make_anchors_yolo_v8(feat_sizes, stride_sizes, 0.5)) @@ -109,8 +112,9 @@ def postprocess_yolov8_inst_seg(outputs: Tuple[np.ndarray, np.ndarray, np.ndarra detect_out = np.concatenate((dbox, y_cls), 1) xd = detect_out.transpose([0, 2, 1]) - nms_bbox, nms_scores, nms_classes, ymask_weights = combined_nms_seg(xd[..., :4], xd[..., 4:84], - ymask_weights, iou_thres, conf, max_out_dets)[0] + nms_bbox, nms_scores, nms_classes, ymask_weights = combined_nms_seg( + xd[..., :4], xd[..., 4:84], ymask_weights, iou_thres, conf, max_out_dets + )[0] if len(nms_scores) == 0: final_masks = y_masks else: @@ -150,7 +154,7 @@ def dist2bbox_yolo_v8(distance, anchor_points, xywh=True, dim=-1): def pad_with_zeros(mask, roi, isp_output_size): new_shape = (isp_output_size.width, isp_output_size.height, mask.shape[2]) padded_mask = np.zeros(new_shape, dtype=mask.dtype) - padded_mask[roi.x:roi.x + mask.shape[0], roi.y:roi.y + mask.shape[1], :] = mask + padded_mask[roi.x : roi.x + mask.shape[0], roi.y : roi.y + mask.shape[1], :] = mask return padded_mask diff --git a/picamera2/devices/imx708/imx708.py b/picamera2/devices/imx708/imx708.py index 631cca02..70e9e276 100644 --- a/picamera2/devices/imx708/imx708.py +++ b/picamera2/devices/imx708/imx708.py @@ -5,7 +5,7 @@ from picamera2 import Picamera2 -HDR_CTRL_ID = 0x009a0915 +HDR_CTRL_ID = 0x009A0915 class IMX708: diff --git a/picamera2/dma_heap.py b/picamera2/dma_heap.py index 38c97374..5c56f88a 100644 --- a/picamera2/dma_heap.py +++ b/picamera2/dma_heap.py @@ -6,24 +6,19 @@ from videodev2 import _IOW, _IOWR _log = logging.getLogger("picamera2") -heapNames = [ - "/dev/dma_heap/vidbuf_cached", - "/dev/dma_heap/linux,cma" -] +heapNames = ["/dev/dma_heap/vidbuf_cached", "/dev/dma_heap/linux,cma"] # Kernel stuff from linux/dma-buf.h class dma_buf_sync(ctypes.Structure): - _fields_ = [ - ('flags', ctypes.c_uint64), - ] + _fields_ = [('flags', ctypes.c_uint64)] -DMA_BUF_SYNC_READ = (1 << 0) -DMA_BUF_SYNC_WRITE = (2 << 0) -DMA_BUF_SYNC_RW = (DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE) -DMA_BUF_SYNC_START = (0 << 2) -DMA_BUF_SYNC_END = (1 << 2) +DMA_BUF_SYNC_READ = 1 << 0 +DMA_BUF_SYNC_WRITE = 2 << 0 +DMA_BUF_SYNC_RW = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE +DMA_BUF_SYNC_START = 0 << 2 +DMA_BUF_SYNC_END = 1 << 2 DMA_BUF_BASE = 'b' DMA_BUF_IOCTL_SYNC = _IOW(DMA_BUF_BASE, 0, dma_buf_sync) diff --git a/picamera2/encoders/encoder.py b/picamera2/encoders/encoder.py index d0d5f559..9f77fa5e 100644 --- a/picamera2/encoders/encoder.py +++ b/picamera2/encoders/encoder.py @@ -289,17 +289,12 @@ def send_streams(self, output): def _send_streams(self, output): # Send video stream information to the output. - FORMAT_TABLE = {"YUV420": "yuv420p", - "BGR888": "rgb24", - "RGB888": "bgr24", - "XBGR8888": "rgba", - "XRGB8888": "bgra"} + FORMAT_TABLE = {"YUV420": "yuv420p", "BGR888": "rgb24", "RGB888": "bgr24", "XBGR8888": "rgba", "XRGB8888": "bgra"} pix_fmt = FORMAT_TABLE.get(self._format) rate = Fraction(1000000, self._framerate) if pix_fmt is None and output.needs_add_stream: raise RuntimeError(f"Output does not support {self._format}") - output._add_stream("video", "rawvideo", pix_fmt=pix_fmt, rate=rate, - width=self.width, height=self.height) + output._add_stream("video", "rawvideo", pix_fmt=pix_fmt, rate=rate, width=self.width, height=self.height) def start(self, quality=None): with self._lock: @@ -318,6 +313,7 @@ def start(self, quality=None): # Save low-powered Pis from importing av unless it is needed. global av import av + self._audio_input_container = av.open(**self.audio_input) self._audio_input_stream = self._audio_input_container.streams.get(audio=0)[0] self._audio_output_container = av.open("/dev/null", 'w', format="null") diff --git a/picamera2/encoders/h264_encoder.py b/picamera2/encoders/h264_encoder.py index a50d7e2e..7d97406c 100644 --- a/picamera2/encoders/h264_encoder.py +++ b/picamera2/encoders/h264_encoder.py @@ -1,17 +1,20 @@ """H264 encoder functionality""" -from videodev2 import (V4L2_CID_MPEG_VIDEO_H264_I_PERIOD, - V4L2_CID_MPEG_VIDEO_H264_LEVEL, - V4L2_CID_MPEG_VIDEO_H264_MAX_QP, - V4L2_CID_MPEG_VIDEO_H264_MIN_QP, - V4L2_CID_MPEG_VIDEO_H264_PROFILE, - V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER, - V4L2_MPEG_VIDEO_H264_LEVEL_4_1, - V4L2_MPEG_VIDEO_H264_LEVEL_4_2, - V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, - V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE, - V4L2_MPEG_VIDEO_H264_PROFILE_HIGH, - V4L2_MPEG_VIDEO_H264_PROFILE_MAIN, V4L2_PIX_FMT_H264) +from videodev2 import ( + V4L2_CID_MPEG_VIDEO_H264_I_PERIOD, + V4L2_CID_MPEG_VIDEO_H264_LEVEL, + V4L2_CID_MPEG_VIDEO_H264_MAX_QP, + V4L2_CID_MPEG_VIDEO_H264_MIN_QP, + V4L2_CID_MPEG_VIDEO_H264_PROFILE, + V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER, + V4L2_MPEG_VIDEO_H264_LEVEL_4_1, + V4L2_MPEG_VIDEO_H264_LEVEL_4_2, + V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, + V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE, + V4L2_MPEG_VIDEO_H264_PROFILE_HIGH, + V4L2_MPEG_VIDEO_H264_PROFILE_MAIN, + V4L2_PIX_FMT_H264, +) from picamera2.encoders import Quality from picamera2.encoders.v4l2_encoder import V4L2Encoder @@ -20,8 +23,9 @@ class H264Encoder(V4L2Encoder): """Uses functionality from V4L2Encoder""" - def __init__(self, bitrate=None, repeat=True, iperiod=None, framerate=None, enable_sps_framerate=False, - qp=None, profile=None): + def __init__( + self, bitrate=None, repeat=True, iperiod=None, framerate=None, enable_sps_framerate=False, qp=None, profile=None + ): """H264 Encoder :param bitrate: Bitrate, default None @@ -53,10 +57,12 @@ def _start(self): self._controls = [] # These names match what FFmpeg uses. - profile_lookup = {"baseline": V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, - "constrained baseline": V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE, - "main": V4L2_MPEG_VIDEO_H264_PROFILE_MAIN, - "high": V4L2_MPEG_VIDEO_H264_PROFILE_HIGH} + profile_lookup = { + "baseline": V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, + "constrained baseline": V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE, + "main": V4L2_MPEG_VIDEO_H264_PROFILE_MAIN, + "high": V4L2_MPEG_VIDEO_H264_PROFILE_HIGH, + } if self.profile: if not isinstance(self.profile, str): raise RuntimeError("Profile should be a string value") @@ -97,15 +103,19 @@ def _start(self): def _setup(self, quality): # If an explicit quality was specified, use it, otherwise try to preserve any bitrate/qp # the user may have set for themselves. + # fmt: off if quality is not None or \ (getattr(self, "bitrate", None) is None and getattr(self, "qp", None) is None): + # fmt: on quality = Quality.MEDIUM if quality is None else quality # These are suggested bitrates for 1080p30 in Mbps + # fmt: off BITRATE_TABLE = {Quality.VERY_LOW: 2, Quality.LOW: 4, Quality.MEDIUM: 6, Quality.HIGH: 9, Quality.VERY_HIGH: 15} + # fmt: on reference_complexity = 1920 * 1080 * 30 actual_complexity = self.width * self.height * getattr(self, "framerate", 30) reference_bitrate = BITRATE_TABLE[quality] * 1000000 diff --git a/picamera2/encoders/jpeg_encoder.py b/picamera2/encoders/jpeg_encoder.py index 52379b90..93f09fac 100644 --- a/picamera2/encoders/jpeg_encoder.py +++ b/picamera2/encoders/jpeg_encoder.py @@ -10,10 +10,7 @@ class JpegEncoder(MultiEncoder): """Uses functionality from MultiEncoder""" - FORMAT_TABLE = {"XBGR8888": "RGBX", - "XRGB8888": "BGRX", - "BGR888": "RGB", - "RGB888": "BGR"} + FORMAT_TABLE = {"XBGR8888": "RGBX", "XRGB8888": "BGRX", "BGR888": "RGB", "RGB888": "BGR"} def __init__(self, num_threads=4, q=None, colour_space=None, colour_subsampling='420'): """Initialises Jpeg encoder @@ -49,13 +46,14 @@ def encode_func(self, request, name): width, height = request.config[name]['size'] Y = m.array[:height, :width] reshaped = m.array.reshape((m.array.shape[0] * 2, m.array.strides[0] // 2)) - U = reshaped[2 * height: 2 * height + height // 2, :width // 2] - V = reshaped[2 * height + height // 2:, :width // 2] + U = reshaped[2 * height : 2 * height + height // 2, : width // 2] + V = reshaped[2 * height + height // 2 :, : width // 2] return simplejpeg.encode_jpeg_yuv_planes(Y, U, V, self.q) if self.colour_space is None: self.colour_space = self.FORMAT_TABLE[request.config[name]["format"]] - return simplejpeg.encode_jpeg(m.array, quality=self.q, colorspace=self.colour_space, - colorsubsampling=self.colour_subsampling) + return simplejpeg.encode_jpeg( + m.array, quality=self.q, colorspace=self.colour_space, colorsubsampling=self.colour_subsampling + ) def _setup(self, quality): # If an explicit quality was specified, use it, otherwise try to preserve any q value @@ -63,9 +61,5 @@ def _setup(self, quality): if quality is not None or getattr(self, "q", None) is None: quality = Quality.MEDIUM if quality is None else quality # Image size and framerate isn't an issue here, you just get what you get. - Q_TABLE = {Quality.VERY_LOW: 25, - Quality.LOW: 35, - Quality.MEDIUM: 50, - Quality.HIGH: 65, - Quality.VERY_HIGH: 80} + Q_TABLE = {Quality.VERY_LOW: 25, Quality.LOW: 35, Quality.MEDIUM: 50, Quality.HIGH: 65, Quality.VERY_HIGH: 80} self.q = Q_TABLE[quality] diff --git a/picamera2/encoders/libav_h264_encoder.py b/picamera2/encoders/libav_h264_encoder.py index 142c2f7d..92935b06 100644 --- a/picamera2/encoders/libav_h264_encoder.py +++ b/picamera2/encoders/libav_h264_encoder.py @@ -19,6 +19,7 @@ def __init__(self, bitrate=None, repeat=True, iperiod=30, framerate=30, qp=None, # Save low-powered Pis from importing av unless it is needed. global av import av + super().__init__() self._codec = "h264" # for now only support h264 self.repeat = repeat @@ -55,15 +56,10 @@ def use_hw(self, value): def _setup(self, quality): # If an explicit quality was specified, use it, otherwise try to preserve any bitrate/qp # the user may have set for themselves. - if quality is not None or \ - (getattr(self, "bitrate", None) is None and getattr(self, "qp", None) is None): + if quality is not None or (getattr(self, "bitrate", None) is None and getattr(self, "qp", None) is None): quality = Quality.MEDIUM if quality is None else quality # These are suggested bitrates for 1080p30 in Mbps - BITRATE_TABLE = {Quality.VERY_LOW: 3, - Quality.LOW: 4, - Quality.MEDIUM: 7, - Quality.HIGH: 10, - Quality.VERY_HIGH: 14} + BITRATE_TABLE = {Quality.VERY_LOW: 3, Quality.LOW: 4, Quality.MEDIUM: 7, Quality.HIGH: 10, Quality.VERY_HIGH: 14} reference_complexity = 1920 * 1080 * 30 actual_complexity = self.width * self.height * getattr(self, "framerate", 30) reference_bitrate = BITRATE_TABLE[quality] * 1000000 @@ -125,11 +121,7 @@ def _start(self): self._stream.codec_context.time_base = Fraction(1, 1000000) self._stream.codec_context.options["tune"] = "zerolatency" - FORMAT_TABLE = {"YUV420": "yuv420p", - "BGR888": "rgb24", - "RGB888": "bgr24", - "XBGR8888": "rgba", - "XRGB8888": "bgra"} + FORMAT_TABLE = {"YUV420": "yuv420p", "BGR888": "rgb24", "RGB888": "bgr24", "XBGR8888": "rgba", "XRGB8888": "bgra"} self._av_input_format = FORMAT_TABLE[self._format] self._request_release_queue = collections.deque() diff --git a/picamera2/encoders/libav_mjpeg_encoder.py b/picamera2/encoders/libav_mjpeg_encoder.py index 05cdff33..8e1136dc 100644 --- a/picamera2/encoders/libav_mjpeg_encoder.py +++ b/picamera2/encoders/libav_mjpeg_encoder.py @@ -16,6 +16,7 @@ def __init__(self, bitrate=None, repeat=True, iperiod=30, framerate=30, qp=None) # Save low-powered Pis from importing av unless it is needed. global av import av + super().__init__() self._codec = "mjpeg" self.repeat = repeat @@ -29,14 +30,9 @@ def __init__(self, bitrate=None, repeat=True, iperiod=30, framerate=30, qp=None) def _setup(self, quality): # If an explicit quality was specified, use it, otherwise try to preserve any bitrate/qp # the user may have set for themselves. - if quality is not None or \ - (getattr(self, "bitrate", None) is None and getattr(self, "qp", None) is None): + if quality is not None or (getattr(self, "bitrate", None) is None and getattr(self, "qp", None) is None): quality = Quality.MEDIUM if quality is None else quality - QP_TABLE = {Quality.VERY_LOW: 31, - Quality.LOW: 15, - Quality.MEDIUM: 10, - Quality.HIGH: 5, - Quality.VERY_HIGH: 3} + QP_TABLE = {Quality.VERY_LOW: 31, Quality.LOW: 15, Quality.MEDIUM: 10, Quality.HIGH: 5, Quality.VERY_HIGH: 3} self.qp = QP_TABLE[quality] def _send_streams(self, output): @@ -78,11 +74,7 @@ def _start(self): self._stream.codec_context.time_base = Fraction(1, 1000000) - FORMAT_TABLE = {"YUV420": "yuv420p", - "BGR888": "rgb24", - "RGB888": "bgr24", - "XBGR8888": "rgba", - "XRGB8888": "bgra"} + FORMAT_TABLE = {"YUV420": "yuv420p", "BGR888": "rgb24", "RGB888": "bgr24", "XBGR8888": "rgba", "XRGB8888": "bgra"} self._av_input_format = FORMAT_TABLE[self._format] self._request_release_queue = collections.deque() diff --git a/picamera2/encoders/mjpeg_encoder.py b/picamera2/encoders/mjpeg_encoder.py index 23a650b2..4666ee11 100644 --- a/picamera2/encoders/mjpeg_encoder.py +++ b/picamera2/encoders/mjpeg_encoder.py @@ -29,11 +29,13 @@ def _setup(self, quality): if quality is not None or getattr(self, "bitrate", None) is None: quality = Quality.MEDIUM if quality is None else quality # These are suggested bitrates for 1080p30 in Mbps - BITRATE_TABLE = {Quality.VERY_LOW: 16, - Quality.LOW: 20, - Quality.MEDIUM: 30, - Quality.HIGH: 40, - Quality.VERY_HIGH: 50} + BITRATE_TABLE = { + Quality.VERY_LOW: 16, + Quality.LOW: 20, + Quality.MEDIUM: 30, + Quality.HIGH: 40, + Quality.VERY_HIGH: 50, + } reference_complexity = 1920 * 1080 * 30 actual_complexity = self.width * self.height * getattr(self, "framerate", 30) reference_bitrate = BITRATE_TABLE[quality] * 1000000 diff --git a/picamera2/encoders/v4l2_encoder.py b/picamera2/encoders/v4l2_encoder.py index 5010aa61..20ca9e75 100644 --- a/picamera2/encoders/v4l2_encoder.py +++ b/picamera2/encoders/v4l2_encoder.py @@ -38,11 +38,13 @@ def __init__(self, bitrate, pixformat): @property def _v4l2_format(self): """The input format to the codec, as a V4L2 type.""" - FORMAT_TABLE = {"RGB888": V4L2_PIX_FMT_BGR24, - "BGR888": V4L2_PIX_FMT_RGB24, - "XBGR8888": V4L2_PIX_FMT_BGR32, - "XRGB8888": V4L2_PIX_FMT_RGBA32, - "YUV420": V4L2_PIX_FMT_YUV420} + FORMAT_TABLE = { + "RGB888": V4L2_PIX_FMT_BGR24, + "BGR888": V4L2_PIX_FMT_RGB24, + "XBGR8888": V4L2_PIX_FMT_BGR32, + "XRGB8888": V4L2_PIX_FMT_RGBA32, + "YUV420": V4L2_PIX_FMT_YUV420, + } if self._format not in FORMAT_TABLE: raise RuntimeError("Unrecognised format", self._format, "for V4L2") return FORMAT_TABLE[self._format] @@ -141,10 +143,16 @@ def _start(self): buffer.length = 1 buffer.m.planes = planes fcntl.ioctl(self.vd, VIDIOC_QUERYBUF, buffer) - self.bufs[i] = (mmap.mmap(self.vd.fileno(), buffer.m.planes[0].length, - mmap.PROT_READ | mmap.PROT_WRITE, - mmap.MAP_SHARED, offset=buffer.m.planes[0].m.mem_offset), - buffer.m.planes[0].length) + self.bufs[i] = ( + mmap.mmap( + self.vd.fileno(), + buffer.m.planes[0].length, + mmap.PROT_READ | mmap.PROT_WRITE, + mmap.MAP_SHARED, + offset=buffer.m.planes[0].m.mem_offset, + ), + buffer.m.planes[0].length, + ) fcntl.ioctl(self.vd, VIDIOC_QBUF, buffer) typev = v4l2_buf_type(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) diff --git a/picamera2/formats.py b/picamera2/formats.py index 5b3afedd..c7450dd6 100644 --- a/picamera2/formats.py +++ b/picamera2/formats.py @@ -1,15 +1,37 @@ -YUV_FORMATS = {"NV21", "NV12", "YUV420", "YVU420", - "YVYU", "YUYV", "UYVY", "VYUY"} +YUV_FORMATS = {"NV21", "NV12", "YUV420", "YVU420", "YVYU", "YUYV", "UYVY", "VYUY"} RGB_FORMATS = {"BGR888", "RGB888", "XBGR8888", "XRGB8888", "RGB161616", "BGR161616"} -BAYER_FORMATS = {"SBGGR8", "SGBRG8", "SGRBG8", "SRGGB8", - "SBGGR10", "SGBRG10", "SGRBG10", "SRGGB10", - "SBGGR10_CSI2P", "SGBRG10_CSI2P", "SGRBG10_CSI2P", "SRGGB10_CSI2P", - "SBGGR12", "SGBRG12", "SGRBG12", "SRGGB12", - "SBGGR12_CSI2P", "SGBRG12_CSI2P", "SGRBG12_CSI2P", "SRGGB12_CSI2P", - "BGGR_PISP_COMP1", "GBRG_PISP_COMP1", "GRBG_PISP_COMP1", "RGGB_PISP_COMP1", - "SBGGR16", "SGBRG16", "SGRBG16", "SRGGB16", } +BAYER_FORMATS = { + "SBGGR8", + "SGBRG8", + "SGRBG8", + "SRGGB8", + "SBGGR10", + "SGBRG10", + "SGRBG10", + "SRGGB10", + "SBGGR10_CSI2P", + "SGBRG10_CSI2P", + "SGRBG10_CSI2P", + "SRGGB10_CSI2P", + "SBGGR12", + "SGBRG12", + "SGRBG12", + "SRGGB12", + "SBGGR12_CSI2P", + "SGBRG12_CSI2P", + "SGRBG12_CSI2P", + "SRGGB12_CSI2P", + "BGGR_PISP_COMP1", + "GBRG_PISP_COMP1", + "GRBG_PISP_COMP1", + "RGGB_PISP_COMP1", + "SBGGR16", + "SGBRG16", + "SGRBG16", + "SRGGB16", +} MONO_FORMATS = {"R8", "R10", "R12", "R16", "R8_CSI2P", "R10_CSI2P", "R12_CSI2P"} diff --git a/picamera2/job.py b/picamera2/job.py index 4cd076f6..3cf9c794 100644 --- a/picamera2/job.py +++ b/picamera2/job.py @@ -24,10 +24,7 @@ class Job(Generic[T]): Picamera2.switch_mode_and_capture_array. """ - def __init__(self, functions: list[Callable[..., Union[ - tuple[bool, Any], - tuple[Literal[True], T]]]], signal_function=None - ): + def __init__(self, functions: list[Callable[..., Union[tuple[bool, Any], tuple[Literal[True], T]]]], signal_function=None): self._functions = functions self._future = Future() self._future.set_running_or_notify_cancel() diff --git a/picamera2/metadata.py b/picamera2/metadata.py index fa6dd3c0..c3532921 100644 --- a/picamera2/metadata.py +++ b/picamera2/metadata.py @@ -1,5 +1,4 @@ - -class Metadata(): +class Metadata: def __init__(self, metadata={}): self.__dict__ = metadata.copy() diff --git a/picamera2/outputs/ffmpegoutput.py b/picamera2/outputs/ffmpegoutput.py index 7fa3e3ab..e37b8544 100644 --- a/picamera2/outputs/ffmpegoutput.py +++ b/picamera2/outputs/ffmpegoutput.py @@ -33,8 +33,18 @@ class FfmpegOutput(Output): audio_samplerate, audio_codec, audio_bitrate - the usual audio parameters. """ - def __init__(self, output_filename, audio=False, audio_device="default", audio_sync=-0.3, - audio_samplerate=48000, audio_codec="aac", audio_bitrate=128000, audio_filter=None, pts=None): + def __init__( + self, + output_filename, + audio=False, + audio_device="default", + audio_sync=-0.3, + audio_samplerate=48000, + audio_codec="aac", + audio_bitrate=128000, + audio_filter=None, + pts=None, + ): super().__init__(pts=pts) self.ffmpeg = None self.output_broken = False @@ -55,18 +65,20 @@ def __init__(self, output_filename, audio=False, audio_device="default", audio_s self.needs_pacing = True def start(self): - general_options = ['-loglevel', 'warning', - '-y'] # -y means overwrite output without asking + general_options = ['-loglevel', 'warning', '-y'] # -y means overwrite output without asking # We have to get FFmpeg to timestamp the video frames as it gets them. This isn't # ideal because we're likely to pick up some jitter, but works passably, and I # don't have a better alternative right now. + # fmt: off video_input = ['-use_wallclock_as_timestamps', '1', '-thread_queue_size', '64', # necessary to prevent warnings '-i', '-'] + # fmt: on video_codec = ['-c:v', 'copy'] audio_input = [] audio_codec = [] if self.audio: + # fmt: off audio_input = ['-itsoffset', str(self.audio_sync), '-f', 'pulse', '-sample_rate', str(self.audio_samplerate), @@ -74,11 +86,13 @@ def start(self): '-i', self.audio_device] audio_codec = ['-b:a', str(self.audio_bitrate), '-c:a', self.audio_codec] + # fmt: on if self.audio_filter: # Check if audio_filter is not empty or None audio_codec.extend(['-af', self.audio_filter]) - command = ['ffmpeg'] + general_options + audio_input + video_input + \ - audio_codec + video_codec + self.output_filename.split() + command = ( + ['ffmpeg'] + general_options + audio_input + video_input + audio_codec + video_codec + self.output_filename.split() + ) # The preexec_fn is a slightly nasty way of ensuring FFmpeg gets stopped if we quit # without calling stop() (which is otherwise not guaranteed). self.ffmpeg = subprocess.Popen(command, stdin=subprocess.PIPE, preexec_fn=lambda: prctl.set_pdeathsig(signal.SIGKILL)) diff --git a/picamera2/outputs/fileoutput.py b/picamera2/outputs/fileoutput.py index 0f0b8921..f36d9c2c 100644 --- a/picamera2/outputs/fileoutput.py +++ b/picamera2/outputs/fileoutput.py @@ -50,8 +50,11 @@ def fileoutput(self, file): self._fileoutput = file else: raise RuntimeError("Must pass io.BufferedIOBase") - if hasattr(self._fileoutput, "raw") and isinstance(self._fileoutput.raw, socket.SocketIO) and \ - self._fileoutput.raw._sock.type == socket.SocketKind.SOCK_DGRAM: + if ( + hasattr(self._fileoutput, "raw") + and isinstance(self._fileoutput.raw, socket.SocketIO) + and self._fileoutput.raw._sock.type == socket.SocketKind.SOCK_DGRAM + ): self._split = True @property @@ -115,7 +118,7 @@ def _write(self, frame, timestamp=None): off = 0 while tosend > 0: lenv = min(tosend, maxsize) - self._fileoutput.write(frame[off:off + lenv]) + self._fileoutput.write(frame[off : off + lenv]) self._fileoutput.flush() off += lenv tosend -= lenv diff --git a/picamera2/outputs/pyavoutput.py b/picamera2/outputs/pyavoutput.py index d359136b..3379f7dc 100644 --- a/picamera2/outputs/pyavoutput.py +++ b/picamera2/outputs/pyavoutput.py @@ -20,6 +20,7 @@ def __init__(self, output_name, format=None, pts=None, options=None): # Save low-powered Pis from importing av unless it is needed. global av import av + super().__init__(pts=pts) self._output_name = output_name self._format = format diff --git a/picamera2/picamera2.py b/picamera2/picamera2.py index abdd7a97..b7cd4e6c 100644 --- a/picamera2/picamera2.py +++ b/picamera2/picamera2.py @@ -14,8 +14,7 @@ from collections.abc import Callable from enum import Enum from functools import partial -from typing import (Any, Generic, Literal, Optional, TypedDict, TypeVar, Union, - cast, overload) +from typing import Any, Generic, Literal, Optional, TypedDict, TypeVar, Union, cast, overload import libcamera import numpy as np @@ -265,9 +264,11 @@ def load_tuning_file(tuning_file, dir=None) -> dict[str, Any]: dirs = [dir] else: platform_dir = "vc4" if Picamera2.platform == Platform.Platform.VC4 else "pisp" - dirs = [os.path.expanduser("~/libcamera/src/ipa/rpi/" + platform_dir + "/data"), - "/usr/local/share/libcamera/ipa/rpi/" + platform_dir, - "/usr/share/libcamera/ipa/rpi/" + platform_dir] + dirs = [ + os.path.expanduser("~/libcamera/src/ipa/rpi/" + platform_dir + "/data"), + "/usr/local/share/libcamera/ipa/rpi/" + platform_dir, + "/usr/share/libcamera/ipa/rpi/" + platform_dir, + ] for directory in dirs: file = os.path.join(directory, tuning_file) if os.path.isfile(file): @@ -299,11 +300,13 @@ def global_camera_info() -> list[GlobalCameraInfo]: Ordered correctly by camera number. Also return the location and rotation of the camera when known, as these may help distinguish which is which. """ + def describe_camera(cam, num): info = {k.name: v for k, v in cam.properties.items() if k.name in ("Model", "Location", "Rotation")} info["Id"] = cam.id info["Num"] = num return cast(GlobalCameraInfo, info) + cameras = [describe_camera(cam, i) for i, cam in enumerate(Picamera2._cm.cms.cameras)] # Sort alphabetically so they are deterministic, but send USB cams to the back of the class. return sorted(cameras, key=lambda cam: ("/usb" not in cam['Id'], cam['Id']), reverse=True) @@ -360,9 +363,9 @@ def __init__(self, camera_num=0, verbose_console=None, tuning=None, allocator=No self.still_configuration_.enable_raw() # ditto self.video_configuration_ = CameraConfiguration(self.create_video_configuration(), self) self.video_configuration_.enable_raw() # ditto - except Exception: + except Exception as e: _log.error("Camera __init__ sequence did not complete.") - raise RuntimeError("Camera __init__ sequence did not complete.") + raise RuntimeError("Camera __init__ sequence did not complete.") from e finally: if tuning_file is not None: tuning_file.close() # delete the temporary file @@ -449,9 +452,14 @@ def camera_properties(self) -> dict[str, Any]: @property def camera_controls(self) -> dict[str, tuple[Any, Any, Any]]: - return {k: (utils.convert_from_libcamera_type(v[1].min), - utils.convert_from_libcamera_type(v[1].max), - utils.convert_from_libcamera_type(v[1].default)) for k, v in self.camera_ctrl_info.items()} + return { + k: ( + utils.convert_from_libcamera_type(v[1].min), + utils.convert_from_libcamera_type(v[1].max), + utils.convert_from_libcamera_type(v[1].default), + ) + for k, v in self.camera_ctrl_info.items() + } @property def title_fields(self) -> list[str]: @@ -468,6 +476,7 @@ def tidy(item): return tuple(tidy(i) for i in item) else: return item + return "".join("{} {} ".format(f, tidy(metadata.get(f, "INVALID"))) for f in fields) # noqa self._title_fields = fields @@ -523,9 +532,9 @@ def _initialize_camera(self): try: self.camera: libcamera.Camera = self._grab_camera(self.camera_idx) - except RuntimeError: + except RuntimeError as e: _log.error("Initialization failed.") - raise RuntimeError("Initialization failed.") + raise RuntimeError("Initialization failed.") from e self.__identify_camera() @@ -558,8 +567,8 @@ def _open_camera(self): """ try: self._initialize_camera() - except RuntimeError: - raise RuntimeError("Failed to initialize camera") + except RuntimeError as e: + raise RuntimeError("Failed to initialize camera") from e # This now throws an error if it can't open the camera. self.camera.acquire() @@ -622,9 +631,11 @@ def area(sz): return sz[0] * sz[1] for mode in modes[1:]: - if area(mode['size']) > area(best_mode['size']) or \ - (is_rpi_camera and area(mode['size']) == area(best_mode['size']) and - SensorFormat(mode['format']).bit_depth > SensorFormat(best_mode['format']).bit_depth): + if area(mode['size']) > area(best_mode['size']) or ( + is_rpi_camera + and area(mode['size']) == area(best_mode['size']) + and SensorFormat(mode['format']).bit_depth > SensorFormat(best_mode['format']).bit_depth + ): best_mode = mode return best_mode @@ -687,8 +698,8 @@ def stop_preview(self) -> None: # The preview windows call the detach_preview method. self._preview.stop() self._preview_stopped.wait() - except Exception: - raise RuntimeError("Unable to stop preview.") + except Exception as e: + raise RuntimeError("Unable to stop preview.") from e def close(self) -> None: """Close camera @@ -763,9 +774,21 @@ def _add_display_and_encode(config, display, encode): _raw_stream_ignore_list = ["bit_depth", "crop_limits", "exposure_limits", "fps", "unpacked"] - def create_preview_configuration(self, main={}, lores=None, raw={}, transform=libcamera.Transform(), - colour_space=libcamera.ColorSpace.Sycc(), buffer_count=4, controls={}, display="main", - encode="main", queue=True, sensor={}, use_case="preview") -> dict[str, Any]: + def create_preview_configuration( + self, + main={}, + lores=None, + raw={}, + transform=libcamera.Transform(), + colour_space=libcamera.ColorSpace.Sycc(), + buffer_count=4, + controls={}, + display="main", + encode="main", + queue=True, + sensor={}, + use_case="preview", + ) -> dict[str, Any]: """Make a configuration suitable for camera preview.""" if self.camera is None: raise RuntimeError("Camera not opened") @@ -778,28 +801,45 @@ def create_preview_configuration(self, main={}, lores=None, raw={}, transform=li lores = self._make_initial_stream_config({"format": "YUV420", "size": main["size"], "preserve_ar": False}, lores) if lores is not None: self.align_stream(lores, optimal=False) - raw = self._make_initial_stream_config({"format": self.sensor_format, "size": main["size"]}, - raw, self._raw_stream_ignore_list) + raw = self._make_initial_stream_config( + {"format": self.sensor_format, "size": main["size"]}, raw, self._raw_stream_ignore_list + ) # Let the framerate vary from 12fps to as fast as possible. if "NoiseReductionMode" in self.camera_controls and "FrameDurationLimits" in self.camera_controls: - controls = {"NoiseReductionMode": libcamera.controls.draft.NoiseReductionModeEnum.Minimal, - "FrameDurationLimits": (100, 83333)} | controls - config = {"use_case": use_case, - "transform": transform, - "colour_space": colour_space, - "buffer_count": buffer_count, - "queue": queue, - "main": main, - "lores": lores, - "raw": raw, - "controls": controls, - "sensor": sensor} + controls = { + "NoiseReductionMode": libcamera.controls.draft.NoiseReductionModeEnum.Minimal, + "FrameDurationLimits": (100, 83333), + } | controls + config = { + "use_case": use_case, + "transform": transform, + "colour_space": colour_space, + "buffer_count": buffer_count, + "queue": queue, + "main": main, + "lores": lores, + "raw": raw, + "controls": controls, + "sensor": sensor, + } self._add_display_and_encode(config, display, encode) return config - def create_still_configuration(self, main={}, lores=None, raw={}, transform=libcamera.Transform(), - colour_space=libcamera.ColorSpace.Sycc(), buffer_count=1, controls={}, - display=None, encode=None, queue=True, sensor={}, use_case="still") -> dict[str, Any]: + def create_still_configuration( + self, + main={}, + lores=None, + raw={}, + transform=libcamera.Transform(), + colour_space=libcamera.ColorSpace.Sycc(), + buffer_count=1, + controls={}, + display=None, + encode=None, + queue=True, + sensor={}, + use_case="still", + ) -> dict[str, Any]: """Make a configuration suitable for still image capture. Default to 2 buffers, as the Gl preview would need them.""" if self.camera is None: raise RuntimeError("Camera not opened") @@ -807,34 +847,52 @@ def create_still_configuration(self, main={}, lores=None, raw={}, transform=libc if not self._is_rpi_camera(): raw = None sensor = None - main = self._make_initial_stream_config({"format": "BGR888", "size": self.sensor_resolution, "preserve_ar": True}, - main) + main = self._make_initial_stream_config( + {"format": "BGR888", "size": self.sensor_resolution, "preserve_ar": True}, main + ) self.align_stream(main, optimal=False) lores = self._make_initial_stream_config({"format": "YUV420", "size": main["size"], "preserve_ar": False}, lores) if lores is not None: self.align_stream(lores, optimal=False) - raw = self._make_initial_stream_config({"format": self.sensor_format, "size": main["size"]}, - raw, self._raw_stream_ignore_list) + raw = self._make_initial_stream_config( + {"format": self.sensor_format, "size": main["size"]}, raw, self._raw_stream_ignore_list + ) # Let the framerate span the entire possible range of the sensor. if "NoiseReductionMode" in self.camera_controls and "FrameDurationLimits" in self.camera_controls: - controls = {"NoiseReductionMode": libcamera.controls.draft.NoiseReductionModeEnum.HighQuality, - "FrameDurationLimits": (100, 1000000 * 1000)} | controls - config = {"use_case": use_case, - "transform": transform, - "colour_space": colour_space, - "buffer_count": buffer_count, - "queue": queue, - "main": main, - "lores": lores, - "raw": raw, - "controls": controls, - "sensor": sensor} + controls = { + "NoiseReductionMode": libcamera.controls.draft.NoiseReductionModeEnum.HighQuality, + "FrameDurationLimits": (100, 1000000 * 1000), + } | controls + config = { + "use_case": use_case, + "transform": transform, + "colour_space": colour_space, + "buffer_count": buffer_count, + "queue": queue, + "main": main, + "lores": lores, + "raw": raw, + "controls": controls, + "sensor": sensor, + } self._add_display_and_encode(config, display, encode) return config - def create_video_configuration(self, main={}, lores=None, raw={}, transform=libcamera.Transform(), - colour_space=None, buffer_count=6, controls={}, display="main", - encode="main", queue=True, sensor={}, use_case="video") -> dict[str, Any]: + def create_video_configuration( + self, + main={}, + lores=None, + raw={}, + transform=libcamera.Transform(), + colour_space=None, + buffer_count=6, + controls={}, + display="main", + encode="main", + queue=True, + sensor={}, + use_case="video", + ) -> dict[str, Any]: """Make a configuration suitable for video recording.""" if self.camera is None: raise RuntimeError("Camera not opened") @@ -847,8 +905,9 @@ def create_video_configuration(self, main={}, lores=None, raw={}, transform=libc lores = self._make_initial_stream_config({"format": "YUV420", "size": main["size"], "preserve_ar": False}, lores) if lores is not None: self.align_stream(lores, optimal=False) - raw = self._make_initial_stream_config({"format": self.sensor_format, "size": main["size"]}, - raw, self._raw_stream_ignore_list) + raw = self._make_initial_stream_config( + {"format": self.sensor_format, "size": main["size"]}, raw, self._raw_stream_ignore_list + ) if colour_space is None: # Choose default colour space according to the video resolution. if main["size"][0] < 1280 or main["size"][1] < 720: @@ -856,18 +915,22 @@ def create_video_configuration(self, main={}, lores=None, raw={}, transform=libc else: colour_space = libcamera.ColorSpace.Rec709() if "NoiseReductionMode" in self.camera_controls and "FrameDurationLimits" in self.camera_controls: - controls = {"NoiseReductionMode": libcamera.controls.draft.NoiseReductionModeEnum.Fast, - "FrameDurationLimits": (33333, 33333)} | controls - config = {"use_case": use_case, - "transform": transform, - "colour_space": colour_space, - "buffer_count": buffer_count, - "queue": queue, - "main": main, - "lores": lores, - "raw": raw, - "controls": controls, - "sensor": sensor} + controls = { + "NoiseReductionMode": libcamera.controls.draft.NoiseReductionModeEnum.Fast, + "FrameDurationLimits": (33333, 33333), + } | controls + config = { + "use_case": use_case, + "transform": transform, + "colour_space": colour_space, + "buffer_count": buffer_count, + "queue": queue, + "main": main, + "lores": lores, + "raw": raw, + "controls": controls, + "sensor": sensor, + } self._add_display_and_encode(config, display, encode) return config @@ -965,8 +1028,8 @@ def _make_libcamera_config(self, camera_config): buffer_count = camera_config["buffer_count"] self._update_libcamera_stream_config(libcamera_config.at(self.main_index), camera_config["main"], buffer_count) libcamera_config.at(self.main_index).color_space = utils.colour_space_to_libcamera( - camera_config["colour_space"], - camera_config["main"]["format"]) + camera_config["colour_space"], camera_config["main"]["format"] + ) if self.lores_index >= 0: self._update_libcamera_stream_config(libcamera_config.at(self.lores_index), camera_config["lores"], buffer_count) # Must be YUV, so no need for colour_space_to_libcamera. @@ -1413,8 +1476,11 @@ def process_requests(self, display) -> None: # If one of the functions we ran reconfigured the camera since this request came out, # then we don't want it going back to the application as the memory is not valid. if display_request is not None: - if display_request.configure_count == self.configure_count and \ - display_request.config['display'] is not None and display_request.display: + if ( + display_request.configure_count == self.configure_count + and display_request.config['display'] is not None + and display_request.display + ): display.render_request(display_request) display_request.release() @@ -1492,28 +1558,20 @@ def wait_for_timestamp_(self, timestamp_ns): return (False, None) @overload - def drop_frames(self, num_frames, wait: None = ..., - signal_function: None = ... - ) -> None: - ... + def drop_frames(self, num_frames, wait: None = ..., signal_function: None = ...) -> None: ... @overload - def drop_frames(self, num_frames, wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[None]: - ... + def drop_frames(self, num_frames, wait: None = ..., signal_function: Callable[[Job], None] = ...) -> Job[None]: ... @overload - def drop_frames(self, num_frames, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> None: - ... + def drop_frames( + self, num_frames, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> None: ... @overload - def drop_frames(self, num_frames, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[None]: - ... + def drop_frames( + self, num_frames, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[None]: ... def drop_frames(self, num_frames, wait=None, signal_function=None) -> Union[None, Job[None]]: """Drop num_frames frames from the camera.""" @@ -1534,31 +1592,46 @@ def capture_file_(self, file_output, name, format=None, exif_data=None): return (True, result) @overload - def capture_file(self, file_output, name="main", format=None, wait: None = ..., - signal_function: None = ..., exif_data=None - ) -> dict[str, Any]: - ... - - @overload - def capture_file(self, file_output, name="main", format=None, wait: None = ..., - signal_function: Callable[[Job], None] = ..., exif_data=None - ) -> Job[dict[str, Any]]: - ... - - @overload - def capture_file(self, file_output, name="main", format=None, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., exif_data=None - ) -> dict[str, Any]: - ... - - @overload - def capture_file(self, file_output, name="main", format=None, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., exif_data=None - ) -> Job[dict[str, Any]]: - ... - - def capture_file(self, file_output, name="main", format=None, wait=None, signal_function=None, exif_data=None - ) -> Union[dict[str, Any], Job[dict[str, Any]]]: + def capture_file( + self, file_output, name="main", format=None, wait: None = ..., signal_function: None = ..., exif_data=None + ) -> dict[str, Any]: ... + + @overload + def capture_file( + self, + file_output, + name="main", + format=None, + wait: None = ..., + signal_function: Callable[[Job], None] = ..., + exif_data=None, + ) -> Job[dict[str, Any]]: ... + + @overload + def capture_file( + self, + file_output, + name="main", + format=None, + wait: Literal[True] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + exif_data=None, + ) -> dict[str, Any]: ... + + @overload + def capture_file( + self, + file_output, + name="main", + format=None, + wait: Literal[False] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + exif_data=None, + ) -> Job[dict[str, Any]]: ... + + def capture_file( + self, file_output, name="main", format=None, wait=None, signal_function=None, exif_data=None + ) -> Union[dict[str, Any], Job[dict[str, Any]]]: """Capture an image to a file in the current camera mode. Return the metadata for the frame captured. @@ -1566,8 +1639,7 @@ def capture_file(self, file_output, name="main", format=None, wait=None, signal_ exif_data - dictionary containing user defined exif data (based on `piexif`). This will overwrite existing exif information generated by picamera2. """ - functions = [partial(self.capture_file_, file_output, name, format=format, - exif_data=exif_data)] + functions = [partial(self.capture_file_, file_output, name, format=format, exif_data=exif_data)] return self.dispatch_functions(functions, wait, signal_function) def switch_mode_(self, camera_config): @@ -1577,28 +1649,22 @@ def switch_mode_(self, camera_config): return (True, self.camera_config) @overload - def switch_mode(self, camera_config, wait: None = ..., - signal_function: None = ... - ) -> dict[str, Any]: - ... + def switch_mode(self, camera_config, wait: None = ..., signal_function: None = ...) -> dict[str, Any]: ... @overload - def switch_mode(self, camera_config, wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[dict[str, Any]]: - ... + def switch_mode( + self, camera_config, wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[dict[str, Any]]: ... @overload - def switch_mode(self, camera_config, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> dict[str, Any]: - ... + def switch_mode( + self, camera_config, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> dict[str, Any]: ... @overload - def switch_mode(self, camera_config, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[dict[str, Any]]: - ... + def switch_mode( + self, camera_config, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[dict[str, Any]]: ... def switch_mode(self, camera_config, wait=None, signal_function=None) -> Union[dict[str, Any], Job[dict[str, Any]]]: """Switch the camera into another mode given by the camera_config.""" @@ -1606,63 +1672,87 @@ def switch_mode(self, camera_config, wait=None, signal_function=None) -> Union[d return self.dispatch_functions(functions, wait, signal_function, immediate=True) @overload - def switch_mode_and_drop_frames(self, camera_config, num_frames, wait: None = ..., - signal_function: None = ... - ) -> None: - ... + def switch_mode_and_drop_frames( + self, camera_config, num_frames, wait: None = ..., signal_function: None = ... + ) -> None: ... @overload - def switch_mode_and_drop_frames(self, camera_config, num_frames, wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[None]: - ... + def switch_mode_and_drop_frames( + self, camera_config, num_frames, wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[None]: ... @overload - def switch_mode_and_drop_frames(self, camera_config, num_frames, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> None: - ... + def switch_mode_and_drop_frames( + self, camera_config, num_frames, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> None: ... @overload - def switch_mode_and_drop_frames(self, camera_config, num_frames, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[None]: - ... + def switch_mode_and_drop_frames( + self, camera_config, num_frames, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[None]: ... - def switch_mode_and_drop_frames(self, camera_config, num_frames, wait=None, signal_function=None - ) -> Union[None, Job[None]]: + def switch_mode_and_drop_frames( + self, camera_config, num_frames, wait=None, signal_function=None + ) -> Union[None, Job[None]]: """Switch the camera into the mode given by camera_config and drop the first num_frames frames.""" - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, num_frames), self.drop_frames_] + functions = [partial(self.switch_mode_, camera_config), partial(self.set_frame_drops_, num_frames), self.drop_frames_] return self.dispatch_functions(functions, wait, signal_function, immediate=True) @overload - def switch_mode_and_capture_file(self, camera_config, file_output, name="main", format=None, wait: None = ..., - signal_function: None = ..., exif_data=None, delay=0 - ) -> dict[str, Any]: - ... - - @overload - def switch_mode_and_capture_file(self, camera_config, file_output, name="main", format=None, wait: None = ..., - signal_function: Callable[[Job], None] = ..., exif_data=None, delay=0 - ) -> Job[dict[str, Any]]: - ... - - @overload - def switch_mode_and_capture_file(self, camera_config, file_output, name="main", format=None, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., exif_data=None, delay=0 - ) -> dict[str, Any]: - ... - - @overload - def switch_mode_and_capture_file(self, camera_config, file_output, name="main", format=None, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., exif_data=None, delay=0 - ) -> Job[dict[str, Any]]: - ... - - def switch_mode_and_capture_file(self, camera_config, file_output, name="main", format=None, - wait=None, signal_function=None, exif_data=None, delay=0 - ) -> Union[dict[str, Any], Job[dict[str, Any]]]: + def switch_mode_and_capture_file( + self, + camera_config, + file_output, + name="main", + format=None, + wait: None = ..., + signal_function: None = ..., + exif_data=None, + delay=0, + ) -> dict[str, Any]: ... + + @overload + def switch_mode_and_capture_file( + self, + camera_config, + file_output, + name="main", + format=None, + wait: None = ..., + signal_function: Callable[[Job], None] = ..., + exif_data=None, + delay=0, + ) -> Job[dict[str, Any]]: ... + + @overload + def switch_mode_and_capture_file( + self, + camera_config, + file_output, + name="main", + format=None, + wait: Literal[True] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + exif_data=None, + delay=0, + ) -> dict[str, Any]: ... + + @overload + def switch_mode_and_capture_file( + self, + camera_config, + file_output, + name="main", + format=None, + wait: Literal[False] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + exif_data=None, + delay=0, + ) -> Job[dict[str, Any]]: ... + + def switch_mode_and_capture_file( + self, camera_config, file_output, name="main", format=None, wait=None, signal_function=None, exif_data=None, delay=0 + ) -> Union[dict[str, Any], Job[dict[str, Any]]]: """Switch the camera into a new (capture) mode, capture an image to file. Then return back to the initial camera mode. @@ -1679,38 +1769,37 @@ def capture_and_switch_back_(self, file_output, preview_config, format, exif_dat self.switch_mode_(preview_config) return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, delay), self.drop_frames_, - partial(capture_and_switch_back_, self, file_output, preview_config, format, - exif_data=exif_data)] + functions = [ + partial(self.switch_mode_, camera_config), + partial(self.set_frame_drops_, delay), + self.drop_frames_, + partial(capture_and_switch_back_, self, file_output, preview_config, format, exif_data=exif_data), + ] return self.dispatch_functions(functions, wait, signal_function, immediate=True) @overload - def switch_mode_and_capture_request(self, camera_config, wait: None = ..., - signal_function: None = ..., delay=0 - ) -> CompletedRequest: - ... + def switch_mode_and_capture_request( + self, camera_config, wait: None = ..., signal_function: None = ..., delay=0 + ) -> CompletedRequest: ... @overload - def switch_mode_and_capture_request(self, camera_config, wait: None = ..., - signal_function: Callable[[Job], None] = ..., delay=0 - ) -> Job[CompletedRequest]: - ... + def switch_mode_and_capture_request( + self, camera_config, wait: None = ..., signal_function: Callable[[Job], None] = ..., delay=0 + ) -> Job[CompletedRequest]: ... @overload - def switch_mode_and_capture_request(self, camera_config, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> CompletedRequest: - ... + def switch_mode_and_capture_request( + self, camera_config, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ..., delay=0 + ) -> CompletedRequest: ... @overload - def switch_mode_and_capture_request(self, camera_config, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> Job[CompletedRequest]: - ... + def switch_mode_and_capture_request( + self, camera_config, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ..., delay=0 + ) -> Job[CompletedRequest]: ... - def switch_mode_and_capture_request(self, camera_config, wait=None, signal_function=None, delay=0 - ) -> Union[CompletedRequest, Job[CompletedRequest]]: + def switch_mode_and_capture_request( + self, camera_config, wait=None, signal_function=None, delay=0 + ) -> Union[CompletedRequest, Job[CompletedRequest]]: """Switch the camera into a new (capture) mode and capture a request, then switch back. Applications should use this with care because it may increase the risk of CMA heap @@ -1726,9 +1815,12 @@ def capture_and_switch_back_(self, preview_config): self.switch_mode_(preview_config) return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, delay), self.drop_frames_, - partial(capture_and_switch_back_, self, preview_config)] + functions = [ + partial(self.switch_mode_, camera_config), + partial(self.set_frame_drops_, delay), + self.drop_frames_, + partial(capture_and_switch_back_, self, preview_config), + ] return self.dispatch_functions(functions, wait, signal_function, immediate=True) def capture_request_(self): @@ -1738,28 +1830,22 @@ def capture_request_(self): return (True, self.completed_requests.pop(0)) @overload - def capture_request(self, wait: None = ..., - signal_function: None = ..., flush=None - ) -> CompletedRequest: - ... + def capture_request(self, wait: None = ..., signal_function: None = ..., flush=None) -> CompletedRequest: ... @overload - def capture_request(self, wait: None = ..., - signal_function: Callable[[Job], None] = ..., flush=None - ) -> Job[CompletedRequest]: - ... + def capture_request( + self, wait: None = ..., signal_function: Callable[[Job], None] = ..., flush=None + ) -> Job[CompletedRequest]: ... @overload - def capture_request(self, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., flush=None - ) -> CompletedRequest: - ... + def capture_request( + self, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ..., flush=None + ) -> CompletedRequest: ... @overload - def capture_request(self, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., flush=None - ) -> Job[CompletedRequest]: - ... + def capture_request( + self, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ..., flush=None + ) -> Job[CompletedRequest]: ... def capture_request(self, wait=None, signal_function=None, flush=None) -> Union[CompletedRequest, Job[CompletedRequest]]: """Fetch the next completed request from the camera system. @@ -1770,36 +1856,32 @@ def capture_request(self, wait=None, signal_function=None, flush=None) -> Union[ # 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_] + functions = [partial(self.wait_for_timestamp_, flush), self.capture_request_] return self.dispatch_functions(functions, wait, signal_function) @overload - def switch_mode_capture_request_and_stop(self, camera_config, wait: None = ..., - signal_function: None = ... - ) -> CompletedRequest: - ... + def switch_mode_capture_request_and_stop( + self, camera_config, wait: None = ..., signal_function: None = ... + ) -> CompletedRequest: ... @overload - def switch_mode_capture_request_and_stop(self, camera_config, wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[CompletedRequest]: - ... + def switch_mode_capture_request_and_stop( + self, camera_config, wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[CompletedRequest]: ... @overload - def switch_mode_capture_request_and_stop(self, camera_config, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> CompletedRequest: - ... + def switch_mode_capture_request_and_stop( + self, camera_config, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> CompletedRequest: ... @overload - def switch_mode_capture_request_and_stop(self, camera_config, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[CompletedRequest]: - ... + def switch_mode_capture_request_and_stop( + self, camera_config, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[CompletedRequest]: ... - def switch_mode_capture_request_and_stop(self, camera_config, wait=None, signal_function=None - ) -> Union[CompletedRequest, Job[CompletedRequest]]: + def switch_mode_capture_request_and_stop( + self, camera_config, wait=None, signal_function=None + ) -> Union[CompletedRequest, Job[CompletedRequest]]: """Switch the camera into a new (capture) mode, capture a request in the new mode and then stop the camera.""" def capture_request_and_stop_(self): @@ -1809,33 +1891,26 @@ def capture_request_and_stop_(self): self.stop_() return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(capture_request_and_stop_, self)] + functions = [partial(self.switch_mode_, camera_config), partial(capture_request_and_stop_, self)] return self.dispatch_functions(functions, wait, signal_function, immediate=True) @overload - def capture_sync_request(self, wait: None = ..., - signal_function: None = ... - ) -> CompletedRequest: - ... + def capture_sync_request(self, wait: None = ..., signal_function: None = ...) -> CompletedRequest: ... @overload - def capture_sync_request(self, wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[CompletedRequest]: - ... + def capture_sync_request( + self, wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[CompletedRequest]: ... @overload - def capture_sync_request(self, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> CompletedRequest: - ... + def capture_sync_request( + self, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> CompletedRequest: ... @overload - def capture_sync_request(self, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[CompletedRequest]: - ... + def capture_sync_request( + self, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[CompletedRequest]: ... def capture_sync_request(self, wait=None, signal_function=None) -> Union[CompletedRequest, Job[CompletedRequest]]: """Return the first request when the camera system has reached sychronisation point. @@ -1888,28 +1963,20 @@ def capture_metadata_(self): return (True, result) @overload - def capture_metadata(self, wait: None = ..., - signal_function: None = ... - ) -> dict[str, Any]: - ... + def capture_metadata(self, wait: None = ..., signal_function: None = ...) -> dict[str, Any]: ... @overload - def capture_metadata(self, wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[dict[str, Any]]: - ... + def capture_metadata(self, wait: None = ..., signal_function: Callable[[Job], None] = ...) -> Job[dict[str, Any]]: ... @overload - def capture_metadata(self, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> dict[str, Any]: - ... + def capture_metadata( + self, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> dict[str, Any]: ... @overload - def capture_metadata(self, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[dict[str, Any]]: - ... + def capture_metadata( + self, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[dict[str, Any]]: ... def capture_metadata(self, wait=None, signal_function=None) -> Union[dict[str, Any], Job[dict[str, Any]]]: """Fetch the metadata from the next camera frame.""" @@ -1925,28 +1992,22 @@ def capture_buffer_(self, name): return (True, result) @overload - def capture_buffer(self, name="main", wait: None = ..., - signal_function: None = ... - ) -> NDArray[np.uint8]: - ... + def capture_buffer(self, name="main", wait: None = ..., signal_function: None = ...) -> NDArray[np.uint8]: ... @overload - def capture_buffer(self, name="main", wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[NDArray[np.uint8]]: - ... + def capture_buffer( + self, name="main", wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[NDArray[np.uint8]]: ... @overload - def capture_buffer(self, name="main", wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> NDArray[np.uint8]: - ... + def capture_buffer( + self, name="main", wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> NDArray[np.uint8]: ... @overload - def capture_buffer(self, name="main", wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[NDArray[np.uint8]]: - ... + def capture_buffer( + self, name="main", wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[NDArray[np.uint8]]: ... def capture_buffer(self, name="main", wait=None, signal_function=None) -> Union[NDArray[np.uint8], Job[NDArray[np.uint8]]]: """Make a 1d numpy array from the next frame in the named stream.""" @@ -1961,63 +2022,64 @@ def capture_buffers_and_metadata_(self, names): return (True, result) @overload - def capture_buffers(self, names=["main"], wait: None = ..., - signal_function: None = ... - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def capture_buffers( + self, names=["main"], wait: None = ..., signal_function: None = ... + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def capture_buffers(self, names=["main"], wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def capture_buffers( + self, names=["main"], wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... @overload - def capture_buffers(self, names=["main"], wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def capture_buffers( + self, names=["main"], wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def capture_buffers(self, names=["main"], wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def capture_buffers( + self, names=["main"], wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... - def capture_buffers(self, names=["main"], wait=None, signal_function=None - ) -> Union[ - tuple[list[NDArray[np.uint8]], dict[str, Any]], - Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]] - ]: + def capture_buffers( + self, names=["main"], wait=None, signal_function=None + ) -> Union[tuple[list[NDArray[np.uint8]], dict[str, Any]], Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]]: """Make a 1d numpy array from the next frame for each of the named streams.""" return self.dispatch_functions([partial(self.capture_buffers_and_metadata_, names)], wait, signal_function) @overload - def switch_mode_and_capture_buffer(self, camera_config, name="main", wait: None = ..., - signal_function: None = ..., delay=0 - ) -> NDArray[np.uint8]: - ... + def switch_mode_and_capture_buffer( + self, camera_config, name="main", wait: None = ..., signal_function: None = ..., delay=0 + ) -> NDArray[np.uint8]: ... @overload - def switch_mode_and_capture_buffer(self, camera_config, name="main", wait: None = ..., - signal_function: Callable[[Job], None] = ..., delay=0 - ) -> Job[NDArray[np.uint8]]: - ... + def switch_mode_and_capture_buffer( + self, camera_config, name="main", wait: None = ..., signal_function: Callable[[Job], None] = ..., delay=0 + ) -> Job[NDArray[np.uint8]]: ... @overload - def switch_mode_and_capture_buffer(self, camera_config, name="main", wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> NDArray[np.uint8]: - ... + def switch_mode_and_capture_buffer( + self, + camera_config, + name="main", + wait: Literal[True] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> NDArray[np.uint8]: ... @overload - def switch_mode_and_capture_buffer(self, camera_config, name="main", wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> Job[NDArray[np.uint8]]: - ... + def switch_mode_and_capture_buffer( + self, + camera_config, + name="main", + wait: Literal[False] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> Job[NDArray[np.uint8]]: ... - def switch_mode_and_capture_buffer(self, camera_config, name="main", wait=None, signal_function=None, delay=0 - ) -> Union[NDArray[np.uint8], Job[NDArray[np.uint8]]]: + def switch_mode_and_capture_buffer( + self, camera_config, name="main", wait=None, signal_function=None, delay=0 + ) -> Union[NDArray[np.uint8], Job[NDArray[np.uint8]]]: """Switch the camera into a new (capture) mode, capture the first buffer. Then return back to the initial camera mode. @@ -2031,40 +2093,47 @@ def capture_buffer_and_switch_back_(self, preview_config, name): self.switch_mode_(preview_config) return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, delay), self.drop_frames_, - partial(capture_buffer_and_switch_back_, self, preview_config, name)] + functions = [ + partial(self.switch_mode_, camera_config), + partial(self.set_frame_drops_, delay), + self.drop_frames_, + partial(capture_buffer_and_switch_back_, self, preview_config, name), + ] return self.dispatch_functions(functions, wait, signal_function, immediate=True) @overload - def switch_mode_and_capture_buffers(self, camera_config, names=["main"], wait: None = ..., - signal_function: None = ..., delay=0 - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def switch_mode_and_capture_buffers( + self, camera_config, names=["main"], wait: None = ..., signal_function: None = ..., delay=0 + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def switch_mode_and_capture_buffers(self, camera_config, names=["main"], wait: None = ..., - signal_function: Callable[[Job], None] = ..., delay=0 - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def switch_mode_and_capture_buffers( + self, camera_config, names=["main"], wait: None = ..., signal_function: Callable[[Job], None] = ..., delay=0 + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... @overload - def switch_mode_and_capture_buffers(self, camera_config, names=["main"], wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def switch_mode_and_capture_buffers( + self, + camera_config, + names=["main"], + wait: Literal[True] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def switch_mode_and_capture_buffers(self, camera_config, names=["main"], wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def switch_mode_and_capture_buffers( + self, + camera_config, + names=["main"], + wait: Literal[False] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... - def switch_mode_and_capture_buffers(self, camera_config, names=["main"], wait=None, signal_function=None, delay=0 - ) -> Union[ - tuple[list[NDArray[np.uint8]], dict[str, Any]], - Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]] - ]: + def switch_mode_and_capture_buffers( + self, camera_config, names=["main"], wait=None, signal_function=None, delay=0 + ) -> Union[tuple[list[NDArray[np.uint8]], dict[str, Any]], Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]]: """Switch the camera into a new (capture) mode, capture the first buffers. Then return back to the initial camera mode. @@ -2078,9 +2147,12 @@ def capture_buffers_and_switch_back_(self, preview_config, names): self.switch_mode_(preview_config) return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, delay), self.drop_frames_, - partial(capture_buffers_and_switch_back_, self, preview_config, names)] + functions = [ + partial(self.switch_mode_, camera_config), + partial(self.set_frame_drops_, delay), + self.drop_frames_, + partial(capture_buffers_and_switch_back_, self, preview_config, names), + ] return self.dispatch_functions(functions, wait, signal_function, immediate=True) def capture_array_(self, name): @@ -2092,28 +2164,22 @@ def capture_array_(self, name): return (True, result) @overload - def capture_array(self, name="main", wait: None = ..., - signal_function: None = ... - ) -> NDArray[np.uint8]: - ... + def capture_array(self, name="main", wait: None = ..., signal_function: None = ...) -> NDArray[np.uint8]: ... @overload - def capture_array(self, name="main", wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[NDArray[np.uint8]]: - ... + def capture_array( + self, name="main", wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[NDArray[np.uint8]]: ... @overload - def capture_array(self, name="main", wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> NDArray[np.uint8]: - ... + def capture_array( + self, name="main", wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> NDArray[np.uint8]: ... @overload - def capture_array(self, name="main", wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[NDArray[np.uint8]]: - ... + def capture_array( + self, name="main", wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[NDArray[np.uint8]]: ... def capture_array(self, name="main", wait=None, signal_function=None) -> Union[NDArray[np.uint8], Job[NDArray[np.uint8]]]: """Make a 2d image from the next frame in the named stream.""" @@ -2128,63 +2194,64 @@ def capture_arrays_and_metadata_(self, names): return (True, result) @overload - def capture_arrays(self, names=["main"], wait: None = ..., - signal_function: None = ... - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def capture_arrays( + self, names=["main"], wait: None = ..., signal_function: None = ... + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def capture_arrays(self, names=["main"], wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def capture_arrays( + self, names=["main"], wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... @overload - def capture_arrays(self, names=["main"], wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def capture_arrays( + self, names=["main"], wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def capture_arrays(self, names=["main"], wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def capture_arrays( + self, names=["main"], wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... - def capture_arrays(self, names=["main"], wait=None, signal_function=None - ) -> Union[ - tuple[list[NDArray[np.uint8]], dict[str, Any]], - Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]] - ]: + def capture_arrays( + self, names=["main"], wait=None, signal_function=None + ) -> Union[tuple[list[NDArray[np.uint8]], dict[str, Any]], Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]]: """Make 2d image arrays from the next frames in the named streams.""" return self.dispatch_functions([partial(self.capture_arrays_and_metadata_, names)], wait, signal_function) @overload - def switch_mode_and_capture_array(self, camera_config, name="main", wait: None = ..., - signal_function: None = ..., delay=0 - ) -> NDArray[np.uint8]: - ... + def switch_mode_and_capture_array( + self, camera_config, name="main", wait: None = ..., signal_function: None = ..., delay=0 + ) -> NDArray[np.uint8]: ... @overload - def switch_mode_and_capture_array(self, camera_config, name="main", wait: None = ..., - signal_function: Callable[[Job], None] = ..., delay=0 - ) -> Job[NDArray[np.uint8]]: - ... + def switch_mode_and_capture_array( + self, camera_config, name="main", wait: None = ..., signal_function: Callable[[Job], None] = ..., delay=0 + ) -> Job[NDArray[np.uint8]]: ... @overload - def switch_mode_and_capture_array(self, camera_config, name="main", wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> NDArray[np.uint8]: - ... + def switch_mode_and_capture_array( + self, + camera_config, + name="main", + wait: Literal[True] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> NDArray[np.uint8]: ... @overload - def switch_mode_and_capture_array(self, camera_config, name="main", wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> Job[NDArray[np.uint8]]: - ... + def switch_mode_and_capture_array( + self, + camera_config, + name="main", + wait: Literal[False] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> Job[NDArray[np.uint8]]: ... - def switch_mode_and_capture_array(self, camera_config, name="main", wait=None, signal_function=None, delay=0 - ) -> Union[NDArray[np.uint8], Job[NDArray[np.uint8]]]: + def switch_mode_and_capture_array( + self, camera_config, name="main", wait=None, signal_function=None, delay=0 + ) -> Union[NDArray[np.uint8], Job[NDArray[np.uint8]]]: """Switch the camera into a new (capture) mode, capture the image array data. Then return back to the initial camera mode. @@ -2198,41 +2265,47 @@ def capture_array_and_switch_back_(self, preview_config, name): self.switch_mode_(preview_config) return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, delay), self.drop_frames_, - partial(capture_array_and_switch_back_, self, preview_config, name)] + functions = [ + partial(self.switch_mode_, camera_config), + partial(self.set_frame_drops_, delay), + self.drop_frames_, + partial(capture_array_and_switch_back_, self, preview_config, name), + ] return self.dispatch_functions(functions, wait, signal_function, immediate=True) @overload - def switch_mode_and_capture_arrays(self, camera_config, names=["main"], wait: None = ..., - signal_function: None = ..., delay=0 - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def switch_mode_and_capture_arrays( + self, camera_config, names=["main"], wait: None = ..., signal_function: None = ..., delay=0 + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def switch_mode_and_capture_arrays(self, camera_config, names=["main"], wait: None = ..., - signal_function: Callable[[Job], None] = ..., delay=0 - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def switch_mode_and_capture_arrays( + self, camera_config, names=["main"], wait: None = ..., signal_function: Callable[[Job], None] = ..., delay=0 + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... @overload - def switch_mode_and_capture_arrays(self, camera_config, names=["main"], wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: - ... + def switch_mode_and_capture_arrays( + self, + camera_config, + names=["main"], + wait: Literal[True] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> tuple[list[NDArray[np.uint8]], dict[str, Any]]: ... @overload - def switch_mode_and_capture_arrays(self, camera_config, names=["main"], wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: - ... + def switch_mode_and_capture_arrays( + self, + camera_config, + names=["main"], + wait: Literal[False] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]: ... - def switch_mode_and_capture_arrays(self, camera_config, names=["main"], wait=None, - signal_function=None, delay=0 - ) -> Union[ - tuple[list[NDArray[np.uint8]], dict[str, Any]], - Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]] - ]: + def switch_mode_and_capture_arrays( + self, camera_config, names=["main"], wait=None, signal_function=None, delay=0 + ) -> Union[tuple[list[NDArray[np.uint8]], dict[str, Any]], Job[tuple[list[NDArray[np.uint8]], dict[str, Any]]]]: """Switch the camera into a new (capture) mode, capture the image arrays. Then return back to the initial camera mode. @@ -2246,9 +2319,12 @@ def capture_arrays_and_switch_back_(self, preview_config, names): self.switch_mode_(preview_config) return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, delay), self.drop_frames_, - partial(capture_arrays_and_switch_back_, self, preview_config, names)] + functions = [ + partial(self.switch_mode_, camera_config), + partial(self.set_frame_drops_, delay), + self.drop_frames_, + partial(capture_arrays_and_switch_back_, self, preview_config, names), + ] return self.dispatch_functions(functions, wait, signal_function, immediate=True) def capture_image_(self, name): @@ -2265,28 +2341,22 @@ def capture_image_(self, name): return (True, result) @overload - def capture_image(self, name="main", wait: None = ..., - signal_function: None = ... - ) -> Image.Image: - ... + def capture_image(self, name="main", wait: None = ..., signal_function: None = ...) -> Image.Image: ... @overload - def capture_image(self, name="main", wait: None = ..., - signal_function: Callable[[Job], None] = ... - ) -> Job[Image.Image]: - ... + def capture_image( + self, name="main", wait: None = ..., signal_function: Callable[[Job], None] = ... + ) -> Job[Image.Image]: ... @overload - def capture_image(self, name="main", wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Image.Image: - ... + def capture_image( + self, name="main", wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Image.Image: ... @overload - def capture_image(self, name="main", wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ... - ) -> Job[Image.Image]: - ... + def capture_image( + self, name="main", wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[Image.Image]: ... def capture_image(self, name="main", wait=None, signal_function=None) -> Union[Image.Image, Job[Image.Image]]: """Make a PIL image from the next frame in the named stream. @@ -2303,31 +2373,38 @@ def capture_image(self, name="main", wait=None, signal_function=None) -> Union[I return self.dispatch_functions([partial(self.capture_image_, name)], wait, signal_function) @overload - def switch_mode_and_capture_image(self, camera_config, name="main", wait: None = ..., - signal_function: None = ..., delay=0 - ) -> Image.Image: - ... + def switch_mode_and_capture_image( + self, camera_config, name="main", wait: None = ..., signal_function: None = ..., delay=0 + ) -> Image.Image: ... @overload - def switch_mode_and_capture_image(self, camera_config, name="main", wait: None = ..., - signal_function: Callable[[Job], None] = ..., delay=0 - ) -> Job[Image.Image]: - ... + def switch_mode_and_capture_image( + self, camera_config, name="main", wait: None = ..., signal_function: Callable[[Job], None] = ..., delay=0 + ) -> Job[Image.Image]: ... @overload - def switch_mode_and_capture_image(self, camera_config, name="main", wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> Image.Image: - ... + def switch_mode_and_capture_image( + self, + camera_config, + name="main", + wait: Literal[True] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> Image.Image: ... @overload - def switch_mode_and_capture_image(self, camera_config, name="main", wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ..., delay=0 - ) -> Job[Image.Image]: - ... + def switch_mode_and_capture_image( + self, + camera_config, + name="main", + wait: Literal[False] = ..., + signal_function: Optional[Callable[[Job], None]] = ..., + delay=0, + ) -> Job[Image.Image]: ... - def switch_mode_and_capture_image(self, camera_config, name="main", wait=None, signal_function=None, delay=0 - ) -> Union[Image.Image, Job[Image.Image]]: + def switch_mode_and_capture_image( + self, camera_config, name="main", wait=None, signal_function=None, delay=0 + ) -> Union[Image.Image, Job[Image.Image]]: """Switch the camera into a new (capture) mode, capture the image. Then return back to the initial camera mode. @@ -2341,9 +2418,12 @@ def capture_image_and_switch_back_(self, preview_config, name): self.switch_mode_(preview_config) return (True, result) - functions = [partial(self.switch_mode_, camera_config), - partial(self.set_frame_drops_, delay), self.drop_frames_, - partial(capture_image_and_switch_back_, self, preview_config, name)] + functions = [ + partial(self.switch_mode_, camera_config), + partial(self.set_frame_drops_, delay), + self.drop_frames_, + partial(capture_image_and_switch_back_, self, preview_config, name), + ] return self.dispatch_functions(functions, wait, signal_function, immediate=True) def start_encoder(self, encoder=None, output=None, pts=None, quality=None, name=None) -> None: @@ -2472,10 +2552,17 @@ def set_overlay(self, overlay) -> None: raise RuntimeError("Overlay must be a 4-channel image") self._preview.set_overlay(overlay) - def start_and_capture_files(self, name="image{:03d}.jpg", - initial_delay=1, preview_mode="preview", - capture_mode="still", num_files=1, delay=1, - show_preview=True, exif_data=None) -> None: + def start_and_capture_files( + self, + name="image{:03d}.jpg", + initial_delay=1, + preview_mode="preview", + capture_mode="still", + num_files=1, + delay=1, + show_preview=True, + exif_data=None, + ) -> None: """This function makes capturing multiple images more convenient. Should only be used in command line line applications (not from a Qt application, for example). @@ -2533,8 +2620,9 @@ def start_and_capture_files(self, name="image{:03d}.jpg", time.sleep(delay) self.stop() - def start_and_capture_file(self, name="image.jpg", delay=1, preview_mode="preview", - capture_mode="still", show_preview=True, exif_data=None) -> None: + def start_and_capture_file( + self, name="image.jpg", delay=1, preview_mode="preview", capture_mode="still", show_preview=True, exif_data=None + ) -> None: """This function makes capturing a single image more convenient. Should only be used in command line line applications (not from a Qt application, for example). @@ -2560,13 +2648,19 @@ def start_and_capture_file(self, name="image.jpg", delay=1, preview_mode="previe exif_data - dictionary containing user defined exif data (based on `piexif`). This will overwrite existing exif information generated by picamera2. """ - self.start_and_capture_files(name=name, initial_delay=delay, preview_mode=preview_mode, - capture_mode=capture_mode, num_files=1, - show_preview=show_preview, - exif_data=exif_data) - - def start_and_record_video(self, output, encoder=None, config=None, quality=Quality.MEDIUM, - show_preview=False, duration=0, audio=False) -> None: + self.start_and_capture_files( + name=name, + initial_delay=delay, + preview_mode=preview_mode, + capture_mode=capture_mode, + num_files=1, + show_preview=show_preview, + exif_data=exif_data, + ) + + def start_and_record_video( + self, output, encoder=None, config=None, quality=Quality.MEDIUM, show_preview=False, duration=0, audio=False + ) -> None: """This function makes video recording more convenient. Should only be used in command line applications (not from a Qt application, for example). @@ -2635,24 +2729,18 @@ def start_and_record_video(self, output, encoder=None, config=None, quality=Qual self.stop_recording() @overload - def autofocus_cycle(self, wait: None = ..., - signal_function: None = ...) -> bool: - ... + def autofocus_cycle(self, wait: None = ..., signal_function: None = ...) -> bool: ... @overload - def autofocus_cycle(self, wait: None = ..., - signal_function: Callable[[Job], None] = ...) -> Job[bool]: - ... + def autofocus_cycle(self, wait: None = ..., signal_function: Callable[[Job], None] = ...) -> Job[bool]: ... @overload - def autofocus_cycle(self, wait: Literal[True] = ..., - signal_function: Optional[Callable[[Job], None]] = ...) -> bool: - ... + def autofocus_cycle(self, wait: Literal[True] = ..., signal_function: Optional[Callable[[Job], None]] = ...) -> bool: ... @overload - def autofocus_cycle(self, wait: Literal[False] = ..., - signal_function: Optional[Callable[[Job], None]] = ...) -> Job[bool]: - ... + def autofocus_cycle( + self, wait: Literal[False] = ..., signal_function: Optional[Callable[[Job], None]] = ... + ) -> Job[bool]: ... def autofocus_cycle(self, wait=None, signal_function=None) -> Union[bool, Job[bool]]: """Switch autofocus to auto mode and run an autofocus cycle. @@ -2670,7 +2758,10 @@ def wait_for_af_state(self, states): # First wait for the scan to start. Once we've seen that, the AF cycle may: # succeed, fail or could go back to idle if it is cancelled. - functions = [partial(wait_for_af_state, self, {controls.AfStateEnum.Scanning}), - partial(wait_for_af_state, self, - {controls.AfStateEnum.Focused, controls.AfStateEnum.Failed, controls.AfStateEnum.Idle})] + functions = [ + partial(wait_for_af_state, self, {controls.AfStateEnum.Scanning}), + partial( + wait_for_af_state, self, {controls.AfStateEnum.Focused, controls.AfStateEnum.Failed, controls.AfStateEnum.Idle} + ), + ] return self.dispatch_functions(functions, wait, signal_function) diff --git a/picamera2/previews/drm_preview.py b/picamera2/previews/drm_preview.py index b1efb1a8..3993db85 100644 --- a/picamera2/previews/drm_preview.py +++ b/picamera2/previews/drm_preview.py @@ -17,7 +17,7 @@ from picamera2.previews.null_preview import * -class DrmManager(): +class DrmManager: def __init__(self): self.lock = threading.Lock() self.use_count = 0 @@ -91,7 +91,7 @@ def render_request(self, completed_request): if self.current and self.own_current: self.current.release() self.current = completed_request - self.own_current = (completed_request.config['buffer_count'] > 1) + self.own_current = completed_request.config['buffer_count'] > 1 if self.own_current: self.current.acquire() @@ -220,10 +220,9 @@ def render_drm(self, picam2, completed_request): h2 = height // 2 stride2 = stride // 2 size = height * stride - drmfb = pykms.DmabufFramebuffer(self.card, width, height, fmt, - [fd, fd, fd], - [stride, stride2, stride2], - [0, size, size + h2 * stride2]) + drmfb = pykms.DmabufFramebuffer( + self.card, width, height, fmt, [fd, fd, fd], [stride, stride2, stride2], [0, size, size + h2 * stride2] + ) else: drmfb = pykms.DmabufFramebuffer(self.card, width, height, fmt, [fd], [stride], [0]) self.drmfbs[fb] = drmfb diff --git a/picamera2/previews/gl_helpers.py b/picamera2/previews/gl_helpers.py index ddd289b7..01c53574 100644 --- a/picamera2/previews/gl_helpers.py +++ b/picamera2/previews/gl_helpers.py @@ -1,9 +1,7 @@ -from ctypes import (CFUNCTYPE, POINTER, c_bool, c_char_p, c_int, c_void_p, - cdll, pointer, util) +from ctypes import CFUNCTYPE, POINTER, c_bool, c_char_p, c_int, c_void_p, cdll, pointer, util from OpenGL import GL as gl -from OpenGL.EGL.VERSION.EGL_1_0 import (EGL_EXTENSIONS, EGLNativeDisplayType, - eglGetProcAddress, eglQueryString) +from OpenGL.EGL.VERSION.EGL_1_0 import EGL_EXTENSIONS, EGLNativeDisplayType, eglGetProcAddress, eglQueryString from OpenGL.GLES2.VERSION.GLES2_2_0 import * from OpenGL.GLES3.VERSION.GLES3_3_0 import * from OpenGL.raw.GLES2 import _types as _cs @@ -29,7 +27,7 @@ def getglEGLImageTargetTexture2DOES(): def str_to_fourcc(str): - assert (len(str) == 4) + assert len(str) == 4 fourcc = 0 for i, v in enumerate([ord(c) for c in str]): fourcc |= v << (i * 8) diff --git a/picamera2/previews/q_gl_picamera2.py b/picamera2/previews/q_gl_picamera2.py index 412a8b93..e08c80df 100644 --- a/picamera2/previews/q_gl_picamera2.py +++ b/picamera2/previews/q_gl_picamera2.py @@ -48,6 +48,7 @@ def choose_config(self): eglBindAPI(EGL_OPENGL_ES_API) + # fmt: off config_attribs = [ EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RED_SIZE, 8, @@ -57,6 +58,7 @@ def choose_config(self): EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE, ] + # fmt: on n = EGLint() configs = (EGLConfig * 1)() @@ -64,10 +66,7 @@ def choose_config(self): self.config = configs[0] def create_context(self): - context_attribs = [ - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE, - ] + context_attribs = [EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE] self.context = eglCreateContext(self.display, self.config, EGL_NO_CONTEXT, context_attribs) eglMakeCurrent(self.display, EGL_NO_SURFACE, EGL_NO_SURFACE, self.context) @@ -75,23 +74,29 @@ def create_context(self): @lru_cache(maxsize=None, typed=False) def _get_qglpicamera2(qt_module: _QT_BINDING): - # Get Qt modules QtCore, QtGui, QtWidgets = _get_qt_modules(qt_module) # Get from QtCore - QSocketNotifier, Qt, pyqtSignal, pyqtSlot = attrgetter( - 'QSocketNotifier', 'Qt', 'pyqtSignal', 'pyqtSlot')(QtCore) + QSocketNotifier, Qt, pyqtSignal, pyqtSlot = attrgetter('QSocketNotifier', 'Qt', 'pyqtSignal', 'pyqtSlot')(QtCore) # Get from QtWidgets - QWidget = attrgetter( - 'QWidget')(QtWidgets) + QWidget = attrgetter('QWidget')(QtWidgets) class QGlPicamera2(QWidget): done_signal = pyqtSignal(object) - def __init__(self, picam2, parent=None, width=640, height=480, bg_colour=(20, 20, 20), - keep_ar=True, transform=None, preview_window=None): + def __init__( + self, + picam2, + parent=None, + width=640, + height=480, + bg_colour=(20, 20, 20), + keep_ar=True, + transform=None, + preview_window=None, + ): super().__init__(parent=parent) self.resize(width, height) @@ -112,8 +117,10 @@ def __init__(self, picam2, parent=None, width=640, height=480, bg_colour=(20, 20 self.title_function = None self.egl = EglState() if picam2.verbose_console: - print(f"EGL {eglQueryString(self.egl.display, EGL_VENDOR).decode()} " - f"{eglQueryString(self.egl.display, EGL_VERSION).decode()}") + print( + f"EGL {eglQueryString(self.egl.display, EGL_VENDOR).decode()} " + f"{eglQueryString(self.egl.display, EGL_VERSION).decode()}" + ) self.init_gl() # set_overlay could be called before the first frame arrives, hence: @@ -123,8 +130,7 @@ def __init__(self, picam2, parent=None, width=640, height=480, bg_colour=(20, 20 picam2.attach_preview(preview_window) self.preview_window = preview_window - self.camera_notifier = QSocketNotifier(self.picamera2.notifyme_r, - QSocketNotifier.Type.Read, self) + self.camera_notifier = QSocketNotifier(self.picamera2.notifyme_r, QSocketNotifier.Type.Read, self) self.camera_notifier.activated.connect(self.handle_requests) # Must always run cleanup when this widget goes away. self.destroyed.connect(lambda: self.cleanup()) @@ -148,7 +154,7 @@ def cleanup(self): self.preview_window.qpicamera2 = None # Some extra EGL cleanup seems to be required. - for (_, buffer) in self.buffers.items(): + for _, buffer in self.buffers.items(): glDeleteTextures(1, [buffer.texture]) self.buffers = {} eglDestroyContext(self.egl.display, self.egl.context) @@ -165,8 +171,7 @@ def paintEngine(self): def create_surface(self): native_surface = c_void_p(self.winId().__int__()) - surface = eglCreateWindowSurface(self.egl.display, self.egl.config, - native_surface, None) + surface = eglCreateWindowSurface(self.egl.display, self.egl.config, native_surface, None) self.surface = surface @@ -223,7 +228,7 @@ def init_gl(self): } """ - vertex_shader = shaders.compileShader(vertShaderSrc_image, GL_VERTEX_SHADER), + vertex_shader = (shaders.compileShader(vertShaderSrc_image, GL_VERTEX_SHADER),) # For some reason I seem to be getting a 1 element tuple back. Absolutely no clue why. if isinstance(vertex_shader, tuple): vertex_shader = vertex_shader[0] @@ -232,15 +237,17 @@ def init_gl(self): self.program_overlay = shaders.compileProgram( shaders.compileShader(vertShaderSrc_overlay, GL_VERTEX_SHADER), - shaders.compileShader(fragShaderSrc_overlay, GL_FRAGMENT_SHADER) + shaders.compileShader(fragShaderSrc_overlay, GL_FRAGMENT_SHADER), ) + # fmt: off vertPositions = [ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, - 0.0, 1.0 + 0.0, 1.0, ] + # fmt: on inputAttrib = glGetAttribLocation(self.program_image, "aPosition") glVertexAttribPointer(inputAttrib, 2, GL_FLOAT, GL_FALSE, 0, vertPositions) @@ -282,6 +289,7 @@ def __init__(self, display, completed_request, max_texture_size): if pixel_format in ("YUV420", "YVU420"): h2 = h // 2 stride2 = cfg.stride // 2 + # fmt: off attribs = [ EGL_WIDTH, w, EGL_HEIGHT, h, @@ -297,7 +305,9 @@ def __init__(self, display, completed_request, max_texture_size): EGL_DMA_BUF_PLANE2_PITCH_EXT, stride2, EGL_NONE, ] + # fmt: on else: + # fmt: off attribs = [ EGL_WIDTH, w, EGL_HEIGHT, h, @@ -307,12 +317,9 @@ def __init__(self, display, completed_request, max_texture_size): EGL_DMA_BUF_PLANE0_PITCH_EXT, cfg.stride, EGL_NONE, ] + # fmt: on - image = eglCreateImageKHR(display, - EGL_NO_CONTEXT, - EGL_LINUX_DMA_BUF_EXT, - None, - attribs) + image = eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, None, attribs) self.texture = glGenTextures(1) glBindTexture(GL_TEXTURE_EXTERNAL_OES, self.texture) @@ -358,7 +365,7 @@ def repaint(self, completed_request, update_viewport=False): if self.stop_count != self.picamera2.stop_count: if self.picamera2.verbose_console: print("Garbage collect", len(self.buffers), "textures") - for (_, buffer) in self.buffers.items(): + for _, buffer in self.buffers.items(): glDeleteTextures(1, [buffer.texture]) self.buffers = {} self.stop_count = self.picamera2.stop_count @@ -366,7 +373,8 @@ def repaint(self, completed_request, update_viewport=False): if self.picamera2.verbose_console: print("Make buffer for request", completed_request.request) self.buffers[completed_request.request] = self.Buffer( - self.egl.display, completed_request, self.egl.max_texture_size) + self.egl.display, completed_request, self.egl.max_texture_size + ) # New buffers mean the image size may change so update the viewport just in case. update_viewport = True @@ -408,7 +416,7 @@ def render_request(self, completed_request): if self.current_request and self.own_current: self.current_request.release() self.current_request = completed_request - self.own_current = (completed_request.config['buffer_count'] > 1) + self.own_current = completed_request.config['buffer_count'] > 1 if self.own_current: self.current_request.acquire() @@ -428,8 +436,10 @@ def recalculate_viewport(self): if not self.keep_ar or not camera_config or camera_config['display'] is None: return 0, 0, window_w, window_h - image_w, image_h = (stream_map[camera_config['display']].configuration.size.width, - stream_map[camera_config['display']].configuration.size.height) + image_w, image_h = ( + stream_map[camera_config['display']].configuration.size.width, + stream_map[camera_config['display']].configuration.size.height, + ) if image_w * window_h > window_w * image_h: w = window_w h = w * image_h // image_w diff --git a/picamera2/previews/q_picamera2.py b/picamera2/previews/q_picamera2.py index ad41504c..79d99232 100644 --- a/picamera2/previews/q_picamera2.py +++ b/picamera2/previews/q_picamera2.py @@ -7,6 +7,7 @@ try: import cv2 + cv2_available = True except ImportError: cv2_available = False @@ -16,28 +17,35 @@ @lru_cache(maxsize=None, typed=False) def _get_qpicamera2(qt_module: _QT_BINDING): - # Get Qt modules QtCore, QtGui, QtWidgets = _get_qt_modules(qt_module) # Get from QtCore QRect, QRectF, QSize, QSocketNotifier, Qt, pyqtSignal, pyqtSlot = attrgetter( - 'QRect', 'QRectF', 'QSize', 'QSocketNotifier', 'Qt', 'pyqtSignal', 'pyqtSlot')(QtCore) + 'QRect', 'QRectF', 'QSize', 'QSocketNotifier', 'Qt', 'pyqtSignal', 'pyqtSlot' + )(QtCore) # Get from QtGui - QBrush, QColor, QImage, QPixmap, QTransform = attrgetter( - 'QBrush', 'QColor', 'QImage', 'QPixmap', 'QTransform')(QtGui) + QBrush, QColor, QImage, QPixmap, QTransform = attrgetter('QBrush', 'QColor', 'QImage', 'QPixmap', 'QTransform')(QtGui) # Get from QtWidgets - QGraphicsScene, QGraphicsView = attrgetter( - 'QGraphicsScene', 'QGraphicsView')(QtWidgets) + QGraphicsScene, QGraphicsView = attrgetter('QGraphicsScene', 'QGraphicsView')(QtWidgets) class QPicamera2(QGraphicsView): done_signal = pyqtSignal(object) update_overlay_signal = pyqtSignal(object) - def __init__(self, picam2, parent=None, width=640, height=480, bg_colour=(20, 20, 20), - keep_ar=True, transform=None, preview_window=None): + def __init__( + self, + picam2, + parent=None, + width=640, + height=480, + bg_colour=(20, 20, 20), + keep_ar=True, + transform=None, + preview_window=None, + ): super().__init__(parent=parent) self.picamera2 = picam2 picam2.attach_preview(preview_window) @@ -60,8 +68,7 @@ def __init__(self, picam2, parent=None, width=640, height=480, bg_colour=(20, 20 self.title_function = None self.update_overlay_signal.connect(self.update_overlay) - self.camera_notifier = QSocketNotifier(self.picamera2.notifyme_r, - QSocketNotifier.Type.Read, self) + self.camera_notifier = QSocketNotifier(self.picamera2.notifyme_r, QSocketNotifier.Type.Read, self) self.camera_notifier.activated.connect(self.handle_requests) # Must always run cleanup when this widget goes away. self.destroyed.connect(lambda: self.cleanup()) @@ -91,8 +98,10 @@ def image_dimensions(self): camera_config = self.picamera2.camera_config if camera_config and camera_config['display'] is not None: # This works even before we receive any camera images. - size = (self.picamera2.stream_map[camera_config['display']].configuration.size.width, - self.picamera2.stream_map[camera_config['display']].configuration.size.height) + size = ( + self.picamera2.stream_map[camera_config['display']].configuration.size.width, + self.picamera2.stream_map[camera_config['display']].configuration.size.height, + ) elif self.image_size is not None: # If the camera is unconfigured, stick with the last size (if available). size = self.image_size diff --git a/picamera2/previews/qt_compatibility.py b/picamera2/previews/qt_compatibility.py index 45f9e73a..2fbc1285 100644 --- a/picamera2/previews/qt_compatibility.py +++ b/picamera2/previews/qt_compatibility.py @@ -38,5 +38,5 @@ def _get_qt_modules(qt_module: _QT_BINDING) -> tuple[ModuleType, ModuleType, Mod QtCoreModule.pyqtSlot = QtCoreModule.Slot return (QtCoreModule, QtGuiModule, QtWidgetsModule) - except ImportError: - raise ImportError(f'{qt_module} is not installed or could not be imported.') + except ImportError as e: + raise ImportError(f'{qt_module} is not installed or could not be imported.') from e diff --git a/picamera2/previews/qt_previews.py b/picamera2/previews/qt_previews.py index a649efaf..cc8c39a0 100644 --- a/picamera2/previews/qt_previews.py +++ b/picamera2/previews/qt_previews.py @@ -33,8 +33,9 @@ def deletepreview(parent, preview, previewretrieveq): @QtCore.pyqtSlot() def createpreview(parent, cam, previewretrieveq): - qpicamera2 = parent.make_picamera2_widget(cam, width=parent.width, height=parent.height, - transform=parent.transform) + qpicamera2 = parent.make_picamera2_widget( + cam, width=parent.width, height=parent.height, transform=parent.transform + ) if parent.x is not None and parent.y is not None: qpicamera2.move(parent.x, parent.y) qpicamera2.setWindowTitle(parent.get_title()) @@ -43,7 +44,6 @@ def createpreview(parent, cam, previewretrieveq): previewretrieveq.put(qpicamera2) class MonitorThread(QtCore.QThread): - previewsignal = QtCore.pyqtSignal(object, object, object) deletesignal = QtCore.pyqtSignal(object, object, object) @@ -129,6 +129,7 @@ def set_title_function(self, function): class QtPreview(QtPreviewBase): def make_picamera2_widget(self, picam2, width=640, height=480, transform=None): from picamera2.previews.qt import QPicamera2 + return QPicamera2(picam2, width=self.width, height=self.height, transform=self.transform, preview_window=self) def get_title(self): @@ -138,6 +139,7 @@ def get_title(self): class QtGlPreview(QtPreviewBase): def make_picamera2_widget(self, picam2, width=640, height=480, transform=None): from picamera2.previews.qt import QGlPicamera2 + return QGlPicamera2(picam2, width=self.width, height=self.height, transform=self.transform, preview_window=self) def get_title(self): diff --git a/picamera2/remote.py b/picamera2/remote.py index ad288e6f..38190ab3 100644 --- a/picamera2/remote.py +++ b/picamera2/remote.py @@ -32,11 +32,12 @@ class Process: """ def __init__( - self, - run: Callable[["RemoteRequest"], Any], - picam2: picamera2.Picamera2, - init: Callable[[], None] | None = None, - timeout: float | None = 30): + self, + run: Callable[["RemoteRequest"], Any], + picam2: picamera2.Picamera2, + init: Callable[[], None] | None = None, + timeout: float | None = 30, + ): """ Initializes the Process. @@ -106,14 +107,15 @@ class _RemoteProcess(mp.Process): """ def __init__( - self, - send_queue: mp.Queue, - return_queue: mp.Queue, - picam2: picamera2.Picamera2, - run: Callable[["RemoteRequest"], Any], - init: Callable[[], None], - *args, - **kwargs): + self, + send_queue: mp.Queue, + return_queue: mp.Queue, + picam2: picamera2.Picamera2, + run: Callable[["RemoteRequest"], Any], + init: Callable[[], None], + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self._send_queue = send_queue self._return_queue = return_queue @@ -271,14 +273,14 @@ def _deserialize(self, process: _RemoteProcess): process._buffer_cache[pid_fd] = self._buffers[name] self.config["transform"] = Transform( - self.config["transform"][0], - self.config["transform"][1], - self.config["transform"][2]) + self.config["transform"][0], self.config["transform"][1], self.config["transform"][2] + ) self.config["colour_space"] = ColorSpace( self.config["colour_space"][0], self.config["colour_space"][1], self.config["colour_space"][2], - self.config["colour_space"][3]) + self.config["colour_space"][3], + ) def _deserialize_buffer(self, fd: int, length: int): """Deserializes a buffer.""" @@ -296,9 +298,9 @@ def _create_array(self, mem, name: str): return array.reshape((height + height // 2, stride)) array = array.reshape((height, stride)) if format in ("RGB888", "BGR888"): - return array[:, :width * 3].reshape((height, width, 3)) + return array[:, : width * 3].reshape((height, width, 3)) elif format in ("XBGR8888", "XRGB8888"): - return array[:, :width * 4].reshape((height, width, 4)) + return array[:, : width * 4].reshape((height, width, 4)) return array def get_metadata(self): @@ -395,12 +397,13 @@ class Pool: """ def __init__( - self, - run: Callable[["RemoteRequest"], Any], - count: int, - picam2: picamera2.Picamera2, - init: Callable[[], None] | None = None, - timeout: float | None = 30): + self, + run: Callable[["RemoteRequest"], Any], + count: int, + picam2: picamera2.Picamera2, + init: Callable[[], None] | None = None, + timeout: float | None = 30, + ): """Initializes the Pool.""" self._processes = [Process(run, picam2, init, timeout) for _ in range(count)] self._process_count = count @@ -416,10 +419,7 @@ def send(self, request: picamera2.request.CompletedRequest, **kwargs): """Sends a request to the child process.""" process = min( enumerate(self._processes), - key=lambda p: ( - len(p[1]._requests_sent), - (p[0] + self._process_index) % self._process_count - ) + key=lambda p: (len(p[1]._requests_sent), (p[0] + self._process_index) % self._process_count), )[1] self._process_index = (self._process_index + 1) % self._process_count diff --git a/picamera2/request.py b/picamera2/request.py index 46516626..d522f0da 100644 --- a/picamera2/request.py +++ b/picamera2/request.py @@ -97,8 +97,9 @@ def __init__(self, request: Any, picam2: "Picamera2") -> None: self.config = self.picam2.camera_config.copy() self.stream_map = self.picam2.stream_map.copy() with self.lock: - self.syncs = [picam2.allocator.sync(self.picam2.allocator, buffer, False) - for buffer in self.request.buffers.values()] + self.syncs = [ + picam2.allocator.sync(self.picam2.allocator, buffer, False) for buffer in self.request.buffers.values() + ] self.picam2.allocator.acquire(self.request.buffers) [sync.__enter__() for sync in self.syncs] @@ -123,7 +124,6 @@ def release(self) -> None: self.request.reuse() controls = self.picam2.controls.get_libcamera_controls() for id, value in controls.items(): - # libcamera now has "ExposureTimeMode" and "AnalogueGainMode" which must be set to # manual for the fixed exposure time or gain to have any effect, and cleared to return # to "auto " mode. We're going to hide that by supplying them automatically as needed. @@ -217,8 +217,9 @@ def make_image(self, name: str, width: Optional[int] = None, height: Optional[in pil_img = pil_img.resize((width, height)) return pil_img - def save(self, name: str, file_output: Any, format: Optional[str] = None, - exif_data: Optional[Dict[str, Any]] = None) -> None: + def save( + self, name: str, file_output: Any, format: Optional[str] = None, exif_data: Optional[Dict[str, Any]] = None + ) -> None: """ Save a JPEG or PNG image of the named stream's buffer. @@ -229,8 +230,9 @@ def save(self, name: str, file_output: Any, format: Optional[str] = None, config = self.config.get(name, None) if config is None: raise RuntimeError(f'Stream {name!r} is not defined') - if (config['format'] == 'YUV420' or (self.FASTER_JPEG and config['format'] != "MJPEG")) and \ - self.picam2.helpers._get_format_str(file_output, format) in ('jpg', 'jpeg'): + if ( + config['format'] == 'YUV420' or (self.FASTER_JPEG and config['format'] != "MJPEG") + ) and self.picam2.helpers._get_format_str(file_output, format) in ('jpg', 'jpeg'): quality = self.picam2.options.get("quality", 90) with MappedArray(self, name) as m: format = self.config[name]["format"] @@ -238,8 +240,8 @@ def save(self, name: str, file_output: Any, format: Optional[str] = None, width, height = self.config[name]['size'] Y = m.array[:height, :width] reshaped = m.array.reshape((m.array.shape[0] * 2, m.array.strides[0] // 2)) - U = reshaped[2 * height: 2 * height + height // 2, :width // 2] - V = reshaped[2 * height + height // 2:, :width // 2] + U = reshaped[2 * height : 2 * height + height // 2, : width // 2] + V = reshaped[2 * height + height // 2 :, : width // 2] output_bytes = simplejpeg.encode_jpeg_yuv_planes(Y, U, V, quality) Y = reshaped = U = V = None else: @@ -264,8 +266,7 @@ def save(self, name: str, file_output: Any, format: Optional[str] = None, if f is not file_output: f.close() else: - return self.picam2.helpers.save(self.make_image(name), self.get_metadata(), file_output, - format, exif_data) + return self.picam2.helpers.save(self.make_image(name), self.get_metadata(), file_output, format, exif_data) def save_dng(self, file_output: Any, name: str = "raw") -> None: """Save a DNG RAW image of the raw stream's buffer.""" @@ -302,17 +303,17 @@ def _make_array_shared(self, buffer: np.ndarray, config: Dict[str, Any]) -> np.n if fmt in ("BGR888", "RGB888"): if stride != w * 3: array = array.reshape((h, stride)) - array = array[:, :w * 3] + array = array[:, : w * 3] image = array.reshape((h, w, 3)) elif fmt in ("XBGR8888", "XRGB8888"): if stride != w * 4: array = array.reshape((h, stride)) - array = array[:, :w * 4] + array = array[:, : w * 4] image = array.reshape((h, w, 4)) elif fmt in ("BGR161616", "RGB161616"): if stride != w * 6: array = array.reshape((h, stride)) - array = array[:, :w * 6] + array = array[:, : w * 6] array = array.view(np.uint16) image = array.reshape((h, w, 3)) elif fmt in ("YUV420", "YVU420"): @@ -352,8 +353,9 @@ def _get_pil_mode(self, fmt): raise RuntimeError(f"Stream format {fmt} not supported for PIL images") return mode - def make_image(self, buffer: np.ndarray, config: Dict[str, Any], width: Optional[int] = None, - height: Optional[int] = None) -> Image.Image: + def make_image( + self, buffer: np.ndarray, config: Dict[str, Any], width: Optional[int] = None, height: Optional[int] = None + ) -> Image.Image: """Make a PIL image from the named stream's buffer.""" fmt = config["format"] if fmt == "MJPEG": @@ -380,14 +382,18 @@ def _prepare_exif(self, metadata, exif_data): exif = b'' if "AnalogueGain" in metadata and "DigitalGain" in metadata: datetime_now = datetime.now().strftime("%Y:%m:%d %H:%M:%S") - zero_ifd = {piexif.ImageIFD.Make: "Raspberry Pi", - piexif.ImageIFD.Model: self.picam2.camera.id, - piexif.ImageIFD.Software: "Picamera2", - piexif.ImageIFD.DateTime: datetime_now} + zero_ifd = { + piexif.ImageIFD.Make: "Raspberry Pi", + piexif.ImageIFD.Model: self.picam2.camera.id, + piexif.ImageIFD.Software: "Picamera2", + piexif.ImageIFD.DateTime: datetime_now, + } total_gain = metadata["AnalogueGain"] * metadata["DigitalGain"] - exif_ifd = {piexif.ExifIFD.DateTimeOriginal: datetime_now, - piexif.ExifIFD.ExposureTime: (metadata["ExposureTime"], 1000000), - piexif.ExifIFD.ISOSpeedRatings: int(total_gain * 100)} + exif_ifd = { + piexif.ExifIFD.DateTimeOriginal: datetime_now, + piexif.ExifIFD.ExposureTime: (metadata["ExposureTime"], 1000000), + piexif.ExifIFD.ISOSpeedRatings: int(total_gain * 100), + } exif_dict = {"0th": zero_ifd, "Exif": exif_ifd} # merge user provided exif data, overwriting the defaults exif_dict = exif_dict | (exif_data or {}) @@ -404,8 +410,14 @@ def _get_format_str(self, file_output, format): else: raise RuntimeError("Cannot determine format to save") - def save(self, img: Image.Image, metadata: Dict[str, Any], file_output: Union[str, Path], format: Optional[str] = None, - exif_data: Optional[Dict] = None) -> None: + def save( + self, + img: Image.Image, + metadata: Dict[str, Any], + file_output: Union[str, Path], + format: Optional[str] = None, + exif_data: Optional[Dict] = None, + ) -> None: """Save a JPEG or PNG image of the named stream's buffer. exif_data - dictionary containing user defined exif data (based on `piexif`). This will @@ -433,7 +445,7 @@ def save(self, img: Image.Image, metadata: Dict[str, Any], file_output: Union[st img.save(file_output, **keywords) end_time = time.monotonic() _log.info(f"Saved {self} to file {file_output}.") - _log.info(f"Time taken for encode: {(end_time-start_time)*1000} ms.") + _log.info(f"Time taken for encode: {(end_time - start_time) * 1000} ms.") def save_dng(self, buffer: np.ndarray, metadata: Dict[str, Any], config: Dict[str, Any], file_output: Any) -> None: """Save a DNG RAW image of the raw stream's buffer.""" @@ -464,7 +476,7 @@ def save_dng(self, buffer: np.ndarray, metadata: Dict[str, Any], config: Dict[st end_time = time.monotonic() _log.info(f"Saved {self} to file {file_output}.") - _log.info(f"Time taken for encode: {(end_time-start_time)*1000} ms.") + _log.info(f"Time taken for encode: {(end_time - start_time) * 1000} ms.") def decompress(self, array: np.ndarray): """Decompress an image buffer that has been compressed with a PiSP compression format.""" diff --git a/picamera2/sensor_format.py b/picamera2/sensor_format.py index f243a546..932a99b1 100644 --- a/picamera2/sensor_format.py +++ b/picamera2/sensor_format.py @@ -4,7 +4,7 @@ from libcamera import Transform -class SensorFormat(): +class SensorFormat: def __init__(self, fmt_string): if "_" in fmt_string: pixels, self.packing = fmt_string.split("_", 1) diff --git a/picamera2/utils.py b/picamera2/utils.py index ea597a97..559cf237 100644 --- a/picamera2/utils.py +++ b/picamera2/utils.py @@ -45,7 +45,7 @@ def colour_space_from_libcamera(colour_space): Transform(transpose=1): Orientation.Rotate90Mirror, Transform(transpose=1, hflip=1): Orientation.Rotate270, Transform(transpose=1, vflip=1): Orientation.Rotate90, - Transform(transpose=1, hflip=1, vflip=1): Orientation.Rotate270Mirror + Transform(transpose=1, hflip=1, vflip=1): Orientation.Rotate270Mirror, } _ORIENTATION_TO_TRANSFORM_TABLE = { @@ -56,7 +56,7 @@ def colour_space_from_libcamera(colour_space): Orientation.Rotate90Mirror: Transform(transpose=1), Orientation.Rotate270: Transform(transpose=1, hflip=1), Orientation.Rotate90: Transform(transpose=1, vflip=1), - Orientation.Rotate270Mirror: Transform(transpose=1, hflip=1, vflip=1) + Orientation.Rotate270Mirror: Transform(transpose=1, hflip=1, vflip=1), } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d286d539 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "picamera2" +version = "0.3.34" +description = "The libcamera-based Python interface to Raspberry Pi cameras, based on the original Picamera library" +readme = "README.md" +license = "BSD-2-Clause" +requires-python = ">=3.9" +authors = [ + { name = "Raspberry Pi & Raspberry Pi Foundation", email = "picamera2@raspberrypi.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.9", + "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", +] +dependencies = [ + "numpy", + "PiDNG", + "piexif", + "pillow", + "simplejpeg", + "videodev2", + "python-prctl", + "av", + "libarchive-c", + "tqdm", + "jsonschema", + "OpenEXR", +] + +[project.optional-dependencies] +gui = ["pyopengl", "PyQt5"] + +[project.urls] +Homepage = "https://github.com/RaspberryPi/picamera2" +"Bug Tracker" = "https://github.com/RaspberryPi/picamera2/issues" + +[tool.setuptools.packages.find] +include = ["picamera2*"] + +[tool.ruff] +line-length = 127 +target-version = "py39" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "T10", # flake8-debugger + "I", # isort +] + +[tool.ruff.lint.isort] +split-on-trailing-comma = false + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"**/qt.py" = ["F401"] +"**/gl_helpers.py" = ["F401", "F403", "F405"] +"**/q_gl_picamera2.py" = ["E402", "F403", "F405"] +"**/v4l2_encoder.py" = ["F403", "F405"] +"**/encoder.py" = ["F403", "F405"] +"**/drm_preview.py" = ["F403", "F405"] +"**/capture_video_raw.py" = ["F403", "F405"] +"**/picamera2.py" = ["B006", "B008"] +"**/configuration.py" = ["B006"] +"**/metadata.py" = ["B006"] +"**/controls.py" = ["B006"] + +[tool.ruff.format] +quote-style = "preserve" +skip-magic-trailing-comma = true diff --git a/requirements-test.txt b/requirements-test.txt index df5d9e42..86f7c55a 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,14 +1,4 @@ -astroid==2.12.14 -pylint==2.15.10 -flake8>=6.0.0 -flake8-bugbear>=22.10.27 -flake8-comprehensions>=3.10 -flake8-debugger -flake8-docstrings>=1.6.0 -flake8-isort>=5.0 -flake8-pylint -flake8-rst-docstrings -flake8-string-format +ruff numpy PiDNG piexif diff --git a/setup.py b/setup.py deleted file mode 100644 index 50571f80..00000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -#! /usr/bin/env python3 - -# Copyright (c) 2021-2022 Raspberry Pi & Raspberry Pi Foundation -# -# SPDX-License-Identifier: BSD-2-Clause - -from setuptools import setup - -with open("README.md") as readme: - long_description = readme.read() - -setup( - name='picamera2', - version='0.3.34', - description='The libcamera-based Python interface to Raspberry Pi cameras, based on the original Picamera library', - long_description=long_description, - long_description_content_type='text/markdown', - author='Raspberry Pi & Raspberry Pi Foundation', - author_email='picamera2@raspberrypi.com', - url='https://github.com/RaspberryPi/picamera2', - project_urls={ - 'Bug Tracker': 'https://github.com/RaspberryPi/picamera2/issues', - }, - license='BSD 2-Clause', - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.9", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - ], - packages=['picamera2', 'picamera2.devices', 'picamera2.devices.hailo', 'picamera2.devices.imx500', - 'picamera2.devices.imx708', 'picamera2.encoders', 'picamera2.outputs', 'picamera2.previews', - 'picamera2.allocators'], - python_requires='>=3.9', - licence='BSD 2-Clause License', - install_requires=['numpy', 'PiDNG', 'piexif', 'pillow', 'simplejpeg', 'videodev2', - 'python-prctl', 'av', 'libarchive-c', 'tqdm', - 'jsonschema', 'OpenEXR'], - extras_require={"gui": ['pyopengl', 'PyQt5']}) diff --git a/tests/aegc.py b/tests/aegc.py index 7a9d4856..a75d1fc9 100755 --- a/tests/aegc.py +++ b/tests/aegc.py @@ -43,6 +43,7 @@ def test_control_auto(control): # Also test that it works when we start the camera. + def test_control_start(control, value): controls = {control: value} config = picam2.create_preview_configuration(controls=controls) diff --git a/tests/allocator_leak_test.py b/tests/allocator_leak_test.py index 4c8a5bd9..0538c779 100644 --- a/tests/allocator_leak_test.py +++ b/tests/allocator_leak_test.py @@ -3,8 +3,7 @@ # Test that the allocators don't leak from picamera2 import Picamera2 -from picamera2.allocators import (DmaAllocator, LibcameraAllocator, - PersistentAllocator) +from picamera2.allocators import DmaAllocator, LibcameraAllocator, PersistentAllocator for _ in range(20): picam2 = Picamera2() diff --git a/tests/app_dual.py b/tests/app_dual.py index 8a8bc048..5c1244ce 100755 --- a/tests/app_dual.py +++ b/tests/app_dual.py @@ -5,8 +5,7 @@ import time from threading import Thread -from PyQt5.QtWidgets import (QApplication, QHBoxLayout, QPushButton, - QVBoxLayout, QWidget) +from PyQt5.QtWidgets import QApplication, QHBoxLayout, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QGlPicamera2, QPicamera2 diff --git a/tests/app_test.py b/tests/app_test.py index 223910e2..86a6a985 100755 --- a/tests/app_test.py +++ b/tests/app_test.py @@ -6,8 +6,7 @@ import time from PyQt5 import QtCore -from PyQt5.QtWidgets import (QApplication, QHBoxLayout, QLabel, QPushButton, - QVBoxLayout, QWidget) +from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QGlPicamera2 diff --git a/tests/app_test_pyqt6.py b/tests/app_test_pyqt6.py index c83e7b06..0220bd79 100755 --- a/tests/app_test_pyqt6.py +++ b/tests/app_test_pyqt6.py @@ -8,8 +8,7 @@ try: from PyQt6 import QtCore - from PyQt6.QtWidgets import (QApplication, QHBoxLayout, QLabel, - QPushButton, QVBoxLayout, QWidget) + from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QGl6Picamera2 as QGlPicamera2 diff --git a/tests/app_test_pyside6.py b/tests/app_test_pyside6.py index fb8bb1a9..64e7a76d 100755 --- a/tests/app_test_pyside6.py +++ b/tests/app_test_pyside6.py @@ -8,8 +8,7 @@ try: from PySide6 import QtCore - from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, - QPushButton, QVBoxLayout, QWidget) + from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from picamera2 import Picamera2 from picamera2.previews.qt import QGlSide6Picamera2 as QGlPicamera2 diff --git a/tests/async_test.py b/tests/async_test.py index d7378bd4..c88c0e96 100755 --- a/tests/async_test.py +++ b/tests/async_test.py @@ -23,7 +23,7 @@ def thread_func(delay): delays = [0.1, 0.07, 0.15] -threads = [Thread(target=thread_func, args=(d, )) for d in delays] +threads = [Thread(target=thread_func, args=(d,)) for d in delays] for thread in threads: thread.start() diff --git a/tests/close_test_multiple.py b/tests/close_test_multiple.py index b02e95d8..4d4de849 100755 --- a/tests/close_test_multiple.py +++ b/tests/close_test_multiple.py @@ -17,7 +17,6 @@ def run_camera(idx): if __name__ == '__main__': - if len(Picamera2.global_camera_info()) <= 1: print("SKIPPED (one camera)") quit() diff --git a/tests/colour_spaces.py b/tests/colour_spaces.py index 4dd87330..dcc69cd7 100644 --- a/tests/colour_spaces.py +++ b/tests/colour_spaces.py @@ -27,29 +27,40 @@ def configure_and_check(config): if libcamera_config.at(picam2.main_index).color_space == check_main_colour_space: print("Main stream colour spaces match", check_main_colour_space) else: - print("ERROR: main stream colour space is", libcamera_config.at(picam2.main_index).color_space, - "expected", check_main_colour_space) + print( + "ERROR: main stream colour space is", + libcamera_config.at(picam2.main_index).color_space, + "expected", + check_main_colour_space, + ) quit() if picam2.lores_index >= 0: if libcamera_config.at(picam2.lores_index).color_space == check_colour_space: print("Lores stream colour spaces match", check_colour_space) else: - print("ERROR: lores stream colour space is", libcamera_config.at(picam2.lores_index).color_space, - "expected", check_colour_space) + print( + "ERROR: lores stream colour space is", + libcamera_config.at(picam2.lores_index).color_space, + "expected", + check_colour_space, + ) quit() if picam2.raw_index >= 0: if libcamera_config.at(picam2.raw_index).color_space == ColorSpace.Raw(): print("Raw stream colour spaces match", ColorSpace.Raw()) else: - print("ERROR: raw stream colour space is", libcamera_config.at(picam2.raw_index).color_space, - "expected", ColorSpace.Raw()) + print( + "ERROR: raw stream colour space is", + libcamera_config.at(picam2.raw_index).color_space, + "expected", + ColorSpace.Raw(), + ) quit() for colour_space in (ColorSpace.Sycc(), ColorSpace.Smpte170m(), ColorSpace.Rec709()): - for format in ("RGB888", "YUV420"): print("Checking with colour space", colour_space, "and format", format) diff --git a/tests/config_with_sensor.py b/tests/config_with_sensor.py index 4701c608..4e5ab9c0 100755 --- a/tests/config_with_sensor.py +++ b/tests/config_with_sensor.py @@ -23,7 +23,6 @@ print("Smallest mode:", min_mode) if max_mode['size'] != min_mode['size']: - picam2.still_configuration.main.size = min_mode['size'] picam2.still_configuration.raw.size = min_mode['size'] picam2.still_configuration.raw.format = min_mode['format'].format diff --git a/tests/crop_test.py b/tests/crop_test.py index 350123f9..9a855199 100644 --- a/tests/crop_test.py +++ b/tests/crop_test.py @@ -13,10 +13,12 @@ picam2 = Picamera2() -for m, l in [(False, False), (False, True), (True, False), (True, True)]: - cfg = picam2.create_video_configuration(main={"size": (1920, 1080), "format": 'XRGB8888', "preserve_ar": m}, - lores={"size": (320, 320), "format": 'XRGB8888', "preserve_ar": l}, - display="main") +for m, lo in [(False, False), (False, True), (True, False), (True, True)]: + cfg = picam2.create_video_configuration( + main={"size": (1920, 1080), "format": 'XRGB8888', "preserve_ar": m}, + lores={"size": (320, 320), "format": 'XRGB8888', "preserve_ar": lo}, + display="main", + ) picam2.configure(cfg) picam2.start(show_preview=True) diff --git a/tests/hailo.py b/tests/hailo.py index 0b5976bd..fe75f602 100644 --- a/tests/hailo.py +++ b/tests/hailo.py @@ -51,8 +51,7 @@ with Picamera2() as picam2: model_h, model_w = input_shape[0], input_shape[1] config = picam2.create_preview_configuration( - main={'size': (1920, 1080), 'format': 'XRGB8888'}, - lores={'size': (model_w, model_h), 'format': 'RGB888'} + main={'size': (1920, 1080), 'format': 'XRGB8888'}, lores={'size': (model_w, model_h), 'format': 'RGB888'} ) picam2.configure(config) picam2.start() @@ -77,8 +76,7 @@ with Picamera2() as picam2: model_h, model_w = input_shape[0], input_shape[1] config = picam2.create_preview_configuration( - main={'size': (1920, 1080), 'format': 'XRGB8888'}, - lores={'size': (model_w, model_h), 'format': 'RGB888'} + main={'size': (1920, 1080), 'format': 'XRGB8888'}, lores={'size': (model_w, model_h), 'format': 'RGB888'} ) picam2.configure(config) picam2.start() diff --git a/tests/mjpeg_server.py b/tests/mjpeg_server.py index 8819b555..38470762 100755 --- a/tests/mjpeg_server.py +++ b/tests/mjpeg_server.py @@ -72,9 +72,7 @@ def do_GET(self): self.wfile.write(frame) self.wfile.write(b'\r\n') except Exception as e: - logging.warning( - 'Removed streaming client %s: %s', - self.client_address, str(e)) + logging.warning('Removed streaming client %s: %s', self.client_address, str(e)) else: self.send_error(404) self.end_headers() diff --git a/tests/mode_test.py b/tests/mode_test.py index a040943d..05087f9c 100755 --- a/tests/mode_test.py +++ b/tests/mode_test.py @@ -20,9 +20,7 @@ def check(raw_config, fps): if raw_config["size"][0] * raw_config["size"][1] > 5e6: print("Not checking", raw_config) return - picam2.video_configuration = picam2.create_video_configuration( - raw=raw_config, - ) + picam2.video_configuration = picam2.create_video_configuration(raw=raw_config) picam2.configure("video") # Check we got the correct raw format camera_config = picam2.camera_configuration() @@ -37,10 +35,11 @@ def check(raw_config, fps): if 'sensor' in camera_config and camera_config['sensor'] is not None: if 'bit_depth' in camera_config['sensor']: set_format.bit_depth = camera_config['sensor']['bit_depth'] - assert set_format.bayer_order == requested_format.bayer_order and \ - set_format.bit_depth == requested_format.bit_depth and \ - (set_format.packing == '') == (requested_format.packing == ''), \ - f'{picam2.camera_configuration()["raw"]["format"]} != {raw_config["format"]}' + assert ( + set_format.bayer_order == requested_format.bayer_order + and set_format.bit_depth == requested_format.bit_depth + and (set_format.packing == '') == (requested_format.packing == '') + ), f'{picam2.camera_configuration()["raw"]["format"]} != {raw_config["format"]}' picam2.set_controls({"FrameRate": fps}) picam2.start(show_preview=True) time.sleep(1) diff --git a/tests/preview_cycle_test.py b/tests/preview_cycle_test.py index 88262a9e..951c81bd 100644 --- a/tests/preview_cycle_test.py +++ b/tests/preview_cycle_test.py @@ -51,9 +51,9 @@ def main(): # Close the camera. picam2.close() - print(f"QT GL Cycle Results: {qtgl2-qtgl1-wait-buffer} s") - print(f"Null Cycle Results: {null2-null1-wait-buffer} s") - print(f"QT Cycle Results: {qt2-qt1-wait-buffer} s") + print(f"QT GL Cycle Results: {qtgl2 - qtgl1 - wait - buffer} s") + print(f"Null Cycle Results: {null2 - null1 - wait - buffer} s") + print(f"QT Cycle Results: {qt2 - qt1 - wait - buffer} s") if __name__ == "__main__": diff --git a/tests/quality_check.py b/tests/quality_check.py index 9b2f664b..88e1f989 100755 --- a/tests/quality_check.py +++ b/tests/quality_check.py @@ -64,17 +64,17 @@ def do_encode(encoder, quality): low_quality = do_encode(MJPEGEncoder(), Quality.VERY_LOW) high_quality = do_encode(MJPEGEncoder(), Quality.VERY_HIGH) print("MJPEGEncoder: low quality", low_quality, "high quality", high_quality) -if (1.5 * low_quality > high_quality): +if 1.5 * low_quality > high_quality: print("Error: MJPEGEncoder file sizes not as expected") low_quality = do_encode(H264Encoder(), Quality.VERY_LOW) high_quality = do_encode(H264Encoder(), Quality.VERY_HIGH) print("H264Encoder: low quality", low_quality, "high quality", high_quality) -if (1.5 * low_quality > high_quality): +if 1.5 * low_quality > high_quality: print("Error: H264Encoder file sizes not as expected") low_quality = do_encode(JpegEncoder(), Quality.VERY_LOW) high_quality = do_encode(JpegEncoder(), Quality.VERY_HIGH) print("JpegEncoder: low quality", low_quality, "high quality", high_quality) -if (1.5 * low_quality > high_quality): +if 1.5 * low_quality > high_quality: print("Error: JpegEncoder file sizes not as expected") diff --git a/tests/stop_slow_framerate.py b/tests/stop_slow_framerate.py index e4381429..dda3d5bb 100755 --- a/tests/stop_slow_framerate.py +++ b/tests/stop_slow_framerate.py @@ -11,7 +11,8 @@ quit() config = picam2.create_preview_configuration( - controls={'FrameRate': 0.2, 'ExposureTime': 5000, 'AnalogueGain': 1.0, 'ColourGains': (1, 1)}) + controls={'FrameRate': 0.2, 'ExposureTime': 5000, 'AnalogueGain': 1.0, 'ColourGains': (1, 1)} +) picam2.configure(config) picam2.start() diff --git a/tests/stride_test.py b/tests/stride_test.py index b1b7e4ea..56963431 100755 --- a/tests/stride_test.py +++ b/tests/stride_test.py @@ -16,7 +16,7 @@ def post_callback(request): # Make right side grey with MappedArray(request, "main") as m1: a1 = m1.array - a1[:, -a1.shape[1] // 2:] = 70 + a1[:, -a1.shape[1] // 2 :] = 70 picam2 = Picamera2(0) @@ -31,9 +31,7 @@ def post_callback(request): # Configure as half frame, with full frame stride so right side is blank picam2.pre_callback = pre_callback picam2.post_callback = post_callback -main_config = picam2.create_preview_configuration( - main={"size": half_size, "stride": stride} -) +main_config = picam2.create_preview_configuration(main={"size": half_size, "stride": stride}) picam2.configure(main_config) picam2.start_preview(True) diff --git a/tests/switch_mode_exposure.py b/tests/switch_mode_exposure.py index e76ca275..21bf7d58 100755 --- a/tests/switch_mode_exposure.py +++ b/tests/switch_mode_exposure.py @@ -10,8 +10,7 @@ # Select the smallest full FoV mode for the preview. preview_mode = None for mode in picam2.sensor_modes: - if mode['crop_limits'][:2] == (0, 0) and \ - (preview_mode is None or mode['size'][0] < preview_mode['size'][0]): + if mode['crop_limits'][:2] == (0, 0) and (preview_mode is None or mode['size'][0] < preview_mode['size'][0]): preview_mode = mode print("Preview mode:", preview_mode) diff --git a/tests/wait_cancel_test.py b/tests/wait_cancel_test.py index 747c04df..47098365 100755 --- a/tests/wait_cancel_test.py +++ b/tests/wait_cancel_test.py @@ -30,7 +30,7 @@ t2 = time.monotonic() print("Stopping took", t2 - t1, "seconds") if t2 - t1 > 0.1: - print(f"ERROR: stopping took too long ({t2-t1} seconds)") + print(f"ERROR: stopping took too long ({t2 - t1} seconds)") with Picamera2() as picam2: config = picam2.create_preview_configuration(controls=controls) diff --git a/tools/checkstyle.py b/tools/checkstyle.py index 9f8d123d..b1f3cd9a 100755 --- a/tools/checkstyle.py +++ b/tools/checkstyle.py @@ -20,10 +20,7 @@ import subprocess import sys -dependencies = { - 'clang-format': True, - 'git': True, -} +dependencies = {'clang-format': True, 'git': True} # ------------------------------------------------------------------------------ # Colour terminal handling @@ -75,6 +72,7 @@ def reset(): # Diff parsing, handling and printing # + class DiffHunkSide(object): """A side of a diff hunk, recording line numbers""" @@ -88,8 +86,7 @@ def __len__(self): class DiffHunk(object): - diff_header_regex = re.compile( - r'@@ -([0-9]+),?([0-9]+)? \+([0-9]+),?([0-9]+)? @@') + diff_header_regex = re.compile(r'@@ -([0-9]+),?([0-9]+)? \+([0-9]+),?([0-9]+)? @@') def __init__(self, line): match = DiffHunk.diff_header_regex.match(line) @@ -104,10 +101,13 @@ def __init__(self, line): self.lines = [] def __repr__(self): - s = '%s@@ -%u,%u +%u,%u @@\n' % \ - (Colours.fg(Colours.Cyan), - self.__from.start, len(self.__from), - self.__to.start, len(self.__to)) + s = '%s@@ -%u,%u +%u,%u @@\n' % ( + Colours.fg(Colours.Cyan), + self.__from.start, + len(self.__from), + self.__to.start, + len(self.__to), + ) for line in self.lines: if line[0] == '-': @@ -181,6 +181,7 @@ def parse_diff(diff): # Commit, Staged Changes & Amendments # + class CommitFile: def __init__(self, name): info = name.split() @@ -208,9 +209,9 @@ def __init__(self, commit): def _parse(self): # Get the commit title and list of files. - ret = subprocess.run(['git', 'show', '--pretty=oneline', '--name-status', - self.commit], - stdout=subprocess.PIPE).stdout.decode('utf-8') + ret = subprocess.run( + ['git', 'show', '--pretty=oneline', '--name-status', self.commit], stdout=subprocess.PIPE + ).stdout.decode('utf-8') files = ret.splitlines() if files[1]: self._files = [CommitFile(f) for f in files[1:]] @@ -226,14 +227,16 @@ def title(self): return self._title def get_diff(self, top_level, filename): - diff = subprocess.run(['git', 'diff', '%s~..%s' % (self.commit, self.commit), - '--', '%s/%s' % (top_level, filename)], - stdout=subprocess.PIPE).stdout.decode('utf-8') + diff = subprocess.run( + ['git', 'diff', '%s~..%s' % (self.commit, self.commit), '--', '%s/%s' % (top_level, filename)], + stdout=subprocess.PIPE, + ).stdout.decode('utf-8') return parse_diff(diff.splitlines(True)) def get_file(self, filename): - return subprocess.run(['git', 'show', '%s:%s' % (self.commit, filename)], - stdout=subprocess.PIPE).stdout.decode('utf-8') + return subprocess.run(['git', 'show', '%s:%s' % (self.commit, filename)], stdout=subprocess.PIPE).stdout.decode( + 'utf-8' + ) class StagedChanges(Commit): @@ -241,15 +244,14 @@ def __init__(self): Commit.__init__(self, '') def _parse(self): - ret = subprocess.run(['git', 'diff', '--staged', '--name-status'], - stdout=subprocess.PIPE).stdout.decode('utf-8') + ret = subprocess.run(['git', 'diff', '--staged', '--name-status'], stdout=subprocess.PIPE).stdout.decode('utf-8') self._title = "Staged changes" self._files = [CommitFile(f) for f in ret.splitlines()] def get_diff(self, top_level, filename): - diff = subprocess.run(['git', 'diff', '--staged', '--', - '%s/%s' % (top_level, filename)], - stdout=subprocess.PIPE).stdout.decode('utf-8') + diff = subprocess.run( + ['git', 'diff', '--staged', '--', '%s/%s' % (top_level, filename)], stdout=subprocess.PIPE + ).stdout.decode('utf-8') return parse_diff(diff.splitlines(True)) @@ -259,18 +261,18 @@ def __init__(self): def _parse(self): # Create a title using HEAD commit - ret = subprocess.run(['git', 'show', '--pretty=oneline', '--no-patch'], - stdout=subprocess.PIPE).stdout.decode('utf-8') + ret = subprocess.run(['git', 'show', '--pretty=oneline', '--no-patch'], stdout=subprocess.PIPE).stdout.decode('utf-8') self._title = 'Amendment of ' + ret.strip() # Extract the list of modified files - ret = subprocess.run(['git', 'diff', '--staged', '--name-status', 'HEAD~'], - stdout=subprocess.PIPE).stdout.decode('utf-8') + ret = subprocess.run(['git', 'diff', '--staged', '--name-status', 'HEAD~'], stdout=subprocess.PIPE).stdout.decode( + 'utf-8' + ) self._files = [CommitFile(f) for f in ret.splitlines()] def get_diff(self, top_level, filename): - diff = subprocess.run(['git', 'diff', '--staged', 'HEAD~', '--', - '%s/%s' % (top_level, filename)], - stdout=subprocess.PIPE).stdout.decode('utf-8') + diff = subprocess.run( + ['git', 'diff', '--staged', 'HEAD~', '--', '%s/%s' % (top_level, filename)], stdout=subprocess.PIPE + ).stdout.decode('utf-8') return parse_diff(diff.splitlines(True)) @@ -278,6 +280,7 @@ def get_diff(self, top_level, filename): # Helpers # + class ClassRegistry(type): def __new__(cls, clsname, bases, attrs): newclass = super().__new__(cls, clsname, bases, attrs) @@ -290,6 +293,7 @@ def __new__(cls, clsname, bases, attrs): # Commit Checkers # + class CommitChecker(metaclass=ClassRegistry): subclasses = [] @@ -314,6 +318,7 @@ def __init__(self, msg): # Style Checkers # + class StyleChecker(metaclass=ClassRegistry): subclasses = [] @@ -356,10 +361,28 @@ def __init__(self, line_number, line, msg, offset=None): class IncludeChecker(StyleChecker): patterns = ('*.cpp', '*.h', '*.hpp') - headers = ('assert', 'ctype', 'errno', 'fenv', 'float', 'inttypes', - 'limits', 'locale', 'setjmp', 'signal', 'stdarg', 'stddef', - 'stdint', 'stdio', 'stdlib', 'string', 'time', 'uchar', 'wchar', - 'wctype') + headers = ( + 'assert', + 'ctype', + 'errno', + 'fenv', + 'float', + 'inttypes', + 'limits', + 'locale', + 'setjmp', + 'signal', + 'stdarg', + 'stddef', + 'stdint', + 'stdio', + 'stdlib', + 'string', + 'time', + 'uchar', + 'wchar', + 'wctype', + ) include_regex = re.compile('^#include ') def __init__(self, content): @@ -379,8 +402,7 @@ def check(self, line_numbers): if header not in IncludeChecker.headers: continue - issues.append(StyleIssue(line_number, line, - 'C compatibility header <%s.h> is preferred' % header)) + issues.append(StyleIssue(line_number, line, 'C compatibility header <%s.h> is preferred' % header)) return issues @@ -398,11 +420,9 @@ def check(self, line_numbers): data = ''.join(self.__content).encode('utf-8') try: - ret = subprocess.run(['pycodestyle', '--ignore=E501', '-'], - input=data, stdout=subprocess.PIPE) + ret = subprocess.run(['pycodestyle', '--ignore=E501', '-'], input=data, stdout=subprocess.PIPE) except FileNotFoundError: - issues.append(StyleIssue( - 0, None, "Please install pycodestyle to validate python additions")) + issues.append(StyleIssue(0, None, "Please install pycodestyle to validate python additions")) return issues results = ret.stdout.decode('utf-8').splitlines() @@ -432,11 +452,9 @@ def check(self, line_numbers): data = ''.join(self.__content).encode('utf-8') try: - ret = subprocess.run(['shellcheck', '-Cnever', '-'], - input=data, stdout=subprocess.PIPE) + ret = subprocess.run(['shellcheck', '-Cnever', '-'], input=data, stdout=subprocess.PIPE) except FileNotFoundError: - issues.append(StyleIssue( - 0, None, "Please install shellcheck to validate shell script additions")) + issues.append(StyleIssue(0, None, "Please install shellcheck to validate shell script additions")) return issues results = ret.stdout.decode('utf-8').splitlines() @@ -462,6 +480,7 @@ def check(self, line_numbers): # Formatters # + class Formatter(metaclass=ClassRegistry): subclasses = [] @@ -498,9 +517,9 @@ class CLangFormatter(Formatter): @classmethod def format(cls, filename, data): - ret = subprocess.run(['clang-format', '-style=file', - '-assume-filename=' + filename], - input=data.encode('utf-8'), stdout=subprocess.PIPE) + ret = subprocess.run( + ['clang-format', '-style=file', '-assume-filename=' + filename], input=data.encode('utf-8'), stdout=subprocess.PIPE + ) return ret.stdout.decode('utf-8') @@ -560,6 +579,7 @@ def format(cls, filename, data): # Style checking # + def check_file(top_level, commit, filename): # Extract the line numbers touched by the commit. commit_diff = commit.get_diff(top_level, filename) @@ -587,8 +607,7 @@ def check_file(top_level, commit, filename): # Split the diff in hunks, recording line number ranges for each hunk, and # filter out hunks that are not touched by the commit. formatted_diff = parse_diff(diff) - formatted_diff = [ - hunk for hunk in formatted_diff if hunk.intersects(lines)] + formatted_diff = [hunk for hunk in formatted_diff if hunk.intersects(lines)] # Check for code issues not related to formatting. issues = [] @@ -611,8 +630,7 @@ def check_file(top_level, commit, filename): if len(issues): issues = sorted(issues, key=lambda i: i.line_number) for issue in issues: - print('%s#%u: %s' % - (Colours.fg(Colours.Yellow), issue.line_number, issue.msg)) + print('%s#%u: %s' % (Colours.fg(Colours.Yellow), issue.line_number, issue.msg)) if issue.line is not None: print('+%s%s' % (issue.line.rstrip(), Colours.reset())) if issue.offset is not None: @@ -632,16 +650,14 @@ def check_style(top_level, commit): # Apply the commit checkers first. for checker in CommitChecker.checkers(): for issue in checker.check(commit, top_level): - print('%s%s%s' % - (Colours.fg(Colours.Yellow), issue.msg, Colours.reset())) + print('%s%s%s' % (Colours.fg(Colours.Yellow), issue.msg, Colours.reset())) issues += 1 # Filter out files we have no checker for. patterns = set() patterns.update(StyleChecker.all_patterns()) patterns.update(Formatter.all_patterns()) - files = [f for f in commit.files() if len( - [p for p in patterns if fnmatch.fnmatch(os.path.basename(f), p)])] + files = [f for f in commit.files() if len([p for p in patterns if fnmatch.fnmatch(os.path.basename(f), p)])] for f in files: issues += check_file(top_level, commit, f) @@ -650,16 +666,14 @@ def check_style(top_level, commit): print("No issue detected") else: print('---') - print("%u potential %s detected, please review" % - (issues, 'issue' if issues == 1 else 'issues')) + print("%u potential %s detected, please review" % (issues, 'issue' if issues == 1 else 'issues')) return issues def extract_commits(revs): """Extract a list of commits on which to operate from a revision or revision range.""" - ret = subprocess.run(['git', 'rev-parse', revs], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + ret = subprocess.run(['git', 'rev-parse', revs], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if ret.returncode != 0: print(ret.stderr.decode('utf-8').splitlines()[0]) return [] @@ -669,8 +683,7 @@ def extract_commits(revs): # If the revlist contains more than one item, pass it to git rev-list to list # each commit individually. if len(revlist) > 1: - ret = subprocess.run(['git', 'rev-list', *revlist], - stdout=subprocess.PIPE) + ret = subprocess.run(['git', 'rev-list', *revlist], stdout=subprocess.PIPE) revlist = ret.stdout.decode('utf-8').splitlines() revlist.reverse() @@ -679,9 +692,7 @@ def extract_commits(revs): def git_top_level(): """Get the absolute path of the git top-level directory.""" - ret = subprocess.run(['git', 'rev-parse', '--show-toplevel'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + ret = subprocess.run(['git', 'rev-parse', '--show-toplevel'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if ret.returncode != 0: print(ret.stderr.decode('utf-8').splitlines()[0]) return None @@ -690,15 +701,22 @@ def git_top_level(): def main(argv): - # Parse command line arguments parser = argparse.ArgumentParser() - parser.add_argument('--staged', '-s', action='store_true', - help='Include the changes in the index. Defaults to False') - parser.add_argument('--amend', '-a', action='store_true', - help='Include changes in the index and the previous patch combined. Defaults to False') - parser.add_argument('revision_range', type=str, default=None, nargs='?', - help='Revision range (as defined by git rev-parse). Defaults to HEAD if not specified.') + parser.add_argument('--staged', '-s', action='store_true', help='Include the changes in the index. Defaults to False') + parser.add_argument( + '--amend', + '-a', + action='store_true', + help='Include changes in the index and the previous patch combined. Defaults to False', + ) + parser.add_argument( + 'revision_range', + type=str, + default=None, + nargs='?', + help='Revision range (as defined by git rev-parse). Defaults to HEAD if not specified.', + ) args = parser.parse_args(argv[1:]) # Check for required dependencies. diff --git a/tools/run_tests.py b/tools/run_tests.py index d180cc0a..3b572334 100755 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -126,13 +126,22 @@ def directoryexists(arg): if __name__ == '__main__': parser = argparse.ArgumentParser(description='picamera2 automated tests') - parser.add_argument('--dir', '-d', action='store', - default='/home/pi/picamera2_tests', help='Folder in which to run tests') - parser.add_argument('--picamera2-dir', '-p', action='store', type=directoryexists, - default='/home/pi/picamera2', help='Location of picamera2 folder') - parser.add_argument('--test-list-files', '-t', action='store', - default='test_list_drm.txt, test_list.txt', - help='Comma-separated list of files, each containing a list of tests to run') + parser.add_argument('--dir', '-d', action='store', default='/home/pi/picamera2_tests', help='Folder in which to run tests') + parser.add_argument( + '--picamera2-dir', + '-p', + action='store', + type=directoryexists, + default='/home/pi/picamera2', + help='Location of picamera2 folder', + ) + parser.add_argument( + '--test-list-files', + '-t', + action='store', + default='test_list_drm.txt, test_list.txt', + help='Comma-separated list of files, each containing a list of tests to run', + ) args = parser.parse_args() dir = args.dir