diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 6b3309d..d019cf5 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -5,7 +5,7 @@ """Library for the Adafruit PyCamera with OV5640 autofocus module""" # pylint: disable=too-many-lines - +import gc import os import struct import time @@ -149,7 +149,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 @@ -232,6 +232,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 @@ -827,6 +833,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") @@ -857,6 +864,89 @@ 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: + # 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 + 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): + # pylint: disable=import-outside-toplevel + from jpegio import JpegDecoder + + """ + 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" + ) + # pylint: disable=import-outside-toplevel + from adafruit_bitmapsaver import save_pixels + from displayio import Bitmap, ColorConverter, Colorspace + + 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 @@ -901,6 +991,22 @@ 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: + 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/docs/conf.py b/docs/conf.py index 3b6c67e..cbf1452 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,6 +41,7 @@ "digitalio", "espcamera", "fourwire", + "jpegio", "micropython", "neopixel", "sdcardio", diff --git a/docs/mock/displayio.py b/docs/mock/displayio.py index b4fd1c5..f18251d 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 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_simple.py b/examples/overlay/code_simple.py new file mode 100644 index 0000000..ffcea6a --- /dev/null +++ b/examples/overlay/code_simple.py @@ -0,0 +1,74 @@ +# 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 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 0000000..eab3d0a Binary files /dev/null and b/examples/overlay/heart_frame_rgb888.bmp differ 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 0000000..e92fc9a Binary files /dev/null and b/examples/overlay/pencil_frame_rgb888.bmp differ 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 diff --git a/optional_requirements.txt b/optional_requirements.txt index d4e27c4..42b579d 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -1,3 +1,6 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense + +adafruit-circuitpython-bitmapsaver +adafruit-circuitpython-imageload