|
3 | 3 | #
|
4 | 4 | # SPDX-License-Identifier: MIT
|
5 | 5 | """Library for the Adafruit PyCamera with OV5640 autofocus module"""
|
6 |
| - |
| 6 | +# pylint: disable=too-many-lines |
| 7 | +import gc |
7 | 8 | import os
|
8 | 9 | import struct
|
9 | 10 | import time
|
|
32 | 33 | from adafruit_display_text import label
|
33 | 34 | from digitalio import DigitalInOut, Pull
|
34 | 35 | from rainbowio import colorwheel
|
| 36 | +from displayio import Bitmap, ColorConverter, Colorspace |
| 37 | +from jpegio import JpegDecoder |
| 38 | +import ulab.numpy as np |
| 39 | +from adafruit_bitmapsaver import save_pixels |
| 40 | +import adafruit_imageload |
35 | 41 |
|
36 | 42 | __version__ = "0.0.0-auto.0"
|
37 | 43 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyCamera.git"
|
@@ -147,7 +153,7 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub
|
147 | 153 | espcamera.FrameSize.QVGA, # 320x240
|
148 | 154 | # espcamera.FrameSize.CIF, # 400x296
|
149 | 155 | # espcamera.FrameSize.HVGA, # 480x320
|
150 |
| - espcamera.FrameSize.VGA, # 640x480 |
| 156 | + espcamera.FrameSize.VGA, # 640x480 |
151 | 157 | espcamera.FrameSize.SVGA, # 800x600
|
152 | 158 | espcamera.FrameSize.XGA, # 1024x768
|
153 | 159 | espcamera.FrameSize.HD, # 1280x720
|
@@ -230,6 +236,12 @@ def __init__(self) -> None: # pylint: disable=too-many-statements
|
230 | 236 | self.display = None
|
231 | 237 | self.pixels = None
|
232 | 238 | self.sdcard = None
|
| 239 | + self._last_saved_image_filename = None |
| 240 | + self.decoder = None |
| 241 | + self._overlay = None |
| 242 | + self.overlay_transparency_color = None |
| 243 | + self.overlay_bmp = None |
| 244 | + self.combined_bmp = None |
233 | 245 | self.splash = displayio.Group()
|
234 | 246 |
|
235 | 247 | # Reset display and I/O expander
|
@@ -797,6 +809,7 @@ def open_next_image(self, extension="jpg"):
|
797 | 809 | os.stat(filename)
|
798 | 810 | except OSError:
|
799 | 811 | break
|
| 812 | + self._last_saved_image_filename = filename |
800 | 813 | print("Writing to", filename)
|
801 | 814 | return open(filename, "wb")
|
802 | 815 |
|
@@ -827,6 +840,78 @@ def capture_jpeg(self):
|
827 | 840 | else:
|
828 | 841 | print("# frame capture failed")
|
829 | 842 |
|
| 843 | + @property |
| 844 | + def overlay(self) -> str: |
| 845 | + """ |
| 846 | + The overlay file to be used. A filepath string that points |
| 847 | + to a .bmp file that has 24bit RGB888 Colorspace. |
| 848 | + The overlay image will be shown in the camera preview, |
| 849 | + and combined to create a modified version of the |
| 850 | + final photo. |
| 851 | + """ |
| 852 | + return self._overlay |
| 853 | + |
| 854 | + @overlay.setter |
| 855 | + def overlay(self, new_overlay_file: str) -> None: |
| 856 | + if self.overlay_bmp is not None: |
| 857 | + self.overlay_bmp.deinit() |
| 858 | + self._overlay = new_overlay_file |
| 859 | + cc888 = ColorConverter(input_colorspace=Colorspace.RGB888) |
| 860 | + self.overlay_bmp, _ = adafruit_imageload.load(new_overlay_file, palette=cc888) |
| 861 | + |
| 862 | + arr = np.frombuffer(self.overlay_bmp, dtype=np.uint16) |
| 863 | + arr.byteswap(inplace=True) |
| 864 | + |
| 865 | + del arr |
| 866 | + |
| 867 | + def _init_jpeg_decoder(self): |
| 868 | + """ |
| 869 | + Initialize the JpegDecoder if it hasn't been already. |
| 870 | + Only needed if overlay is used. |
| 871 | + """ |
| 872 | + if self.decoder is None: |
| 873 | + self.decoder = JpegDecoder() |
| 874 | + |
| 875 | + def blit_overlay_into_last_capture(self): |
| 876 | + """ |
| 877 | + Create a modified version of the last photo taken that pastes |
| 878 | + the overlay image on top of the photo and saves the new version |
| 879 | + in a separate but similarly named .bmp file on the SDCard. |
| 880 | + """ |
| 881 | + if self.overlay_bmp is None: |
| 882 | + raise ValueError( |
| 883 | + "Must set overlay before calling blit_overlay_into_last_capture" |
| 884 | + ) |
| 885 | + |
| 886 | + self._init_jpeg_decoder() |
| 887 | + |
| 888 | + width, height = self.decoder.open(self._last_saved_image_filename) |
| 889 | + photo_bitmap = Bitmap(width, height, 65535) |
| 890 | + |
| 891 | + self.decoder.decode(photo_bitmap, scale=0, x=0, y=0) |
| 892 | + |
| 893 | + bitmaptools.blit( |
| 894 | + photo_bitmap, |
| 895 | + self.overlay_bmp, |
| 896 | + 0, |
| 897 | + 0, |
| 898 | + skip_source_index=self.overlay_transparency_color, |
| 899 | + skip_dest_index=None, |
| 900 | + ) |
| 901 | + |
| 902 | + cc565_swapped = ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED) |
| 903 | + save_pixels( |
| 904 | + self._last_saved_image_filename.replace(".jpg", "_modified.bmp"), |
| 905 | + photo_bitmap, |
| 906 | + cc565_swapped, |
| 907 | + ) |
| 908 | + |
| 909 | + # RAM cleanup |
| 910 | + photo_bitmap.deinit() |
| 911 | + del photo_bitmap |
| 912 | + del cc565_swapped |
| 913 | + gc.collect() |
| 914 | + |
830 | 915 | def continuous_capture_start(self):
|
831 | 916 | """Switch the camera to continuous-capture mode"""
|
832 | 917 | pass # pylint: disable=unnecessary-pass
|
@@ -872,6 +957,20 @@ def blit(self, bitmap, x_offset=0, y_offset=32):
|
872 | 957 | for status information.
|
873 | 958 | """
|
874 | 959 |
|
| 960 | + if self.overlay_bmp is not None: |
| 961 | + if self.combined_bmp is None: |
| 962 | + self.combined_bmp = Bitmap(bitmap.width, bitmap.height, 65535) |
| 963 | + |
| 964 | + bitmaptools.blit(self.combined_bmp, bitmap, 0, 0) |
| 965 | + |
| 966 | + bitmaptools.rotozoom( |
| 967 | + self.combined_bmp, |
| 968 | + self.overlay_bmp, |
| 969 | + scale=0.75, |
| 970 | + skip_index=self.overlay_transparency_color, |
| 971 | + ) |
| 972 | + bitmap = self.combined_bmp |
| 973 | + |
875 | 974 | self._display_bus.send(
|
876 | 975 | 42, struct.pack(">hh", 80 + x_offset, 80 + x_offset + bitmap.width - 1)
|
877 | 976 | )
|
|
0 commit comments