ov5640_stopmotion_kaluga1_3.py
1 # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 # SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries 3 # 4 # SPDX-License-Identifier: Unlicense 5 6 """ 7 Take a 10-frame stop motion GIF image. 8 9 This example requires: 10 * `Espressif Kaluga v1.3 <https://www.adafruit.com/product/4729>`_ with compatible LCD display 11 * `MicroSD card breakout board + <https://www.adafruit.com/product/254>`_ connected as follows: 12 * CLK to board.IO18 13 * DI to board.IO14 14 * DO to board.IO17 15 * CS to IO12 16 * GND to GND 17 * 5V to 5V 18 * A compatible SD card inserted in the SD card slot 19 * A compatible OV5640 camera module connected to the camera header 20 21 To use: 22 23 Insert an SD card and power on. 24 25 Set up the first frame using the viewfinder. Click the REC button to take a frame. 26 27 Set up the next frame using the viewfinder. The previous and current frames are 28 blended together on the display, which is called an "onionskin". Click the REC 29 button to take the next frame. 30 31 After 10 frames are recorded, the GIF is complete and you can begin recording another. 32 33 34 About the Kaluga development kit: 35 36 The Kaluga development kit comes in two versions (v1.2 and v1.3); this demo is 37 tested on v1.3. 38 39 The audio board must be mounted between the Kaluga and the LCD, it provides the 40 I2C pull-ups(!) 41 42 The v1.3 development kit's LCD can have one of two chips, the ili9341 or 43 st7789. Furthermore, there are at least 2 ILI9341 variants, which differ 44 by rotation. This example is written for one if the ILI9341 variants, 45 the one which usually uses rotation=90 to get a landscape display. 46 """ 47 48 import os 49 import struct 50 51 import analogio 52 import bitmaptools 53 import board 54 import busio 55 import displayio 56 import gifio 57 import sdcardio 58 import storage 59 60 import adafruit_ov5640 61 62 V_RECORD = int(2.41 * 65536 / 3.3) 63 V_FUZZ = 2000 64 65 a = analogio.AnalogIn(board.IO6) 66 67 68 def record_pressed(): 69 value = a.value 70 return abs(value - V_RECORD) < V_FUZZ 71 72 73 displayio.release_displays() 74 spi = busio.SPI(MOSI=board.LCD_MOSI, clock=board.LCD_CLK) 75 display_bus = displayio.FourWire( 76 spi, 77 command=board.LCD_D_C, 78 chip_select=board.LCD_CS, 79 reset=board.LCD_RST, 80 baudrate=80_000_000, 81 ) 82 _INIT_SEQUENCE = ( 83 b"\x01\x80\x80" # Software reset then delay 0x80 (128ms) 84 b"\xEF\x03\x03\x80\x02" 85 b"\xCF\x03\x00\xC1\x30" 86 b"\xED\x04\x64\x03\x12\x81" 87 b"\xE8\x03\x85\x00\x78" 88 b"\xCB\x05\x39\x2C\x00\x34\x02" 89 b"\xF7\x01\x20" 90 b"\xEA\x02\x00\x00" 91 b"\xc0\x01\x23" # Power control VRH[5:0] 92 b"\xc1\x01\x10" # Power control SAP[2:0];BT[3:0] 93 b"\xc5\x02\x3e\x28" # VCM control 94 b"\xc7\x01\x86" # VCM control2 95 b"\x36\x01\x40" # Memory Access Control 96 b"\x37\x01\x00" # Vertical scroll zero 97 b"\x3a\x01\x55" # COLMOD: Pixel Format Set 98 b"\xb1\x02\x00\x18" # Frame Rate Control (In Normal Mode/Full Colors) 99 b"\xb6\x03\x08\x82\x27" # Display Function Control 100 b"\xF2\x01\x00" # 3Gamma Function Disable 101 b"\x26\x01\x01" # Gamma curve selected 102 b"\xe0\x0f\x0F\x31\x2B\x0C\x0E\x08\x4E\xF1\x37\x07\x10\x03\x0E\x09\x00" # Set Gamma 103 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma 104 b"\x11\x80\x78" # Exit Sleep then delay 0x78 (120ms) 105 b"\x29\x80\x78" # Display on then delay 0x78 (120ms) 106 ) 107 108 display = displayio.Display(display_bus, _INIT_SEQUENCE, width=320, height=240) 109 110 sd_spi = busio.SPI(clock=board.IO18, MOSI=board.IO14, MISO=board.IO17) 111 sd_cs = board.IO12 112 sdcard = sdcardio.SDCard(sd_spi, sd_cs, baudrate=24_000_000) 113 vfs = storage.VfsFat(sdcard) 114 storage.mount(vfs, "/sd") 115 116 bus = busio.I2C(scl=board.CAMERA_SIOC, sda=board.CAMERA_SIOD) 117 cam = adafruit_ov5640.OV5640( 118 bus, 119 data_pins=board.CAMERA_DATA, 120 clock=board.CAMERA_PCLK, 121 vsync=board.CAMERA_VSYNC, 122 href=board.CAMERA_HREF, 123 mclk=board.CAMERA_XCLK, 124 size=adafruit_ov5640.OV5640_SIZE_240X240, 125 ) 126 127 128 def exists(filename): 129 try: 130 os.stat(filename) 131 return True 132 except OSError as _: 133 return False 134 135 136 _image_counter = 0 137 138 139 def next_filename(extension="jpg"): 140 global _image_counter # pylint: disable=global-statement 141 while True: 142 filename = f"/sd/img{_image_counter:04d}.{extension}" 143 if exists(filename): 144 print(f"File exists: {filename}", end="\r") 145 _image_counter += 1 146 continue 147 print() 148 return filename 149 150 151 # Pre-cache the next image number 152 next_filename("gif") 153 154 # Blank the whole display, we'll draw what we want with directio 155 empty_group = displayio.Group() 156 display.root_group = empty_group 157 display.auto_refresh = False 158 display.refresh() 159 160 161 def open_next_image(extension="jpg"): 162 while True: 163 filename = next_filename(extension) 164 print("# writing to", filename) 165 return open(filename, "wb") 166 167 168 cam.flip_x = False 169 cam.flip_y = False 170 chip_id = cam.chip_id 171 print(f"Detected 0x{chip_id:x}") 172 cam.test_pattern = False 173 cam.effect = adafruit_ov5640.OV5640_SPECIAL_EFFECT_NONE 174 cam.saturation = 3 175 176 # Alternately recording to these two bitmaps 177 rec1 = displayio.Bitmap(cam.width, cam.height, 65536) 178 rec2 = displayio.Bitmap(cam.width, cam.height, 65536) 179 # Prior frame kept here 180 old_frame = displayio.Bitmap(cam.width, cam.height, 65536) 181 # Displayed (onion skinned) frame here 182 onionskin = displayio.Bitmap(cam.width, cam.height, 65536) 183 184 ow = (display.width - onionskin.width) // 2 185 oh = (display.height - onionskin.height) // 2 186 display_bus.send(42, struct.pack(">hh", ow, onionskin.width + ow - 1)) 187 display_bus.send(43, struct.pack(">hh", oh, onionskin.height + ow - 1)) 188 189 190 class ContinuousCapture: 191 def __init__(self, camera, buffer1, buffer2): 192 camera = getattr(camera, "_imagecapture", camera) 193 self._camera = camera 194 print("buffer1", buffer1) 195 print("buffer2", buffer2) 196 camera.continuous_capture_start(buffer1, buffer2) 197 198 def __exit__(self, exc_type, exc_val, exc_tb): 199 self._camera.continuous_capture_stop() 200 201 def __enter__(self): 202 return self 203 204 def get_frame(self): 205 return self._camera.continuous_capture_get_frame() 206 207 __next__ = get_frame 208 209 210 def wait_record_pressed_update_display(first_frame, cap): 211 while record_pressed(): 212 pass 213 while True: 214 frame = cap.get_frame() 215 if record_pressed(): 216 return frame 217 218 if first_frame: 219 # First frame -- display as-is 220 display_bus.send(44, frame) 221 else: 222 bitmaptools.alphablend( 223 onionskin, old_frame, frame, displayio.Colorspace.RGB565_SWAPPED 224 ) 225 display_bus.send(44, onionskin) 226 227 228 def take_stop_motion_gif(n_frames=10, replay_frame_time=0.3): 229 print(f"0/{n_frames}") 230 with ContinuousCapture(cam, rec1, rec2) as cap: 231 frame = wait_record_pressed_update_display(True, cap) 232 with open_next_image("gif") as f, gifio.GifWriter( 233 f, cam.width, cam.height, displayio.Colorspace.RGB565_SWAPPED, dither=True 234 ) as g: 235 g.add_frame(frame, replay_frame_time) 236 for i in range(1, n_frames): 237 print(f"{i}/{n_frames}") 238 239 # CircuitPython Versions <= 8.2.0 240 if hasattr(old_frame, "blit"): 241 old_frame.blit( 242 0, 0, frame, x1=0, y1=0, x2=frame.width, y2=frame.height 243 ) 244 245 # CircuitPython Versions >= 9.0.0 246 else: 247 bitmaptools.blit( 248 old_frame, 249 frame, 250 0, 251 0, 252 x1=0, 253 y1=0, 254 x2=frame.width, 255 y2=frame.height, 256 ) 257 258 frame = wait_record_pressed_update_display(False, cap) 259 g.add_frame(frame, replay_frame_time) 260 print("done") 261 262 263 est_frame_size = cam.width * cam.height * 128 // 126 + 1 264 est_hdr_size = 1000 265 266 dither = True 267 while True: 268 take_stop_motion_gif()