/ examples / ov5640_stopmotion_kaluga1_3.py
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()