|
5 | 5 | """Library for the Adafruit PyCamera with OV5640 autofocus module""" |
6 | 6 |
|
7 | 7 | # pylint: disable=too-many-lines |
8 | | - |
| 8 | +import gc |
9 | 9 | import os |
10 | 10 | import struct |
11 | 11 | import time |
@@ -149,7 +149,7 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub |
149 | 149 | espcamera.FrameSize.QVGA, # 320x240 |
150 | 150 | # espcamera.FrameSize.CIF, # 400x296 |
151 | 151 | # espcamera.FrameSize.HVGA, # 480x320 |
152 | | - espcamera.FrameSize.VGA, # 640x480 |
| 152 | + espcamera.FrameSize.VGA, # 640x480 |
153 | 153 | espcamera.FrameSize.SVGA, # 800x600 |
154 | 154 | espcamera.FrameSize.XGA, # 1024x768 |
155 | 155 | espcamera.FrameSize.HD, # 1280x720 |
@@ -232,6 +232,12 @@ def __init__(self) -> None: # pylint: disable=too-many-statements |
232 | 232 | self.display = None |
233 | 233 | self.pixels = None |
234 | 234 | self.sdcard = None |
| 235 | + self._last_saved_image_filename = None |
| 236 | + self.decoder = None |
| 237 | + self._overlay = None |
| 238 | + self.overlay_transparency_color = None |
| 239 | + self.overlay_bmp = None |
| 240 | + self.combined_bmp = None |
235 | 241 | self.splash = displayio.Group() |
236 | 242 |
|
237 | 243 | # Reset display and I/O expander |
@@ -827,6 +833,7 @@ def open_next_image(self, extension="jpg"): |
827 | 833 | os.stat(filename) |
828 | 834 | except OSError: |
829 | 835 | break |
| 836 | + self._last_saved_image_filename = filename |
830 | 837 | print("Writing to", filename) |
831 | 838 | return open(filename, "wb") |
832 | 839 |
|
@@ -857,6 +864,89 @@ def capture_jpeg(self): |
857 | 864 | else: |
858 | 865 | print("# frame capture failed") |
859 | 866 |
|
| 867 | + @property |
| 868 | + def overlay(self) -> str: |
| 869 | + """ |
| 870 | + The overlay file to be used. A filepath string that points |
| 871 | + to a .bmp file that has 24bit RGB888 Colorspace. |
| 872 | + The overlay image will be shown in the camera preview, |
| 873 | + and combined to create a modified version of the |
| 874 | + final photo. |
| 875 | + """ |
| 876 | + return self._overlay |
| 877 | + |
| 878 | + @overlay.setter |
| 879 | + def overlay(self, new_overlay_file: str) -> None: |
| 880 | + # pylint: disable=import-outside-toplevel |
| 881 | + from displayio import ColorConverter, Colorspace |
| 882 | + import ulab.numpy as np |
| 883 | + import adafruit_imageload |
| 884 | + |
| 885 | + if self.overlay_bmp is not None: |
| 886 | + self.overlay_bmp.deinit() |
| 887 | + self._overlay = new_overlay_file |
| 888 | + cc888 = ColorConverter(input_colorspace=Colorspace.RGB888) |
| 889 | + self.overlay_bmp, _ = adafruit_imageload.load(new_overlay_file, palette=cc888) |
| 890 | + |
| 891 | + arr = np.frombuffer(self.overlay_bmp, dtype=np.uint16) |
| 892 | + arr.byteswap(inplace=True) |
| 893 | + |
| 894 | + del arr |
| 895 | + |
| 896 | + def _init_jpeg_decoder(self): |
| 897 | + # pylint: disable=import-outside-toplevel |
| 898 | + from jpegio import JpegDecoder |
| 899 | + |
| 900 | + """ |
| 901 | + Initialize the JpegDecoder if it hasn't been already. |
| 902 | + Only needed if overlay is used. |
| 903 | + """ |
| 904 | + if self.decoder is None: |
| 905 | + self.decoder = JpegDecoder() |
| 906 | + |
| 907 | + def blit_overlay_into_last_capture(self): |
| 908 | + """ |
| 909 | + Create a modified version of the last photo taken that pastes |
| 910 | + the overlay image on top of the photo and saves the new version |
| 911 | + in a separate but similarly named .bmp file on the SDCard. |
| 912 | + """ |
| 913 | + if self.overlay_bmp is None: |
| 914 | + raise ValueError( |
| 915 | + "Must set overlay before calling blit_overlay_into_last_capture" |
| 916 | + ) |
| 917 | + # pylint: disable=import-outside-toplevel |
| 918 | + from adafruit_bitmapsaver import save_pixels |
| 919 | + from displayio import Bitmap, ColorConverter, Colorspace |
| 920 | + |
| 921 | + self._init_jpeg_decoder() |
| 922 | + |
| 923 | + width, height = self.decoder.open(self._last_saved_image_filename) |
| 924 | + photo_bitmap = Bitmap(width, height, 65535) |
| 925 | + |
| 926 | + self.decoder.decode(photo_bitmap, scale=0, x=0, y=0) |
| 927 | + |
| 928 | + bitmaptools.blit( |
| 929 | + photo_bitmap, |
| 930 | + self.overlay_bmp, |
| 931 | + 0, |
| 932 | + 0, |
| 933 | + skip_source_index=self.overlay_transparency_color, |
| 934 | + skip_dest_index=None, |
| 935 | + ) |
| 936 | + |
| 937 | + cc565_swapped = ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED) |
| 938 | + save_pixels( |
| 939 | + self._last_saved_image_filename.replace(".jpg", "_modified.bmp"), |
| 940 | + photo_bitmap, |
| 941 | + cc565_swapped, |
| 942 | + ) |
| 943 | + |
| 944 | + # RAM cleanup |
| 945 | + photo_bitmap.deinit() |
| 946 | + del photo_bitmap |
| 947 | + del cc565_swapped |
| 948 | + gc.collect() |
| 949 | + |
860 | 950 | def continuous_capture_start(self): |
861 | 951 | """Switch the camera to continuous-capture mode""" |
862 | 952 | pass # pylint: disable=unnecessary-pass |
@@ -901,6 +991,22 @@ def blit(self, bitmap, x_offset=0, y_offset=32): |
901 | 991 | The default preview capture is 240x176, leaving 32 pixel rows at the top and bottom |
902 | 992 | for status information. |
903 | 993 | """ |
| 994 | + # pylint: disable=import-outside-toplevel |
| 995 | + from displayio import Bitmap |
| 996 | + |
| 997 | + if self.overlay_bmp is not None: |
| 998 | + if self.combined_bmp is None: |
| 999 | + self.combined_bmp = Bitmap(bitmap.width, bitmap.height, 65535) |
| 1000 | + |
| 1001 | + bitmaptools.blit(self.combined_bmp, bitmap, 0, 0) |
| 1002 | + |
| 1003 | + bitmaptools.rotozoom( |
| 1004 | + self.combined_bmp, |
| 1005 | + self.overlay_bmp, |
| 1006 | + scale=0.75, |
| 1007 | + skip_index=self.overlay_transparency_color, |
| 1008 | + ) |
| 1009 | + bitmap = self.combined_bmp |
904 | 1010 |
|
905 | 1011 | self._display_bus.send( |
906 | 1012 | 42, struct.pack(">hh", 80 + x_offset, 80 + x_offset + bitmap.width - 1) |
|
0 commit comments