From 60a6326612e9353352c2502ebe147e4c4c128e9e Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 20 Dec 2023 12:42:22 -0500 Subject: [PATCH 01/70] capture into jpeg --- adafruit_pycamera/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 3892a55..aad2237 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -703,6 +703,25 @@ def continuous_capture_start(self): """Switch the camera to continuous-capture mode""" pass # pylint: disable=unnecessary-pass + def capture_into_jpeg(self): + """Captures an image and returns it in JPEG format. + + Returns: + bytes: The captured image in JPEG format, otherwise None if the capture failed. + """ + self.camera.reconfigure( + pixel_format=espcamera.PixelFormat.JPEG, + frame_size=self.resolution_to_frame_size[self._resolution], + ) + time.sleep(0.1) + jpeg = self.camera.take(1) + if jpeg is not None: + print(f"Captured {len(jpeg)} bytes of jpeg data") + print("Resolution %d x %d" % (self.camera.width, self.camera.height)) + else: + print("JPEG capture failed") + return jpeg + def capture_into_bitmap(self, bitmap): """Capture an image and blit it into the given bitmap""" bitmaptools.blit(bitmap, self.continuous_capture(), 0, 0) From 0aa18147f88966f4cdf36d1ce2781f91d3f71561 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 11:16:45 -0600 Subject: [PATCH 02/70] Factor out a PyCameraBase class This gives the coder control over what is initialized. For the image review app, for instance, the camera & autofocus would not be initialized. Support for the display backlight is also added. --- adafruit_pycamera/__init__.py | 199 +++++++++++++++++++--------------- examples/camera/code.py | 1 - examples/ipcam2/code.py | 1 - 3 files changed, 113 insertions(+), 88 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index aad2237..04a2763 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -71,8 +71,10 @@ _NVM_MODE = const(3) -class PyCamera: # pylint: disable=too-many-instance-attributes,too-many-public-methods - """Wrapper class for the PyCamera hardware""" +class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-public-methods + """Base class for PyCamera hardware""" + + """Wrapper class for the PyCamera hardware with lots of smarts""" _finalize_firmware_load = ( 0x3022, @@ -176,59 +178,41 @@ class PyCamera: # pylint: disable=too-many-instance-attributes,too-many-public- b"\x29\x80\x05" # _DISPON and Delay 5ms ) - def i2c_scan(self): - """Print an I2C bus scan""" - while not self._i2c.try_lock(): - pass - - try: - print( - "I2C addresses found:", - [hex(device_address) for device_address in self._i2c.scan()], - ) - finally: # unlock the i2c bus when ctrl-c'ing out of the loop - self._i2c.unlock() - def __init__(self) -> None: # pylint: disable=too-many-statements - self._timestamp = time.monotonic() + displayio.release_displays() self._i2c = board.I2C() self._spi = board.SPI() - self.deinit_display() - - self.splash = displayio.Group() - self._sd_label = label.Label( - terminalio.FONT, text="SD ??", color=0x0, x=150, y=10, scale=2 - ) - self._effect_label = label.Label( - terminalio.FONT, text="EFFECT", color=0xFFFFFF, x=4, y=10, scale=2 - ) - self._mode_label = label.Label( - terminalio.FONT, text="MODE", color=0xFFFFFF, x=150, y=10, scale=2 - ) + self._timestamp = time.monotonic() + self._bigbuf = None + self._botbar = None + self._camera_device = None + self._display_bus = None + self._effect_label = None + self._image_counter = 0 + self._mode_label = None + self._res_label = None + self._sd_label = None + self._topbar = None + self.accel = None + self.camera = None + self.display = None + self.pixels = None + self.sdcard = None + self.splash = None - # turn on the display first, its reset line may be shared with the IO expander(?) - if not self.display: - self.init_display() + # Reset display and I/O expander + self._tft_aw_reset = DigitalInOut(board.TFT_RESET) + self._tft_aw_reset.switch_to_output(False) + time.sleep(0.05) + self._tft_aw_reset.switch_to_output(True) self.shutter_button = DigitalInOut(board.BUTTON) self.shutter_button.switch_to_input(Pull.UP) self.shutter = Button(self.shutter_button) - print("reset camera") self._cam_reset = DigitalInOut(board.CAMERA_RESET) self._cam_pwdn = DigitalInOut(board.CAMERA_PWDN) - self._cam_reset.switch_to_output(False) - self._cam_pwdn.switch_to_output(True) - time.sleep(0.01) - self._cam_pwdn.switch_to_output(False) - time.sleep(0.01) - self._cam_reset.switch_to_output(True) - time.sleep(0.01) - - print("pre cam @", time.monotonic() - self._timestamp) - self.i2c_scan() - # AW9523 GPIO expander self._aw = adafruit_aw9523.AW9523(self._i2c, address=0x58) print("Found AW9523") @@ -260,17 +244,40 @@ def make_debounced_expander_pin(pin_no): self.mute = make_expander_output(_AW_MUTE, False) - self.sdcard = None - try: - self.mount_sd_card() - except RuntimeError: - pass # no card found, its ok! - print("sdcard done @", time.monotonic() - self._timestamp) + def make_camera_ui(self): + """Create displayio widgets for the standard camera UI""" + self.splash = displayio.Group() + self._sd_label = label.Label( + terminalio.FONT, text="SD ??", color=0x0, x=150, y=10, scale=2 + ) + self._effect_label = label.Label( + terminalio.FONT, text="EFFECT", color=0xFFFFFF, x=4, y=10, scale=2 + ) + self._mode_label = label.Label( + terminalio.FONT, text="MODE", color=0xFFFFFF, x=150, y=10, scale=2 + ) + self._topbar = displayio.Group() + self._res_label = label.Label( + terminalio.FONT, text="", color=0xFFFFFF, x=0, y=10, scale=2 + ) + self._topbar.append(self._res_label) + self._topbar.append(self._sd_label) + + self._botbar = displayio.Group(x=0, y=210) + self._botbar.append(self._effect_label) + self._botbar.append(self._mode_label) + + self.splash.append(self._topbar) + self.splash.append(self._botbar) + def init_accelerometer(self): + """Initialize the accelerometer""" # lis3dh accelerometer self.accel = adafruit_lis3dh.LIS3DH_I2C(self._i2c, address=0x19) self.accel.range = adafruit_lis3dh.RANGE_2_G + def init_neopixel(self): + """Initialize the neopixels (onboard & ring)""" # main board neopixel neopix = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.1) neopix.fill(0) @@ -282,6 +289,17 @@ def make_debounced_expander_pin(pin_no): ) self.pixels.fill(0) + def init_camera(self, init_autofocus=True) -> None: + """Initialize the camera, by default including autofocus""" + print("reset camera") + self._cam_reset.switch_to_output(False) + self._cam_pwdn.switch_to_output(True) + time.sleep(0.01) + self._cam_pwdn.switch_to_output(False) + time.sleep(0.01) + self._cam_reset.switch_to_output(True) + time.sleep(0.01) + print("Initializing camera") self.camera = espcamera.Camera( data_pins=board.CAMERA_DATA, @@ -305,33 +323,12 @@ def make_debounced_expander_pin(pin_no): self.camera.address, ) ) - print("camera done @", time.monotonic() - self._timestamp) - print(dir(self.camera)) self._camera_device = I2CDevice(self._i2c, self.camera.address) - # display.auto_refresh = False self.camera.hmirror = False self.camera.vflip = True - self._bigbuf = None - - self._topbar = displayio.Group() - self._res_label = label.Label( - terminalio.FONT, text="", color=0xFFFFFF, x=0, y=10, scale=2 - ) - self._topbar.append(self._res_label) - self._topbar.append(self._sd_label) - - self._botbar = displayio.Group(x=0, y=210) - self._botbar.append(self._effect_label) - self._botbar.append(self._mode_label) - - self.splash.append(self._topbar) - self.splash.append(self._botbar) - self.display.root_group = self.splash - self.display.refresh() - self.led_color = 0 self.led_level = 0 @@ -340,6 +337,10 @@ def make_debounced_expander_pin(pin_no): self.camera.saturation = 3 self.resolution = microcontroller.nvm[_NVM_RESOLUTION] self.mode = microcontroller.nvm[_NVM_MODE] + + if init_autofocus: + self.autofocus_init() + print("init done @", time.monotonic() - self._timestamp) def autofocus_init_from_file(self, filename): @@ -526,7 +527,7 @@ def resolution(self, res): self._res_label.text = self.resolutions[res] self.display.refresh() - def init_display(self, reset=True): + def init_display(self): """Initialize the TFT display""" # construct displayio by hand displayio.release_displays() @@ -534,7 +535,7 @@ def init_display(self, reset=True): self._spi, command=board.TFT_DC, chip_select=board.TFT_CS, - reset=board.TFT_RESET if reset else None, + reset=None, baudrate=60_000_000, ) self.display = board.DISPLAY @@ -546,6 +547,7 @@ def init_display(self, reset=True): height=240, colstart=80, auto_refresh=False, + backlight_pin=board.TFT_BACKLIGHT, ) self.display.root_group = self.splash self.display.refresh() @@ -562,7 +564,7 @@ def display_message(self, message, color=0xFF0000, scale=3): text_area = label.Label(terminalio.FONT, text=message, color=color, scale=scale) text_area.anchor_point = (0.5, 0.5) if not self.display: - self.init_display(None) + self.init_display() text_area.anchored_position = (self.display.width / 2, self.display.height / 2) # Show it @@ -572,10 +574,11 @@ def display_message(self, message, color=0xFF0000, scale=3): def mount_sd_card(self): """Attempt to mount the SD card""" - self._sd_label.text = "NO SD" - self._sd_label.color = 0xFF0000 + if self._sd_label is not None: + self._sd_label.text = "NO SD" + self._sd_label.color = 0xFF0000 if not self.card_detect.value: - raise RuntimeError("SD card detection failed") + raise RuntimeError("No SD card inserted") if self.sdcard: self.sdcard.deinit() # depower SD card @@ -585,6 +588,7 @@ def mount_sd_card(self): # deinit display and SPI bus because we need to drive all SD pins LOW # to ensure nothing, not even an I/O pin, could possibly power the SD # card + had_display = self.display is not None self.deinit_display() self._spi.deinit() sckpin = DigitalInOut(board.SCK) @@ -604,14 +608,18 @@ def mount_sd_card(self): self._card_power.value = False card_cs.deinit() print("sdcard init @", time.monotonic() - self._timestamp) - self.sdcard = sdcardio.SDCard(self._spi, board.CARD_CS, baudrate=20_000_000) - vfs = storage.VfsFat(self.sdcard) - print("mount vfs @", time.monotonic() - self._timestamp) - storage.mount(vfs, "/sd") - self.init_display(None) - self._image_counter = 0 - self._sd_label.text = "SD OK" - self._sd_label.color = 0x00FF00 + try: + self.sdcard = sdcardio.SDCard(self._spi, board.CARD_CS, baudrate=20_000_000) + vfs = storage.VfsFat(self.sdcard) + print("mount vfs @", time.monotonic() - self._timestamp) + storage.mount(vfs, "/sd") + self._image_counter = 0 + if self._sd_label is not None: + self._sd_label.text = "SD OK" + self._sd_label.color = 0x00FF00 + finally: + if had_display: + self.init_display() def unmount_sd_card(self): """Unmount the SD card, if mounted""" @@ -619,8 +627,9 @@ def unmount_sd_card(self): storage.umount("/sd") except OSError: pass - self._sd_label.text = "NO SD" - self._sd_label.color = 0xFF0000 + if self._sd_label is not None: + self._sd_label.text = "NO SD" + self._sd_label.color = 0xFF0000 def keys_debounce(self): """Debounce all keys. @@ -775,3 +784,21 @@ def led_color(self, new_color): self.pixels.fill(colors) else: self.pixels[:] = colors + + +class PyCamera(PyCameraBase): + """Wrapper class for the PyCamera hardware""" + + def __init__(self, init_autofocus=True): + super().__init__() + + self.make_camera_ui() + self.init_accelerometer() + self.init_neopixel() + self.init_display() + self.init_camera(init_autofocus) + try: + self.mount_sd_card() + except Exception as exc: # pylint: disable=broad-exception-caught + # No SD card inserted, it's OK + print(exc) diff --git a/examples/camera/code.py b/examples/camera/code.py index de10475..ddb0d5e 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -12,7 +12,6 @@ import adafruit_pycamera pycam = adafruit_pycamera.PyCamera() -pycam.autofocus_init() # pycam.live_preview_mode() settings = (None, "resolution", "effect", "mode", "led_level", "led_color") diff --git a/examples/ipcam2/code.py b/examples/ipcam2/code.py index ce3c2ce..9d93434 100644 --- a/examples/ipcam2/code.py +++ b/examples/ipcam2/code.py @@ -29,7 +29,6 @@ supervisor.runtime.autoreload = False pycam = adafruit_pycamera.PyCamera() -pycam.autofocus_init() if wifi.radio.ipv4_address: # use alt port if web workflow enabled From c3a682adfca7c93ddbcf3a3498f048a7474bec42 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 11:17:22 -0600 Subject: [PATCH 03/70] Speed up autofocus init The firmware can be written in 254-byte chunks, and it's much speedier. --- adafruit_pycamera/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 04a2763..64bc601 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -384,9 +384,19 @@ def autofocus_init_from_bitstream(self, firmware: bytes): raise RuntimeError(f"Autofocus not supported on {self.camera.sensor_name}") self.write_camera_register(0x3000, 0x20) # reset autofocus coprocessor + time.sleep(0.01) - for addr, val in enumerate(firmware): - self.write_camera_register(0x8000 + addr, val) + arr = bytearray(256) + with self._camera_device as i2c: + for offset in range(0, len(firmware), 254): + num_firmware_bytes = min(254, len(firmware) - offset) + reg = offset + 0x8000 + arr[0] = reg >> 8 + arr[1] = reg & 0xFF + arr[2 : 2 + num_firmware_bytes] = firmware[ + offset : offset + num_firmware_bytes + ] + i2c.write(arr, end=2 + num_firmware_bytes) self.write_camera_list(self._finalize_firmware_load) for _ in range(100): From 512efd86e1abe8d93656771422202c0a61e9400e Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 12:08:27 -0600 Subject: [PATCH 04/70] Ensure the splash group always exists so we can call pycam.display_message even without the rest of the camera ui --- adafruit_pycamera/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 64bc601..1b91fbc 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -198,7 +198,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self.display = None self.pixels = None self.sdcard = None - self.splash = None + self.splash = displayio.Group() # Reset display and I/O expander self._tft_aw_reset = DigitalInOut(board.TFT_RESET) @@ -246,7 +246,6 @@ def make_debounced_expander_pin(pin_no): def make_camera_ui(self): """Create displayio widgets for the standard camera UI""" - self.splash = displayio.Group() self._sd_label = label.Label( terminalio.FONT, text="SD ??", color=0x0, x=150, y=10, scale=2 ) From 82d23355fdc6ffc787e182c0ad34289dda083d99 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 12:08:54 -0600 Subject: [PATCH 05/70] Allow setting X & Y offsets for the direct bitmap blit this is useful for apps that don't have the top/bottom area reserved --- adafruit_pycamera/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 1b91fbc..4f85208 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -751,7 +751,7 @@ def continuous_capture(self): or the camera's capture mode is changed""" return self.camera.take(1) - def blit(self, bitmap): + def blit(self, bitmap, x_offset=0, y_offset=32): """Display a bitmap direct to the LCD, bypassing displayio This can be more efficient than displaying a bitmap as a displayio @@ -762,8 +762,12 @@ def blit(self, bitmap): for status information. """ - self._display_bus.send(42, struct.pack(">hh", 80, 80 + bitmap.width - 1)) - self._display_bus.send(43, struct.pack(">hh", 32, 32 + bitmap.height - 1)) + self._display_bus.send( + 42, struct.pack(">hh", 80 + x_offset, 80 + x_offset + bitmap.width - 1) + ) + self._display_bus.send( + 43, struct.pack(">hh", y_offset, y_offset + bitmap.height - 1) + ) self._display_bus.send(44, bitmap) @property From 6da6447a899f97e95d456738f7a1855cecd02dac Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 12:20:00 -0600 Subject: [PATCH 06/70] Add an image viewer application --- examples/viewer/code.py | 153 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 examples/viewer/code.py diff --git a/examples/viewer/code.py b/examples/viewer/code.py new file mode 100644 index 0000000..51abc6b --- /dev/null +++ b/examples/viewer/code.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +"""Image viewer + +This will display all *jpeg* format images on the inserted SD card. + +Press up or down to move by +- 10 images. +Press left or right to move by +- 1 image. + +Otherwise, images cycle every DISPLAY_INTERVAL milliseconds (default 8000 = 8 seconds) +""" + +import time +import os +import displayio +from jpegio import JpegDecoder +from adafruit_ticks import ticks_less, ticks_ms, ticks_add, ticks_diff +from adafruit_pycamera import PyCameraBase + +DISPLAY_INTERVAL = 8000 # milliseconds + +decoder = JpegDecoder() + +pycam = PyCameraBase() +pycam.init_display() + + +def load_resized_image(bitmap, filename): + print(f"loading {filename}") + bitmap.fill(0b01000_010000_01000) # fill with a middle grey + + bw, bh = bitmap.width, bitmap.height + t0 = ticks_ms() + h, w = decoder.open(filename) + t1 = ticks_ms() + print(f"{ticks_diff(t1, t0)}ms to open") + scale = 0 + print(f"Full image size is {w}x{h}") + print(f"Bitmap is {bw}x{bh} pixels") + while (w >> scale) > bw or (h >> scale) > bh and scale < 3: + scale += 1 + sw = w >> scale + sh = h >> scale + print(f"will load at {scale=}, giving {sw}x{sh} pixels") + + if sw > bw: # left/right sides cut off + x = 0 + x1 = (sw - bw) // 2 + else: # horizontally centered + x = (bw - sw) // 2 + x1 = 0 + + if sh > bh: # top/bottom sides cut off + y = 0 + y1 = (sh - bh) // 2 + else: # vertically centered + y = (bh - sh) // 2 + y1 = 0 + + print(f"{x=} {y=} {x1=} {y1=}") + decoder.decode(bitmap, x=x, y=y, x1=x1, y1=y1, scale=scale) + t1 = ticks_ms() + print(f"{ticks_diff(t1, t0)}ms to decode") + + +def mount_sd(): + if not pycam.card_detect.value: + pycam.display_message("No SD card\ninserted", color=0xFF0000) + return [] + pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) + for _ in range(3): + try: + print("Mounting card") + pycam.mount_sd_card() + print("Success!") + break + except OSError as e: + print("Retrying!", e) + time.sleep(0.5) + else: + pycam.display_message("SD Card\nFailed!", color=0xFF0000) + time.sleep(0.5) + all_images = [ + f"/sd/{filename}" + for filename in os.listdir("/sd") + if filename.lower().endswith(".jpg") + ] + pycam.display_message(f"Found {len(all_images)}\nimages", color=0xFFFFFF) + time.sleep(0.5) + pycam.display.refresh() + return all_images + + +def main(): + image_counter = 0 + last_image_counter = 0 + deadline = ticks_ms() + all_images = mount_sd() + + bitmap = displayio.Bitmap(pycam.display.width, pycam.display.height, 65535) + + while True: + pycam.keys_debounce() + if pycam.card_detect.fell: + print("SD card removed") + pycam.unmount_sd_card() + pycam.display_message("SD Card\nRemoved", color=0xFFFFFF) + time.sleep(0.5) + pycam.display.refresh() + all_images = [] + + now = ticks_ms() + if pycam.card_detect.rose: + print("SD card inserted") + all_images = mount_sd() + image_counter = 0 + deadline = now + + if all_images: + if pycam.up.fell: + image_counter = (last_image_counter - 10) % len(all_images) + deadline = now + + if pycam.down.fell: + image_counter = (last_image_counter + 10) % len(all_images) + deadline = now + + if pycam.left.fell: + image_counter = (last_image_counter - 1) % len(all_images) + deadline = now + + if pycam.right.fell: + image_counter = (last_image_counter + 1) % len(all_images) + deadline = now + + if ticks_less(deadline, now): + print(now, deadline, ticks_less(deadline, now), all_images) + deadline = ticks_add(deadline, DISPLAY_INTERVAL) + filename = all_images[image_counter] + last_image_counter = image_counter + image_counter = (image_counter + 1) % len(all_images) + try: + load_resized_image(bitmap, filename) + except Exception as e: # pylint: disable=broad-exception-caught + pycam.display_message(f"Failed to read\n{filename}", color=0xFF0000) + print(e) + deadline = ticks_add(now, 500) + pycam.blit(bitmap, y_offset=0) + + +main() From 63bef1689ed14269707863333f79c65d2265e2cd Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 13:07:41 -0600 Subject: [PATCH 07/70] Avoid deprecation warnings --- adafruit_pycamera/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 4f85208..89bb283 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -17,6 +17,8 @@ import bitmaptools import board import displayio +import fourwire +import busdisplay import espcamera import microcontroller import neopixel @@ -540,16 +542,15 @@ def init_display(self): """Initialize the TFT display""" # construct displayio by hand displayio.release_displays() - self._display_bus = displayio.FourWire( + self._display_bus = fourwire.FourWire( self._spi, command=board.TFT_DC, chip_select=board.TFT_CS, reset=None, baudrate=60_000_000, ) - self.display = board.DISPLAY # init specially since we are going to write directly below - self.display = displayio.Display( + self.display = busdisplay.BusDisplay( self._display_bus, self._INIT_SEQUENCE, width=240, From 05dc862452fdb8113f0d91026eb46df7673256f8 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 13:14:16 -0600 Subject: [PATCH 08/70] fix doc build --- docs/conf.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 03efd7f..66f51d2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,20 +28,22 @@ # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. autodoc_mock_imports = [ - "bitmaptools", "adafruit_aw9523", + "adafruit_debouncer", + "adafruit_display_text", "adafruit_lis3dh", + "bitmaptools", + "busdisplay", + "busio", + "digitalio", "displayio", "espcamera", + "fourwire", + "micropython", "neopixel", "sdcardio", "storage", "terminalio", - "adafruit_debouncer", - "adafruit_display_text", - "digitalio", - "busio", - "micropython", ] autodoc_preserve_defaults = True From 336fd0487521de074aed6d3cd2bcb9eed52787d9 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 16:01:56 -0600 Subject: [PATCH 09/70] add image processing, operates in float space which uses too much memory! --- adafruit_pycamera/imageprocessing.py | 253 +++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 adafruit_pycamera/imageprocessing.py diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py new file mode 100644 index 0000000..c8d5fc2 --- /dev/null +++ b/adafruit_pycamera/imageprocessing.py @@ -0,0 +1,253 @@ +import sys +import struct +import displayio + +try: + import numpy as np +except: + import ulab.numpy as np + + +def _bytes_per_row(source_width: int) -> int: + pixel_bytes = 3 * source_width + padding_bytes = (4 - (pixel_bytes % 4)) % 4 + return pixel_bytes + padding_bytes + + +def _write_bmp_header(output_file: BufferedWriter, filesize: int) -> None: + output_file.write(bytes("BM", "ascii")) + output_file.write(struct.pack(" None: + output_file.write(struct.pack(" threshold) + arr * (arr <= threshold) + + +def solarize(bitmap, threshold=0.5): + """Apply a solarize filter to an image""" + return bitmap_channel_filter1( + bitmap, + lambda r: solarize_channel(r, threshold), + lambda g: solarize_channel(r, threshold), + lambda b: solarize_channel(b, threshold), + ) + + +def sepia(bitmap): + """Apply a sepia filter to an image + + based on some coefficients I found on the internet""" + return bitmap_channel_filter3( + bitmap, + lambda r, g, b: 0.393 * r + 0.769 * g + 0.189 * b, + lambda r, g, b: 0.349 * r + 0.686 * g + 0.168 * b, + lambda r, g, b: 0.272 * r + 0.534 * g + 0.131 * b, + ) + + +def greyscale(bitmap): + """Convert an image to greyscale""" + r, g, b = bitmap_to_components_rgb565(bitmap) + l = 0.2989 * r + 0.5870 * g + 0.1140 * b + return bitmap_from_components_rgb565(l, l, l) + + +def red_cast(bitmap): + return bitmap_channel_filter1( + bitmap, lambda r: r, lambda g: g * 0.5, lambda b: b * 0.5 + ) + + +def green_cast(bitmap): + return bitmap_channel_filter1( + bitmap, lambda r: r * 0.5, lambda g: g, lambda b: b * 0.5 + ) + + +def blue_cast(bitmap): + return bitmap_channel_filter1( + bitmap, lambda r: r * 0.5, lambda g: g * 0.5, lambda b: b + ) + + +def blur(bitmap): + return bitmap_separable_filter(bitmap, np.array([0.25, 0.5, 0.25])) + + +def sharpen(bitmap): + y = 1 / 5 + return bitmap_separable_filter(bitmap, np.array([-y, -y, 2 - y, -y, -y])) + + +def edgedetect(bitmap): + coefficients = np.array([-1, 0, 1]) + r, g, b = bitmap_to_components_rgb565(bitmap) + r = separable_filter(r, coefficients, coefficients) + 0.5 + g = separable_filter(g, coefficients, coefficients) + 0.5 + b = separable_filter(b, coefficients, coefficients) + 0.5 + return bitmap_from_components_rgb565(r, g, b) From e93b3d8ef422804b1ed4d7b9a90395947d58ffca Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 18:44:43 -0600 Subject: [PATCH 10/70] add image processing This is memory efficient enough to operate on 240x208 pixel images. (An earlier iteration which used float32s was not) Typically an algorithm takes 1 to 4 seconds to run on an image. Channel operations such as solarize are faster, while convolution operations like sharpen are slower. A range of algorithms are provided and there are building blocks to create others. --- adafruit_pycamera/imageprocessing.py | 268 +++++++++++++++------------ examples/filter/code.py | 172 +++++++++++++++++ 2 files changed, 317 insertions(+), 123 deletions(-) create mode 100644 examples/filter/code.py diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index c8d5fc2..425f8c3 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -1,20 +1,22 @@ -import sys +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT +"""Routines for performing image manipulation""" + import struct -import displayio -try: - import numpy as np -except: - import ulab.numpy as np +import ulab.numpy as np def _bytes_per_row(source_width: int) -> int: + """Internal function to determine bitmap bytes per row""" pixel_bytes = 3 * source_width padding_bytes = (4 - (pixel_bytes % 4)) % 4 return pixel_bytes + padding_bytes -def _write_bmp_header(output_file: BufferedWriter, filesize: int) -> None: +def _write_bmp_header(output_file, filesize): + """Internal function to write bitmap header""" output_file.write(bytes("BM", "ascii")) output_file.write(struct.pack(" None: output_file.write(struct.pack(" None: +def _write_dib_header(output_file, width: int, height: int) -> None: + """Internal function to write bitmap "dib" header""" output_file.write(struct.pack(" N output_file.write(b"\x00") -def components_to_file_rgb565(output_file, r, g, b): +def components_to_bitmap(output_file, r, g, b): + """Write image components to an uncompressed 24-bit .bmp format file""" height, width = r.shape pixel_bytes = 3 * width padding_bytes = (4 - (pixel_bytes % 4)) % 4 filesize = 54 + height * (pixel_bytes + padding_bytes) _write_bmp_header(output_file, filesize) _write_dib_header(output_file, width, height) - p = b"\0" * padding_bytes - m = memoryview(buffer_from_components_rgb888(r, g, b)) - for i in range(0, len(m), pixel_bytes)[::-1]: - output_file.write(m[i : i + pixel_bytes]) - output_file.write(p) + pad = b"\0" * padding_bytes + view = memoryview(buffer_from_components_rgb888(r, g, b)) + # Write out image data in reverse order with padding between rows + for i in range(0, len(view), pixel_bytes)[::-1]: + output_file.write(view[i : i + pixel_bytes]) + output_file.write(pad) -def np_convolve_same(a, v): - """Perform the np.convolve(mode=same) operation +def _np_convolve_same(arr, coeffs): + """Internal function to perform the np.convolve(arr, coeffs, mode="same") operation This is not directly supported on ulab, so we have to slice the "full" mode result """ - if len(a) < len(v): - a, v = v, a - tmp = np.convolve(a, v) - n = len(a) - c = (len(v) - 1) // 2 - result = tmp[c : c + n] + if len(arr) < len(coeffs): + arr, coeffs = coeffs, arr + tmp = np.convolve(arr, coeffs) + n = len(arr) + offset = (len(coeffs) - 1) // 2 + result = tmp[offset : offset + n] return result @@ -66,58 +71,60 @@ def np_convolve_same(a, v): def bitmap_as_array(bitmap): - ### XXX todo: work on blinka + """Create an array object that accesses the bitmap data""" if bitmap.width % 2: raise ValueError("Can only work on even-width bitmaps") - return ( - np.frombuffer(bitmap, dtype=np.uint16) - .reshape((bitmap.height, bitmap.width)) - .byteswap() - ) + return np.frombuffer(bitmap, dtype=np.uint16).reshape((bitmap.height, bitmap.width)) + + +def array_cast(arr, dtype): + """Cast an array to a given type and shape. The new type must match the original + type's size in bytes.""" + return np.frombuffer(arr, dtype=dtype).reshape(arr.shape) def bitmap_to_components_rgb565(bitmap): - """Convert a RGB65_BYTESWAPPED image to float32 components in the [0,1] inclusive range""" - arr = bitmap_as_array(bitmap) + """Convert a RGB65_BYTESWAPPED image to int16 components in the [0,255] inclusive range - r = np.right_shift(arr, 11) * (1.0 / FIVE_BITS) - g = (np.right_shift(arr, 5) & SIX_BITS) * (1.0 / SIX_BITS) - b = (arr & FIVE_BITS) * (1.0 / FIVE_BITS) + This requires higher memory than uint8, but allows more arithmetic on pixel values; + converting back to bitmap clamps values to the appropriate range.""" + arr = bitmap_as_array(bitmap) + arr.byteswap(inplace=True) + r = array_cast(np.right_shift(arr, 8) & 0xF8, np.int16) + g = array_cast(np.right_shift(arr, 3) & 0xFC, np.int16) + b = array_cast(np.left_shift(arr, 3) & 0xF8, np.int16) + arr.byteswap(inplace=True) return r, g, b -def bitmap_from_components_rgb565(r, g, b): - """Convert the float32 components to a bitmap""" - h, w = r.shape - result = displayio.Bitmap(w, h, 65535) - return bitmap_from_components_inplace_rgb565(result, r, g, b) +def bitmap_from_components_inplace_rgb565( + bitmap, r, g, b +): # pylint: disable=invalid-name + """Update a bitmap in-place with new RGB values""" + dest = bitmap_as_array(bitmap) + r = array_cast(np.maximum(np.minimum(r, 255), 0), np.uint16) + g = array_cast(np.maximum(np.minimum(g, 255), 0), np.uint16) + b = array_cast(np.maximum(np.minimum(b, 255), 0), np.uint16) + dest[:] = np.left_shift(r & 0xF8, 8) + dest[:] |= np.left_shift(g & 0xFC, 3) + dest[:] |= np.right_shift(b, 3) + dest.byteswap(inplace=True) + return bitmap -def bitmap_from_components_inplace_rgb565(bitmap, r, g, b): - arr = bitmap_as_array(bitmap) - r = np.array(np.maximum(np.minimum(r, 1.0), 0.0) * FIVE_BITS, dtype=np.uint16) - g = np.array(np.maximum(np.minimum(g, 1.0), 0.0) * SIX_BITS, dtype=np.uint16) - b = np.array(np.maximum(np.minimum(b, 1.0), 0.0) * FIVE_BITS, dtype=np.uint16) - arr = np.left_shift(r, 11) - arr[:] |= np.left_shift(g, 5) - arr[:] |= b - arr = arr.byteswap().flatten() - dest = np.frombuffer(bitmap, dtype=np.uint16) - dest[:] = arr - return bitmap +def as_flat(arr): + """Flatten an array, ensuring no copy is made""" + return np.frombuffer(arr, arr.dtype) def buffer_from_components_rgb888(r, g, b): - """Convert the float32 components to a RGB888 buffer in memory""" - r = np.array( - np.maximum(np.minimum(r, 1.0), 0.0) * EIGHT_BITS, dtype=np.uint8 - ).flatten() - g = np.array( - np.maximum(np.minimum(g, 1.0), 0.0) * EIGHT_BITS, dtype=np.uint8 - ).flatten() - b = np.array( - np.maximum(np.minimum(b, 1.0), 0.0) * EIGHT_BITS, dtype=np.uint8 - ).flatten() + """Convert the individual color components to a single RGB888 buffer in memory""" + r = as_flat(r) + g = as_flat(g) + b = as_flat(b) + r = np.maximum(np.minimum(r, 0x3F), 0) + g = np.maximum(np.minimum(g, 0x3F), 0) + b = np.maximum(np.minimum(b, 0x3F), 0) result = np.zeros(3 * len(r), dtype=np.uint8) result[2::3] = r result[1::3] = g @@ -125,129 +132,144 @@ def buffer_from_components_rgb888(r, g, b): return result -def separable_filter(data, vh, vv=None): - """Apply a separable filter to a 2d array. - - If the vertical coefficients ``vv`` are none, the ``vh`` components are - used for vertical too.""" - if vv is None: - vv = vh +def symmetric_filter_inplace(data, coeffs, scale): + """Apply a symmetric separable filter to a 2d array, changing it in place. - result = data[:] + The same filter is applied to image rows and image columns. This is appropriate for + many common kinds of image filters such as blur, sharpen, and edge detect. + Normally, scale is sum(coeffs).""" # First run the filter across each row - n_rows = result.shape[0] + n_rows = data.shape[0] for i in range(n_rows): - result[i, :] = np_convolve_same(result[i, :], vh) + data[i, :] = _np_convolve_same(data[i, :], coeffs) // scale # Run the filter across each column - n_cols = result.shape[1] + n_cols = data.shape[1] for i in range(n_cols): - result[:, i] = np_convolve_same(result[:, i], vv) + data[:, i] = _np_convolve_same(data[:, i], coeffs) // scale - return result + return data -def bitmap_separable_filter(bitmap, vh, vv=None): - """Apply a separable filter to an image, returning a new image""" +def bitmap_symmetric_filter_inplace(bitmap, coeffs, scale): + """Apply a symmetric filter to an image, updating the original image""" r, g, b = bitmap_to_components_rgb565(bitmap) - r = separable_filter(r, vh, vv) - g = separable_filter(g, vh, vv) - b = separable_filter(b, vh, vv) - return bitmap_from_components_rgb565(r, g, b) + symmetric_filter_inplace(r, coeffs, scale) + symmetric_filter_inplace(g, coeffs, scale) + symmetric_filter_inplace(b, coeffs, scale) + return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) -def bitmap_channel_filter3( +def bitmap_channel_filter3_inplace( bitmap, r_func=lambda r, g, b: r, g_func=lambda r, g, b: g, b_func=lambda r, g, b: b ): - """Perform channel filtering where each function recieves all 3 channels""" + """Perform channel filtering in place, updating the original image + + Each callback function recieves all 3 channels""" r, g, b = bitmap_to_components_rgb565(bitmap) r = r_func(r, g, b) g = g_func(r, g, b) b = b_func(r, g, b) - return bitmap_from_components_rgb565(r, g, b) + return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) -def bitmap_channel_filter1( +def bitmap_channel_filter1_inplace( bitmap, r_func=lambda r: r, g_func=lambda g: g, b_func=lambda b: b ): - """Perform channel filtering where each function recieves just one channel""" - return bitmap_channel_filter3( - bitmap, - lambda r, g, b: r_func(r), - lambda r, g, b: g_func(g), - lambda r, g, b: b_func(b), - ) + """Perform channel filtering in place, updating the original image + Each callback function recieves just its own channel data.""" + r, g, b = bitmap_to_components_rgb565(bitmap) + r[:] = r_func(r) + g[:] = g_func(g) + b[:] = b_func(b) + return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) -def solarize_channel(c, threshold=0.5): + +def solarize_channel(data, threshold=128): """Solarize an image channel. If the channel value is above a threshold, it is inverted. Otherwise, it is unchanged. """ - return (-1 * arr) * (arr > threshold) + arr * (arr <= threshold) + return (255 - data) * (data > threshold) + data * (data <= threshold) -def solarize(bitmap, threshold=0.5): - """Apply a solarize filter to an image""" - return bitmap_channel_filter1( - bitmap, - lambda r: solarize_channel(r, threshold), - lambda g: solarize_channel(r, threshold), - lambda b: solarize_channel(b, threshold), - ) +def solarize(bitmap, threshold=128): + """Apply a per-channel solarize filter to an image in place""" + + def do_solarize(channel): + return solarize_channel(channel, threshold) + + return bitmap_channel_filter1_inplace(bitmap, do_solarize, do_solarize, do_solarize) def sepia(bitmap): - """Apply a sepia filter to an image + """Apply a sepia filter to an image in place based on some coefficients I found on the internet""" - return bitmap_channel_filter3( + return bitmap_channel_filter3_inplace( bitmap, - lambda r, g, b: 0.393 * r + 0.769 * g + 0.189 * b, - lambda r, g, b: 0.349 * r + 0.686 * g + 0.168 * b, - lambda r, g, b: 0.272 * r + 0.534 * g + 0.131 * b, + lambda r, g, b: np.right_shift(50 * r + 98 * g + 24 * b, 7), + lambda r, g, b: np.right_shift(44 * r + 88 * g + 42 * b, 7), + lambda r, g, b: np.right_shift(35 * r + 69 * g + 17 * b, 7), ) def greyscale(bitmap): """Convert an image to greyscale""" r, g, b = bitmap_to_components_rgb565(bitmap) - l = 0.2989 * r + 0.5870 * g + 0.1140 * b - return bitmap_from_components_rgb565(l, l, l) + luminance = np.right_shift(38 * r + 75 * g + 15 * b, 7) + return bitmap_from_components_inplace_rgb565( + bitmap, luminance, luminance, luminance + ) + + +def _identity(channel): + """An internal function to return a channel unchanged""" + return channel + + +def _half(channel): + """An internal function to divide channel values by two""" + return channel // 2 def red_cast(bitmap): - return bitmap_channel_filter1( - bitmap, lambda r: r, lambda g: g * 0.5, lambda b: b * 0.5 - ) + """Give an image a red cast by dividing G and B channels in half""" + return bitmap_channel_filter1_inplace(bitmap, _identity, _half, _half) def green_cast(bitmap): - return bitmap_channel_filter1( - bitmap, lambda r: r * 0.5, lambda g: g, lambda b: b * 0.5 - ) + """Give an image a green cast by dividing R and B channels in half""" + return bitmap_channel_filter1_inplace(bitmap, _half, _identity, _half) def blue_cast(bitmap): - return bitmap_channel_filter1( - bitmap, lambda r: r * 0.5, lambda g: g * 0.5, lambda b: b - ) + """Give an image a blue cast by dividing R and G channels in half""" + return bitmap_channel_filter1_inplace(bitmap, _half, _half, _identity) def blur(bitmap): - return bitmap_separable_filter(bitmap, np.array([0.25, 0.5, 0.25])) + """Blur a bitmap""" + return bitmap_symmetric_filter_inplace(bitmap, np.array([1, 2, 1]), scale=4) def sharpen(bitmap): - y = 1 / 5 - return bitmap_separable_filter(bitmap, np.array([-y, -y, 2 - y, -y, -y])) + """Sharpen a bitmap""" + return bitmap_symmetric_filter_inplace( + bitmap, np.array([-1, -1, 9, -1, -1]), scale=5 + ) def edgedetect(bitmap): + """Run an edge detection routine on a bitmap""" coefficients = np.array([-1, 0, 1]) r, g, b = bitmap_to_components_rgb565(bitmap) - r = separable_filter(r, coefficients, coefficients) + 0.5 - g = separable_filter(g, coefficients, coefficients) + 0.5 - b = separable_filter(b, coefficients, coefficients) + 0.5 - return bitmap_from_components_rgb565(r, g, b) + symmetric_filter_inplace(r, coefficients, scale=1) + r += 128 + symmetric_filter_inplace(g, coefficients, scale=1) + g += 128 + symmetric_filter_inplace(b, coefficients, scale=1) + b += 128 + return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) diff --git a/examples/filter/code.py b/examples/filter/code.py new file mode 100644 index 0000000..31fdc4d --- /dev/null +++ b/examples/filter/code.py @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +"""Image viewer + +This will display all *jpeg* format images on the inserted SD card, in random order. +Each time an image is displayed, one of the pre-defined image filters is performed on it. + +Images cycle every DISPLAY_INTERVAL milliseconds (default 8000 = 8 seconds) as long as +they can be processed fast enough. Pressing any of the 4 direction buttons will start a +new image processing as soon as possible. +""" + +import time +import os +import random +import displayio +from jpegio import JpegDecoder +from adafruit_ticks import ticks_less, ticks_ms, ticks_add, ticks_diff +from adafruit_pycamera import PyCameraBase +from adafruit_pycamera import imageprocessing + +effects = [ + imageprocessing.blue_cast, + imageprocessing.blur, + imageprocessing.edgedetect, + imageprocessing.green_cast, + imageprocessing.greyscale, + imageprocessing.red_cast, + imageprocessing.sepia, + imageprocessing.sharpen, + imageprocessing.solarize, +] + + +def random_choice(seq): + return seq[random.randrange(0, len(seq))] + + +DISPLAY_INTERVAL = 8000 # milliseconds + +decoder = JpegDecoder() + +pycam = PyCameraBase() +pycam.init_display() + + +def load_resized_image(bitmap, filename): + print(f"loading {filename}") + bitmap.fill(0b01000_010000_01000) # fill with a middle grey + + bw, bh = bitmap.width, bitmap.height + t0 = ticks_ms() + h, w = decoder.open(filename) + t1 = ticks_ms() + print(f"{ticks_diff(t1, t0)}ms to open") + scale = 0 + print(f"Full image size is {w}x{h}") + print(f"Bitmap is {bw}x{bh} pixels") + while (w >> scale) > bw or (h >> scale) > bh and scale < 3: + scale += 1 + sw = w >> scale + sh = h >> scale + print(f"will load at {scale=}, giving {sw}x{sh} pixels") + + if sw > bw: # left/right sides cut off + x = 0 + x1 = (sw - bw) // 2 + else: # horizontally centered + x = (bw - sw) // 2 + x1 = 0 + + if sh > bh: # top/bottom sides cut off + y = 0 + y1 = (sh - bh) // 2 + else: # vertically centered + y = (bh - sh) // 2 + y1 = 0 + + print(f"{x=} {y=} {x1=} {y1=}") + decoder.decode(bitmap, x=x, y=y, x1=x1, y1=y1, scale=scale) + t1 = ticks_ms() + print(f"{ticks_diff(t1, t0)}ms to decode") + + +def mount_sd(): + if not pycam.card_detect.value: + pycam.display_message("No SD card\ninserted", color=0xFF0000) + return [] + pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) + for _ in range(3): + try: + print("Mounting card") + pycam.mount_sd_card() + print("Success!") + break + except OSError as e: + print("Retrying!", e) + time.sleep(0.5) + else: + pycam.display_message("SD Card\nFailed!", color=0xFF0000) + time.sleep(0.5) + all_images = [ + f"/sd/{filename}" + for filename in os.listdir("/sd") + if filename.lower().endswith(".jpg") + ] + pycam.display_message(f"Found {len(all_images)}\nimages", color=0xFFFFFF) + time.sleep(0.5) + pycam.display.refresh() + return all_images + + +def main(): + deadline = ticks_ms() + all_images = mount_sd() + + bitmap = displayio.Bitmap(pycam.display.width, pycam.display.height - 32, 65535) + + while True: + pycam.keys_debounce() + if pycam.card_detect.fell: + print("SD card removed") + pycam.unmount_sd_card() + pycam.display_message("SD Card\nRemoved", color=0xFFFFFF) + time.sleep(0.5) + pycam.display.refresh() + all_images = [] + + now = ticks_ms() + if pycam.card_detect.rose: + print("SD card inserted") + all_images = mount_sd() + deadline = now + + if all_images: + if pycam.up.fell: + deadline = now + + if pycam.down.fell: + deadline = now + + if pycam.left.fell: + deadline = now + + if pycam.right.fell: + deadline = now + + if ticks_less(deadline, now): + print(now, deadline, ticks_less(deadline, now), all_images) + deadline = ticks_add(deadline, DISPLAY_INTERVAL) + filename = random_choice(all_images) + effect = random.choice(effects) + try: + load_resized_image(bitmap, filename) + except Exception as e: # pylint: disable=broad-exception-caught + pycam.display_message(f"Failed to read\n{filename}", color=0xFF0000) + print(e) + deadline = ticks_add(now, 0) + try: + print(f"applying {effect=}") + t0 = ticks_ms() + bitmap = effect(bitmap) + t1 = ticks_ms() + print(f"{ticks_diff(t1, t0)}ms to apply effect") + except MemoryError as e: + print(e) + pycam.blit(bitmap) + + +main() From bfb86dc1d8bb7f26b2dab446684c29a0f9b8966c Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 19:01:28 -0600 Subject: [PATCH 11/70] Enhance documentation & edge filtering --- adafruit_pycamera/imageprocessing.py | 81 ++++++++++++++++++---------- docs/api.rst | 2 + docs/conf.py | 1 + examples/filter/code.py | 10 ++++ 4 files changed, 66 insertions(+), 28 deletions(-) diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index 425f8c3..b280c11 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -70,29 +70,32 @@ def _np_convolve_same(arr, coeffs): EIGHT_BITS = 0b11111111 -def bitmap_as_array(bitmap): +def _bitmap_as_array(bitmap): """Create an array object that accesses the bitmap data""" if bitmap.width % 2: raise ValueError("Can only work on even-width bitmaps") return np.frombuffer(bitmap, dtype=np.uint16).reshape((bitmap.height, bitmap.width)) -def array_cast(arr, dtype): +def _array_cast(arr, dtype): """Cast an array to a given type and shape. The new type must match the original type's size in bytes.""" return np.frombuffer(arr, dtype=dtype).reshape(arr.shape) def bitmap_to_components_rgb565(bitmap): - """Convert a RGB65_BYTESWAPPED image to int16 components in the [0,255] inclusive range + """Convert a RGB565_BYTESWAPPED image to int16 components in the [0,255] inclusive range This requires higher memory than uint8, but allows more arithmetic on pixel values; - converting back to bitmap clamps values to the appropriate range.""" - arr = bitmap_as_array(bitmap) + converting back to bitmap clamps values to the appropriate range. + + This only works on images whose width is a multiple of 2 pixels. + """ + arr = _bitmap_as_array(bitmap) arr.byteswap(inplace=True) - r = array_cast(np.right_shift(arr, 8) & 0xF8, np.int16) - g = array_cast(np.right_shift(arr, 3) & 0xFC, np.int16) - b = array_cast(np.left_shift(arr, 3) & 0xF8, np.int16) + r = _array_cast(np.right_shift(arr, 8) & 0xF8, np.int16) + g = _array_cast(np.right_shift(arr, 3) & 0xFC, np.int16) + b = _array_cast(np.left_shift(arr, 3) & 0xF8, np.int16) arr.byteswap(inplace=True) return r, g, b @@ -101,10 +104,10 @@ def bitmap_from_components_inplace_rgb565( bitmap, r, g, b ): # pylint: disable=invalid-name """Update a bitmap in-place with new RGB values""" - dest = bitmap_as_array(bitmap) - r = array_cast(np.maximum(np.minimum(r, 255), 0), np.uint16) - g = array_cast(np.maximum(np.minimum(g, 255), 0), np.uint16) - b = array_cast(np.maximum(np.minimum(b, 255), 0), np.uint16) + dest = _bitmap_as_array(bitmap) + r = _array_cast(np.maximum(np.minimum(r, 255), 0), np.uint16) + g = _array_cast(np.maximum(np.minimum(g, 255), 0), np.uint16) + b = _array_cast(np.maximum(np.minimum(b, 255), 0), np.uint16) dest[:] = np.left_shift(r & 0xF8, 8) dest[:] |= np.left_shift(g & 0xFC, 3) dest[:] |= np.right_shift(b, 3) @@ -112,16 +115,16 @@ def bitmap_from_components_inplace_rgb565( return bitmap -def as_flat(arr): - """Flatten an array, ensuring no copy is made""" +def _as_flat(arr): + """Internal routine to flatten an array, ensuring no copy is made""" return np.frombuffer(arr, arr.dtype) def buffer_from_components_rgb888(r, g, b): """Convert the individual color components to a single RGB888 buffer in memory""" - r = as_flat(r) - g = as_flat(g) - b = as_flat(b) + r = _as_flat(r) + g = _as_flat(g) + b = _as_flat(b) r = np.maximum(np.minimum(r, 0x3F), 0) g = np.maximum(np.minimum(g, 0x3F), 0) b = np.maximum(np.minimum(b, 0x3F), 0) @@ -139,21 +142,26 @@ def symmetric_filter_inplace(data, coeffs, scale): many common kinds of image filters such as blur, sharpen, and edge detect. Normally, scale is sum(coeffs).""" - # First run the filter across each row + row_filter_inplace(data, coeffs, scale) + column_filter_inplace(data, coeffs, scale) + + +def row_filter_inplace(data, coeffs, scale): + """Apply a filter to data in rows, changing it in place""" n_rows = data.shape[0] for i in range(n_rows): data[i, :] = _np_convolve_same(data[i, :], coeffs) // scale - # Run the filter across each column + +def column_filter_inplace(data, coeffs, scale): + """Apply a filter to data in columns, changing it in place""" n_cols = data.shape[1] for i in range(n_cols): data[:, i] = _np_convolve_same(data[:, i], coeffs) // scale - return data - def bitmap_symmetric_filter_inplace(bitmap, coeffs, scale): - """Apply a symmetric filter to an image, updating the original image""" + """Apply the same filter to an image by rows and then by columns, updating the original image""" r, g, b = bitmap_to_components_rgb565(bitmap) symmetric_filter_inplace(r, coeffs, scale) symmetric_filter_inplace(g, coeffs, scale) @@ -262,14 +270,31 @@ def sharpen(bitmap): ) +def _edge_filter_component(data, coefficients): + """Internal filter to apply H+V edge detection to an image component""" + data_copy = data[:] + row_filter_inplace(data, coefficients, scale=1) + column_filter_inplace(data_copy, coefficients, scale=1) + data += data_copy + data += 128 + + def edgedetect(bitmap): """Run an edge detection routine on a bitmap""" coefficients = np.array([-1, 0, 1]) r, g, b = bitmap_to_components_rgb565(bitmap) - symmetric_filter_inplace(r, coefficients, scale=1) - r += 128 - symmetric_filter_inplace(g, coefficients, scale=1) - g += 128 - symmetric_filter_inplace(b, coefficients, scale=1) - b += 128 + _edge_filter_component(r, coefficients) + _edge_filter_component(g, coefficients) + _edge_filter_component(b, coefficients) return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) + + +def edgedetect_greyscale(bitmap): + """Run an edge detection routine on a bitmap in greyscale""" + coefficients = np.array([-1, 0, 1]) + r, g, b = bitmap_to_components_rgb565(bitmap) + luminance = np.right_shift(38 * r + 75 * g + 15 * b, 7) + _edge_filter_component(luminance, coefficients) + return bitmap_from_components_inplace_rgb565( + bitmap, luminance, luminance, luminance + ) diff --git a/docs/api.rst b/docs/api.rst index 811e43d..06d67f8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,3 +6,5 @@ .. automodule:: adafruit_pycamera :members: +.. automodule:: adafruit_pycamera.imageprocessing + :members: diff --git a/docs/conf.py b/docs/conf.py index 66f51d2..447aa6e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,6 +44,7 @@ "sdcardio", "storage", "terminalio", + "ulab", ] autodoc_preserve_defaults = True diff --git a/examples/filter/code.py b/examples/filter/code.py index 31fdc4d..7792315 100644 --- a/examples/filter/code.py +++ b/examples/filter/code.py @@ -25,6 +25,7 @@ imageprocessing.blue_cast, imageprocessing.blur, imageprocessing.edgedetect, + imageprocessing.edgedetect_grayscale, imageprocessing.green_cast, imageprocessing.greyscale, imageprocessing.red_cast, @@ -47,6 +48,15 @@ def random_choice(seq): def load_resized_image(bitmap, filename): + """Load an image at the best scale into a given bitmap + + If the image can be scaled down until it fits within the bitmap, this routine + does so, leaving equal space at the sides of the image (known as letterboxing + or pillarboxing). + + If the image cannot be scaled down, the most central part of the image is loaded + into the bitmap.""" + print(f"loading {filename}") bitmap.fill(0b01000_010000_01000) # fill with a middle grey From 9021fd06d649ab60c0cc1546daf3938ec057b9f9 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 Jan 2024 19:14:58 -0600 Subject: [PATCH 12/70] fix sepia effect --- adafruit_pycamera/imageprocessing.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index b280c11..52b4778 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -213,14 +213,14 @@ def do_solarize(channel): def sepia(bitmap): - """Apply a sepia filter to an image in place - - based on some coefficients I found on the internet""" - return bitmap_channel_filter3_inplace( + """Apply a sepia filter to an image in place""" + r, g, b = bitmap_to_components_rgb565(bitmap) + luminance = np.right_shift(38 * r + 75 * g + 15 * b, 7) + return bitmap_from_components_inplace_rgb565( bitmap, - lambda r, g, b: np.right_shift(50 * r + 98 * g + 24 * b, 7), - lambda r, g, b: np.right_shift(44 * r + 88 * g + 42 * b, 7), - lambda r, g, b: np.right_shift(35 * r + 69 * g + 17 * b, 7), + luminance, + np.right_shift(luminance * 113, 7), + np.right_shift(luminance * 88, 7), ) From a27968d419026af3b2193ac04a1d9b2561bce64b Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 5 Jan 2024 10:05:02 -0600 Subject: [PATCH 13/70] Slight performance improvements of image processing np.minimum / maximum are surprisingly slow! --- adafruit_pycamera/imageprocessing.py | 61 ++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index 52b4778..751bf57 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -4,9 +4,37 @@ """Routines for performing image manipulation""" import struct +from adafruit_ticks import ticks_ms, ticks_diff +from micropython import const import ulab.numpy as np +# Optionally enable reporting of time taken inside tagged functions +_DO_TIME_REPORT = const(0) + +if _DO_TIME_REPORT: + + def _timereport(func): + """Report time taken within the function""" + name = str(func).split()[1] + + def inner(*args, **kw): + start = ticks_ms() + try: + return func(*args, **kw) + finally: + end = ticks_ms() + duration = ticks_diff(end, start) + print(f"{name}: {duration}ms") + + return inner + +else: + + def _timereport(func): + """A do-nothing decorator for when timing report is not desired""" + return func + def _bytes_per_row(source_width: int) -> int: """Internal function to determine bitmap bytes per row""" @@ -83,11 +111,14 @@ def _array_cast(arr, dtype): return np.frombuffer(arr, dtype=dtype).reshape(arr.shape) +@_timereport def bitmap_to_components_rgb565(bitmap): """Convert a RGB565_BYTESWAPPED image to int16 components in the [0,255] inclusive range This requires higher memory than uint8, but allows more arithmetic on pixel values; - converting back to bitmap clamps values to the appropriate range. + but values are masked (not clamped) back down to the 0-255 range, so while intermediate + values can be -32768..32767 the values passed into bitmap_from_components_inplace_rgb565 + muts be 0..255 This only works on images whose width is a multiple of 2 pixels. """ @@ -100,17 +131,20 @@ def bitmap_to_components_rgb565(bitmap): return r, g, b +@_timereport def bitmap_from_components_inplace_rgb565( bitmap, r, g, b ): # pylint: disable=invalid-name """Update a bitmap in-place with new RGB values""" dest = _bitmap_as_array(bitmap) - r = _array_cast(np.maximum(np.minimum(r, 255), 0), np.uint16) - g = _array_cast(np.maximum(np.minimum(g, 255), 0), np.uint16) - b = _array_cast(np.maximum(np.minimum(b, 255), 0), np.uint16) - dest[:] = np.left_shift(r & 0xF8, 8) - dest[:] |= np.left_shift(g & 0xFC, 3) - dest[:] |= np.right_shift(b, 3) + r = _array_cast(r, np.uint16) + g = _array_cast(g, np.uint16) + b = _array_cast(b, np.uint16) + dest[:] = ( + np.left_shift(r & 0xF8, 8) + | np.left_shift(g & 0xFC, 3) + | np.right_shift(b & 0xF8, 3) + ) dest.byteswap(inplace=True) return bitmap @@ -125,13 +159,10 @@ def buffer_from_components_rgb888(r, g, b): r = _as_flat(r) g = _as_flat(g) b = _as_flat(b) - r = np.maximum(np.minimum(r, 0x3F), 0) - g = np.maximum(np.minimum(g, 0x3F), 0) - b = np.maximum(np.minimum(b, 0x3F), 0) result = np.zeros(3 * len(r), dtype=np.uint8) - result[2::3] = r - result[1::3] = g - result[0::3] = b + result[2::3] = r & 0xFF + result[1::3] = g & 0xFF + result[0::3] = b & 0xFF return result @@ -146,6 +177,7 @@ def symmetric_filter_inplace(data, coeffs, scale): column_filter_inplace(data, coeffs, scale) +@_timereport def row_filter_inplace(data, coeffs, scale): """Apply a filter to data in rows, changing it in place""" n_rows = data.shape[0] @@ -153,6 +185,7 @@ def row_filter_inplace(data, coeffs, scale): data[i, :] = _np_convolve_same(data[i, :], coeffs) // scale +@_timereport def column_filter_inplace(data, coeffs, scale): """Apply a filter to data in columns, changing it in place""" n_cols = data.shape[1] @@ -169,6 +202,7 @@ def bitmap_symmetric_filter_inplace(bitmap, coeffs, scale): return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) +@_timereport def bitmap_channel_filter3_inplace( bitmap, r_func=lambda r, g, b: r, g_func=lambda r, g, b: g, b_func=lambda r, g, b: b ): @@ -182,6 +216,7 @@ def bitmap_channel_filter3_inplace( return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) +@_timereport def bitmap_channel_filter1_inplace( bitmap, r_func=lambda r: r, g_func=lambda g: g, b_func=lambda b: b ): From 6234432a1ec7d37b46f7d3b7343ef38ef688dddd Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 5 Jan 2024 10:54:06 -0600 Subject: [PATCH 14/70] mock adafruit ticks during doc build --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 447aa6e..b78c877 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,6 +32,7 @@ "adafruit_debouncer", "adafruit_display_text", "adafruit_lis3dh", + "adafruit_ticks", "bitmaptools", "busdisplay", "busio", From 3689a8783e29889265c4e59d3a9150530b7feb72 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 26 Jan 2024 11:06:20 -0600 Subject: [PATCH 15/70] Update image processing demo for newest CircuitPython --- LICENSES/CC0-1.0.txt | 121 +++++++ adafruit_pycamera/imageprocessing.py | 339 +++--------------- adafruit_pycamera/ironbow.py | 50 +++ examples/filter/code.py | 221 ++++-------- examples/filter/cornell_box_208x208.jpg | Bin 0 -> 10161 bytes .../filter/cornell_box_208x208.jpg.license | 3 + 6 files changed, 294 insertions(+), 440 deletions(-) create mode 100644 LICENSES/CC0-1.0.txt create mode 100644 adafruit_pycamera/ironbow.py create mode 100644 examples/filter/cornell_box_208x208.jpg create mode 100644 examples/filter/cornell_box_208x208.jpg.license diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index 751bf57..d7fa770 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -3,333 +3,82 @@ # SPDX-License-Identifier: MIT """Routines for performing image manipulation""" -import struct -from adafruit_ticks import ticks_ms, ticks_diff +import bitmapfilter -from micropython import const -import ulab.numpy as np +from adafruit_pycamera.ironbow import ironbow_palette -# Optionally enable reporting of time taken inside tagged functions -_DO_TIME_REPORT = const(0) +sepia_weights = bitmapfilter.ChannelMixer( + 0.393, 0.769, 0.189, 0.349, 0.686, 0.168, 0.272, 0.534, 0.131 +) -if _DO_TIME_REPORT: - def _timereport(func): - """Report time taken within the function""" - name = str(func).split()[1] - - def inner(*args, **kw): - start = ticks_ms() - try: - return func(*args, **kw) - finally: - end = ticks_ms() - duration = ticks_diff(end, start) - print(f"{name}: {duration}ms") - - return inner - -else: - - def _timereport(func): - """A do-nothing decorator for when timing report is not desired""" - return func - - -def _bytes_per_row(source_width: int) -> int: - """Internal function to determine bitmap bytes per row""" - pixel_bytes = 3 * source_width - padding_bytes = (4 - (pixel_bytes % 4)) % 4 - return pixel_bytes + padding_bytes - - -def _write_bmp_header(output_file, filesize): - """Internal function to write bitmap header""" - output_file.write(bytes("BM", "ascii")) - output_file.write(struct.pack(" None: - """Internal function to write bitmap "dib" header""" - output_file.write(struct.pack(" threshold) + data * (data <= threshold) - - -def solarize(bitmap, threshold=128): - """Apply a per-channel solarize filter to an image in place""" - - def do_solarize(channel): - return solarize_channel(channel, threshold) - - return bitmap_channel_filter1_inplace(bitmap, do_solarize, do_solarize, do_solarize) +def negative(bitmap, mask=None): + """Invert an image""" + bitmapfilter.mix(bitmap, negative_weights, mask=mask) + return bitmap -def sepia(bitmap): - """Apply a sepia filter to an image in place""" - r, g, b = bitmap_to_components_rgb565(bitmap) - luminance = np.right_shift(38 * r + 75 * g + 15 * b, 7) - return bitmap_from_components_inplace_rgb565( - bitmap, - luminance, - np.right_shift(luminance * 113, 7), - np.right_shift(luminance * 88, 7), - ) +greyscale_weights = bitmapfilter.ChannelMixer( + 0.299, 0.587, 0.114, 0.299, 0.587, 0.114, 0.299, 0.587, 0.114 +) -def greyscale(bitmap): +def greyscale(bitmap, mask=None): """Convert an image to greyscale""" - r, g, b = bitmap_to_components_rgb565(bitmap) - luminance = np.right_shift(38 * r + 75 * g + 15 * b, 7) - return bitmap_from_components_inplace_rgb565( - bitmap, luminance, luminance, luminance - ) - - -def _identity(channel): - """An internal function to return a channel unchanged""" - return channel - - -def _half(channel): - """An internal function to divide channel values by two""" - return channel // 2 + bitmapfilter.mix(bitmap, greyscale_weights, mask=mask) + return bitmap -def red_cast(bitmap): +def red_cast(bitmap, mask=None): """Give an image a red cast by dividing G and B channels in half""" - return bitmap_channel_filter1_inplace(bitmap, _identity, _half, _half) + bitmapfilter.mix(bitmap, bitmapfilter.ChannelScale(1, 0.5, 0.5), mask=mask) + return bitmap -def green_cast(bitmap): +def green_cast(bitmap, mask=None): """Give an image a green cast by dividing R and B channels in half""" - return bitmap_channel_filter1_inplace(bitmap, _half, _identity, _half) + bitmapfilter.mix(bitmap, bitmapfilter.ChannelScale(0.5, 1, 0.5), mask=mask) + return bitmap -def blue_cast(bitmap): +def blue_cast(bitmap, mask=None): """Give an image a blue cast by dividing R and G channels in half""" - return bitmap_channel_filter1_inplace(bitmap, _half, _half, _identity) + bitmapfilter.mix(bitmap, bitmapfilter.ChannelScale(0.5, 0.5, 1), mask=mask) + return bitmap -def blur(bitmap): +def blur(bitmap, mask=None): """Blur a bitmap""" - return bitmap_symmetric_filter_inplace(bitmap, np.array([1, 2, 1]), scale=4) + bitmapfilter.morph(bitmap, (1, 2, 1, 2, 4, 2, 1, 2, 1), mask=mask) + return bitmap -def sharpen(bitmap): +def sharpen(bitmap, mask=None): """Sharpen a bitmap""" - return bitmap_symmetric_filter_inplace( - bitmap, np.array([-1, -1, 9, -1, -1]), scale=5 - ) + bitmapfilter.morph(bitmap, (-1, -2, -1, -2, 13, -2, -1, -2, -1), mask=mask) + return bitmap -def _edge_filter_component(data, coefficients): - """Internal filter to apply H+V edge detection to an image component""" - data_copy = data[:] - row_filter_inplace(data, coefficients, scale=1) - column_filter_inplace(data_copy, coefficients, scale=1) - data += data_copy - data += 128 +def emboss(bitmap, mask=None): + """Run an emboss filter on the bitmap""" + bitmapfilter.morph(bitmap, (-2, -1, 0, -1, 0, 1, 0, 1, 2), add=0.5, mask=mask) -def edgedetect(bitmap): - """Run an edge detection routine on a bitmap""" - coefficients = np.array([-1, 0, 1]) - r, g, b = bitmap_to_components_rgb565(bitmap) - _edge_filter_component(r, coefficients) - _edge_filter_component(g, coefficients) - _edge_filter_component(b, coefficients) - return bitmap_from_components_inplace_rgb565(bitmap, r, g, b) +def emboss_greyscale(bitmap, mask=None): + """Run an emboss filter on the bitmap in greyscale""" + greyscale(bitmap, mask=mask) + return emboss(bitmap, mask=mask) -def edgedetect_greyscale(bitmap): - """Run an edge detection routine on a bitmap in greyscale""" - coefficients = np.array([-1, 0, 1]) - r, g, b = bitmap_to_components_rgb565(bitmap) - luminance = np.right_shift(38 * r + 75 * g + 15 * b, 7) - _edge_filter_component(luminance, coefficients) - return bitmap_from_components_inplace_rgb565( - bitmap, luminance, luminance, luminance - ) +def ironbow(bitmap, mask=None): + """Convert an image to false color using the 'ironbow palette'""" + return bitmapfilter.false_color(bitmap, ironbow_palette, mask=mask) diff --git a/adafruit_pycamera/ironbow.py b/adafruit_pycamera/ironbow.py new file mode 100644 index 0000000..adea56f --- /dev/null +++ b/adafruit_pycamera/ironbow.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT +"""The 'ironbow' palette used to convert images to false color""" + +import displayio + +ironbow_palette = displayio.Palette(256) + +# fmt: off +for i, pi in enumerate( + [ + 0xFFFFFF, 0xFFFFFF, 0xFFFEFF, 0xF7FEF7, 0xF7FDF7, 0xF7FDF7, 0xF7FCF7, 0xEFFCEF, + 0xEFFFEF, 0xEFFFEF, 0xEFFAEF, 0xE7FAE7, 0xE7FDE7, 0xE7FDE7, 0xE7F8E7, 0xDEF8DE, + 0xDEFFDE, 0xDEFFDE, 0xDEFEDE, 0xD6FED6, 0xD6F5D6, 0xD6F5D6, 0xD6F4D6, 0xCEF4CE, + 0xCEFFCE, 0xCEFFCE, 0xCEFACE, 0xC6FAC6, 0xC6F5C6, 0xC6F5C6, 0xC6F0C6, 0xBDF0BD, + 0xBDBFBD, 0xBDBFBD, 0xBDBEBD, 0xB5BEB5, 0xB5BDB5, 0xB5BDB5, 0xB5BCB5, 0xB5BCB5, + 0xADAFAD, 0xADAFAD, 0xADAAAD, 0xADAAAD, 0xA5ADA5, 0xA5ADA5, 0xA5A8A5, 0xA5A8A5, + 0x9CBF9C, 0x9CBF9C, 0x9CBE9C, 0x9CBE9C, 0x94B594, 0x94B594, 0x94B494, 0x94B494, + 0x8CAF8C, 0x8CAF8C, 0x8CAA8C, 0x8CAA8C, 0x84A584, 0x84A584, 0x84A084, 0x84A084, + 0x7B7F7B, 0x7B7F7B, 0x7B7E7B, 0x7B7E7B, 0x737D73, 0x737D73, 0x737C73, 0x737C73, + 0x6B7F6B, 0x6B7F6B, 0x6B7A6B, 0x6B7A6B, 0x637D63, 0x637D63, 0x637863, 0x637863, + 0x5A5F5A, 0x5A5F5A, 0x5A5E5A, 0x5A5E5A, 0x525552, 0x525552, 0x525452, 0x525452, + 0x4A5F4A, 0x4A5F4A, 0x4A5A4A, 0x4A5A4A, 0x4A554A, 0x425542, 0x425042, 0x425042, + 0x423F42, 0x393F39, 0x393E39, 0x393E39, 0x393D39, 0x313D31, 0x313C31, 0x313C31, + 0x312F31, 0x292F29, 0x292A29, 0x292A29, 0x292D29, 0x212D21, 0x212821, 0x212821, + 0x211F21, 0x181F18, 0x181E18, 0x181E18, 0x181518, 0x101510, 0x101410, 0x101410, + 0x100F10, 0x080F08, 0x080A08, 0x080A08, 0x080508, 0x000500, 0x000000, 0x000000, + 0x000008, 0x000010, 0x000018, 0x080021, 0x080029, 0x080029, 0x080031, 0x100039, + 0x100042, 0x10004A, 0x180052, 0x18005A, 0x180063, 0x18006B, 0x21006B, 0x210073, + 0x21007B, 0x29007B, 0x31007B, 0x31007B, 0x39007B, 0x39007B, 0x42007B, 0x4A007B, + 0x4A0084, 0x520084, 0x520084, 0x5A0084, 0x630084, 0x630084, 0x6B0084, 0x6B0084, + 0x73008C, 0x7B008C, 0x7B008C, 0x84008C, 0x84058C, 0x8C058C, 0x94058C, 0x94058C, + 0x9C058C, 0x9C058C, 0xA5058C, 0xA5058C, 0xAD058C, 0xB5058C, 0xB50A8C, 0xBD0A8C, + 0xBD0A8C, 0xBD0F84, 0xC6147B, 0xC6157B, 0xC61573, 0xC61E6B, 0xCE1F6B, 0xCE2863, + 0xCE2863, 0xCE2D5A, 0xD62A52, 0xD62F52, 0xD62F4A, 0xDE3C42, 0xDE3D42, 0xDE3E39, + 0xDE3E31, 0xDE3F31, 0xDE5029, 0xE75529, 0xE75A29, 0xE75A21, 0xE75F21, 0xE75421, + 0xE75521, 0xE75E18, 0xE75F18, 0xE75F18, 0xEF7810, 0xEF7D10, 0xEF7A10, 0xEF7F08, + 0xEF7C08, 0xEF7D08, 0xEF7D08, 0xEF7E08, 0xEF7F08, 0xEFA008, 0xEFA508, 0xF7AA08, + 0xF7AF10, 0xF7B410, 0xF7B510, 0xF7BE10, 0xF7BF10, 0xF7A810, 0xF7A810, 0xF7AD10, + 0xF7AA10, 0xF7AF10, 0xF7BC10, 0xF7BD10, 0xF7BE10, 0xF7BF10, 0xF7BF10, 0xFFF018, + 0xFFF518, 0xFFFA18, 0xFFFF18, 0xFFF418, 0xFFF518, 0xFFFE18, 0xFFFE21, 0xFFFF21, + 0xFFF829, 0xFFFD31, 0xFFFD42, 0xFFFA52, 0xFFFA63, 0xFFFA6B, 0xFFFF7B, 0xFFFF8C, + 0xFFFC94, 0xFFFCA5, 0xFFFDB5, 0xFFFDBD, 0xFFFECE, 0xFFFEDE, 0xFFFFEF, 0xFFFF18, + ] +): + ironbow_palette[i] = pi +del i +del pi +del displayio diff --git a/examples/filter/code.py b/examples/filter/code.py index 7792315..94d90f3 100644 --- a/examples/filter/code.py +++ b/examples/filter/code.py @@ -2,44 +2,63 @@ # # SPDX-License-Identifier: Unlicense -"""Image viewer +"""Effects Demonstration -This will display all *jpeg* format images on the inserted SD card, in random order. -Each time an image is displayed, one of the pre-defined image filters is performed on it. +This will apply a nubmer of effects to a single image. -Images cycle every DISPLAY_INTERVAL milliseconds (default 8000 = 8 seconds) as long as -they can be processed fast enough. Pressing any of the 4 direction buttons will start a -new image processing as soon as possible. +Press any of the directional buttons to immediately apply a new effect. + +Otherwise, effects cycle every DISPLAY_INTERVAL milliseconds (default 2000 = 2 seconds) """ -import time -import os -import random import displayio from jpegio import JpegDecoder +from adafruit_display_text.label import Label from adafruit_ticks import ticks_less, ticks_ms, ticks_add, ticks_diff -from adafruit_pycamera import PyCameraBase +from font_free_mono_bold_24 import FONT +import bitmapfilter + from adafruit_pycamera import imageprocessing +from adafruit_pycamera import PyCameraBase effects = [ - imageprocessing.blue_cast, - imageprocessing.blur, - imageprocessing.edgedetect, - imageprocessing.edgedetect_grayscale, - imageprocessing.green_cast, - imageprocessing.greyscale, - imageprocessing.red_cast, - imageprocessing.sepia, - imageprocessing.sharpen, - imageprocessing.solarize, + ("blue cast", imageprocessing.blue_cast), + ("blur", imageprocessing.blur), + ("bright", lambda b: bitmapfilter.mix(b, bitmapfilter.ChannelScale(2.0, 2.0, 2.0))), + ("emboss", imageprocessing.emboss), + ("green cast", imageprocessing.green_cast), + ("greyscale", imageprocessing.greyscale), + ("ironbow", imageprocessing.ironbow), + ( + "low contrast", + lambda b: bitmapfilter.mix( + b, bitmapfilter.ChannelScaleOffset(0.5, 0.5, 0.5, 0.5, 0.5, 0.5) + ), + ), + ("negative", imageprocessing.negative), + ("red cast", imageprocessing.red_cast), + ("sepia", imageprocessing.sepia), + ("sharpen", imageprocessing.sharpen), + ("solarize", bitmapfilter.solarize), + ( + "swap r/b", + lambda b: bitmapfilter.mix( + b, bitmapfilter.ChannelMixer(0, 0, 1, 0, 1, 0, 1, 0, 0) + ), + ), ] -def random_choice(seq): - return seq[random.randrange(0, len(seq))] +def cycle(seq): + while True: + for s in seq: + yield s + +effects_cycle = iter(cycle(effects)) -DISPLAY_INTERVAL = 8000 # milliseconds + +DISPLAY_INTERVAL = 2000 # milliseconds decoder = JpegDecoder() @@ -47,136 +66,48 @@ def random_choice(seq): pycam.init_display() -def load_resized_image(bitmap, filename): - """Load an image at the best scale into a given bitmap - - If the image can be scaled down until it fits within the bitmap, this routine - does so, leaving equal space at the sides of the image (known as letterboxing - or pillarboxing). - - If the image cannot be scaled down, the most central part of the image is loaded - into the bitmap.""" - - print(f"loading {filename}") - bitmap.fill(0b01000_010000_01000) # fill with a middle grey - - bw, bh = bitmap.width, bitmap.height - t0 = ticks_ms() - h, w = decoder.open(filename) - t1 = ticks_ms() - print(f"{ticks_diff(t1, t0)}ms to open") - scale = 0 - print(f"Full image size is {w}x{h}") - print(f"Bitmap is {bw}x{bh} pixels") - while (w >> scale) > bw or (h >> scale) > bh and scale < 3: - scale += 1 - sw = w >> scale - sh = h >> scale - print(f"will load at {scale=}, giving {sw}x{sh} pixels") - - if sw > bw: # left/right sides cut off - x = 0 - x1 = (sw - bw) // 2 - else: # horizontally centered - x = (bw - sw) // 2 - x1 = 0 - - if sh > bh: # top/bottom sides cut off - y = 0 - y1 = (sh - bh) // 2 - else: # vertically centered - y = (bh - sh) // 2 - y1 = 0 - - print(f"{x=} {y=} {x1=} {y1=}") - decoder.decode(bitmap, x=x, y=y, x1=x1, y1=y1, scale=scale) - t1 = ticks_ms() - print(f"{ticks_diff(t1, t0)}ms to decode") - - -def mount_sd(): - if not pycam.card_detect.value: - pycam.display_message("No SD card\ninserted", color=0xFF0000) - return [] - pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) - for _ in range(3): - try: - print("Mounting card") - pycam.mount_sd_card() - print("Success!") - break - except OSError as e: - print("Retrying!", e) - time.sleep(0.5) - else: - pycam.display_message("SD Card\nFailed!", color=0xFF0000) - time.sleep(0.5) - all_images = [ - f"/sd/{filename}" - for filename in os.listdir("/sd") - if filename.lower().endswith(".jpg") - ] - pycam.display_message(f"Found {len(all_images)}\nimages", color=0xFFFFFF) - time.sleep(0.5) - pycam.display.refresh() - return all_images +def main(): + filename = "/cornell_box_208x208.jpg" + bitmap = displayio.Bitmap(208, 208, 65535) + bitmap0 = displayio.Bitmap(208, 208, 65535) + decoder.open(filename) + decoder.decode(bitmap0) + + label = Label(font=FONT, x=0, y=8) + pycam.display.root_group = label + pycam.display.refresh() -def main(): deadline = ticks_ms() - all_images = mount_sd() + while True: + now = ticks_ms() + if pycam.up.fell: + deadline = now - bitmap = displayio.Bitmap(pycam.display.width, pycam.display.height - 32, 65535) + if pycam.down.fell: + deadline = now - while True: - pycam.keys_debounce() - if pycam.card_detect.fell: - print("SD card removed") - pycam.unmount_sd_card() - pycam.display_message("SD Card\nRemoved", color=0xFFFFFF) - time.sleep(0.5) - pycam.display.refresh() - all_images = [] + if pycam.left.fell: + deadline = now - now = ticks_ms() - if pycam.card_detect.rose: - print("SD card inserted") - all_images = mount_sd() + if pycam.right.fell: deadline = now - if all_images: - if pycam.up.fell: - deadline = now - - if pycam.down.fell: - deadline = now - - if pycam.left.fell: - deadline = now - - if pycam.right.fell: - deadline = now - - if ticks_less(deadline, now): - print(now, deadline, ticks_less(deadline, now), all_images) - deadline = ticks_add(deadline, DISPLAY_INTERVAL) - filename = random_choice(all_images) - effect = random.choice(effects) - try: - load_resized_image(bitmap, filename) - except Exception as e: # pylint: disable=broad-exception-caught - pycam.display_message(f"Failed to read\n{filename}", color=0xFF0000) - print(e) - deadline = ticks_add(now, 0) - try: - print(f"applying {effect=}") - t0 = ticks_ms() - bitmap = effect(bitmap) - t1 = ticks_ms() - print(f"{ticks_diff(t1, t0)}ms to apply effect") - except MemoryError as e: - print(e) - pycam.blit(bitmap) + if ticks_less(deadline, now): + memoryview(bitmap)[:] = memoryview(bitmap0) + deadline = ticks_add(deadline, DISPLAY_INTERVAL) + + effect_name, effect = next(effects_cycle) # random.choice(effects) + print(effect) + print(f"applying {effect=}") + t0 = ticks_ms() + effect(bitmap) + t1 = ticks_ms() + dt = ticks_diff(t1, t0) + print(f"{dt}ms to apply effect") + pycam.blit(bitmap, x_offset=16) + label.text = f"{dt:4}ms: {effect_name}" + pycam.display.refresh() main() diff --git a/examples/filter/cornell_box_208x208.jpg b/examples/filter/cornell_box_208x208.jpg new file mode 100644 index 0000000000000000000000000000000000000000..20ab2c541ccd168cfb7eeb86f089b8d89b1b1997 GIT binary patch literal 10161 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 ZSwgqspdZe9W&yBn$6|oJO6Yy*e*sm$qFev~ literal 0 HcmV?d00001 diff --git a/examples/filter/cornell_box_208x208.jpg.license b/examples/filter/cornell_box_208x208.jpg.license new file mode 100644 index 0000000..2aa27ab --- /dev/null +++ b/examples/filter/cornell_box_208x208.jpg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 SeeSchloss + +SPDX-License-Identifier: CC0-1.0 From d68e862f41fae95dfaf55df843d1e8c4ef0318c9 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 26 Jan 2024 11:12:22 -0600 Subject: [PATCH 16/70] Fix doc building a non-default mock of displayio.Bitmap is provided for doc building --- adafruit_pycamera/ironbow.py | 1 + docs/api.rst | 2 ++ docs/conf.py | 3 ++- docs/mock/displayio.py | 11 +++++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 docs/mock/displayio.py diff --git a/adafruit_pycamera/ironbow.py b/adafruit_pycamera/ironbow.py index adea56f..11ce719 100644 --- a/adafruit_pycamera/ironbow.py +++ b/adafruit_pycamera/ironbow.py @@ -6,6 +6,7 @@ import displayio ironbow_palette = displayio.Palette(256) +"""A palette often used to convert images to false color""" # fmt: off for i, pi in enumerate( diff --git a/docs/api.rst b/docs/api.rst index 06d67f8..4e86a8c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -8,3 +8,5 @@ :members: .. automodule:: adafruit_pycamera.imageprocessing :members: +.. automodule:: adafruit_pycamera.ironbow + :members: diff --git a/docs/conf.py b/docs/conf.py index b78c877..3b6c67e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,7 @@ import sys sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath("mock")) # -- General configuration ------------------------------------------------ @@ -34,10 +35,10 @@ "adafruit_lis3dh", "adafruit_ticks", "bitmaptools", + "bitmapfilter", "busdisplay", "busio", "digitalio", - "displayio", "espcamera", "fourwire", "micropython", diff --git a/docs/mock/displayio.py b/docs/mock/displayio.py new file mode 100644 index 0000000..b4fd1c5 --- /dev/null +++ b/docs/mock/displayio.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + + +class Palette: + def __init__(self, i): + self._data = [0] * i + + def __setitem__(self, idx, value): + self._data[idx] = value From f417055983349b53e5d51aedcbf51f1b2e8efd37 Mon Sep 17 00:00:00 2001 From: ladyada Date: Sat, 27 Jan 2024 13:09:42 -0500 Subject: [PATCH 17/70] fix for https://github.com/adafruit/Adafruit_CircuitPython_PyCamera/issues/18 --- adafruit_pycamera/__init__.py | 4 ++-- examples/camera/code.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 89bb283..bd76ad9 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -63,9 +63,9 @@ _AW_CARDDET = const(8) _AW_SDPWR = const(9) _AW_DOWN = const(15) -_AW_LEFT = const(14) +_AW_RIGHT = const(14) _AW_UP = const(13) -_AW_RIGHT = const(12) +_AW_LEFT = const(12) _AW_OK = const(11) _NVM_RESOLUTION = const(1) diff --git a/examples/camera/code.py b/examples/camera/code.py index ddb0d5e..179d66e 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -158,15 +158,15 @@ key = settings[curr_setting] if key: setattr(pycam, key, getattr(pycam, key) - 1) - if pycam.left.fell: - print("LF") + if pycam.right.fell: + print("RT") curr_setting = (curr_setting + 1) % len(settings) print(settings[curr_setting]) # new_res = min(len(pycam.resolutions)-1, pycam.get_resolution()+1) # pycam.set_resolution(pycam.resolutions[new_res]) pycam.select_setting(settings[curr_setting]) - if pycam.right.fell: - print("RT") + if pycam.left.fell: + print("LF") curr_setting = (curr_setting - 1 + len(settings)) % len(settings) print(settings[curr_setting]) pycam.select_setting(settings[curr_setting]) From 40ff754214faaf75e2e54a7c3fec0bf4fba6f4a3 Mon Sep 17 00:00:00 2001 From: ladyada Date: Sat, 27 Jan 2024 13:58:22 -0500 Subject: [PATCH 18/70] some beginnings of timelapse --- adafruit_pycamera/__init__.py | 65 +++++++++++++++++++++++++++++++++-- examples/camera/code.py | 19 +++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index bd76ad9..472e951 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -71,6 +71,7 @@ _NVM_RESOLUTION = const(1) _NVM_EFFECT = const(2) _NVM_MODE = const(3) +_NVM_TIMELAPSE_RATE = const(4) class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -168,7 +169,25 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub "Sepia", "Solarize", ) - modes = ("JPEG", "GIF", "GBOY", "STOP") + + timelapse_rates = ( + 5, + 10, + 20, + 30, + 60, + 90, + 60 * 2, + 60 * 3, + 60 * 4, + 60 * 5, + 60 * 10, + 60 * 15, + 60 * 30, + 60 * 60 + ) + + modes = ("JPEG", "GIF", "GBOY", "STOP", "LAPS") _INIT_SEQUENCE = ( b"\x01\x80\x78" # _SWRESET and Delay 120ms @@ -249,13 +268,13 @@ def make_debounced_expander_pin(pin_no): def make_camera_ui(self): """Create displayio widgets for the standard camera UI""" self._sd_label = label.Label( - terminalio.FONT, text="SD ??", color=0x0, x=150, y=10, scale=2 + terminalio.FONT, text="SD ??", color=0x0, x=170, y=10, scale=2 ) self._effect_label = label.Label( terminalio.FONT, text="EFFECT", color=0xFFFFFF, x=4, y=10, scale=2 ) self._mode_label = label.Label( - terminalio.FONT, text="MODE", color=0xFFFFFF, x=150, y=10, scale=2 + terminalio.FONT, text="MODE", color=0xFFFFFF, x=170, y=10, scale=2 ) self._topbar = displayio.Group() self._res_label = label.Label( @@ -268,8 +287,19 @@ def make_camera_ui(self): self._botbar.append(self._effect_label) self._botbar.append(self._mode_label) + self._timelapsebar = displayio.Group(x=0, y=180) + self._timelapse_rate_label = label.Label( + terminalio.FONT, text="Time", color=0xFFFFFF, x=150, y=10, scale=2 + ) + self._timelapsestatus_label = label.Label( + terminalio.FONT, text="Status", color=0xFFFFFF, x=0, y=10, scale=2 + ) + self._timelapsebar.append(self._timelapse_rate_label) + self._timelapsebar.append(self._timelapsestatus_label) + self.splash.append(self._topbar) self.splash.append(self._botbar) + self.splash.append(self._timelapsebar) def init_accelerometer(self): """Initialize the accelerometer""" @@ -338,6 +368,7 @@ def init_camera(self, init_autofocus=True) -> None: self.camera.saturation = 3 self.resolution = microcontroller.nvm[_NVM_RESOLUTION] self.mode = microcontroller.nvm[_NVM_MODE] + self.timelapse_rate = microcontroller.nvm[_NVM_TIMELAPSE_RATE] if init_autofocus: self.autofocus_init() @@ -461,6 +492,9 @@ def select_setting(self, setting_name): self._res_label.text = self.resolutions[self._resolution] self._mode_label.color = 0xFFFFFF self._mode_label.background_color = 0x0 + self._timelapse_rate_label.color = 0xFFFFFF + self._timelapse_rate_label.background_color = 0x0 + if setting_name == "effect": self._effect_label.color = 0x0 self._effect_label.background_color = 0xFFFFFF @@ -478,6 +512,13 @@ def select_setting(self, setting_name): self._res_label.text = "LED CLR" self._res_label.color = 0x0 self._res_label.background_color = 0xFFFFFF + elif setting_name == "led_color": + self._res_label.text = "LED CLR" + self._res_label.color = 0x0 + self._res_label.background_color = 0xFFFFFF + elif setting_name == "timelapse_rate": + self._timelapse_rate_label.color = 0x0 + self._timelapse_rate_label.background_color = 0xFFFFFF self.display.refresh() @property @@ -538,6 +579,24 @@ def resolution(self, res): self._res_label.text = self.resolutions[res] self.display.refresh() + + @property + def timelapse_rate(self): + """Get or set the amount of time between timelapse shots""" + return self._timelapse_rate + + @timelapse_rate.setter + def timelapse_rate(self, setting): + setting = (setting + len(self.timelapse_rates)) % len(self.timelapse_rates) + self._timelapse_rate = setting + if self.timelapse_rates[setting] < 60: + self._timelapse_rate_label.text = "%d S" % self.timelapse_rates[setting] + else: + self._timelapse_rate_label.text = "%d M" % (self.timelapse_rates[setting] / 60) + microcontroller.nvm[_NVM_TIMELAPSE_RATE] = setting + self.display.refresh() + + def init_display(self): """Initialize the TFT display""" # construct displayio by hand diff --git a/examples/camera/code.py b/examples/camera/code.py index 179d66e..6e4e81e 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -14,13 +14,15 @@ pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() -settings = (None, "resolution", "effect", "mode", "led_level", "led_color") +settings = (None, "resolution", "effect", "mode", "led_level", "led_color", "timelapse_rate") curr_setting = 0 print("Starting!") # pycam.tone(200, 0.1) last_frame = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) onionskin = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) +timelapse_remaining = None + while True: if pycam.mode_text == "STOP" and pycam.stop_motion_frame != 0: # alpha blend @@ -34,6 +36,15 @@ last_frame, pycam.continuous_capture(), displayio.Colorspace.RGB565_SWAPPED ) pycam.blit(last_frame) + elif pycam.mode_text == "LAPS": + pycam.blit(pycam.continuous_capture()) + pycam._timelapse_rate_label.text = pycam._timelapse_rate_label.text + if timelapse_remaining is None: + pycam._timelapsestatus_label.text = "STOP" + #pycam.display_message("Timelapse\nNot Running\nPress Select to set timing") + else: + pycam.display_message("%d Seconds left", timelapse_remaining) + pycam.display.refresh() else: pycam.blit(pycam.continuous_capture()) # print("\t\t", capture_time, blit_time) @@ -127,6 +138,7 @@ except RuntimeError as e: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) + if pycam.card_detect.fell: print("SD card removed") pycam.unmount_sd_card() @@ -152,6 +164,7 @@ print("UP") key = settings[curr_setting] if key: + print("getting", key, getattr(pycam, key)) setattr(pycam, key, getattr(pycam, key) + 1) if pycam.down.fell: print("DN") @@ -161,6 +174,8 @@ if pycam.right.fell: print("RT") curr_setting = (curr_setting + 1) % len(settings) + if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelapse_rate": + curr_setting = (curr_setting + 1) % len(settings) print(settings[curr_setting]) # new_res = min(len(pycam.resolutions)-1, pycam.get_resolution()+1) # pycam.set_resolution(pycam.resolutions[new_res]) @@ -168,6 +183,8 @@ if pycam.left.fell: print("LF") curr_setting = (curr_setting - 1 + len(settings)) % len(settings) + if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelaps_rate": + curr_setting = (curr_setting + 1) % len(settings) print(settings[curr_setting]) pycam.select_setting(settings[curr_setting]) # new_res = max(1, pycam.get_resolution()-1) From ad34c6c0a113055f2b1be045f691431325ce073d Mon Sep 17 00:00:00 2001 From: ladyada Date: Sat, 27 Jan 2024 14:44:12 -0500 Subject: [PATCH 19/70] more timelapse! --- adafruit_pycamera/__init__.py | 6 ++++-- examples/camera/code.py | 27 ++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 472e951..97f9de7 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -289,11 +289,13 @@ def make_camera_ui(self): self._timelapsebar = displayio.Group(x=0, y=180) self._timelapse_rate_label = label.Label( - terminalio.FONT, text="Time", color=0xFFFFFF, x=150, y=10, scale=2 + terminalio.FONT, text="Time", color=0xFFFFFF,x=90, y=10, scale=2 ) + self._timelapse_rate_label.background_color = None self._timelapsestatus_label = label.Label( terminalio.FONT, text="Status", color=0xFFFFFF, x=0, y=10, scale=2 ) + self._timelapsestatus_label.background_color = None self._timelapsebar.append(self._timelapse_rate_label) self._timelapsebar.append(self._timelapsestatus_label) @@ -493,7 +495,7 @@ def select_setting(self, setting_name): self._mode_label.color = 0xFFFFFF self._mode_label.background_color = 0x0 self._timelapse_rate_label.color = 0xFFFFFF - self._timelapse_rate_label.background_color = 0x0 + self._timelapse_rate_label.background_color = None if setting_name == "effect": self._effect_label.color = 0x0 diff --git a/examples/camera/code.py b/examples/camera/code.py index 6e4e81e..a2e9ddc 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Unlicense import time - +import math import bitmaptools import displayio import gifio @@ -22,6 +22,7 @@ last_frame = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) onionskin = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) timelapse_remaining = None +timelapse_timestamp = None while True: if pycam.mode_text == "STOP" and pycam.stop_motion_frame != 0: @@ -41,10 +42,24 @@ pycam._timelapse_rate_label.text = pycam._timelapse_rate_label.text if timelapse_remaining is None: pycam._timelapsestatus_label.text = "STOP" - #pycam.display_message("Timelapse\nNot Running\nPress Select to set timing") else: - pycam.display_message("%d Seconds left", timelapse_remaining) + timelapse_remaining = timelapse_timestamp - time.time() + pycam._timelapsestatus_label.text = f"{timelapse_remaining}s / " pycam.display.refresh() + + if timelapse_remaining is not None and timelapse_remaining <= 0: + #pycam.tone(200, 0.1) # uncomment to add a beep when a photo is taken + try: + pycam.display_message("Snap!", color=0x0000FF) + pycam.capture_jpeg() + except TypeError as e: + pycam.display_message("Failed", color=0xFF0000) + time.sleep(0.5) + except RuntimeError as e: + pycam.display_message("Error\nNo SD Card", color=0xFF0000) + time.sleep(0.5) + pycam.live_preview_mode() + timelapse_timestamp = time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 else: pycam.blit(pycam.continuous_capture()) # print("\t\t", capture_time, blit_time) @@ -193,3 +208,9 @@ print("SEL") if pycam.ok.fell: print("OK") + if pycam.mode_text == "LAPS": + if timelapse_remaining is None: # stopped + timelapse_remaining = pycam.timelapse_rates[pycam.timelapse_rate] + timelapse_timestamp = time.time() + timelapse_remaining + 1 + else: # is running, turn off + timelapse_remaining = None From fa81fa01a812c839b95dd4bb6b75ef73c962856e Mon Sep 17 00:00:00 2001 From: ladyada Date: Sat, 27 Jan 2024 15:07:20 -0500 Subject: [PATCH 20/70] add low power modes: med power turns off the camera preview, low power also dims the screen --- adafruit_pycamera/__init__.py | 26 ++++++++++++++++++++++++-- examples/camera/code.py | 19 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 97f9de7..9ffe4f2 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -72,6 +72,7 @@ _NVM_EFFECT = const(2) _NVM_MODE = const(3) _NVM_TIMELAPSE_RATE = const(4) +_NVM_TIMELAPSE_SUBMODE = const(5) class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -186,6 +187,12 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub 60 * 30, 60 * 60 ) + + timelapse_submodes = ( + "HiPwr", + "MedPwr", + "LowPwr" + ) modes = ("JPEG", "GIF", "GBOY", "STOP", "LAPS") @@ -288,16 +295,18 @@ def make_camera_ui(self): self._botbar.append(self._mode_label) self._timelapsebar = displayio.Group(x=0, y=180) + self._timelapse_submode_label = label.Label( + terminalio.FONT, text="SubM", color=0xFFFFFF,x=160, y=10, scale=2 + ) self._timelapse_rate_label = label.Label( terminalio.FONT, text="Time", color=0xFFFFFF,x=90, y=10, scale=2 ) - self._timelapse_rate_label.background_color = None self._timelapsestatus_label = label.Label( terminalio.FONT, text="Status", color=0xFFFFFF, x=0, y=10, scale=2 ) - self._timelapsestatus_label.background_color = None self._timelapsebar.append(self._timelapse_rate_label) self._timelapsebar.append(self._timelapsestatus_label) + self._timelapsebar.append(self._timelapse_submode_label) self.splash.append(self._topbar) self.splash.append(self._botbar) @@ -371,6 +380,7 @@ def init_camera(self, init_autofocus=True) -> None: self.resolution = microcontroller.nvm[_NVM_RESOLUTION] self.mode = microcontroller.nvm[_NVM_MODE] self.timelapse_rate = microcontroller.nvm[_NVM_TIMELAPSE_RATE] + self.timelapse_submode = microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE] if init_autofocus: self.autofocus_init() @@ -599,6 +609,18 @@ def timelapse_rate(self, setting): self.display.refresh() + @property + def timelapse_submode(self): + """Get or set the power mode for timelapsing""" + return self._timelapse_submode + + @timelapse_submode.setter + def timelapse_submode(self, setting): + setting = (setting + len(self.timelapse_submodes)) % len(self.timelapse_submodes) + self._timelapse_submode = setting + self._timelapse_submode_label.text = self.timelapse_submodes[self._timelapse_submode] + microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE] = setting + def init_display(self): """Initialize the TFT display""" # construct displayio by hand diff --git a/examples/camera/code.py b/examples/camera/code.py index a2e9ddc..53db67d 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -38,16 +38,26 @@ ) pycam.blit(last_frame) elif pycam.mode_text == "LAPS": - pycam.blit(pycam.continuous_capture()) - pycam._timelapse_rate_label.text = pycam._timelapse_rate_label.text if timelapse_remaining is None: pycam._timelapsestatus_label.text = "STOP" else: timelapse_remaining = timelapse_timestamp - time.time() pycam._timelapsestatus_label.text = f"{timelapse_remaining}s / " + pycam._timelapse_rate_label.text = pycam._timelapse_rate_label.text + pycam._timelapse_submode_label.text = pycam._timelapse_submode_label.text + + # only in high power mode do we continuously preview + if (timelapse_remaining is None) or (pycam._timelapse_submode_label.text == "HiPwr"): + pycam.blit(pycam.continuous_capture()) + if pycam._timelapse_submode_label.text == "LowPwr" and (timelapse_remaining is not None): + pycam.display.brightness = 0.05 + else: + pycam.display.brightness = 1 pycam.display.refresh() if timelapse_remaining is not None and timelapse_remaining <= 0: + # no matter what, show what was just on the camera + pycam.blit(pycam.continuous_capture()) #pycam.tone(200, 0.1) # uncomment to add a beep when a photo is taken try: pycam.display_message("Snap!", color=0x0000FF) @@ -59,6 +69,8 @@ pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() + pycam.display.refresh() + pycam.blit(pycam.continuous_capture()) timelapse_timestamp = time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 else: pycam.blit(pycam.continuous_capture()) @@ -206,6 +218,9 @@ # pycam.set_resolution(pycam.resolutions[new_res]) if pycam.select.fell: print("SEL") + if pycam.mode_text == "LAPS": + pycam.timelapse_submode += 1 + pycam.display.refresh() if pycam.ok.fell: print("OK") if pycam.mode_text == "LAPS": From 60e15dd80429563db4e97f6b067e0d5abae26d6c Mon Sep 17 00:00:00 2001 From: ladyada Date: Sun, 28 Jan 2024 17:37:14 -0500 Subject: [PATCH 21/70] allow saving and setting exposure/shutter, gain and balance so that it can be frozen during timelapse --- adafruit_pycamera/__init__.py | 49 +++++++++++++++++++++++++++++++++++ examples/camera/code.py | 13 ++++++++++ 2 files changed, 62 insertions(+) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 9ffe4f2..8b15924 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -882,6 +882,55 @@ def led_color(self, new_color): else: self.pixels[:] = colors + def get_camera_autosettings(self): + exposure = (self.read_camera_register(0x3500) << 12) + \ + (self.read_camera_register(0x3501) << 4) + \ + (self.read_camera_register(0x3502) >> 4); + wb = [self.read_camera_register(x) for x in \ + (0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405)] + + settings = { + 'gain': self.read_camera_register(0x350b), + 'exposure': exposure, + 'wb': wb + } + return settings + + def set_camera_wb(self, wb_register_values=None): + if wb_register_values is None: + # just set to auto balance + self.camera.whitebal = True + return + + if len(wb_register_values) != 6: + raise RuntimeError("Please pass in 0x3400~0x3405 inclusive!") + + self.write_camera_register(0x3212, 0x03) + self.write_camera_register(0x3406, 0x01) + for i, reg_val in enumerate(wb_register_values): + self.write_camera_register(0x3400+i, reg_val) + self.write_camera_register(0x3212, 0x13) + self.write_camera_register(0x3212, 0xa3) + + def set_camera_exposure(self, new_exposure=None): + if new_exposure is None: + # just set auto expose + self.camera.exposure_ctrl = True + return + exposure_ctrl = False + + self.write_camera_register(0x3500, (new_exposure >> 12) & 0xFF) + self.write_camera_register(0x3501, (new_exposure >> 4) & 0xFF) + self.write_camera_register(0x3502, (new_exposure << 4) & 0xFF) + + def set_camera_gain(self, new_gain=None): + if new_gain is None: + # just set auto expose + self.camera.gain_ctrl = True + return + + self.camera.gain_ctrl = False + self.write_camera_register(0x350b, new_gain) class PyCamera(PyCameraBase): """Wrapper class for the PyCamera hardware""" diff --git a/examples/camera/code.py b/examples/camera/code.py index 53db67d..02a2a6b 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -225,7 +225,20 @@ print("OK") if pycam.mode_text == "LAPS": if timelapse_remaining is None: # stopped + print("Starting timelapse") timelapse_remaining = pycam.timelapse_rates[pycam.timelapse_rate] timelapse_timestamp = time.time() + timelapse_remaining + 1 + # dont let the camera take over auto-settings + saved_settings = pycam.get_camera_autosettings() + #print(f"Current exposure {saved_settings['exposure']}, gain {saved_settings['gain']}, wb {saved_settings['wb']}") + pycam.set_camera_exposure(saved_settings['exposure']) + pycam.set_camera_gain(saved_settings['gain']) + pycam.set_camera_wb(saved_settings['wb']) else: # is running, turn off + print("Stopping timelapse") + timelapse_remaining = None + pycam.camera.exposure_ctrl = True + pycam.set_camera_gain(None) # go back to autogain + pycam.set_camera_wb(None) # go back to autobalance + pycam.set_camera_exposure(None) # go back to auto shutter From 008d5532ceeee97e4fccdc394a10924eb144d9fd Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 29 Jan 2024 14:07:58 -0600 Subject: [PATCH 22/70] Bring code up to standards --- adafruit_pycamera/__init__.py | 116 +++++++++++++++++++++------------- examples/camera/code.py | 53 ++++++++++------ 2 files changed, 107 insertions(+), 62 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 8b15924..591ca59 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -185,15 +185,11 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub 60 * 10, 60 * 15, 60 * 30, - 60 * 60 + 60 * 60, ) - timelapse_submodes = ( - "HiPwr", - "MedPwr", - "LowPwr" - ) - + timelapse_submodes = ("HiPwr", "MedPwr", "LowPwr") + modes = ("JPEG", "GIF", "GBOY", "STOP", "LAPS") _INIT_SEQUENCE = ( @@ -213,6 +209,11 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._timestamp = time.monotonic() self._bigbuf = None self._botbar = None + self._timelapsebar = None + self.timelapse_rate_label = None + self._timelapsestatus = None + self.timelapsestatus_label = None + self.timelapse_submode_label = None self._camera_device = None self._display_bus = None self._effect_label = None @@ -295,19 +296,19 @@ def make_camera_ui(self): self._botbar.append(self._mode_label) self._timelapsebar = displayio.Group(x=0, y=180) - self._timelapse_submode_label = label.Label( - terminalio.FONT, text="SubM", color=0xFFFFFF,x=160, y=10, scale=2 + self.timelapse_submode_label = label.Label( + terminalio.FONT, text="SubM", color=0xFFFFFF, x=160, y=10, scale=2 ) - self._timelapse_rate_label = label.Label( - terminalio.FONT, text="Time", color=0xFFFFFF,x=90, y=10, scale=2 + self.timelapse_rate_label = label.Label( + terminalio.FONT, text="Time", color=0xFFFFFF, x=90, y=10, scale=2 ) - self._timelapsestatus_label = label.Label( + self.timelapsestatus_label = label.Label( terminalio.FONT, text="Status", color=0xFFFFFF, x=0, y=10, scale=2 ) - self._timelapsebar.append(self._timelapse_rate_label) - self._timelapsebar.append(self._timelapsestatus_label) - self._timelapsebar.append(self._timelapse_submode_label) - + self._timelapsebar.append(self.timelapse_rate_label) + self._timelapsebar.append(self.timelapsestatus_label) + self._timelapsebar.append(self.timelapse_submode_label) + self.splash.append(self._topbar) self.splash.append(self._botbar) self.splash.append(self._timelapsebar) @@ -504,9 +505,9 @@ def select_setting(self, setting_name): self._res_label.text = self.resolutions[self._resolution] self._mode_label.color = 0xFFFFFF self._mode_label.background_color = 0x0 - self._timelapse_rate_label.color = 0xFFFFFF - self._timelapse_rate_label.background_color = None - + self.timelapse_rate_label.color = 0xFFFFFF + self.timelapse_rate_label.background_color = None + if setting_name == "effect": self._effect_label.color = 0x0 self._effect_label.background_color = 0xFFFFFF @@ -529,8 +530,8 @@ def select_setting(self, setting_name): self._res_label.color = 0x0 self._res_label.background_color = 0xFFFFFF elif setting_name == "timelapse_rate": - self._timelapse_rate_label.color = 0x0 - self._timelapse_rate_label.background_color = 0xFFFFFF + self.timelapse_rate_label.color = 0x0 + self.timelapse_rate_label.background_color = 0xFFFFFF self.display.refresh() @property @@ -591,7 +592,6 @@ def resolution(self, res): self._res_label.text = self.resolutions[res] self.display.refresh() - @property def timelapse_rate(self): """Get or set the amount of time between timelapse shots""" @@ -602,13 +602,14 @@ def timelapse_rate(self, setting): setting = (setting + len(self.timelapse_rates)) % len(self.timelapse_rates) self._timelapse_rate = setting if self.timelapse_rates[setting] < 60: - self._timelapse_rate_label.text = "%d S" % self.timelapse_rates[setting] + self.timelapse_rate_label.text = "%d S" % self.timelapse_rates[setting] else: - self._timelapse_rate_label.text = "%d M" % (self.timelapse_rates[setting] / 60) + self.timelapse_rate_label.text = "%d M" % ( + self.timelapse_rates[setting] / 60 + ) microcontroller.nvm[_NVM_TIMELAPSE_RATE] = setting self.display.refresh() - @property def timelapse_submode(self): """Get or set the power mode for timelapsing""" @@ -616,9 +617,13 @@ def timelapse_submode(self): @timelapse_submode.setter def timelapse_submode(self, setting): - setting = (setting + len(self.timelapse_submodes)) % len(self.timelapse_submodes) + setting = (setting + len(self.timelapse_submodes)) % len( + self.timelapse_submodes + ) self._timelapse_submode = setting - self._timelapse_submode_label.text = self.timelapse_submodes[self._timelapse_submode] + self.timelapse_submode_label.text = self.timelapse_submodes[ + self._timelapse_submode + ] microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE] = setting def init_display(self): @@ -883,54 +888,79 @@ def led_color(self, new_color): self.pixels[:] = colors def get_camera_autosettings(self): - exposure = (self.read_camera_register(0x3500) << 12) + \ - (self.read_camera_register(0x3501) << 4) + \ - (self.read_camera_register(0x3502) >> 4); - wb = [self.read_camera_register(x) for x in \ - (0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405)] - + """Collect all the settings related to exposure and white balance""" + exposure = ( + (self.read_camera_register(0x3500) << 12) + + (self.read_camera_register(0x3501) << 4) + + (self.read_camera_register(0x3502) >> 4) + ) + white_balance = [ + self.read_camera_register(x) + for x in (0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405) + ] + settings = { - 'gain': self.read_camera_register(0x350b), - 'exposure': exposure, - 'wb': wb - } + "gain": self.read_camera_register(0x350B), + "exposure": exposure, + "wb": white_balance, + } return settings def set_camera_wb(self, wb_register_values=None): + """Set the camera white balance. + + The argument of `None` selects auto white balance, while + a list of 6 numbers sets a specific white balance. + + The numbers can come from the datasheet or from + ``get_camera_autosettings()["wb"]``.""" if wb_register_values is None: # just set to auto balance self.camera.whitebal = True return - + if len(wb_register_values) != 6: raise RuntimeError("Please pass in 0x3400~0x3405 inclusive!") self.write_camera_register(0x3212, 0x03) self.write_camera_register(0x3406, 0x01) for i, reg_val in enumerate(wb_register_values): - self.write_camera_register(0x3400+i, reg_val) + self.write_camera_register(0x3400 + i, reg_val) self.write_camera_register(0x3212, 0x13) - self.write_camera_register(0x3212, 0xa3) + self.write_camera_register(0x3212, 0xA3) def set_camera_exposure(self, new_exposure=None): + """Set the camera's exposure values + + The argument of `None` selects auto exposure. + + Otherwise, the new exposure data should come from + ``get_camera_autosettings()["exposure"].""" if new_exposure is None: # just set auto expose self.camera.exposure_ctrl = True return - exposure_ctrl = False + self.camera.exposure_ctrl = False self.write_camera_register(0x3500, (new_exposure >> 12) & 0xFF) self.write_camera_register(0x3501, (new_exposure >> 4) & 0xFF) self.write_camera_register(0x3502, (new_exposure << 4) & 0xFF) - + def set_camera_gain(self, new_gain=None): + """Set the camera's exposure values + + The argument of `None` selects auto gain control. + + Otherwise, the new exposure data should come from + ``get_camera_autosettings()["gain"].""" if new_gain is None: # just set auto expose self.camera.gain_ctrl = True return self.camera.gain_ctrl = False - self.write_camera_register(0x350b, new_gain) + self.write_camera_register(0x350B, new_gain) + class PyCamera(PyCameraBase): """Wrapper class for the PyCamera hardware""" diff --git a/examples/camera/code.py b/examples/camera/code.py index 02a2a6b..4f8c0a5 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Unlicense import time -import math import bitmaptools import displayio import gifio @@ -14,7 +13,15 @@ pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() -settings = (None, "resolution", "effect", "mode", "led_level", "led_color", "timelapse_rate") +settings = ( + None, + "resolution", + "effect", + "mode", + "led_level", + "led_color", + "timelapse_rate", +) curr_setting = 0 print("Starting!") @@ -39,17 +46,23 @@ pycam.blit(last_frame) elif pycam.mode_text == "LAPS": if timelapse_remaining is None: - pycam._timelapsestatus_label.text = "STOP" + pycam.timelapsestatus_label.text = "STOP" else: timelapse_remaining = timelapse_timestamp - time.time() - pycam._timelapsestatus_label.text = f"{timelapse_remaining}s / " - pycam._timelapse_rate_label.text = pycam._timelapse_rate_label.text - pycam._timelapse_submode_label.text = pycam._timelapse_submode_label.text + pycam.timelapsestatus_label.text = f"{timelapse_remaining}s / " + # Manually updating the label text a second time ensures that the label + # is re-painted over the blitted preview. + pycam.timelapse_rate_label.text = pycam.timelapse_rate_label.text + pycam.timelapse_submode_label.text = pycam.timelapse_submode_label.text # only in high power mode do we continuously preview - if (timelapse_remaining is None) or (pycam._timelapse_submode_label.text == "HiPwr"): + if (timelapse_remaining is None) or ( + pycam.timelapse_submode_label.text == "HiPwr" + ): pycam.blit(pycam.continuous_capture()) - if pycam._timelapse_submode_label.text == "LowPwr" and (timelapse_remaining is not None): + if pycam.timelapse_submode_label.text == "LowPwr" and ( + timelapse_remaining is not None + ): pycam.display.brightness = 0.05 else: pycam.display.brightness = 1 @@ -58,7 +71,7 @@ if timelapse_remaining is not None and timelapse_remaining <= 0: # no matter what, show what was just on the camera pycam.blit(pycam.continuous_capture()) - #pycam.tone(200, 0.1) # uncomment to add a beep when a photo is taken + # pycam.tone(200, 0.1) # uncomment to add a beep when a photo is taken try: pycam.display_message("Snap!", color=0x0000FF) pycam.capture_jpeg() @@ -71,7 +84,9 @@ pycam.live_preview_mode() pycam.display.refresh() pycam.blit(pycam.continuous_capture()) - timelapse_timestamp = time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 + timelapse_timestamp = ( + time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 + ) else: pycam.blit(pycam.continuous_capture()) # print("\t\t", capture_time, blit_time) @@ -224,21 +239,21 @@ if pycam.ok.fell: print("OK") if pycam.mode_text == "LAPS": - if timelapse_remaining is None: # stopped + if timelapse_remaining is None: # stopped print("Starting timelapse") timelapse_remaining = pycam.timelapse_rates[pycam.timelapse_rate] timelapse_timestamp = time.time() + timelapse_remaining + 1 # dont let the camera take over auto-settings saved_settings = pycam.get_camera_autosettings() - #print(f"Current exposure {saved_settings['exposure']}, gain {saved_settings['gain']}, wb {saved_settings['wb']}") - pycam.set_camera_exposure(saved_settings['exposure']) - pycam.set_camera_gain(saved_settings['gain']) - pycam.set_camera_wb(saved_settings['wb']) - else: # is running, turn off + # print(f"Current exposure {saved_settings=}") + pycam.set_camera_exposure(saved_settings["exposure"]) + pycam.set_camera_gain(saved_settings["gain"]) + pycam.set_camera_wb(saved_settings["wb"]) + else: # is running, turn off print("Stopping timelapse") timelapse_remaining = None pycam.camera.exposure_ctrl = True - pycam.set_camera_gain(None) # go back to autogain - pycam.set_camera_wb(None) # go back to autobalance - pycam.set_camera_exposure(None) # go back to auto shutter + pycam.set_camera_gain(None) # go back to autogain + pycam.set_camera_wb(None) # go back to autobalance + pycam.set_camera_exposure(None) # go back to auto shutter From 5b02b8fca582fbb8fc10eca65471c6d5de6848c6 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 29 Jan 2024 14:19:27 -0600 Subject: [PATCH 23/70] fix doc build --- adafruit_pycamera/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 591ca59..718d082 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -935,7 +935,7 @@ def set_camera_exposure(self, new_exposure=None): The argument of `None` selects auto exposure. Otherwise, the new exposure data should come from - ``get_camera_autosettings()["exposure"].""" + ``get_camera_autosettings()["exposure"]``.""" if new_exposure is None: # just set auto expose self.camera.exposure_ctrl = True @@ -952,7 +952,7 @@ def set_camera_gain(self, new_gain=None): The argument of `None` selects auto gain control. Otherwise, the new exposure data should come from - ``get_camera_autosettings()["gain"].""" + ``get_camera_autosettings()["gain"]``.""" if new_gain is None: # just set auto expose self.camera.gain_ctrl = True From a486f77b3e35176ecc2bbf3f42def5da37387096 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 30 Jan 2024 11:10:13 -0600 Subject: [PATCH 24/70] Add common blend mode functions --- adafruit_pycamera/imageprocessing.py | 120 +++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index d7fa770..f8bdbd7 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -82,3 +82,123 @@ def emboss_greyscale(bitmap, mask=None): def ironbow(bitmap, mask=None): """Convert an image to false color using the 'ironbow palette'""" return bitmapfilter.false_color(bitmap, ironbow_palette, mask=mask) + + +# pylint: disable=invalid-name +def alphablend_func_factory(frac, nfrac=None): + """Create an alpha-blending function for a specific fractional value + + The resulting function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``. + """ + if nfrac is None: + nfrac = 1 - frac + + def inner(a, b): + return frac * a + nfrac * b + + return inner + + +def screen_func(a, b): + """The 'screen' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return 1 - (1 - a) * (1 - b) + + +def overlay_func(a, b): + """The 'overlay' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return 2 * a * b if a < 0.5 else 1 - 2 * (1 - a) * (1 - b) + + +def hard_light_func(a, b): + """The 'hard light' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return 2 * a * b if b < 0.5 else 1 - 2 * (1 - a) * (1 - b) + + +# illusions.hu formula version +def soft_light_func(a, b): + """The 'soft light' blend mode. + + There are various soft light blend functions. The "illusions.hu" variant of + soft light is used. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a ** (2 ** (2 * 0.5 - b)) + + +def color_dodge_func(a, b): + """The 'color dodge' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a / (1 - b) if b != 1 else 1 + + +def linear_dodge_func(a, b): + """The 'linear dodge' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a + b + + +def divide_func(a, b): + """The 'divide' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a / b if b else 1 + + +def multiply_func(a, b): + """The 'multiply' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a * b + + +def subtract_func(a, b): + """The 'subtract' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a - b + + +def color_burn_func(a, b): + """The 'color burn' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a * (1 - b) + + +def linear_burn_func(a, b): + """The 'linear burn' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a + b - 1 + + +darken_only_func = min +"""The 'darken only' blend mode. + +This function can be used with ``bitmapfilter.blend`` and +``bitmapfilter.blend_precompute``.""" +lighten_only_func = max +"""The 'screen' blend mode. + +This function can be used with ``bitmapfilter.blend`` and +``bitmapfilter.blend_precompute``.""" From 8741b57e26e8d70dfaae675925c2601b2e45f0e9 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 30 Jan 2024 12:06:22 -0600 Subject: [PATCH 25/70] Add blend examples this includes most of the blend functions listed on the related wikipedia article, though only a few are tested. --- adafruit_pycamera/imageprocessing.py | 2 +- examples/filter/code.py | 94 ++++++++++++++++++ examples/filter/testpattern_208x208.jpg | Bin 0 -> 6144 bytes .../filter/testpattern_208x208.jpg.license | 3 + 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 examples/filter/testpattern_208x208.jpg create mode 100644 examples/filter/testpattern_208x208.jpg.license diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index f8bdbd7..98932aa 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -85,7 +85,7 @@ def ironbow(bitmap, mask=None): # pylint: disable=invalid-name -def alphablend_func_factory(frac, nfrac=None): +def alphablend_maker(frac, nfrac=None): """Create an alpha-blending function for a specific fractional value The resulting function can be used with ``bitmapfilter.blend`` and diff --git a/examples/filter/code.py b/examples/filter/code.py index 94d90f3..8d4598c 100644 --- a/examples/filter/code.py +++ b/examples/filter/code.py @@ -21,7 +21,95 @@ from adafruit_pycamera import imageprocessing from adafruit_pycamera import PyCameraBase + +blend_50_50 = bitmapfilter.blend_precompute(imageprocessing.alphablend_maker(0.5)) +screen = bitmapfilter.blend_precompute(imageprocessing.screen_func) +overlay = bitmapfilter.blend_precompute(imageprocessing.overlay_func) +hard_light = bitmapfilter.blend_precompute(imageprocessing.hard_light_func) +soft_light = bitmapfilter.blend_precompute(imageprocessing.soft_light_func) +color_dodge = bitmapfilter.blend_precompute(imageprocessing.color_dodge_func) +# linear_dodge = bitmapfilter.blend_precompute(imageprocessing.linear_dodge_func) +# divide = bitmapfilter.blend_precompute(imageprocessing.divide_func) +multiply = bitmapfilter.blend_precompute(imageprocessing.multiply_func) +# subtract = bitmapfilter.blend_precompute(imageprocessing.subtract_func) +# color_burn = bitmapfilter.blend_precompute(imageprocessing.color_burn_func) +# linear_burn = bitmapfilter.blend_precompute(imageprocessing.linear_burn_func) +# darken_only = bitmapfilter.blend_precompute(min) +# lighten_only = bitmapfilter.blend_precompute(max) + + +def blender(f): + def inner(b): + return bitmapfilter.blend(b, b, testpattern, f) + + return inner + + +def reverse_blender(f): + def inner(b): + return bitmapfilter.blend(b, testpattern, b, f) + + return inner + + +inverse_greyscale_weights = bitmapfilter.ChannelMixer( + 1 - 0.299, + 1 - 0.587, + 1 - 0.114, + 1 - 0.299, + 1 - 0.587, + 1 - 0.114, + 1 - 0.299, + 1 - 0.587, + 1 - 0.114, +) + +blur_more = [ + 4, + 15, + 24, + 15, + 4, + 15, + 61, + 97, + 61, + 15, + 24, + 97, + 154, + 97, + 24, + 15, + 61, + 97, + 61, + 15, + 4, + 15, + 24, + 15, + 4, +] + + +# "Sketch" filter based on +# https://www.freecodecamp.org/news/sketchify-turn-any-image-into-a-pencil-sketch-with-10-lines-of-code-cf67fa4f68ce/ +def sketch(b): + bitmapfilter.mix(b, inverse_greyscale_weights) + memoryview(auxbuffer)[:] = memoryview(b) + bitmapfilter.morph(auxbuffer, blur_more) + bitmapfilter.blend(b, auxbuffer, b, color_dodge) + bitmapfilter.mix(b, inverse_greyscale_weights) # get rid of magenta halos + return b + + effects = [ + ("sketch", sketch), + ("50/50", blender(blend_50_50)), + ("multiply", blender(multiply)), + ("soft light", blender(soft_light)), + ("hard_light", blender(hard_light)), ("blue cast", imageprocessing.blue_cast), ("blur", imageprocessing.blur), ("bright", lambda b: bitmapfilter.mix(b, bitmapfilter.ChannelScale(2.0, 2.0, 2.0))), @@ -65,6 +153,9 @@ def cycle(seq): pycam = PyCameraBase() pycam.init_display() +testpattern = displayio.Bitmap(208, 208, 65535) +auxbuffer = displayio.Bitmap(208, 208, 65535) + def main(): filename = "/cornell_box_208x208.jpg" @@ -74,6 +165,9 @@ def main(): decoder.open(filename) decoder.decode(bitmap0) + decoder.open("/testpattern_208x208.jpg") + decoder.decode(testpattern) + label = Label(font=FONT, x=0, y=8) pycam.display.root_group = label pycam.display.refresh() diff --git a/examples/filter/testpattern_208x208.jpg b/examples/filter/testpattern_208x208.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94b01de2da807a7e3faf36b1d8410f1fdd52040f GIT binary patch literal 6144 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 literal 0 HcmV?d00001 diff --git a/examples/filter/testpattern_208x208.jpg.license b/examples/filter/testpattern_208x208.jpg.license new file mode 100644 index 0000000..746796b --- /dev/null +++ b/examples/filter/testpattern_208x208.jpg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries + +SPDX-License-Identifier: Unlicense From dd3a66504649813975e72dd20f8f3aa6cac172fc Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 30 Jan 2024 13:56:33 -0600 Subject: [PATCH 26/70] Add a boot.py to auto-create /sd at boot time --- examples/camera/boot.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/camera/boot.py diff --git a/examples/camera/boot.py b/examples/camera/boot.py new file mode 100644 index 0000000..83662bc --- /dev/null +++ b/examples/camera/boot.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +"""Automatically create the /sd mount point at boot time""" + +import os +import storage + +storage.remount("/", readonly=False) + +try: + os.mkdir("/sd") +except OSError: + pass # It's probably 'file exists', OK to ignore + +storage.remount("/", readonly=True) From 644890e0fe95c43648735dbdab4776be2a411eb5 Mon Sep 17 00:00:00 2001 From: ladyada Date: Sat, 3 Feb 2024 14:56:45 -0500 Subject: [PATCH 27/70] manual focus control! thanks https://github.com/xupsh/Vision-Zynq/blob/master/ov5640_init/OV5640.c#L249 :) --- adafruit_pycamera/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 718d082..3368e23 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -48,6 +48,8 @@ _OV5640_CMD_TRIGGER_AUTOFOCUS = const(0x03) _OV5640_CMD_AUTO_AUTOFOCUS = const(0x04) _OV5640_CMD_RELEASE_FOCUS = const(0x08) +_OV5640_CMD_AF_SET_VCM_STEP = const(0x1a) +_OV5640_CMD_AF_GET_VCM_STEP = const(0x1b) _OV5640_CMD_MAIN = const(0x3022) _OV5640_CMD_ACK = const(0x3023) @@ -493,6 +495,22 @@ def autofocus(self) -> list[int]: print(f"zones focused: {zone_focus}") return zone_focus + @property + def autofocus_vcm_step(self): + """Get the voice coil motor step location""" + if not self._send_autofocus_command(_OV5640_CMD_AF_GET_VCM_STEP, "get vcm step"): + return None + return self.read_camera_register(_OV5640_CMD_PARA4) + + @autofocus_vcm_step.setter + def autofocus_vcm_step(self, step): + """Get the voice coil motor step location, from 0 to 255""" + if not (0 <= step <= 255): + raise RuntimeError("VCM step must be 0 to 255") + self.write_camera_register(_OV5640_CMD_PARA3, 0x00) + self.write_camera_register(_OV5640_CMD_PARA4, step) + self._send_autofocus_command(_OV5640_CMD_AF_SET_VCM_STEP, "set vcm step") + def select_setting(self, setting_name): """For the point & shoot camera mode, control what setting is being set""" self._effect_label.color = 0xFFFFFF From 71d1d1c92d47a83a0b5e9da7b2a5757a782d0172 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 5 Feb 2024 21:23:40 -0800 Subject: [PATCH 28/70] Fix some SPDX declarations --- LICENSE | 3 ++- README.rst.license | 2 +- adafruit_pycamera/__init__.py | 1 + docs/api.rst.license | 3 ++- docs/examples.rst.license | 3 ++- docs/index.rst.license | 3 ++- examples/camera/boot.py | 1 + examples/camera/code.py | 1 + examples/qrio/code.py | 1 - pyproject.toml | 2 +- requirements.txt | 2 +- 11 files changed, 14 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index 0a7b93e..00dd992 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) -Copyright (c) 2023 Jeff Epler for Adafruit Industries for Adafruit Industries +Copyright (c) 2023 Limor Fried for Adafruit Industries +Copyright (c) 2023 Jeff Epler for Adafruit Industries Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst.license b/README.rst.license index dd81d34..57bef7c 100644 --- a/README.rst.license +++ b/README.rst.license @@ -1,5 +1,5 @@ SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries -SPDX-FileCopyrightText: Copyright (c) 2023 Jeff Epler for Adafruit Industries for Adafruit Industries +SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries SPDX-License-Identifier: MIT diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 718d082..423ca6c 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +# SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: MIT """Library for the Adafruit PyCamera with OV5640 autofocus module""" diff --git a/docs/api.rst.license b/docs/api.rst.license index 3b17a1d..57bef7c 100644 --- a/docs/api.rst.license +++ b/docs/api.rst.license @@ -1,4 +1,5 @@ SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -SPDX-FileCopyrightText: Copyright (c) 2023 Jeff Epler for Adafruit Industries for Adafruit Industries +SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries SPDX-License-Identifier: MIT diff --git a/docs/examples.rst.license b/docs/examples.rst.license index 3b17a1d..57bef7c 100644 --- a/docs/examples.rst.license +++ b/docs/examples.rst.license @@ -1,4 +1,5 @@ SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -SPDX-FileCopyrightText: Copyright (c) 2023 Jeff Epler for Adafruit Industries for Adafruit Industries +SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries SPDX-License-Identifier: MIT diff --git a/docs/index.rst.license b/docs/index.rst.license index 3b17a1d..57bef7c 100644 --- a/docs/index.rst.license +++ b/docs/index.rst.license @@ -1,4 +1,5 @@ SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -SPDX-FileCopyrightText: Copyright (c) 2023 Jeff Epler for Adafruit Industries for Adafruit Industries +SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries SPDX-License-Identifier: MIT diff --git a/examples/camera/boot.py b/examples/camera/boot.py index 83662bc..84e2250 100644 --- a/examples/camera/boot.py +++ b/examples/camera/boot.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +# SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: Unlicense diff --git a/examples/camera/code.py b/examples/camera/code.py index 4f8c0a5..553c035 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +# SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: Unlicense diff --git a/examples/qrio/code.py b/examples/qrio/code.py index 0f5720c..bc04fb3 100644 --- a/examples/qrio/code.py +++ b/examples/qrio/code.py @@ -1,5 +1,4 @@ # SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries -# SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries # # SPDX-License-Identifier: Unlicense diff --git a/pyproject.toml b/pyproject.toml index 5fbf4d7..8e00f88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney, written for Adafruit Industries -# SPDX-FileCopyrightText: Copyright (c) 2023 Jeff Epler for Adafruit Industries for Adafruit Industries +# SPDX-FileCopyrightText: Jeff Epler for Adafruit Industries # # SPDX-License-Identifier: MIT diff --git a/requirements.txt b/requirements.txt index 67f979e..f562ea4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -# SPDX-FileCopyrightText: Copyright (c) 2023 Jeff Epler for Adafruit Industries for Adafruit Industries +# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries # # SPDX-License-Identifier: MIT From 2748e0c4ca728892e694cb909a90d34238ca6f59 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 8 Feb 2024 09:15:38 -0800 Subject: [PATCH 29/70] Fix pylint & black nits (yeah I hate that 'not' one too but pylint demands it) --- adafruit_pycamera/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 3368e23..44f4a7e 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -48,8 +48,8 @@ _OV5640_CMD_TRIGGER_AUTOFOCUS = const(0x03) _OV5640_CMD_AUTO_AUTOFOCUS = const(0x04) _OV5640_CMD_RELEASE_FOCUS = const(0x08) -_OV5640_CMD_AF_SET_VCM_STEP = const(0x1a) -_OV5640_CMD_AF_GET_VCM_STEP = const(0x1b) +_OV5640_CMD_AF_SET_VCM_STEP = const(0x1A) +_OV5640_CMD_AF_GET_VCM_STEP = const(0x1B) _OV5640_CMD_MAIN = const(0x3022) _OV5640_CMD_ACK = const(0x3023) @@ -498,14 +498,16 @@ def autofocus(self) -> list[int]: @property def autofocus_vcm_step(self): """Get the voice coil motor step location""" - if not self._send_autofocus_command(_OV5640_CMD_AF_GET_VCM_STEP, "get vcm step"): + if not self._send_autofocus_command( + _OV5640_CMD_AF_GET_VCM_STEP, "get vcm step" + ): return None return self.read_camera_register(_OV5640_CMD_PARA4) @autofocus_vcm_step.setter def autofocus_vcm_step(self, step): """Get the voice coil motor step location, from 0 to 255""" - if not (0 <= step <= 255): + if not 0 <= step <= 255: raise RuntimeError("VCM step must be 0 to 255") self.write_camera_register(_OV5640_CMD_PARA3, 0x00) self.write_camera_register(_OV5640_CMD_PARA4, step) From 00691dd49504a912a7b7a2218a7786d3456a9c5a Mon Sep 17 00:00:00 2001 From: Anne Barela <1911920+TheKitty@users.noreply.github.com> Date: Mon, 19 Feb 2024 15:02:27 -0500 Subject: [PATCH 30/70] add basic_camera code For JP per Limor --- examples/basic_camera/code.py | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 examples/basic_camera/code.py diff --git a/examples/basic_camera/code.py b/examples/basic_camera/code.py new file mode 100644 index 0000000..cebda71 --- /dev/null +++ b/examples/basic_camera/code.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries +# +# SPDX-License-Identifier: MIT +''' simple point-and-shoot camera example. No bells! Zero whistles! ''' + +import time +import adafruit_pycamera # pylint: disable=import-error + +pycam = adafruit_pycamera.PyCamera() +pycam.mode = 0 # only mode 0 (JPEG) will work in this example + +# User settings - try changing these: +pycam.resolution = 8 # 0-12 preset resolutions: +# 0: 240x240, 1: 320x240, 2: 640x480, 3: 800x600, 4: 1024x768, +# 5: 1280x720, 6: 1280x1024, 7: 1600x1200, 8: 1920x1080, 9: 2048x1536, +# 10: 2560x1440, 11: 2560x1600, 12: 2560x1920 +pycam.led_level = 1 # 0-4 preset brightness levels +pycam.led_color = 0 # 0-7 preset colors: 0: white, 1: green, 2: yellow, 3: red, +# 4: pink, 5: blue, 6: teal, 7: rainbow +pycam.effect = 0 # 0-7 preset FX: 0: normal, 1: invert, 2: b&w, 3: red, +# 4: green, 5: blue, 6: sepia, 7: solarize + +print("Simple camera ready.") +pycam.tone(800, 0.1) +pycam.tone(1200, 0.05) + +while True: + pycam.blit(pycam.continuous_capture()) + pycam.keys_debounce() + + if pycam.shutter.short_count: + print("Shutter released") + pycam.tone(1200, 0.05) + pycam.tone(1600, 0.05) + try: + pycam.display_message("snap", color=0x00DD00) + pycam.capture_jpeg() + pycam.live_preview_mode() + except TypeError as exception: + pycam.display_message("Failed", color=0xFF0000) + time.sleep(0.5) + pycam.live_preview_mode() + except RuntimeError as exception: + pycam.display_message("Error\nNo SD Card", color=0xFF0000) + time.sleep(0.5) + + if pycam.card_detect.fell: + print("SD card removed") + pycam.unmount_sd_card() + pycam.display.refresh() + + if pycam.card_detect.rose: + print("SD card inserted") + pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) + for _ in range(3): + try: + print("Mounting card") + pycam.mount_sd_card() + print("Success!") + break + except OSError as exception: + print("Retrying!", exception) + time.sleep(0.5) + else: + pycam.display_message("SD Card\nFailed!", color=0xFF0000) + time.sleep(0.5) + pycam.display.refresh() From 34f32dc2453585086e24ee36796ea36d1082e317 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 10:18:32 -0600 Subject: [PATCH 31/70] overlay feature and example --- adafruit_pycamera/__init__.py | 103 +++++++++++++++++- examples/basic_camera/code.py | 6 +- examples/overlay/code.py | 74 +++++++++++++ examples/overlay/heart_frame_rgb888.bmp | Bin 0 -> 230538 bytes .../overlay/heart_frame_rgb888.bmp.license | 2 + examples/overlay/pencil_frame_rgb888.bmp | Bin 0 -> 307338 bytes .../overlay/pencil_frame_rgb888.bmp.license | 2 + 7 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 examples/overlay/code.py create mode 100644 examples/overlay/heart_frame_rgb888.bmp create mode 100644 examples/overlay/heart_frame_rgb888.bmp.license create mode 100644 examples/overlay/pencil_frame_rgb888.bmp create mode 100644 examples/overlay/pencil_frame_rgb888.bmp.license diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index e0fa35f..071b2df 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -3,7 +3,8 @@ # # SPDX-License-Identifier: MIT """Library for the Adafruit PyCamera with OV5640 autofocus module""" - +# pylint: disable=too-many-lines +import gc import os import struct import time @@ -32,6 +33,11 @@ from adafruit_display_text import label from digitalio import DigitalInOut, Pull from rainbowio import colorwheel +from displayio import Bitmap, ColorConverter, Colorspace +from jpegio import JpegDecoder +import ulab.numpy as np +from adafruit_bitmapsaver import save_pixels +import adafruit_imageload __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyCamera.git" @@ -147,7 +153,7 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub espcamera.FrameSize.QVGA, # 320x240 # espcamera.FrameSize.CIF, # 400x296 # espcamera.FrameSize.HVGA, # 480x320 - espcamera.FrameSize.VGA, # 640x480 + espcamera.FrameSize.VGA, # 640x480 espcamera.FrameSize.SVGA, # 800x600 espcamera.FrameSize.XGA, # 1024x768 espcamera.FrameSize.HD, # 1280x720 @@ -230,6 +236,12 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self.display = None self.pixels = None self.sdcard = None + self._last_saved_image_filename = None + self.decoder = None + self._overlay = None + self.overlay_transparency_color = None + self.overlay_bmp = None + self.combined_bmp = None self.splash = displayio.Group() # Reset display and I/O expander @@ -797,6 +809,7 @@ def open_next_image(self, extension="jpg"): os.stat(filename) except OSError: break + self._last_saved_image_filename = filename print("Writing to", filename) return open(filename, "wb") @@ -827,6 +840,78 @@ def capture_jpeg(self): else: print("# frame capture failed") + @property + def overlay(self) -> str: + """ + The overlay file to be used. A filepath string that points + to a .bmp file that has 24bit RGB888 Colorspace. + The overlay image will be shown in the camera preview, + and combined to create a modified version of the + final photo. + """ + return self._overlay + + @overlay.setter + def overlay(self, new_overlay_file: str) -> None: + if self.overlay_bmp is not None: + self.overlay_bmp.deinit() + self._overlay = new_overlay_file + cc888 = ColorConverter(input_colorspace=Colorspace.RGB888) + self.overlay_bmp, _ = adafruit_imageload.load(new_overlay_file, palette=cc888) + + arr = np.frombuffer(self.overlay_bmp, dtype=np.uint16) + arr.byteswap(inplace=True) + + del arr + + def _init_jpeg_decoder(self): + """ + Initialize the JpegDecoder if it hasn't been already. + Only needed if overlay is used. + """ + if self.decoder is None: + self.decoder = JpegDecoder() + + def blit_overlay_into_last_capture(self): + """ + Create a modified version of the last photo taken that pastes + the overlay image on top of the photo and saves the new version + in a separate but similarly named .bmp file on the SDCard. + """ + if self.overlay_bmp is None: + raise ValueError( + "Must set overlay before calling blit_overlay_into_last_capture" + ) + + self._init_jpeg_decoder() + + width, height = self.decoder.open(self._last_saved_image_filename) + photo_bitmap = Bitmap(width, height, 65535) + + self.decoder.decode(photo_bitmap, scale=0, x=0, y=0) + + bitmaptools.blit( + photo_bitmap, + self.overlay_bmp, + 0, + 0, + skip_source_index=self.overlay_transparency_color, + skip_dest_index=None, + ) + + cc565_swapped = ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED) + save_pixels( + self._last_saved_image_filename.replace(".jpg", "_modified.bmp"), + photo_bitmap, + cc565_swapped, + ) + + # RAM cleanup + photo_bitmap.deinit() + del photo_bitmap + del cc565_swapped + gc.collect() + def continuous_capture_start(self): """Switch the camera to continuous-capture mode""" pass # pylint: disable=unnecessary-pass @@ -872,6 +957,20 @@ def blit(self, bitmap, x_offset=0, y_offset=32): for status information. """ + if self.overlay_bmp is not None: + if self.combined_bmp is None: + self.combined_bmp = Bitmap(bitmap.width, bitmap.height, 65535) + + bitmaptools.blit(self.combined_bmp, bitmap, 0, 0) + + bitmaptools.rotozoom( + self.combined_bmp, + self.overlay_bmp, + scale=0.75, + skip_index=self.overlay_transparency_color, + ) + bitmap = self.combined_bmp + self._display_bus.send( 42, struct.pack(">hh", 80 + x_offset, 80 + x_offset + bitmap.width - 1) ) diff --git a/examples/basic_camera/code.py b/examples/basic_camera/code.py index cebda71..a58b4a3 100644 --- a/examples/basic_camera/code.py +++ b/examples/basic_camera/code.py @@ -1,16 +1,16 @@ # SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries # # SPDX-License-Identifier: MIT -''' simple point-and-shoot camera example. No bells! Zero whistles! ''' +""" simple point-and-shoot camera example. No bells! Zero whistles! """ import time -import adafruit_pycamera # pylint: disable=import-error +import adafruit_pycamera # pylint: disable=import-error pycam = adafruit_pycamera.PyCamera() pycam.mode = 0 # only mode 0 (JPEG) will work in this example # User settings - try changing these: -pycam.resolution = 8 # 0-12 preset resolutions: +pycam.resolution = 8 # 0-12 preset resolutions: # 0: 240x240, 1: 320x240, 2: 640x480, 3: 800x600, 4: 1024x768, # 5: 1280x720, 6: 1280x1024, 7: 1600x1200, 8: 1920x1080, 9: 2048x1536, # 10: 2560x1440, 11: 2560x1600, 12: 2560x1920 diff --git a/examples/overlay/code.py b/examples/overlay/code.py new file mode 100644 index 0000000..95f098f --- /dev/null +++ b/examples/overlay/code.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 202 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" simple point-and-shoot camera example, with an overlay frame image. """ + +import time +import traceback +import adafruit_pycamera # pylint: disable=import-error + +pycam = adafruit_pycamera.PyCamera() +pycam.mode = 0 # only mode 0 (JPEG) will work in this example + +# User settings - try changing these: +pycam.resolution = 1 # 0-12 preset resolutions: +# 0: 240x240, 1: 320x240, 2: 640x480 + +pycam.led_level = 1 # 0-4 preset brightness levels +pycam.led_color = 0 # 0-7 preset colors: 0: white, 1: green, 2: yellow, 3: red, +# 4: pink, 5: blue, 6: teal, 7: rainbow +pycam.effect = 0 # 0-7 preset FX: 0: normal, 1: invert, 2: b&w, 3: red, +# 4: green, 5: blue, 6: sepia, 7: solarize + +print("Overlay example camera ready.") +pycam.tone(800, 0.1) +pycam.tone(1200, 0.05) + +pycam.overlay = "/heart_frame_rgb888.bmp" +pycam.overlay_transparency_color = 0xE007 + +while True: + pycam.blit(pycam.continuous_capture()) + pycam.keys_debounce() + + if pycam.shutter.short_count: + print("Shutter released") + pycam.tone(1200, 0.05) + pycam.tone(1600, 0.05) + try: + pycam.display_message("snap", color=0x00DD00) + pycam.capture_jpeg() + pycam.display_message("overlay", color=0x00DD00) + pycam.blit_overlay_into_last_capture() + pycam.live_preview_mode() + except TypeError as exception: + traceback.print_exception(exception) + pycam.display_message("Failed", color=0xFF0000) + time.sleep(0.5) + pycam.live_preview_mode() + except RuntimeError as exception: + pycam.display_message("Error\nNo SD Card", color=0xFF0000) + time.sleep(0.5) + + if pycam.card_detect.fell: + print("SD card removed") + pycam.unmount_sd_card() + pycam.display.refresh() + + if pycam.card_detect.rose: + print("SD card inserted") + pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) + for _ in range(3): + try: + print("Mounting card") + pycam.mount_sd_card() + print("Success!") + break + except OSError as exception: + print("Retrying!", exception) + time.sleep(0.5) + else: + pycam.display_message("SD Card\nFailed!", color=0xFF0000) + time.sleep(0.5) + pycam.display.refresh() diff --git a/examples/overlay/heart_frame_rgb888.bmp b/examples/overlay/heart_frame_rgb888.bmp new file mode 100644 index 0000000000000000000000000000000000000000..eab3d0a6c726972ed1a3dcd981e348c92bcaa41f GIT binary patch literal 230538 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 C7>2F@ literal 0 HcmV?d00001 diff --git a/examples/overlay/heart_frame_rgb888.bmp.license b/examples/overlay/heart_frame_rgb888.bmp.license new file mode 100644 index 0000000..831eb5c --- /dev/null +++ b/examples/overlay/heart_frame_rgb888.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT diff --git a/examples/overlay/pencil_frame_rgb888.bmp b/examples/overlay/pencil_frame_rgb888.bmp new file mode 100644 index 0000000000000000000000000000000000000000..e92fc9a2fc1fe84ee9ffd3806a152cfe8a1274ae GIT binary patch literal 307338 zcmeI)S!|W%7RK@7IbIzTuT%_)p5PcFlpzu^8Vo3grYe&&V4)D^v1ODYP|DCUqhT;8 zLK%vLma)?=P&QbB(iRFWVGt-#D1}lelomps!+LQWJ>Hm{ul+90pSrUdii`ZR*0a~U zzpw4^K|<<3!|X@G!^fnDk9Qh{g;hO#ZWQ+Kuzx&!4tx0LFSH1A5Bue#9|{Y5^q>7w z9p3L2^V=W%?Wf27`!n_M|Huad2q5r(D$wZh$Mml!pHSl_&#LK*FQ`SU@CKEjKK}F{ z^XFIo*#7r)<7Zsj4pJX~d+nwFm_I-A$9k`McyrBKyH&|& z?`!mdIzs!Mq#Z6 z=hmvvo^mB@+@h?+&+07)sgFPX$Nc$~Kb_NR6qQq_Whd)YT=l@^;Hpz~N^7|QTC`@J5ZK?wgVoT z(A@J3wEUw!{`4R7=STk74rtdpmgZm3owP}R%D&p&b98stY*no8=km{Xz~H#oJ%Gx~evF>R+_`1^3byH_Vzg4+&WF=-+k=Pz`J9@l`;Gk&oYqu_|t#P zpI`Z78F(VTz01K13nF!E#{}KYnBuaqdgs?J`);I8QT66=y12B9EdOi=+&<`e&oYqu z_}l9s{m1x1|9$TZWnb0ip{m>%qx0WHy8KHY_OiD0_wFoA zef;fpkp5%-{Ky~6K)cuF`1Cj3dAMrR7*(cBc6*=u`NQnHwqcaJFXrr`2&E5g<95K5 zXz$L#)W@IxWB%;@5I^$AGSKdIu^h7V$o1sWkNzFMW#83x!*y*g|1p1l<&R~c-RqJ+skJ)~+kMa#>qoe~FO+?DPmEm?E}0YQ@^5qB zm$bfT6VEb``uNj-%%8m<;z#~i1|AsK%;lh+M{M7->?>c@*WC|j*>`0{4_#W;RYmdd z%knR|_j5}6;wjHEkox%B>mdEd{P~eTmVtJ^v)%J&=Ml@ivhTlg&!Oxq{q{rIdw~Vh zJ1S{#E3KFpr;Y2^c$R_G$DjUV{`|@x%fReWuep0(?0vb*%e%Y1&+dZ>Wnbz12<=@s zKq;FxX!*(&O3%vBwk?}H%RuVmZ}(i$f6SjB`C}QFHN36M!Jj9#bN9d4JutTSS@!K) zJV+NWUvl3+=0M&-9VI<%175AHaC=~WUak@o1B!~?uZWL!dzOLJ$DjUV z{_MJdANgY$XkYV@9NW?Dced|US5>&Zue9uf+XLW^F5COAUM|(){33nXYnQrr52{bp0mZ}?%CavmzDQ#y z7J8b0)W@IxWB&ZgAKL-5Gk5Cbi6ZR|r0ZaAP|*W26%iS9yPxIV%-JQH7I(zupMAd9 zAJoU6{$u|9${)+KJ=qxw<^){!wSO<554!DD?D)e)HxzYJ$meG@azj^AocO5|Cql}9`K9+0tg_000Iag zfB*srAbo(Ff`j6}+R!z@x_}Io0)= zh1AEN{$u|9${*VS+YL+9l)^gYUw@#?OSPI`T%#FZ9d&eS+j_}B>f=xU zF@JvLkL`eV-kn@f>-N8M)zX=Bzv@=z4BZdT(~Wh5T>h2Jcv}UN+tf=2QXhZ%kNLCn zr(gNgcmG{2JW;E>(`R%qFwJG(y_`hZqjK#)S^n7$cxXa%&oj{SkNWu2f6SjB`C~hv zUF%qye?fQBCjBY6Ih_zftkAULmzhrwjFR!Y)elw(DIM^_}kfo{$u_cI)5gn?sL~Vt}N-Q+RUkc z$v*qNn)J!Kw6eE51OGDiRRu;i^DG0YkH1}OqyLycKk~=Uz;>P2da!Is2bXnsvS+C- zbGFMv%f8#&<8*WDG?jn%u{#6rjtN)B@K-#`Kd-^XEtYSOylyz2$Q7 z{JhS(oihISy)Tq~Rhx&Za$}6ne-r8QFMZg{+S1>e=Gy-UYFz3 z-*o5Us!d~5nKIe!eeUNEv+vr5QSQE&vx_2>KD3S70aK#AI}1}EfBKL4v-d;%$REo< zyVu2X$j&3zlSe=Lcl?%pSJw^KwKe^9^~Y$PoZCe^28FvF@TaJzo}GoMk3ap#{P~qX zmVtJ!Oa7$R?mTSwL07CF;r6~z_Sro#c1^fsPNd7f&3#|e`kqZZ%RuVmPyaE0_I`*T z`C}P)U|chogLWRVeb2J5d{tj}KcHpbl@&d7X<1hl#lJ7hzvSM}Dd~%+Jj+1p<8QBn z^dIx*NB&p_+WpRU&!3$~Ec42~|H?gwvaj^p4`uHK7EJG`q`|GUVqTm!u3zI>22vk? z`j7eZD}O8lvq!z=?s>8IqR#o*) zm6cysVbKw-OIoF@^zBMYO7SHBsEjHk{k7b~J%|~)f=xUF@JvMk7ZEmhSl1ck*1PU zC*1C5dtZ6kc^xk~tzOZ)^l8ui>Nnt^%f9iGj%vinJWn%_`uNj-%%5NRleQyOM~@fj zN=3PB@4I@rREP76^kuJI>fSx5K2Zl06I&?DzPR`zjh$HNY5q|kfBKL4^DBRB2h7gg zsgoy)v^$WlgSkOP56DzRWYFz?mUlB}muOnt5to1V`CfleAAkCf`SUA(EYJ32XDFBx zaM{=Xy?{RGwpX#^4=Z7QiOaodQ;&I`dwf=xUF@Lsu`H?@NENb&cn%?P@t-k$p z6+f$3^A?}fmf=xUF@JvL kkL6J3h#d9kv9H0iFO-4Q$DjUV{z7@cGXe-8fIyi31ND^EuK)l5 literal 0 HcmV?d00001 diff --git a/examples/overlay/pencil_frame_rgb888.bmp.license b/examples/overlay/pencil_frame_rgb888.bmp.license new file mode 100644 index 0000000..831eb5c --- /dev/null +++ b/examples/overlay/pencil_frame_rgb888.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT From bca899bc0e4765644156e9686d6634fd0a0009c8 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 10:31:49 -0600 Subject: [PATCH 32/70] mockdoc for displayio --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 3b6c67e..968bc52 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,6 +39,7 @@ "busdisplay", "busio", "digitalio", + "displayio" "espcamera", "fourwire", "micropython", From 12f21fbacdb7dbcaada6994aff797a5a34570774 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 11:01:46 -0600 Subject: [PATCH 33/70] format --- docs/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 968bc52..95c6641 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,8 +39,7 @@ "busdisplay", "busio", "digitalio", - "displayio" - "espcamera", + "displayio" "espcamera", "fourwire", "micropython", "neopixel", From bebfd04d84072ea9b125756414c917ee59a836f9 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 11:17:00 -0600 Subject: [PATCH 34/70] add optional requirement and fix docs build --- docs/conf.py | 3 ++- docs/mock/displayio.py | 21 +++++++++++++++++++++ optional_requirements.txt | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 95c6641..cbf1452 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,8 +39,9 @@ "busdisplay", "busio", "digitalio", - "displayio" "espcamera", + "espcamera", "fourwire", + "jpegio", "micropython", "neopixel", "sdcardio", diff --git a/docs/mock/displayio.py b/docs/mock/displayio.py index b4fd1c5..fb8979d 100644 --- a/docs/mock/displayio.py +++ b/docs/mock/displayio.py @@ -9,3 +9,24 @@ def __init__(self, i): def __setitem__(self, idx, value): self._data[idx] = value + + +class ColorConverter: + def __init__(self, colorspace): + self._colorspace = colorspace + + def convert(self, color_value) -> int: + pass + + +class Bitmap: + def __init__(self, width, height, color_count): + pass + + +class Colorspace: + pass + + +class Display: + pass \ No newline at end of file diff --git a/optional_requirements.txt b/optional_requirements.txt index d4e27c4..51f25c9 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -1,3 +1,5 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense + +adafruit_bitmapsaver \ No newline at end of file From 2bb9b90874029561b64d47f9c8af4801e9954c15 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 11:18:58 -0600 Subject: [PATCH 35/70] fix requirement. code format --- docs/mock/displayio.py | 2 +- optional_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/mock/displayio.py b/docs/mock/displayio.py index fb8979d..f18251d 100644 --- a/docs/mock/displayio.py +++ b/docs/mock/displayio.py @@ -29,4 +29,4 @@ class Colorspace: class Display: - pass \ No newline at end of file + pass diff --git a/optional_requirements.txt b/optional_requirements.txt index 51f25c9..f46e5d6 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -2,4 +2,4 @@ # # SPDX-License-Identifier: Unlicense -adafruit_bitmapsaver \ No newline at end of file +adafruit-circuitpython-bitmapsaver From ecce803b2d9021e8e4be493f3cd7762d7477c772 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 11:22:21 -0600 Subject: [PATCH 36/70] add imageload to optional reqs --- optional_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/optional_requirements.txt b/optional_requirements.txt index f46e5d6..42b579d 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -3,3 +3,4 @@ # SPDX-License-Identifier: Unlicense adafruit-circuitpython-bitmapsaver +adafruit-circuitpython-imageload From a7a3c0eebf4c5e80b05227e52fb58d4fd78b7bdf Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 11:53:31 -0600 Subject: [PATCH 37/70] move imports so other libraries are optional. --- adafruit_pycamera/__init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 071b2df..59a8e3d 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -33,11 +33,6 @@ from adafruit_display_text import label from digitalio import DigitalInOut, Pull from rainbowio import colorwheel -from displayio import Bitmap, ColorConverter, Colorspace -from jpegio import JpegDecoder -import ulab.numpy as np -from adafruit_bitmapsaver import save_pixels -import adafruit_imageload __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyCamera.git" @@ -853,6 +848,11 @@ def overlay(self) -> str: @overlay.setter def overlay(self, new_overlay_file: str) -> None: + # pylint: disable=import-outside-toplevel + from displayio import ColorConverter, Colorspace + import ulab.numpy as np + import adafruit_imageload + if self.overlay_bmp is not None: self.overlay_bmp.deinit() self._overlay = new_overlay_file @@ -865,6 +865,9 @@ def overlay(self, new_overlay_file: str) -> None: del arr def _init_jpeg_decoder(self): + # pylint: disable=import-outside-toplevel + from jpegio import JpegDecoder + """ Initialize the JpegDecoder if it hasn't been already. Only needed if overlay is used. @@ -882,6 +885,9 @@ def blit_overlay_into_last_capture(self): raise ValueError( "Must set overlay before calling blit_overlay_into_last_capture" ) + # pylint: disable=import-outside-toplevel + from adafruit_bitmapsaver import save_pixels + from displayio import Bitmap, ColorConverter, Colorspace self._init_jpeg_decoder() @@ -956,6 +962,8 @@ def blit(self, bitmap, x_offset=0, y_offset=32): The default preview capture is 240x176, leaving 32 pixel rows at the top and bottom for status information. """ + # pylint: disable=import-outside-toplevel + from displayio import Bitmap if self.overlay_bmp is not None: if self.combined_bmp is None: From 30544c7587fb37712e326451b6f919ff313cf5e2 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Feb 2024 19:10:40 -0600 Subject: [PATCH 38/70] rename to code_simple and add overlay selector version --- examples/overlay/code_select.py | 91 ++++++++++++++++++++ examples/overlay/{code.py => code_simple.py} | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 examples/overlay/code_select.py rename examples/overlay/{code.py => code_simple.py} (97%) diff --git a/examples/overlay/code_select.py b/examples/overlay/code_select.py new file mode 100644 index 0000000..480675e --- /dev/null +++ b/examples/overlay/code_select.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" simple point-and-shoot camera example, with overly selecting using select button. + +Place all overlay files inside /sd/overlays/ directory. +""" +import os +import time +import traceback +import adafruit_pycamera # pylint: disable=import-error + + +pycam = adafruit_pycamera.PyCamera() +pycam.mode = 0 # only mode 0 (JPEG) will work in this example + +# User settings - try changing these: +pycam.resolution = 1 # 0-12 preset resolutions: +# 0: 240x240, 1: 320x240, 2: 640x480 + +pycam.led_level = 1 # 0-4 preset brightness levels +pycam.led_color = 0 # 0-7 preset colors: 0: white, 1: green, 2: yellow, 3: red, +# 4: pink, 5: blue, 6: teal, 7: rainbow +pycam.effect = 0 # 0-7 preset FX: 0: normal, 1: invert, 2: b&w, 3: red, +# 4: green, 5: blue, 6: sepia, 7: solarize + +print("Overlay example camera ready.") +pycam.tone(800, 0.1) +pycam.tone(1200, 0.05) + +overlay_files = os.listdir("/sd/overlays/") +cur_overlay_idx = 0 + +pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}" +pycam.overlay_transparency_color = 0xE007 + +overlay_files = os.listdir("/sd/overlays/") +cur_overlay_idx = 0 + +while True: + pycam.blit(pycam.continuous_capture()) + pycam.keys_debounce() + # print(dir(pycam.select)) + if pycam.select.fell: + cur_overlay_idx += 1 + if cur_overlay_idx >= len(overlay_files): + cur_overlay_idx = 0 + print(f"changing overlay to {overlay_files[cur_overlay_idx]}") + pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}" + + if pycam.shutter.short_count: + print("Shutter released") + pycam.tone(1200, 0.05) + pycam.tone(1600, 0.05) + try: + pycam.display_message("snap", color=0x00DD00) + pycam.capture_jpeg() + pycam.display_message("overlay", color=0x00DD00) + pycam.blit_overlay_into_last_capture() + pycam.live_preview_mode() + except TypeError as exception: + traceback.print_exception(exception) + pycam.display_message("Failed", color=0xFF0000) + time.sleep(0.5) + pycam.live_preview_mode() + except RuntimeError as exception: + pycam.display_message("Error\nNo SD Card", color=0xFF0000) + time.sleep(0.5) + + if pycam.card_detect.fell: + print("SD card removed") + pycam.unmount_sd_card() + pycam.display.refresh() + + if pycam.card_detect.rose: + print("SD card inserted") + pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) + for _ in range(3): + try: + print("Mounting card") + pycam.mount_sd_card() + print("Success!") + break + except OSError as exception: + print("Retrying!", exception) + time.sleep(0.5) + else: + pycam.display_message("SD Card\nFailed!", color=0xFF0000) + time.sleep(0.5) + pycam.display.refresh() diff --git a/examples/overlay/code.py b/examples/overlay/code_simple.py similarity index 97% rename from examples/overlay/code.py rename to examples/overlay/code_simple.py index 95f098f..ffcea6a 100644 --- a/examples/overlay/code.py +++ b/examples/overlay/code_simple.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries -# SPDX-FileCopyrightText: Copyright (c) 202 Tim Cocks for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT """ simple point-and-shoot camera example, with an overlay frame image. """ From a6e2d38bdd88570b4dcd27dd64b8aaa4ebb4125d Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 21 Feb 2024 12:54:06 -0500 Subject: [PATCH 39/70] Check for inconsistent CIRCUITPY and display update msg --- adafruit_pycamera/__init__.py | 37 ++++++++++++++++++++++++++++++++--- examples/basic_camera/code.py | 6 +++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index e0fa35f..6b3309d 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: MIT """Library for the Adafruit PyCamera with OV5640 autofocus module""" +# pylint: disable=too-many-lines + import os import struct import time @@ -276,6 +278,32 @@ def make_debounced_expander_pin(pin_no): self.mute = make_expander_output(_AW_MUTE, False) + self.check_for_update_needed() + + def check_for_update_needed(self): + """Check whether CIRCUITPY is too big, indicating it was created + by a version of CircuitPython older than 9.0.0 beta 2. + If so display and print a message and hang. + """ + circuitpy_stat = os.statvfs("/") + # CIRCUITPY should be 960KB or so. >1MB is too large + # and indicates an older version of CircuitPython + # created the filesystem. + if circuitpy_stat[1] * circuitpy_stat[2] > 1000000: + message = """\ +CIRCUITPY problem. +Backup. Update CPy +to >= 9.0.0-beta.2. +Reformat: + import storage + storage.erase_ +filesystem() +See Learn Guide.""" + self.display_message(message, color=0xFFFFFF, scale=2, full_screen=True) + print(message) + while True: + pass + def make_camera_ui(self): """Create displayio widgets for the standard camera UI""" self._sd_label = label.Label( @@ -678,13 +706,15 @@ def deinit_display(self): self._display_bus = None self.display = None - def display_message(self, message, color=0xFF0000, scale=3): + def display_message(self, message, color=0xFF0000, scale=3, full_screen=False): """Display a message on the TFT""" text_area = label.Label(terminalio.FONT, text=message, color=color, scale=scale) - text_area.anchor_point = (0.5, 0.5) + text_area.anchor_point = (0, 0) if full_screen else (0.5, 0.5) if not self.display: self.init_display() - text_area.anchored_position = (self.display.width / 2, self.display.height / 2) + text_area.anchored_position = ( + (0, 0) if full_screen else (self.display.width / 2, self.display.height / 2) + ) # Show it self.splash.append(text_area) @@ -994,6 +1024,7 @@ def __init__(self, init_autofocus=True): self.init_neopixel() self.init_display() self.init_camera(init_autofocus) + try: self.mount_sd_card() except Exception as exc: # pylint: disable=broad-exception-caught diff --git a/examples/basic_camera/code.py b/examples/basic_camera/code.py index cebda71..a58b4a3 100644 --- a/examples/basic_camera/code.py +++ b/examples/basic_camera/code.py @@ -1,16 +1,16 @@ # SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries # # SPDX-License-Identifier: MIT -''' simple point-and-shoot camera example. No bells! Zero whistles! ''' +""" simple point-and-shoot camera example. No bells! Zero whistles! """ import time -import adafruit_pycamera # pylint: disable=import-error +import adafruit_pycamera # pylint: disable=import-error pycam = adafruit_pycamera.PyCamera() pycam.mode = 0 # only mode 0 (JPEG) will work in this example # User settings - try changing these: -pycam.resolution = 8 # 0-12 preset resolutions: +pycam.resolution = 8 # 0-12 preset resolutions: # 0: 240x240, 1: 320x240, 2: 640x480, 3: 800x600, 4: 1024x768, # 5: 1280x720, 6: 1280x1024, 7: 1600x1200, 8: 1920x1080, 9: 2048x1536, # 10: 2560x1440, 11: 2560x1600, 12: 2560x1920 From 2e2215b890240af81af1589d0ad98d1cda1b36dc Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 21 Feb 2024 18:51:55 -0600 Subject: [PATCH 40/70] Rename examples & tweak slightly --- examples/{ipcam => web_camera}/code.py | 13 ++++++++++--- examples/{ipcam2 => web_settings_explorer}/code.py | 1 + .../htdocs/index.html | 0 .../htdocs/index.js | 0 .../htdocs/metadata.js | 0 .../make_web_metadata.py | 0 6 files changed, 11 insertions(+), 3 deletions(-) rename examples/{ipcam => web_camera}/code.py (80%) rename examples/{ipcam2 => web_settings_explorer}/code.py (98%) rename examples/{ipcam2 => web_settings_explorer}/htdocs/index.html (100%) rename examples/{ipcam2 => web_settings_explorer}/htdocs/index.js (100%) rename examples/{ipcam2 => web_settings_explorer}/htdocs/metadata.js (100%) rename examples/{ipcam2 => web_settings_explorer}/make_web_metadata.py (100%) diff --git a/examples/ipcam/code.py b/examples/web_camera/code.py similarity index 80% rename from examples/ipcam/code.py rename to examples/web_camera/code.py index 49865b8..55cc82b 100644 --- a/examples/ipcam/code.py +++ b/examples/web_camera/code.py @@ -22,7 +22,14 @@ pycam.camera.quality = 6 server = Server(socketpool.SocketPool(wifi.radio)) -PORT = 81 +if wifi.radio.ipv4_address: + # use alt port if web workflow enabled + port = 8080 +else: + # connect to wifi and use standard http port otherwise + wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD")) + port = 80 + BOUNDARY = b"FRAME" + binascii.hexlify(os.urandom(8)) @@ -51,7 +58,7 @@ def body(): async def poll(interval): - server.start(str(wifi.radio.ipv4_address), port=PORT) + server.start(str(wifi.radio.ipv4_address), port=port) while True: try: server.poll() @@ -65,6 +72,6 @@ async def main(): await asyncio.gather(poll_task) -pycam.display_message(f"{wifi.radio.ipv4_address}:{PORT}/", scale=2) +pycam.display_message(f"{wifi.radio.ipv4_address}:{port}/", scale=2) asyncio.run(main()) diff --git a/examples/ipcam2/code.py b/examples/web_settings_explorer/code.py similarity index 98% rename from examples/ipcam2/code.py rename to examples/web_settings_explorer/code.py index 9d93434..696e584 100644 --- a/examples/ipcam2/code.py +++ b/examples/web_settings_explorer/code.py @@ -119,4 +119,5 @@ def property_common(obj, request): return JSONResponse(request, {"error": str(exc)}, status=BAD_REQUEST_400) +pycam.display_message(f"{wifi.radio.ipv4_address}:{port}/", scale=2) server.serve_forever(str(wifi.radio.ipv4_address), port) diff --git a/examples/ipcam2/htdocs/index.html b/examples/web_settings_explorer/htdocs/index.html similarity index 100% rename from examples/ipcam2/htdocs/index.html rename to examples/web_settings_explorer/htdocs/index.html diff --git a/examples/ipcam2/htdocs/index.js b/examples/web_settings_explorer/htdocs/index.js similarity index 100% rename from examples/ipcam2/htdocs/index.js rename to examples/web_settings_explorer/htdocs/index.js diff --git a/examples/ipcam2/htdocs/metadata.js b/examples/web_settings_explorer/htdocs/metadata.js similarity index 100% rename from examples/ipcam2/htdocs/metadata.js rename to examples/web_settings_explorer/htdocs/metadata.js diff --git a/examples/ipcam2/make_web_metadata.py b/examples/web_settings_explorer/make_web_metadata.py similarity index 100% rename from examples/ipcam2/make_web_metadata.py rename to examples/web_settings_explorer/make_web_metadata.py From 027bdac00d5c36b9ae43b5427de5424e3d7df5b3 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sat, 24 Feb 2024 11:52:51 -0600 Subject: [PATCH 41/70] add support for overlay_position --- adafruit_pycamera/__init__.py | 18 +++++++++++++++--- examples/overlay/code_select.py | 12 +++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index d019cf5..1c6ad6f 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -238,6 +238,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self.overlay_transparency_color = None self.overlay_bmp = None self.combined_bmp = None + self.preview_scale = None + self.overlay_position = [None, None] self.splash = displayio.Group() # Reset display and I/O expander @@ -645,6 +647,8 @@ def resolution(self, res): microcontroller.nvm[_NVM_RESOLUTION] = res self._resolution = res self._res_label.text = self.resolutions[res] + _width = int(self.resolutions[self.resolution].split("x")[0]) + self.preview_scale = 240 / _width self.display.refresh() @property @@ -928,8 +932,8 @@ def blit_overlay_into_last_capture(self): bitmaptools.blit( photo_bitmap, self.overlay_bmp, - 0, - 0, + self.overlay_position[0] if self.overlay_position[0] is not None else 0, + self.overlay_position[1] if self.overlay_position[1] is not None else 0, skip_source_index=self.overlay_transparency_color, skip_dest_index=None, ) @@ -1003,8 +1007,16 @@ def blit(self, bitmap, x_offset=0, y_offset=32): bitmaptools.rotozoom( self.combined_bmp, self.overlay_bmp, - scale=0.75, + scale=self.preview_scale, skip_index=self.overlay_transparency_color, + ox=int(self.overlay_position[0] * self.preview_scale) + if self.overlay_position[0] is not None + else None, + oy=int(self.overlay_position[1] * self.preview_scale) + if self.overlay_position[1] is not None + else None, + px=0 if self.overlay_position[0] is not None else None, + py=0 if self.overlay_position[1] is not None else None, ) bitmap = self.combined_bmp diff --git a/examples/overlay/code_select.py b/examples/overlay/code_select.py index 480675e..441433d 100644 --- a/examples/overlay/code_select.py +++ b/examples/overlay/code_select.py @@ -11,7 +11,6 @@ import traceback import adafruit_pycamera # pylint: disable=import-error - pycam = adafruit_pycamera.PyCamera() pycam.mode = 0 # only mode 0 (JPEG) will work in this example @@ -34,6 +33,7 @@ pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}" pycam.overlay_transparency_color = 0xE007 +pycam.overlay_position = [0, 0] overlay_files = os.listdir("/sd/overlays/") cur_overlay_idx = 0 @@ -49,6 +49,16 @@ print(f"changing overlay to {overlay_files[cur_overlay_idx]}") pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}" + if not pycam.down.value: + pycam.overlay_position[1] += 1 * (int(pycam.down.current_duration / 0.3) + 1) + if not pycam.up.value: + pycam.overlay_position[1] -= 1 * (int(pycam.up.current_duration / 0.3) + 1) + + if not pycam.left.value: + pycam.overlay_position[0] -= 1 * (int(pycam.left.current_duration / 0.3) + 1) + if not pycam.right.value: + pycam.overlay_position[0] += 1 * (int(pycam.right.current_duration / 0.3) + 1) + if pycam.shutter.short_count: print("Shutter released") pycam.tone(1200, 0.05) From c6b250216ef9f46f10c96a7712c6132d4897fc0d Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sat, 24 Feb 2024 11:53:47 -0600 Subject: [PATCH 42/70] add blinka emoji to overlays --- examples/overlay/blinka_emoji_rgb888.bmp | Bin 0 -> 49290 bytes .../overlay/blinka_emoji_rgb888.bmp.license | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 examples/overlay/blinka_emoji_rgb888.bmp create mode 100644 examples/overlay/blinka_emoji_rgb888.bmp.license diff --git a/examples/overlay/blinka_emoji_rgb888.bmp b/examples/overlay/blinka_emoji_rgb888.bmp new file mode 100644 index 0000000000000000000000000000000000000000..6a26a1f0d0c3f0e3e7ac691a1bc4405ca0bad605 GIT binary patch literal 49290 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 literal 0 HcmV?d00001 diff --git a/examples/overlay/blinka_emoji_rgb888.bmp.license b/examples/overlay/blinka_emoji_rgb888.bmp.license new file mode 100644 index 0000000..831eb5c --- /dev/null +++ b/examples/overlay/blinka_emoji_rgb888.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT From aca887be8dc120698ae40f9a785cecf32c225de6 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sun, 25 Feb 2024 09:27:53 -0600 Subject: [PATCH 43/70] allow custom filenaming --- adafruit_pycamera/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index d019cf5..8c68479 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -820,14 +820,17 @@ def live_preview_mode(self): # self.effect = self._effect self.continuous_capture_start() - def open_next_image(self, extension="jpg"): + def open_next_image(self, extension="jpg", filename_prefix="img"): """Return an opened numbered file on the sdcard, such as "img01234.jpg".""" try: os.stat("/sd") except OSError as exc: # no SD card! raise RuntimeError("No SD card mounted") from exc while True: - filename = "/sd/img%04d.%s" % (self._image_counter, extension) + filename = f"/sd/{filename_prefix}%04d.%s" % ( + self._image_counter, + extension, + ) self._image_counter += 1 try: os.stat(filename) @@ -837,7 +840,7 @@ def open_next_image(self, extension="jpg"): print("Writing to", filename) return open(filename, "wb") - def capture_jpeg(self): + def capture_jpeg(self, filename_prefix="img"): """Capture a jpeg file and save it to the SD card""" try: os.stat("/sd") @@ -855,7 +858,7 @@ def capture_jpeg(self): print(f"Captured {len(jpeg)} bytes of jpeg data") print("Resolution %d x %d" % (self.camera.width, self.camera.height)) - with self.open_next_image() as dest: + with self.open_next_image(filename_prefix=filename_prefix) as dest: chunksize = 16384 for offset in range(0, len(jpeg), chunksize): dest.write(jpeg[offset : offset + chunksize]) From 23c7fa5c288f283603835c3172dbd814851411be Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sun, 25 Feb 2024 10:25:33 -0600 Subject: [PATCH 44/70] timestamp filename example --- examples/timestamp_filename/code.py | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 examples/timestamp_filename/code.py diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py new file mode 100644 index 0000000..1022cef --- /dev/null +++ b/examples/timestamp_filename/code.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" simple point-and-shoot camera example. With NTP and internal RTC to + add timestamp to photo filenames. Must install adafruit_ntp library!""" + +import time +import adafruit_pycamera # pylint: disable=import-error +import wifi +import socketpool +import adafruit_ntp +import rtc +import time + +pool = socketpool.SocketPool(wifi.radio) +ntp = adafruit_ntp.NTP(pool, tz_offset=0) +rtc.RTC().datetime = ntp.datetime + +pycam = adafruit_pycamera.PyCamera() +pycam.mode = 0 # only mode 0 (JPEG) will work in this example + +# User settings - try changing these: +pycam.resolution = 2 # 0-12 preset resolutions: +# 0: 240x240, 1: 320x240, 2: 640x480, 3: 800x600, 4: 1024x768, +# 5: 1280x720, 6: 1280x1024, 7: 1600x1200, 8: 1920x1080, 9: 2048x1536, +# 10: 2560x1440, 11: 2560x1600, 12: 2560x1920 +pycam.led_level = 1 # 0-4 preset brightness levels +pycam.led_color = 0 # 0-7 preset colors: 0: white, 1: green, 2: yellow, 3: red, +# 4: pink, 5: blue, 6: teal, 7: rainbow +pycam.effect = 0 # 0-7 preset FX: 0: normal, 1: invert, 2: b&w, 3: red, +# 4: green, 5: blue, 6: sepia, 7: solarize + +print("Simple camera ready.") +pycam.tone(800, 0.1) +pycam.tone(1200, 0.05) + +while True: + pycam.blit(pycam.continuous_capture()) + pycam.keys_debounce() + + if pycam.shutter.short_count: + print("Shutter released") + pycam.tone(1200, 0.05) + pycam.tone(1600, 0.05) + try: + pycam.display_message("snap", color=0x00DD00) + timestamp = f"img_{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday}_{time.localtime().tm_hour:02}-{time.localtime().tm_min:02}-{time.localtime().tm_sec:02}_" + pycam.capture_jpeg(filename_prefix=timestamp) + pycam.live_preview_mode() + except TypeError as exception: + pycam.display_message("Failed", color=0xFF0000) + time.sleep(0.5) + pycam.live_preview_mode() + except RuntimeError as exception: + pycam.display_message("Error\nNo SD Card", color=0xFF0000) + time.sleep(0.5) + + if pycam.card_detect.fell: + print("SD card removed") + pycam.unmount_sd_card() + pycam.display.refresh() + + if pycam.card_detect.rose: + print("SD card inserted") + pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) + for _ in range(3): + try: + print("Mounting card") + pycam.mount_sd_card() + print("Success!") + break + except OSError as exception: + print("Retrying!", exception) + time.sleep(0.5) + else: + pycam.display_message("SD Card\nFailed!", color=0xFF0000) + time.sleep(0.5) + pycam.display.refresh() From 994dd1ccef8877b208778171169e076fa889b6a4 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sun, 25 Feb 2024 10:27:32 -0600 Subject: [PATCH 45/70] pylint fixes --- examples/timestamp_filename/code.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py index 1022cef..03308af 100644 --- a/examples/timestamp_filename/code.py +++ b/examples/timestamp_filename/code.py @@ -5,12 +5,11 @@ add timestamp to photo filenames. Must install adafruit_ntp library!""" import time -import adafruit_pycamera # pylint: disable=import-error import wifi import socketpool -import adafruit_ntp import rtc -import time +import adafruit_ntp +import adafruit_pycamera # pylint: disable=import-error pool = socketpool.SocketPool(wifi.radio) ntp = adafruit_ntp.NTP(pool, tz_offset=0) @@ -44,7 +43,11 @@ pycam.tone(1600, 0.05) try: pycam.display_message("snap", color=0x00DD00) - timestamp = f"img_{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday}_{time.localtime().tm_hour:02}-{time.localtime().tm_min:02}-{time.localtime().tm_sec:02}_" + # pylint: disable=line-too-long + timestamp = ( + f"img_{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday}" + f"_{time.localtime().tm_hour:02}-{time.localtime().tm_min:02}-{time.localtime().tm_sec:02}_" + ) pycam.capture_jpeg(filename_prefix=timestamp) pycam.live_preview_mode() except TypeError as exception: From 91969b403c6edfc4fac63ffaf238bfaf7a3399cf Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sun, 25 Feb 2024 10:30:55 -0600 Subject: [PATCH 46/70] more details in comment --- examples/timestamp_filename/code.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py index 03308af..3ac59d3 100644 --- a/examples/timestamp_filename/code.py +++ b/examples/timestamp_filename/code.py @@ -2,7 +2,10 @@ # # SPDX-License-Identifier: MIT """ simple point-and-shoot camera example. With NTP and internal RTC to - add timestamp to photo filenames. Must install adafruit_ntp library!""" + add timestamp to photo filenames. Must install adafruit_ntp library! + Example code assumes WIFI credentials are properly setup and web workflow + enabled in settings.toml. If not, you'll need to add code to manually connect + to your network.""" import time import wifi From 16c74281a6216846f10a3c93d20444b4b8d52f6b Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 29 Feb 2024 20:21:22 -0600 Subject: [PATCH 47/70] overlay_scale feature --- adafruit_pycamera/__init__.py | 15 +++++++++------ examples/overlay/code_select.py | 9 ++++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 1c6ad6f..45dcb3c 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -240,6 +240,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self.combined_bmp = None self.preview_scale = None self.overlay_position = [None, None] + self.overlay_scale = 1.0 self.splash = displayio.Group() # Reset display and I/O expander @@ -929,13 +930,15 @@ def blit_overlay_into_last_capture(self): self.decoder.decode(photo_bitmap, scale=0, x=0, y=0) - bitmaptools.blit( + bitmaptools.rotozoom( photo_bitmap, self.overlay_bmp, - self.overlay_position[0] if self.overlay_position[0] is not None else 0, - self.overlay_position[1] if self.overlay_position[1] is not None else 0, - skip_source_index=self.overlay_transparency_color, - skip_dest_index=None, + ox=self.overlay_position[0] if self.overlay_position[0] is not None else 0, + oy=self.overlay_position[1] if self.overlay_position[1] is not None else 0, + px=0 if self.overlay_position[0] is not None else None, + py=0 if self.overlay_position[1] is not None else None, + skip_index=self.overlay_transparency_color, + scale=self.overlay_scale, ) cc565_swapped = ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED) @@ -1007,7 +1010,7 @@ def blit(self, bitmap, x_offset=0, y_offset=32): bitmaptools.rotozoom( self.combined_bmp, self.overlay_bmp, - scale=self.preview_scale, + scale=self.preview_scale * self.overlay_scale, skip_index=self.overlay_transparency_color, ox=int(self.overlay_position[0] * self.preview_scale) if self.overlay_position[0] is not None diff --git a/examples/overlay/code_select.py b/examples/overlay/code_select.py index 441433d..da7ca6f 100644 --- a/examples/overlay/code_select.py +++ b/examples/overlay/code_select.py @@ -2,9 +2,16 @@ # SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT -""" simple point-and-shoot camera example, with overly selecting using select button. +""" simple point-and-shoot camera example, with overly capabilities. Place all overlay files inside /sd/overlays/ directory. + +Usage: + +Select Button - Change to the next overlay file +OK Button - Change between position and scale modes +D-Pad - Change the overlay's position or scale depending on which mode + we're currently in. """ import os import time From 1920616c0c49dbe457003d8e5459bdf86a6ca8e4 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 29 Feb 2024 20:23:08 -0600 Subject: [PATCH 48/70] overlay_scale in the example --- examples/overlay/code_select.py | 53 ++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/examples/overlay/code_select.py b/examples/overlay/code_select.py index da7ca6f..665d4e2 100644 --- a/examples/overlay/code_select.py +++ b/examples/overlay/code_select.py @@ -2,22 +2,21 @@ # SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT -""" simple point-and-shoot camera example, with overly capabilities. +""" simple point-and-shoot camera example, with overly selecting using select button. Place all overlay files inside /sd/overlays/ directory. - -Usage: - -Select Button - Change to the next overlay file -OK Button - Change between position and scale modes -D-Pad - Change the overlay's position or scale depending on which mode - we're currently in. """ import os import time import traceback import adafruit_pycamera # pylint: disable=import-error +MODE_POSITION = 0 +MODE_SCALE = 1 +CURRENT_MODE = 0 + +int_scale = 100 + pycam = adafruit_pycamera.PyCamera() pycam.mode = 0 # only mode 0 (JPEG) will work in this example @@ -56,16 +55,34 @@ print(f"changing overlay to {overlay_files[cur_overlay_idx]}") pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}" - if not pycam.down.value: - pycam.overlay_position[1] += 1 * (int(pycam.down.current_duration / 0.3) + 1) - if not pycam.up.value: - pycam.overlay_position[1] -= 1 * (int(pycam.up.current_duration / 0.3) + 1) - - if not pycam.left.value: - pycam.overlay_position[0] -= 1 * (int(pycam.left.current_duration / 0.3) + 1) - if not pycam.right.value: - pycam.overlay_position[0] += 1 * (int(pycam.right.current_duration / 0.3) + 1) - + if CURRENT_MODE == MODE_POSITION: + if not pycam.down.value: + pycam.overlay_position[1] += 1 * ( + int(pycam.down.current_duration / 0.3) + 1 + ) + if not pycam.up.value: + pycam.overlay_position[1] -= 1 * (int(pycam.up.current_duration / 0.3) + 1) + if not pycam.left.value: + pycam.overlay_position[0] -= 1 * ( + int(pycam.left.current_duration / 0.3) + 1 + ) + if not pycam.right.value: + pycam.overlay_position[0] += 1 * ( + int(pycam.right.current_duration / 0.3) + 1 + ) + if CURRENT_MODE == MODE_SCALE: + if pycam.down.fell: + int_scale -= 10 + pycam.overlay_scale = int_scale / 100 + print(pycam.overlay_scale) + if pycam.up.fell: + int_scale += 10 + pycam.overlay_scale = int_scale / 100 + print(pycam.overlay_scale) + + if pycam.ok.fell: + CURRENT_MODE = MODE_POSITION if CURRENT_MODE == MODE_SCALE else MODE_SCALE + print(f"Changing mode to: {CURRENT_MODE}") if pycam.shutter.short_count: print("Shutter released") pycam.tone(1200, 0.05) From c8d4a046b25520cd9c9d5b6dd7d1c91a0bb1cb15 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Mon, 4 Mar 2024 14:52:23 -0600 Subject: [PATCH 49/70] reduce to single string format. Fix long line in example for CircuitPython --- adafruit_pycamera/__init__.py | 5 +---- examples/timestamp_filename/code.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 8c68479..9b056bc 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -827,10 +827,7 @@ def open_next_image(self, extension="jpg", filename_prefix="img"): except OSError as exc: # no SD card! raise RuntimeError("No SD card mounted") from exc while True: - filename = f"/sd/{filename_prefix}%04d.%s" % ( - self._image_counter, - extension, - ) + filename = f"/sd/{filename_prefix}{self._image_counter}.{extension}" self._image_counter += 1 try: os.stat(filename) diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py index 3ac59d3..93f4a8b 100644 --- a/examples/timestamp_filename/code.py +++ b/examples/timestamp_filename/code.py @@ -47,10 +47,7 @@ try: pycam.display_message("snap", color=0x00DD00) # pylint: disable=line-too-long - timestamp = ( - f"img_{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday}" - f"_{time.localtime().tm_hour:02}-{time.localtime().tm_min:02}-{time.localtime().tm_sec:02}_" - ) + timestamp = f"img_{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday}_{time.localtime().tm_hour:02}-{time.localtime().tm_min:02}-{time.localtime().tm_sec:02}_" pycam.capture_jpeg(filename_prefix=timestamp) pycam.live_preview_mode() except TypeError as exception: From 9410a211ae72c14449fe5662c16cfe141a114079 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Mon, 4 Mar 2024 14:59:36 -0600 Subject: [PATCH 50/70] refactor longline to use format() --- examples/timestamp_filename/code.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py index 93f4a8b..3edd7fa 100644 --- a/examples/timestamp_filename/code.py +++ b/examples/timestamp_filename/code.py @@ -47,7 +47,14 @@ try: pycam.display_message("snap", color=0x00DD00) # pylint: disable=line-too-long - timestamp = f"img_{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday}_{time.localtime().tm_hour:02}-{time.localtime().tm_min:02}-{time.localtime().tm_sec:02}_" + timestamp = "img_{}-{}-{}_{:02}-{:02}-{:02}_".format( + time.localtime().tm_year, + time.localtime().tm_mon, + time.localtime().tm_mday, + time.localtime().tm_hour, + time.localtime().tm_min, + time.localtime().tm_sec, + ) pycam.capture_jpeg(filename_prefix=timestamp) pycam.live_preview_mode() except TypeError as exception: From b57c955df4183ac226e3cfec6acd29228cf99b1a Mon Sep 17 00:00:00 2001 From: foamyguy Date: Mon, 4 Mar 2024 15:00:04 -0600 Subject: [PATCH 51/70] remove pylint disable --- examples/timestamp_filename/code.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py index 3edd7fa..05c2a2e 100644 --- a/examples/timestamp_filename/code.py +++ b/examples/timestamp_filename/code.py @@ -46,7 +46,6 @@ pycam.tone(1600, 0.05) try: pycam.display_message("snap", color=0x00DD00) - # pylint: disable=line-too-long timestamp = "img_{}-{}-{}_{:02}-{:02}-{:02}_".format( time.localtime().tm_year, time.localtime().tm_mon, From 85d76995c65a0cb37a356fc58263fd4afd5a5e2a Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 5 Mar 2024 11:24:49 -0600 Subject: [PATCH 52/70] leading 0s format --- adafruit_pycamera/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 9b056bc..a7cbe35 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -827,7 +827,7 @@ def open_next_image(self, extension="jpg", filename_prefix="img"): except OSError as exc: # no SD card! raise RuntimeError("No SD card mounted") from exc while True: - filename = f"/sd/{filename_prefix}{self._image_counter}.{extension}" + filename = f"/sd/{filename_prefix}{self._image_counter:04d}.{extension}" self._image_counter += 1 try: os.stat(filename) From e0dc69aaf9d5d64b6c5ce1e9a51564d7bce560d6 Mon Sep 17 00:00:00 2001 From: Fred Meyer <56278928+VPTechOps@users.noreply.github.com> Date: Sun, 17 Mar 2024 10:21:26 -0700 Subject: [PATCH 53/70] Update code.py to handle RTC and NTP --- examples/camera/code.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/examples/camera/code.py b/examples/camera/code.py index 553c035..5a3ae9a 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -3,11 +3,49 @@ # # SPDX-License-Identifier: Unlicense +import ipaddress +import ssl +import wifi +import socketpool +import adafruit_requests +import os +import rtc +import adafruit_ntp +from adafruit_datetime import datetime + import time import bitmaptools import displayio import gifio import ulab.numpy as np +UTC_OFFSET = os.getenv('UTC_OFFSET') +TZ = os.getenv('TZ') + +# Wifi details are in settings.toml file, also, +# timezone info should be included to allow local time and DST adjustments +# # UTC_OFFSET, if present, will override TZ and DST and no API query will be done +# UTC_OFFSET=-25200 +# # TZ="America/Phoenix" + +try: + print("Connecting to %s"%os.getenv("CIRCUITPY_WIFI_SSID")) + wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")) + print("Connected to %s!"%os.getenv("CIRCUITPY_WIFI_SSID")) + print("My IP address is", wifi.radio.ipv4_address) + pool = socketpool.SocketPool(wifi.radio) + + if UTC_OFFSET is None: + requests = adafruit_requests.Session(pool, ssl.create_default_context()) + response = requests.get("http://worldtimeapi.org/api/timezone/" + TZ) + response_as_json = response.json() + UTC_OFFSET = response_as_json["raw_offset"] + response_as_json["dst_offset"] + + ntp = adafruit_ntp.NTP(pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600) + + rtc.RTC().datetime = ntp.datetime +except Exception as e: + print("Wifi error:", e) + print("Time not set") import adafruit_pycamera From 076ea3c6452eb50a4f52415433f5656ee8be8c56 Mon Sep 17 00:00:00 2001 From: Fred Meyer <56278928+VPTechOps@users.noreply.github.com> Date: Sun, 17 Mar 2024 10:36:59 -0700 Subject: [PATCH 54/70] Update code.py to correct errors in pull request checks --- examples/camera/code.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index 5a3ae9a..4f2e17a 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -3,21 +3,21 @@ # # SPDX-License-Identifier: Unlicense -import ipaddress +import adafruit_pycamera + import ssl -import wifi import socketpool import adafruit_requests import os import rtc import adafruit_ntp -from adafruit_datetime import datetime - import time +import wifi import bitmaptools import displayio import gifio import ulab.numpy as np + UTC_OFFSET = os.getenv('UTC_OFFSET') TZ = os.getenv('TZ') From 481409725594815284201c905da0c810905d7497 Mon Sep 17 00:00:00 2001 From: Fred Meyer <56278928+VPTechOps@users.noreply.github.com> Date: Sun, 17 Mar 2024 10:40:00 -0700 Subject: [PATCH 55/70] Update code.py correct errora in pull request checks --- examples/camera/code.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index 4f2e17a..71c4d7f 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -47,8 +47,6 @@ print("Wifi error:", e) print("Time not set") -import adafruit_pycamera - pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() From 272f80e2c07b8769885d8d0260c9e1be80a14dfa Mon Sep 17 00:00:00 2001 From: Fred Meyer <56278928+VPTechOps@users.noreply.github.com> Date: Sun, 17 Mar 2024 10:51:20 -0700 Subject: [PATCH 56/70] Update code.py Correct errors in pull request checks --- examples/camera/code.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index 71c4d7f..61c0a56 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -3,21 +3,21 @@ # # SPDX-License-Identifier: Unlicense -import adafruit_pycamera - import ssl +import os +import time import socketpool import adafruit_requests -import os import rtc import adafruit_ntp -import time import wifi import bitmaptools import displayio import gifio import ulab.numpy as np +import adafruit_pycamera + UTC_OFFSET = os.getenv('UTC_OFFSET') TZ = os.getenv('TZ') From ffa8575fc389bad95b9292af1c518ed12ebba3a9 Mon Sep 17 00:00:00 2001 From: Fred Meyer Date: Sun, 17 Mar 2024 16:57:17 -0700 Subject: [PATCH 57/70] Change to run pre-commit --- examples/camera/code.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index 61c0a56..a4e6868 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -2,7 +2,6 @@ # SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: Unlicense - import ssl import os import time @@ -18,19 +17,21 @@ import adafruit_pycamera -UTC_OFFSET = os.getenv('UTC_OFFSET') -TZ = os.getenv('TZ') - # Wifi details are in settings.toml file, also, -# timezone info should be included to allow local time and DST adjustments +# timezone info should be included in settings.toml to allow local time and DST adjustments # # UTC_OFFSET, if present, will override TZ and DST and no API query will be done # UTC_OFFSET=-25200 # # TZ="America/Phoenix" +UTC_OFFSET = os.getenv("UTC_OFFSET") +TZ = os.getenv("TZ") + try: - print("Connecting to %s"%os.getenv("CIRCUITPY_WIFI_SSID")) - wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")) - print("Connected to %s!"%os.getenv("CIRCUITPY_WIFI_SSID")) + print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}") + wifi.radio.connect( + os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") + ) + print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") print("My IP address is", wifi.radio.ipv4_address) pool = socketpool.SocketPool(wifi.radio) From bf348c19d46424d4dc3cac940a759ba94f06809f Mon Sep 17 00:00:00 2001 From: Fred Meyer Date: Sun, 17 Mar 2024 17:26:13 -0700 Subject: [PATCH 58/70] Correct 1 more pre-commit error --- examples/camera/code.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index a4e6868..a8dfc48 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -26,11 +26,11 @@ UTC_OFFSET = os.getenv("UTC_OFFSET") TZ = os.getenv("TZ") -try: - print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}") - wifi.radio.connect( - os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") +print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}") +wifi.radio.connect( + os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") ) +if wifi.radio.connected: print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") print("My IP address is", wifi.radio.ipv4_address) pool = socketpool.SocketPool(wifi.radio) @@ -44,9 +44,8 @@ ntp = adafruit_ntp.NTP(pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600) rtc.RTC().datetime = ntp.datetime -except Exception as e: - print("Wifi error:", e) - print("Time not set") +else: + print("Wifi failed to connect. Time not set.") pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() From 3fb24e986a43998a9beaa0105fe7e27fb24ba526 Mon Sep 17 00:00:00 2001 From: Fred Meyer Date: Sun, 17 Mar 2024 17:59:58 -0700 Subject: [PATCH 59/70] Correct DOS file endings --- examples/camera/code.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index 5a3ae9a..6c957c0 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -2,35 +2,37 @@ # SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: Unlicense - -import ipaddress import ssl -import wifi +import os +import time import socketpool import adafruit_requests -import os import rtc import adafruit_ntp -from adafruit_datetime import datetime - -import time +import wifi import bitmaptools import displayio import gifio import ulab.numpy as np -UTC_OFFSET = os.getenv('UTC_OFFSET') -TZ = os.getenv('TZ') + +import adafruit_pycamera # Wifi details are in settings.toml file, also, -# timezone info should be included to allow local time and DST adjustments +# timezone info should be included in settings.toml to allow local time and DST adjustments + # # UTC_OFFSET, if present, will override TZ and DST and no API query will be done # UTC_OFFSET=-25200 # # TZ="America/Phoenix" -try: - print("Connecting to %s"%os.getenv("CIRCUITPY_WIFI_SSID")) - wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")) - print("Connected to %s!"%os.getenv("CIRCUITPY_WIFI_SSID")) +UTC_OFFSET = os.getenv("UTC_OFFSET") +TZ = os.getenv("TZ") + +print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}") +wifi.radio.connect( + os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") + ) +if wifi.radio.connected: + print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") print("My IP address is", wifi.radio.ipv4_address) pool = socketpool.SocketPool(wifi.radio) @@ -43,11 +45,8 @@ ntp = adafruit_ntp.NTP(pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600) rtc.RTC().datetime = ntp.datetime -except Exception as e: - print("Wifi error:", e) - print("Time not set") - -import adafruit_pycamera +else: + print("Wifi failed to connect. Time not set.") pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() From bf5559c3d3dc13c00187328cf4195af3c21b1e5d Mon Sep 17 00:00:00 2001 From: Fred Meyer Date: Sun, 17 Mar 2024 18:13:18 -0700 Subject: [PATCH 60/70] Correct Another DOS line ending --- examples/camera/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index 37b7b0b..1825f23 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -29,7 +29,7 @@ print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}") wifi.radio.connect( os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") - ) +) if wifi.radio.connected: print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") print("My IP address is", wifi.radio.ipv4_address) From 1e219f2d85a90cacab3819a224b15da727ed2997 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 10 May 2024 17:39:41 -0500 Subject: [PATCH 61/70] continue if no WIFI --- examples/camera/code.py | 42 +++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/examples/camera/code.py b/examples/camera/code.py index 1825f23..a03ea64 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -27,25 +27,35 @@ TZ = os.getenv("TZ") print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}") -wifi.radio.connect( - os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") -) -if wifi.radio.connected: - print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") - print("My IP address is", wifi.radio.ipv4_address) - pool = socketpool.SocketPool(wifi.radio) +SSID = os.getenv("CIRCUITPY_WIFI_SSID") +PASSWORD = os.getenv("CIRCUITPY_WIFI_PASSWORD") + +if SSID and PASSWORD: + wifi.radio.connect( + os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") + ) + if wifi.radio.connected: + print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") + print("My IP address is", wifi.radio.ipv4_address) + pool = socketpool.SocketPool(wifi.radio) - if UTC_OFFSET is None: - requests = adafruit_requests.Session(pool, ssl.create_default_context()) - response = requests.get("http://worldtimeapi.org/api/timezone/" + TZ) - response_as_json = response.json() - UTC_OFFSET = response_as_json["raw_offset"] + response_as_json["dst_offset"] + if UTC_OFFSET is None: + requests = adafruit_requests.Session(pool, ssl.create_default_context()) + response = requests.get("http://worldtimeapi.org/api/timezone/" + TZ) + response_as_json = response.json() + UTC_OFFSET = response_as_json["raw_offset"] + response_as_json["dst_offset"] + print(f"UTC_OFFSET: {UTC_OFFSET}") - ntp = adafruit_ntp.NTP(pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600) + ntp = adafruit_ntp.NTP( + pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600 + ) - rtc.RTC().datetime = ntp.datetime + print(f"ntp time: {ntp.datetime}") + rtc.RTC().datetime = ntp.datetime + else: + print("Wifi failed to connect. Time not set.") else: - print("Wifi failed to connect. Time not set.") + print("Wifi config not found in settintgs.toml. Time not set.") pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() @@ -199,7 +209,7 @@ t0 = t1 pycam._mode_label.text = "GIF" # pylint: disable=protected-access print(f"\nfinal size {f.tell()} for {i} frames") - print(f"average framerate {i/(t1-t00)}fps") + print(f"average framerate {i / (t1 - t00)}fps") print(f"best {max(ft)} worst {min(ft)} std. deviation {np.std(ft)}") f.close() pycam.display.refresh() From ae3dd31989e3ff0a8c6ba2863b126496c5947279 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 28 Sep 2024 11:08:15 -0400 Subject: [PATCH 62/70] use espcamera reset functionality; other minor changes - espcamera knows how to toggle the power and reset pins to do a reset. Let it do it. - Make docstring be one string - use I2C write_then_readinto --- .pre-commit-config.yaml | 2 +- adafruit_pycamera/__init__.py | 30 +++++++++++------------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70ade69..374676d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pycqa/pylint - rev: v2.17.4 + rev: v3.3.1 hooks: - id: pylint name: pylint (library code) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 45dcb3c..6a4807c 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -81,9 +81,10 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-public-methods - """Base class for PyCamera hardware""" + """Base class for PyCamera hardware - """Wrapper class for the PyCamera hardware with lots of smarts""" + Wrapper class for the PyCamera hardware with lots of smarts + """ _finalize_firmware_load = ( 0x3022, @@ -253,9 +254,6 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self.shutter_button.switch_to_input(Pull.UP) self.shutter = Button(self.shutter_button) - self._cam_reset = DigitalInOut(board.CAMERA_RESET) - self._cam_pwdn = DigitalInOut(board.CAMERA_PWDN) - # AW9523 GPIO expander self._aw = adafruit_aw9523.AW9523(self._i2c, address=0x58) print("Found AW9523") @@ -374,14 +372,6 @@ def init_neopixel(self): def init_camera(self, init_autofocus=True) -> None: """Initialize the camera, by default including autofocus""" - print("reset camera") - self._cam_reset.switch_to_output(False) - self._cam_pwdn.switch_to_output(True) - time.sleep(0.01) - self._cam_pwdn.switch_to_output(False) - time.sleep(0.01) - self._cam_reset.switch_to_output(True) - time.sleep(0.01) print("Initializing camera") self.camera = espcamera.Camera( @@ -390,6 +380,8 @@ def init_camera(self, init_autofocus=True) -> None: pixel_clock_pin=board.CAMERA_PCLK, vsync_pin=board.CAMERA_VSYNC, href_pin=board.CAMERA_HREF, + powerdown_pin=board.CAMERA_PWDN, + reset_pin=board.CAMERA_RESET, pixel_format=espcamera.PixelFormat.RGB565, frame_size=espcamera.FrameSize.HQVGA, i2c=board.I2C(), @@ -455,13 +447,13 @@ def write_camera_list(self, reg_list: Sequence[int]) -> None: def read_camera_register(self, reg: int) -> int: """Read a 1-byte camera register""" - b = bytearray(2) - b[0] = reg >> 8 - b[1] = reg & 0xFF + b_out = bytearray(2) + b_out[0] = reg >> 8 + b_out[1] = reg & 0xFF + b_in = bytearray(1) with self._camera_device as i2c: - i2c.write(b) - i2c.readinto(b, end=1) - return b[0] + i2c.write_then_readinto(b_out, b_in) + return b_in[0] def autofocus_init_from_bitstream(self, firmware: bytes): """Initialize the autofocus engine from a bytestring""" From 5ca0969597e48fe8e1ec00a2e9324c702b71945c Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 28 Sep 2024 11:19:16 -0400 Subject: [PATCH 63/70] pylint wants yield from --- examples/filter/code.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/filter/code.py b/examples/filter/code.py index 8d4598c..207b55f 100644 --- a/examples/filter/code.py +++ b/examples/filter/code.py @@ -139,8 +139,7 @@ def sketch(b): def cycle(seq): while True: - for s in seq: - yield s + yield from seq effects_cycle = iter(cycle(effects)) From dcdeb5fcfcef4a4c2c8e49745915432b96c5a47b Mon Sep 17 00:00:00 2001 From: foamyguy Date: Mon, 7 Oct 2024 09:24:05 -0500 Subject: [PATCH 64/70] remove deprecated get_html_theme_path() call Signed-off-by: foamyguy --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index cbf1452..9993665 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -137,7 +137,6 @@ import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" -html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, From 3e5695361bd05dbf20be03ccb7239bb7b451f64a Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 14 Jan 2025 11:32:34 -0600 Subject: [PATCH 65/70] add sphinx configuration to rtd.yaml Signed-off-by: foamyguy --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b79ec5b..fe4faae 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,6 +8,9 @@ # Required version: 2 +sphinx: + configuration: docs/conf.py + build: os: ubuntu-20.04 tools: From 2aa48460f89a1347991cb05915e7beac9370447d Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 16 Jan 2025 15:33:42 -0600 Subject: [PATCH 66/70] update rtd badge url --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index af6a199..ff8c96e 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Introduction ============ -.. image:: https://readthedocs.org/projects/adafruit-circuitpython-pycamera/badge/?version=latest +.. image:: https://readthedocs.org/projects/pycamera/badge/?version=latest :target: https://docs.circuitpython.org/projects/pycamera/en/latest/ :alt: Documentation Status From 9bccbf87204ce4fc81bdffdb90ba1a059cd72a71 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 16 Jan 2025 15:35:00 -0600 Subject: [PATCH 67/70] update rtd badge url --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ff8c96e..48e9ca9 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Introduction ============ -.. image:: https://readthedocs.org/projects/pycamera/badge/?version=latest +.. image:: https://readthedocs.org/projects/circuitpython-pycamera/badge/?version=latest :target: https://docs.circuitpython.org/projects/pycamera/en/latest/ :alt: Documentation Status From 85ede001e8d62edd250c33d2b68138c7e36f2173 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 17 Jan 2025 15:37:25 -0600 Subject: [PATCH 68/70] last_saved_filename property --- adafruit_pycamera/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 32d8e14..1ef4c94 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -946,6 +946,13 @@ def blit_overlay_into_last_capture(self): del cc565_swapped gc.collect() + @property + def last_saved_filename(self) -> str: + """ + The filename of the last image saved. + """ + return self._last_saved_image_filename + def continuous_capture_start(self): """Switch the camera to continuous-capture mode""" pass # pylint: disable=unnecessary-pass From 95642150fd82cc6bbf83d09c92b0422de3dc0f40 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 16 May 2025 18:48:25 +0000 Subject: [PATCH 69/70] change to ruff --- .gitattributes | 11 + .pre-commit-config.yaml | 43 +-- .pylintrc | 399 ------------------------- README.rst | 6 +- adafruit_pycamera/__init__.py | 88 ++---- adafruit_pycamera/imageprocessing.py | 1 - docs/api.rst | 3 + docs/conf.py | 6 +- examples/basic_camera/code.py | 9 +- examples/camera/boot.py | 1 + examples/camera/code.py | 51 ++-- examples/filter/code.py | 14 +- examples/overlay/code_select.py | 20 +- examples/overlay/code_simple.py | 7 +- examples/qrio/code.py | 4 +- examples/timestamp_filename/code.py | 33 +- examples/viewer/code.py | 12 +- examples/web_camera/code.py | 3 +- examples/web_settings_explorer/code.py | 12 +- ruff.toml | 108 +++++++ 20 files changed, 236 insertions(+), 595 deletions(-) create mode 100644 .gitattributes delete mode 100644 .pylintrc create mode 100644 ruff.toml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..21c125c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +.py text eol=lf +.rst text eol=lf +.txt text eol=lf +.yaml text eol=lf +.toml text eol=lf +.license text eol=lf +.md text eol=lf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 374676d..ff19dde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,42 +1,21 @@ -# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries # # SPDX-License-Identifier: Unlicense repos: - - repo: https://github.com/python/black - rev: 23.3.0 - hooks: - - id: black - - repo: https://github.com/fsfe/reuse-tool - rev: v1.1.2 - hooks: - - id: reuse - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/pycqa/pylint - rev: v3.3.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.4 hooks: - - id: pylint - name: pylint (library code) - types: [python] - args: - - --disable=consider-using-f-string - exclude: "^(docs/|examples/|tests/|setup.py$)" - - id: pylint - name: pylint (example code) - description: Run pylint rules on "examples/*.py" files - types: [python] - files: "^examples/" - args: - - --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code - - id: pylint - name: pylint (test code) - description: Run pylint rules on "tests/*.py" files - types: [python] - files: "^tests/" - args: - - --disable=missing-docstring,consider-using-f-string,duplicate-code + - id: ruff-format + - id: ruff + args: ["--fix"] + - repo: https://github.com/fsfe/reuse-tool + rev: v3.0.1 + hooks: + - id: reuse diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index f945e92..0000000 --- a/.pylintrc +++ /dev/null @@ -1,399 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Add files or directories to the ignore-list. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the ignore-list. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins=pylint.extensions.no_self_use - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -# disable=import-error,raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,deprecated-str-translate-call -disable=raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,import-error,pointless-string-statement,unspecified-encoding - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable= - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -# notes=FIXME,XXX,TODO -notes=FIXME,XXX - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules=board - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,future.builtins - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -# expected-line-ending-format= -expected-line-ending-format=LF - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=yes - -# Minimum lines number of a similarity. -min-similarity-lines=12 - - -[BASIC] - -# Regular expression matching correct argument names -argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Regular expression matching correct attribute names -attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct class names -# class-rgx=[A-Z_][a-zA-Z0-9]+$ -class-rgx=[A-Z_][a-zA-Z0-9_]+$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Regular expression matching correct function names -function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Good variable names which should always be accepted, separated by a comma -# good-names=i,j,k,ex,Run,_ -good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct method names -method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Regular expression matching correct variable names -variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Maximum number of attributes for a class (see R0902). -# max-attributes=7 -max-attributes=11 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=1 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=builtins.Exception diff --git a/README.rst b/README.rst index 48e9ca9..c674acc 100644 --- a/README.rst +++ b/README.rst @@ -17,9 +17,9 @@ Introduction :alt: Build Status -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code Style: Black +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Code Style: Ruff Library for the Adafruit PyCamera diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 1ef4c94..e6d63ac 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: MIT """Library for the Adafruit PyCamera with OV5640 autofocus module""" -# pylint: disable=too-many-lines import gc import os import struct @@ -19,10 +18,10 @@ import adafruit_lis3dh import bitmaptools import board -import displayio -import fourwire import busdisplay +import displayio import espcamera +import fourwire import microcontroller import neopixel import pwmio @@ -80,7 +79,7 @@ _NVM_TIMELAPSE_SUBMODE = const(5) -class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-public-methods +class PyCameraBase: """Base class for PyCamera hardware Wrapper class for the PyCamera hardware with lots of smarts @@ -201,14 +200,14 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub _INIT_SEQUENCE = ( b"\x01\x80\x78" # _SWRESET and Delay 120ms b"\x11\x80\x05" # _SLPOUT and Delay 5ms - b"\x3A\x01\x55" # _COLMOD + b"\x3a\x01\x55" # _COLMOD b"\x21\x00" # _INVON Hack b"\x13\x00" # _NORON - b"\x36\x01\xA0" # _MADCTL + b"\x36\x01\xa0" # _MADCTL b"\x29\x80\x05" # _DISPON and Delay 5ms ) - def __init__(self) -> None: # pylint: disable=too-many-statements + def __init__(self) -> None: displayio.release_displays() self._i2c = board.I2C() self._spi = board.SPI() @@ -273,7 +272,7 @@ def make_debounced_expander_pin(pin_no): pin.switch_to_input() return Debouncer(make_expander_input(pin_no)) - self.up = make_debounced_expander_pin(_AW_UP) # pylint: disable=invalid-name + self.up = make_debounced_expander_pin(_AW_UP) self.left = make_debounced_expander_pin(_AW_LEFT) self.right = make_debounced_expander_pin(_AW_RIGHT) self.down = make_debounced_expander_pin(_AW_DOWN) @@ -313,9 +312,7 @@ def check_for_update_needed(self): def make_camera_ui(self): """Create displayio widgets for the standard camera UI""" - self._sd_label = label.Label( - terminalio.FONT, text="SD ??", color=0x0, x=170, y=10, scale=2 - ) + self._sd_label = label.Label(terminalio.FONT, text="SD ??", color=0x0, x=170, y=10, scale=2) self._effect_label = label.Label( terminalio.FONT, text="EFFECT", color=0xFFFFFF, x=4, y=10, scale=2 ) @@ -323,9 +320,7 @@ def make_camera_ui(self): terminalio.FONT, text="MODE", color=0xFFFFFF, x=170, y=10, scale=2 ) self._topbar = displayio.Group() - self._res_label = label.Label( - terminalio.FONT, text="", color=0xFFFFFF, x=0, y=10, scale=2 - ) + self._res_label = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=0, y=10, scale=2) self._topbar.append(self._res_label) self._topbar.append(self._sd_label) @@ -365,9 +360,7 @@ def init_neopixel(self): neopix.deinit() # front bezel neopixels - self.pixels = neopixel.NeoPixel( - board.A1, 8, brightness=0.1, pixel_order=neopixel.RGBW - ) + self.pixels = neopixel.NeoPixel(board.A1, 8, brightness=0.1, pixel_order=neopixel.RGBW) self.pixels.fill(0) def init_camera(self, init_autofocus=True) -> None: @@ -470,9 +463,7 @@ def autofocus_init_from_bitstream(self, firmware: bytes): reg = offset + 0x8000 arr[0] = reg >> 8 arr[1] = reg & 0xFF - arr[2 : 2 + num_firmware_bytes] = firmware[ - offset : offset + num_firmware_bytes - ] + arr[2 : 2 + num_firmware_bytes] = firmware[offset : offset + num_firmware_bytes] i2c.write(arr, end=2 + num_firmware_bytes) self.write_camera_list(self._finalize_firmware_load) @@ -486,9 +477,7 @@ def autofocus_init_from_bitstream(self, firmware: bytes): def autofocus_init(self): """Initialize the autofocus engine from ov5640_autofocus.bin""" if "/" in __file__: - binfile = ( - __file__.rsplit("/", 1)[0].rsplit(".", 1)[0] + "/ov5640_autofocus.bin" - ) + binfile = __file__.rsplit("/", 1)[0].rsplit(".", 1)[0] + "/ov5640_autofocus.bin" else: binfile = "ov5640_autofocus.bin" print(binfile) @@ -499,7 +488,7 @@ def autofocus_status(self): """Read the camera autofocus status register""" return self.read_camera_register(_OV5640_CMD_FW_STATUS) - def _send_autofocus_command(self, command, msg): # pylint: disable=unused-argument + def _send_autofocus_command(self, command, msg): self.write_camera_register(_OV5640_CMD_ACK, 0x01) # clear command ack self.write_camera_register(_OV5640_CMD_MAIN, command) # send command for _ in range(100): @@ -519,18 +508,14 @@ def autofocus(self) -> list[int]: return [False] * 5 if not self._send_autofocus_command(_OV5640_CMD_TRIGGER_AUTOFOCUS, "autofocus"): return [False] * 5 - zone_focus = [ - self.read_camera_register(_OV5640_CMD_PARA0 + i) for i in range(5) - ] + zone_focus = [self.read_camera_register(_OV5640_CMD_PARA0 + i) for i in range(5)] print(f"zones focused: {zone_focus}") return zone_focus @property def autofocus_vcm_step(self): """Get the voice coil motor step location""" - if not self._send_autofocus_command( - _OV5640_CMD_AF_GET_VCM_STEP, "get vcm step" - ): + if not self._send_autofocus_command(_OV5640_CMD_AF_GET_VCM_STEP, "get vcm step"): return None return self.read_camera_register(_OV5640_CMD_PARA4) @@ -549,7 +534,7 @@ def select_setting(self, setting_name): self._effect_label.background_color = 0x0 self._res_label.color = 0xFFFFFF self._res_label.background_color = 0x0 - if self.mode_text in ("GIF", "GBOY"): + if self.mode_text in {"GIF", "GBOY"}: self._res_label.text = "" else: self._res_label.text = self.resolutions[self._resolution] @@ -601,7 +586,7 @@ def mode(self, setting): self._mode_label.text = self.modes[setting] if self.modes[setting] == "STOP": self.stop_motion_frame = 0 - if self.modes[setting] in ("GIF", "GBOY"): + if self.modes[setting] in {"GIF", "GBOY"}: self._res_label.text = "" else: self.resolution = self.resolution # kick it to reset the display @@ -656,9 +641,7 @@ def timelapse_rate(self, setting): if self.timelapse_rates[setting] < 60: self.timelapse_rate_label.text = "%d S" % self.timelapse_rates[setting] else: - self.timelapse_rate_label.text = "%d M" % ( - self.timelapse_rates[setting] / 60 - ) + self.timelapse_rate_label.text = "%d M" % (self.timelapse_rates[setting] / 60) microcontroller.nvm[_NVM_TIMELAPSE_RATE] = setting self.display.refresh() @@ -669,13 +652,9 @@ def timelapse_submode(self): @timelapse_submode.setter def timelapse_submode(self, setting): - setting = (setting + len(self.timelapse_submodes)) % len( - self.timelapse_submodes - ) + setting = (setting + len(self.timelapse_submodes)) % len(self.timelapse_submodes) self._timelapse_submode = setting - self.timelapse_submode_label.text = self.timelapse_submodes[ - self._timelapse_submode - ] + self.timelapse_submode_label.text = self.timelapse_submodes[self._timelapse_submode] microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE] = setting def init_display(self): @@ -800,9 +779,7 @@ def keys_debounce(self): def tone(self, frequency, duration=0.1): """Play a tone on the internal speaker""" - with pwmio.PWMOut( - board.SPEAKER, frequency=int(frequency), variable_frequency=False - ) as pwm: + with pwmio.PWMOut(board.SPEAKER, frequency=int(frequency), variable_frequency=False) as pwm: self.mute.value = True pwm.duty_cycle = 0x8000 time.sleep(duration) @@ -874,10 +851,9 @@ def overlay(self) -> str: @overlay.setter def overlay(self, new_overlay_file: str) -> None: - # pylint: disable=import-outside-toplevel - from displayio import ColorConverter, Colorspace - import ulab.numpy as np import adafruit_imageload + import ulab.numpy as np + from displayio import ColorConverter, Colorspace if self.overlay_bmp is not None: self.overlay_bmp.deinit() @@ -891,7 +867,6 @@ def overlay(self, new_overlay_file: str) -> None: del arr def _init_jpeg_decoder(self): - # pylint: disable=import-outside-toplevel from jpegio import JpegDecoder """ @@ -908,10 +883,7 @@ def blit_overlay_into_last_capture(self): in a separate but similarly named .bmp file on the SDCard. """ if self.overlay_bmp is None: - raise ValueError( - "Must set overlay before calling blit_overlay_into_last_capture" - ) - # pylint: disable=import-outside-toplevel + raise ValueError("Must set overlay before calling blit_overlay_into_last_capture") from adafruit_bitmapsaver import save_pixels from displayio import Bitmap, ColorConverter, Colorspace @@ -955,7 +927,7 @@ def last_saved_filename(self) -> str: def continuous_capture_start(self): """Switch the camera to continuous-capture mode""" - pass # pylint: disable=unnecessary-pass + pass def capture_into_jpeg(self): """Captures an image and returns it in JPEG format. @@ -997,7 +969,6 @@ def blit(self, bitmap, x_offset=0, y_offset=32): The default preview capture is 240x176, leaving 32 pixel rows at the top and bottom for status information. """ - # pylint: disable=import-outside-toplevel from displayio import Bitmap if self.overlay_bmp is not None: @@ -1025,9 +996,7 @@ def blit(self, bitmap, x_offset=0, y_offset=32): self._display_bus.send( 42, struct.pack(">hh", 80 + x_offset, 80 + x_offset + bitmap.width - 1) ) - self._display_bus.send( - 43, struct.pack(">hh", y_offset, y_offset + bitmap.height - 1) - ) + self._display_bus.send(43, struct.pack(">hh", y_offset, y_offset + bitmap.height - 1)) self._display_bus.send(44, bitmap) @property @@ -1066,8 +1035,7 @@ def get_camera_autosettings(self): + (self.read_camera_register(0x3502) >> 4) ) white_balance = [ - self.read_camera_register(x) - for x in (0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405) + self.read_camera_register(x) for x in (0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405) ] settings = { @@ -1147,6 +1115,6 @@ def __init__(self, init_autofocus=True): try: self.mount_sd_card() - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: # No SD card inserted, it's OK print(exc) diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index 98932aa..f218f08 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -84,7 +84,6 @@ def ironbow(bitmap, mask=None): return bitmapfilter.false_color(bitmap, ironbow_palette, mask=mask) -# pylint: disable=invalid-name def alphablend_maker(frac, nfrac=None): """Create an alpha-blending function for a specific fractional value diff --git a/docs/api.rst b/docs/api.rst index 4e86a8c..ac61409 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,6 +4,9 @@ .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" +API Reference +############# + .. automodule:: adafruit_pycamera :members: .. automodule:: adafruit_pycamera.imageprocessing diff --git a/docs/conf.py b/docs/conf.py index 9993665..2ea50a3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # # SPDX-License-Identifier: MIT @@ -75,9 +73,7 @@ creation_year = "2023" current_year = str(datetime.datetime.now().year) year_duration = ( - current_year - if current_year == creation_year - else creation_year + " - " + current_year + current_year if current_year == creation_year else creation_year + " - " + current_year ) copyright = year_duration + " Jeff Epler for Adafruit Industries" author = "Jeff Epler for Adafruit Industries" diff --git a/examples/basic_camera/code.py b/examples/basic_camera/code.py index a58b4a3..ef2fd87 100644 --- a/examples/basic_camera/code.py +++ b/examples/basic_camera/code.py @@ -1,10 +1,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries # # SPDX-License-Identifier: MIT -""" simple point-and-shoot camera example. No bells! Zero whistles! """ +"""simple point-and-shoot camera example. No bells! Zero whistles!""" import time -import adafruit_pycamera # pylint: disable=import-error + +import adafruit_pycamera pycam = adafruit_pycamera.PyCamera() pycam.mode = 0 # only mode 0 (JPEG) will work in this example @@ -36,11 +37,11 @@ pycam.display_message("snap", color=0x00DD00) pycam.capture_jpeg() pycam.live_preview_mode() - except TypeError as exception: + except TypeError: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() - except RuntimeError as exception: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) diff --git a/examples/camera/boot.py b/examples/camera/boot.py index 84e2250..4106fc6 100644 --- a/examples/camera/boot.py +++ b/examples/camera/boot.py @@ -6,6 +6,7 @@ """Automatically create the /sd mount point at boot time""" import os + import storage storage.remount("/", readonly=False) diff --git a/examples/camera/code.py b/examples/camera/code.py index a03ea64..8c89f35 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -2,18 +2,19 @@ # SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: Unlicense -import ssl import os +import ssl import time -import socketpool -import adafruit_requests -import rtc + import adafruit_ntp -import wifi +import adafruit_requests import bitmaptools import displayio import gifio +import rtc +import socketpool import ulab.numpy as np +import wifi import adafruit_pycamera @@ -31,9 +32,7 @@ PASSWORD = os.getenv("CIRCUITPY_WIFI_PASSWORD") if SSID and PASSWORD: - wifi.radio.connect( - os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") - ) + wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")) if wifi.radio.connected: print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") print("My IP address is", wifi.radio.ipv4_address) @@ -46,9 +45,7 @@ UTC_OFFSET = response_as_json["raw_offset"] + response_as_json["dst_offset"] print(f"UTC_OFFSET: {UTC_OFFSET}") - ntp = adafruit_ntp.NTP( - pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600 - ) + ntp = adafruit_ntp.NTP(pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600) print(f"ntp time: {ntp.datetime}") rtc.RTC().datetime = ntp.datetime @@ -103,13 +100,9 @@ pycam.timelapse_submode_label.text = pycam.timelapse_submode_label.text # only in high power mode do we continuously preview - if (timelapse_remaining is None) or ( - pycam.timelapse_submode_label.text == "HiPwr" - ): + if (timelapse_remaining is None) or (pycam.timelapse_submode_label.text == "HiPwr"): pycam.blit(pycam.continuous_capture()) - if pycam.timelapse_submode_label.text == "LowPwr" and ( - timelapse_remaining is not None - ): + if pycam.timelapse_submode_label.text == "LowPwr" and (timelapse_remaining is not None): pycam.display.brightness = 0.05 else: pycam.display.brightness = 1 @@ -122,18 +115,16 @@ try: pycam.display_message("Snap!", color=0x0000FF) pycam.capture_jpeg() - except TypeError as e: + except TypeError: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) - except RuntimeError as e: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() pycam.display.refresh() pycam.blit(pycam.continuous_capture()) - timelapse_timestamp = ( - time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 - ) + timelapse_timestamp = time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 else: pycam.blit(pycam.continuous_capture()) # print("\t\t", capture_time, blit_time) @@ -153,10 +144,10 @@ try: pycam.display_message("Snap!", color=0x0000FF) pycam.capture_jpeg() - except TypeError as e: + except TypeError: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) - except RuntimeError as e: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() @@ -164,7 +155,7 @@ if pycam.mode_text == "GBOY": try: f = pycam.open_next_image("gif") - except RuntimeError as e: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) continue @@ -181,13 +172,13 @@ if pycam.mode_text == "GIF": try: f = pycam.open_next_image("gif") - except RuntimeError as e: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) continue i = 0 ft = [] - pycam._mode_label.text = "RECORDING" # pylint: disable=protected-access + pycam._mode_label.text = "RECORDING" pycam.display.refresh() with gifio.GifWriter( @@ -207,7 +198,7 @@ ft.append(1 / (t1 - t0)) print(end=".") t0 = t1 - pycam._mode_label.text = "GIF" # pylint: disable=protected-access + pycam._mode_label.text = "GIF" print(f"\nfinal size {f.tell()} for {i} frames") print(f"average framerate {i / (t1 - t00)}fps") print(f"best {max(ft)} worst {min(ft)} std. deviation {np.std(ft)}") @@ -220,11 +211,11 @@ pycam.display_message("Snap!", color=0x0000FF) pycam.capture_jpeg() pycam.live_preview_mode() - except TypeError as e: + except TypeError: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() - except RuntimeError as e: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) diff --git a/examples/filter/code.py b/examples/filter/code.py index 207b55f..aacf487 100644 --- a/examples/filter/code.py +++ b/examples/filter/code.py @@ -11,16 +11,14 @@ Otherwise, effects cycle every DISPLAY_INTERVAL milliseconds (default 2000 = 2 seconds) """ +import bitmapfilter import displayio -from jpegio import JpegDecoder from adafruit_display_text.label import Label -from adafruit_ticks import ticks_less, ticks_ms, ticks_add, ticks_diff +from adafruit_ticks import ticks_add, ticks_diff, ticks_less, ticks_ms from font_free_mono_bold_24 import FONT -import bitmapfilter - -from adafruit_pycamera import imageprocessing -from adafruit_pycamera import PyCameraBase +from jpegio import JpegDecoder +from adafruit_pycamera import PyCameraBase, imageprocessing blend_50_50 = bitmapfilter.blend_precompute(imageprocessing.alphablend_maker(0.5)) screen = bitmapfilter.blend_precompute(imageprocessing.screen_func) @@ -130,9 +128,7 @@ def sketch(b): ("solarize", bitmapfilter.solarize), ( "swap r/b", - lambda b: bitmapfilter.mix( - b, bitmapfilter.ChannelMixer(0, 0, 1, 0, 1, 0, 1, 0, 0) - ), + lambda b: bitmapfilter.mix(b, bitmapfilter.ChannelMixer(0, 0, 1, 0, 1, 0, 1, 0, 0)), ), ] diff --git a/examples/overlay/code_select.py b/examples/overlay/code_select.py index 665d4e2..6735c53 100644 --- a/examples/overlay/code_select.py +++ b/examples/overlay/code_select.py @@ -2,14 +2,16 @@ # SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT -""" simple point-and-shoot camera example, with overly selecting using select button. +"""simple point-and-shoot camera example, with overly selecting using select button. Place all overlay files inside /sd/overlays/ directory. """ + import os import time import traceback -import adafruit_pycamera # pylint: disable=import-error + +import adafruit_pycamera MODE_POSITION = 0 MODE_SCALE = 1 @@ -57,19 +59,13 @@ if CURRENT_MODE == MODE_POSITION: if not pycam.down.value: - pycam.overlay_position[1] += 1 * ( - int(pycam.down.current_duration / 0.3) + 1 - ) + pycam.overlay_position[1] += 1 * (int(pycam.down.current_duration / 0.3) + 1) if not pycam.up.value: pycam.overlay_position[1] -= 1 * (int(pycam.up.current_duration / 0.3) + 1) if not pycam.left.value: - pycam.overlay_position[0] -= 1 * ( - int(pycam.left.current_duration / 0.3) + 1 - ) + pycam.overlay_position[0] -= 1 * (int(pycam.left.current_duration / 0.3) + 1) if not pycam.right.value: - pycam.overlay_position[0] += 1 * ( - int(pycam.right.current_duration / 0.3) + 1 - ) + pycam.overlay_position[0] += 1 * (int(pycam.right.current_duration / 0.3) + 1) if CURRENT_MODE == MODE_SCALE: if pycam.down.fell: int_scale -= 10 @@ -98,7 +94,7 @@ pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() - except RuntimeError as exception: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) diff --git a/examples/overlay/code_simple.py b/examples/overlay/code_simple.py index ffcea6a..4bd821f 100644 --- a/examples/overlay/code_simple.py +++ b/examples/overlay/code_simple.py @@ -2,11 +2,12 @@ # SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT -""" simple point-and-shoot camera example, with an overlay frame image. """ +"""simple point-and-shoot camera example, with an overlay frame image.""" import time import traceback -import adafruit_pycamera # pylint: disable=import-error + +import adafruit_pycamera pycam = adafruit_pycamera.PyCamera() pycam.mode = 0 # only mode 0 (JPEG) will work in this example @@ -47,7 +48,7 @@ pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() - except RuntimeError as exception: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) diff --git a/examples/qrio/code.py b/examples/qrio/code.py index bc04fb3..026106f 100644 --- a/examples/qrio/code.py +++ b/examples/qrio/code.py @@ -14,8 +14,8 @@ from adafruit_pycamera import PyCamera pycam = PyCamera() -pycam._mode_label.text = "QR SCAN" # pylint: disable=protected-access -pycam._res_label.text = "" # pylint: disable=protected-access +pycam._mode_label.text = "QR SCAN" +pycam._res_label.text = "" pycam.effect = 0 pycam.camera.hmirror = False pycam.display.refresh() diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py index 05c2a2e..3dfebcc 100644 --- a/examples/timestamp_filename/code.py +++ b/examples/timestamp_filename/code.py @@ -1,18 +1,20 @@ # SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT -""" simple point-and-shoot camera example. With NTP and internal RTC to - add timestamp to photo filenames. Must install adafruit_ntp library! - Example code assumes WIFI credentials are properly setup and web workflow - enabled in settings.toml. If not, you'll need to add code to manually connect - to your network.""" +"""simple point-and-shoot camera example. With NTP and internal RTC to +add timestamp to photo filenames. Must install adafruit_ntp library! +Example code assumes WIFI credentials are properly setup and web workflow +enabled in settings.toml. If not, you'll need to add code to manually connect +to your network.""" import time -import wifi -import socketpool -import rtc + import adafruit_ntp -import adafruit_pycamera # pylint: disable=import-error +import rtc +import socketpool +import wifi + +import adafruit_pycamera pool = socketpool.SocketPool(wifi.radio) ntp = adafruit_ntp.NTP(pool, tz_offset=0) @@ -46,21 +48,14 @@ pycam.tone(1600, 0.05) try: pycam.display_message("snap", color=0x00DD00) - timestamp = "img_{}-{}-{}_{:02}-{:02}-{:02}_".format( - time.localtime().tm_year, - time.localtime().tm_mon, - time.localtime().tm_mday, - time.localtime().tm_hour, - time.localtime().tm_min, - time.localtime().tm_sec, - ) + timestamp = f"img_{time.localtime().tm_year}-{time.localtime().tm_mon}-{time.localtime().tm_mday}_{time.localtime().tm_hour:02}-{time.localtime().tm_min:02}-{time.localtime().tm_sec:02}_" # noqa: E501 pycam.capture_jpeg(filename_prefix=timestamp) pycam.live_preview_mode() - except TypeError as exception: + except TypeError: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() - except RuntimeError as exception: + except RuntimeError: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) diff --git a/examples/viewer/code.py b/examples/viewer/code.py index 51abc6b..db7d387 100644 --- a/examples/viewer/code.py +++ b/examples/viewer/code.py @@ -12,11 +12,13 @@ Otherwise, images cycle every DISPLAY_INTERVAL milliseconds (default 8000 = 8 seconds) """ -import time import os +import time + import displayio +from adafruit_ticks import ticks_add, ticks_diff, ticks_less, ticks_ms from jpegio import JpegDecoder -from adafruit_ticks import ticks_less, ticks_ms, ticks_add, ticks_diff + from adafruit_pycamera import PyCameraBase DISPLAY_INTERVAL = 8000 # milliseconds @@ -83,9 +85,7 @@ def mount_sd(): pycam.display_message("SD Card\nFailed!", color=0xFF0000) time.sleep(0.5) all_images = [ - f"/sd/{filename}" - for filename in os.listdir("/sd") - if filename.lower().endswith(".jpg") + f"/sd/{filename}" for filename in os.listdir("/sd") if filename.lower().endswith(".jpg") ] pycam.display_message(f"Found {len(all_images)}\nimages", color=0xFFFFFF) time.sleep(0.5) @@ -143,7 +143,7 @@ def main(): image_counter = (image_counter + 1) % len(all_images) try: load_resized_image(bitmap, filename) - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: pycam.display_message(f"Failed to read\n{filename}", color=0xFF0000) print(e) deadline = ticks_add(now, 500) diff --git a/examples/web_camera/code.py b/examples/web_camera/code.py index 55cc82b..4b0d777 100644 --- a/examples/web_camera/code.py +++ b/examples/web_camera/code.py @@ -51,8 +51,7 @@ def body(): request, body, headers={ - "Content-Type": "multipart/x-mixed-replace; boundary=%s" - % BOUNDARY.decode("ascii") + "Content-Type": "multipart/x-mixed-replace; boundary=%s" % BOUNDARY.decode("ascii") }, ) diff --git a/examples/web_settings_explorer/code.py b/examples/web_settings_explorer/code.py index 696e584..e6f3cd4 100644 --- a/examples/web_settings_explorer/code.py +++ b/examples/web_settings_explorer/code.py @@ -69,17 +69,13 @@ def lcd(request: Request) -> Response: def take_jpeg(request: Request) -> Response: pycam.camera.reconfigure( pixel_format=espcamera.PixelFormat.JPEG, - frame_size=pycam.resolution_to_frame_size[ - pycam._resolution # pylint: disable=protected-access - ], + frame_size=pycam.resolution_to_frame_size[pycam._resolution], ) try: jpeg = pycam.camera.take(1) if jpeg is not None: return Response(request, bytes(jpeg), content_type="image/jpeg") - return Response( - request, "", content_type="text/plain", status=INTERNAL_SERVER_ERROR_500 - ) + return Response(request, "", content_type="text/plain", status=INTERNAL_SERVER_ERROR_500) finally: pycam.live_preview_mode() @@ -109,13 +105,13 @@ def property_common(obj, request): try: current_value = getattr(obj, propname, None) return JSONResponse(request, current_value) - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: return Response(request, {"error": str(exc)}, status=BAD_REQUEST_400) else: new_value = json.loads(value) setattr(obj, propname, new_value) return JSONResponse(request, {"status": "OK"}) - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: return JSONResponse(request, {"error": str(exc)}, status=BAD_REQUEST_400) diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..73e9efc --- /dev/null +++ b/ruff.toml @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +target-version = "py38" +line-length = 100 + +[lint] +preview = true +select = ["I", "PL", "UP"] + +extend-select = [ + "D419", # empty-docstring + "E501", # line-too-long + "W291", # trailing-whitespace + "PLC0414", # useless-import-alias + "PLC2401", # non-ascii-name + "PLC2801", # unnecessary-dunder-call + "PLC3002", # unnecessary-direct-lambda-call + "E999", # syntax-error + "PLE0101", # return-in-init + "F706", # return-outside-function + "F704", # yield-outside-function + "PLE0116", # continue-in-finally + "PLE0117", # nonlocal-without-binding + "PLE0241", # duplicate-bases + "PLE0302", # unexpected-special-method-signature + "PLE0604", # invalid-all-object + "PLE0605", # invalid-all-format + "PLE0643", # potential-index-error + "PLE0704", # misplaced-bare-raise + "PLE1141", # dict-iter-missing-items + "PLE1142", # await-outside-async + "PLE1205", # logging-too-many-args + "PLE1206", # logging-too-few-args + "PLE1307", # bad-string-format-type + "PLE1310", # bad-str-strip-call + "PLE1507", # invalid-envvar-value + "PLE2502", # bidirectional-unicode + "PLE2510", # invalid-character-backspace + "PLE2512", # invalid-character-sub + "PLE2513", # invalid-character-esc + "PLE2514", # invalid-character-nul + "PLE2515", # invalid-character-zero-width-space + "PLR0124", # comparison-with-itself + "PLR0202", # no-classmethod-decorator + "PLR0203", # no-staticmethod-decorator + "UP004", # useless-object-inheritance + "PLR0206", # property-with-parameters + "PLR0904", # too-many-public-methods + "PLR0911", # too-many-return-statements + "PLR0912", # too-many-branches + "PLR0913", # too-many-arguments + "PLR0914", # too-many-locals + "PLR0915", # too-many-statements + "PLR0916", # too-many-boolean-expressions + "PLR1702", # too-many-nested-blocks + "PLR1704", # redefined-argument-from-local + "PLR1711", # useless-return + "C416", # unnecessary-comprehension + "PLR1733", # unnecessary-dict-index-lookup + "PLR1736", # unnecessary-list-index-lookup + + # ruff reports this rule is unstable + #"PLR6301", # no-self-use + + "PLW0108", # unnecessary-lambda + "PLW0120", # useless-else-on-loop + "PLW0127", # self-assigning-variable + "PLW0129", # assert-on-string-literal + "B033", # duplicate-value + "PLW0131", # named-expr-without-context + "PLW0245", # super-without-brackets + "PLW0406", # import-self + "PLW0602", # global-variable-not-assigned + "PLW0603", # global-statement + "PLW0604", # global-at-module-level + + # fails on the try: import typing used by libraries + #"F401", # unused-import + + "F841", # unused-variable + "E722", # bare-except + "PLW0711", # binary-op-exception + "PLW1501", # bad-open-mode + "PLW1508", # invalid-envvar-default + "PLW1509", # subprocess-popen-preexec-fn + "PLW2101", # useless-with-lock + "PLW3301", # nested-min-max +] + +ignore = [ + "PLR2004", # magic-value-comparison + "UP030", # format literals + "PLW1514", # unspecified-encoding + "PLR0913", # too-many-arguments + "PLR0915", # too-many-statements + "PLR0917", # too-many-positional-arguments + "PLR0904", # too-many-public-methods + "PLR0912", # too-many-branches + "PLR0916", # too-many-boolean-expressions + "PLR6301", # could-be-static no-self-use + "PLC0415", # import outside toplevel + "PLC2701", # private import +] + +[format] +line-ending = "lf" From 31c5564f648ede40dcce9a9467e35f120ddf484f Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 4 Jun 2025 10:00:20 -0500 Subject: [PATCH 70/70] update rtd.yml file Signed-off-by: foamyguy --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index fe4faae..ee38fa0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,7 +12,7 @@ sphinx: configuration: docs/conf.py build: - os: ubuntu-20.04 + os: ubuntu-lts-latest tools: python: "3"