The focus stacking mode has been added to the main "fancy camera" example. You can see this page on using the features as originally created.
There are two variables to adjust in the code -- FOCUS_STEPS
and FOCUS_START
.
The FOCUS_STEPS
default to 20
which means the lens will adjust 20 steps per photo taken, until the maximum near focus value of 255 has been reached. Set this to 5 or 10 for finer focal steps (and more images saved to your SD card.)
FOCUS_START
is set to 30
by default, meaning the first photo taken won't be at maximum far focus, which is often much farther than needed for typical medium shots. Set this down to 0 for full far focus.
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries, and the code.py file. To get everything you need, click on the Download Project Bundle button below, and uncompress the .zip file.
Connect your computer to the board via a known good USB power+data cable. A new flash drive should show up as CIRCUITPY.
Drag the contents of the uncompressed bundle directory onto your board CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries # SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # SPDX-FileCopyrightText: 2024 John Park for Adafruit Industries # # SPDX-License-Identifier: Unlicense ''' Focus stacking example. Set FOCUS_STEPS (5-10 is a good range) for 0-255 range Set STACK to True, or set to False to have JPEG mode take snapshots as usual. ''' import time import bitmaptools import displayio import gifio import ulab.numpy as np import adafruit_pycamera pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() settings = ( None, "resolution", "effect", "mode", "led_level", "led_color", "timelapse_rate" ) curr_setting = 0 print("Starting!") pycam.tone(440, 0.1) last_frame = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) onionskin = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) timelapse_remaining = None timelapse_timestamp = None STACK = True # mode placeholder FOCUS_STEPS = 20 # number of focus steps to increment during bracket from 0-255 FOCUS_START = 30 # optionally, start the focus closer focus_stacking = False while True: if pycam.mode_text == "STOP" and pycam.stop_motion_frame != 0: # alpha blend new_frame = pycam.continuous_capture() bitmaptools.alphablend( onionskin, last_frame, new_frame, displayio.Colorspace.RGB565_SWAPPED ) pycam.blit(onionskin) elif pycam.mode_text == "GBOY": bitmaptools.dither( last_frame, pycam.continuous_capture(), displayio.Colorspace.RGB565_SWAPPED ) pycam.blit(last_frame) elif pycam.mode_text == "LAPS": if timelapse_remaining is None: pycam.timelapsestatus_label.text = "STOP" else: timelapse_remaining = timelapse_timestamp - time.time() pycam.timelapsestatus_label.text = f"{timelapse_remaining}s / " # Manually updating the label text a second time ensures that the label # is re-painted over the blitted preview. pycam.timelapse_rate_label.text = pycam.timelapse_rate_label.text pycam.timelapse_submode_label.text = pycam.timelapse_submode_label.text # only in high power mode do we continuously preview if (timelapse_remaining is None) or ( pycam.timelapse_submode_label.text == "HiPwr" ): pycam.blit(pycam.continuous_capture()) if pycam.timelapse_submode_label.text == "LowPwr" and ( timelapse_remaining is not None ): pycam.display.brightness = 0.05 else: pycam.display.brightness = 1 pycam.display.refresh() if timelapse_remaining is not None and timelapse_remaining <= 0: # no matter what, show what was just on the camera pycam.blit(pycam.continuous_capture()) # pycam.tone(200, 0.1) # uncomment to add a beep when a photo is taken try: pycam.display_message("Snap!", color=0x0000FF) pycam.capture_jpeg() except TypeError as e: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) except RuntimeError as e: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() pycam.display.refresh() pycam.blit(pycam.continuous_capture()) timelapse_timestamp = ( time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 ) else: pycam.blit(pycam.continuous_capture()) pycam.keys_debounce() if pycam.shutter.long_press: print("FOCUS") print(pycam.autofocus_status) pycam.autofocus() print(pycam.autofocus_status) if pycam.shutter.short_count: print("Shutter released") if pycam.mode_text == "STOP": pycam.capture_into_bitmap(last_frame) pycam.stop_motion_frame += 1 try: pycam.display_message("Snap!", color=0x0000FF) pycam.capture_jpeg() except TypeError as e: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) except RuntimeError as e: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() if pycam.mode_text == "GBOY": try: f = pycam.open_next_image("gif") except RuntimeError as e: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) continue with gifio.GifWriter( f, pycam.camera.width, pycam.camera.height, displayio.Colorspace.RGB565_SWAPPED, dither=True, ) as g: g.add_frame(last_frame, 1) if pycam.mode_text == "GIF": try: f = pycam.open_next_image("gif") except RuntimeError as e: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) continue i = 0 ft = [] pycam._mode_label.text = "RECORDING" # pylint: disable=protected-access pycam.display.refresh() with gifio.GifWriter( f, pycam.camera.width, pycam.camera.height, displayio.Colorspace.RGB565_SWAPPED, dither=True, ) as g: t00 = t0 = time.monotonic() while (i < 15) or not pycam.shutter_button.value: i += 1 _gifframe = pycam.continuous_capture() g.add_frame(_gifframe, 0.12) pycam.blit(_gifframe) t1 = time.monotonic() ft.append(1 / (t1 - t0)) print(end=".") t0 = t1 pycam._mode_label.text = "GIF" # pylint: disable=protected-access print(f"\nfinal size {f.tell()} for {i} frames") print(f"average framerate {i/(t1-t00)}fps") print(f"best {max(ft)} worst {min(ft)} std. deviation {np.std(ft)}") f.close() pycam.display.refresh() if pycam.mode_text == "JPEG": pycam.tone(200, 0.1) if STACK: focus_stacking = True print("Start focus stack!") pycam.autofocus_vcm_step = FOCUS_START saved_settings = pycam.get_camera_autosettings() pycam.set_camera_exposure(saved_settings["exposure"]) pycam.set_camera_gain(saved_settings["gain"]) pycam.set_camera_wb(saved_settings["wb"]) else: try: pycam.display_message("Snap!", color=0x0000FF) pycam.capture_jpeg() pycam.live_preview_mode() except TypeError as e: pycam.display_message("Failed", color=0xFF0000) time.sleep(0.5) pycam.live_preview_mode() except RuntimeError as e: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) if focus_stacking: vcm_step = pycam.autofocus_vcm_step vcm_step = min(255, vcm_step + FOCUS_STEPS) if vcm_step < 255: pycam.capture_jpeg() pycam.tone(1600 + (vcm_step*10), 0.05) pycam.autofocus_vcm_step = vcm_step pycam.display_message(str(vcm_step), color=0xFF00FF) pycam.live_preview_mode() print("Now at focus", pycam.autofocus_vcm_step) else: focus_stacking = False print("Done stacking!") pycam.autofocus_vcm_step = FOCUS_START pycam.camera.exposure_ctrl = True pycam.set_camera_gain(None) # go back to autogain pycam.set_camera_wb(None) # go back to autobalance pycam.set_camera_exposure(None) # go back to auto shutter pycam.live_preview_mode() time.sleep(0.01) 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 e: print("Retrying!", e) time.sleep(0.5) else: pycam.display_message("SD Card\nFailed!", color=0xFF0000) time.sleep(0.5) pycam.display.refresh() if pycam.up.fell: print("UP") key = settings[curr_setting] if key: print("getting", key, getattr(pycam, key)) setattr(pycam, key, getattr(pycam, key) + 1) if pycam.down.fell: print("DN") key = settings[curr_setting] if key: setattr(pycam, key, getattr(pycam, key) - 1) if pycam.right.fell: print("RT") curr_setting = (curr_setting + 1) % len(settings) if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelapse_rate": curr_setting = (curr_setting + 1) % len(settings) print(settings[curr_setting]) # new_res = min(len(pycam.resolutions)-1, pycam.get_resolution()+1) # pycam.set_resolution(pycam.resolutions[new_res]) pycam.select_setting(settings[curr_setting]) if pycam.left.fell: print("LF") curr_setting = (curr_setting - 1 + len(settings)) % len(settings) if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelaps_rate": curr_setting = (curr_setting + 1) % len(settings) print(settings[curr_setting]) pycam.select_setting(settings[curr_setting]) # new_res = max(1, pycam.get_resolution()-1) # pycam.set_resolution(pycam.resolutions[new_res]) if pycam.select.fell: print("SEL") if pycam.mode_text == "LAPS": pycam.timelapse_submode += 1 pycam.display.refresh() if pycam.ok.fell: print("OK") if pycam.mode_text == "LAPS": if timelapse_remaining is None: # stopped print("Starting timelapse") timelapse_remaining = pycam.timelapse_rates[pycam.timelapse_rate] timelapse_timestamp = time.time() + timelapse_remaining + 1 # dont let the camera take over auto-settings saved_settings = pycam.get_camera_autosettings() # print(f"Current exposure {saved_settings=}") pycam.set_camera_exposure(saved_settings["exposure"]) pycam.set_camera_gain(saved_settings["gain"]) pycam.set_camera_wb(saved_settings["wb"]) else: # is running, turn off print("Stopping timelapse") timelapse_remaining = None pycam.camera.exposure_ctrl = True pycam.set_camera_gain(None) # go back to autogain pycam.set_camera_wb(None) # go back to autobalance pycam.set_camera_exposure(None) # go back to auto shutter
The code works the same way as the primary camera example for the adafruit_pycamera
library, as covered here, with the following additions:
Variables
The STACK
variable is set to True
, which means pressing the shutter release in JPEG
mode will begin shooting the multiple focus images. Set this False
to use regular snapshot mode.
FOCUS_STEPS
specifies how many steps to adjust the lens per shot. The voice coil motor can be set from 0-255
(far-to-near focus length) so a step setting of 10
would generate 25 steps. Settings from 5-20
are typical, although you can go all the way down to 1
if you want to capture with the highest focal granularity.
FOCUS_START
is used to set the farthest position in case you are shooting a medium or close range shot, you won't need a lot of frames captured at the far distances. 0
is the farthest possible focus, objects 12" away from the camera will be in focus starting around the 95
mark, 4" is in focus at about 130
.
focus_stacking
is used to hold the state while the set of photos is being shot.
STACK = True # mode placeholder FOCUS_STEPS = 20 # number of focus steps to increment during bracket from 0-255 FOCUS_START = 30 # optionally, start the focus closer focus_stacking = False
Focus Stack
With the MEMENTO in JPEG mode and the STACK
set True, when you press the short shutter release here's what happens:
-
pycam.autofocus_vcm_step
is set to theFOCUS_START
value -- this moves the lens to its farthest focal distance (physically closer to the sensor) by energizing the voice coil motor a precise amount - the exposure, gain, and white balance values are set and locked so there is no perceived flickering between frames
pycam.autofocus_vcm_step = FOCUS_START saved_settings = pycam.get_camera_autosettings() pycam.set_camera_exposure(saved_settings["exposure"]) pycam.set_camera_gain(saved_settings["gain"]) pycam.set_camera_wb(saved_settings["wb"])
Focus, Shoot, Repeat
Next the focus stacking loop begins.
- a photo is saved to SD card
- the
pycam.autofocus_vcm_step
is adjusted by theFOCUS_STEPS
amount, which moves the lens farther from the sensor, thus bringing closer objects in focus
This repeats until the lens has moved to it's nearest focal position.
vcm_step = pycam.autofocus_vcm_step vcm_step = min(255, vcm_step + FOCUS_STEPS) if vcm_step < 255: pycam.capture_jpeg() pycam.tone(1600 + (vcm_step*10), 0.05) pycam.autofocus_vcm_step = vcm_step pycam.display_message(str(vcm_step), color=0xFF00FF) pycam.live_preview_mode() print("Now at focus", pycam.autofocus_vcm_step)
Reset
When the focus stack has been captured, the MEMENTO resets the following:
- lens returns to its farthest focus distance
- exposure control is set back to auto
- gain control returns to auto
- white balance returns to auto
pycam.autofocus_vcm_step = FOCUS_START pycam.camera.exposure_ctrl = True pycam.set_camera_gain(None) # go back to autogain pycam.set_camera_wb(None) # go back to autobalance pycam.set_camera_exposure(None) # go back to auto shutter pycam.live_preview_mode()
Text editor powered by tinymce.