Introduction: Snow Flake Microscopy With a Raspberry Pi Microscope

About: Scientist working in in-vitro diagnostics industry. Playing with all types of sensor as a spare time hobby. Aiming for simple and inexpensive tools and projects for STEM, with a bit of science and a bit of sil…

Snow flake microscopy - who doesn't love these nice images of hexagonal beauties, each one different in shape?

I always wanted to make such images myself and when I had been building a microscope using the Raspberry Pi HQ camera, the Pimorini microscope lens, a Raspberry Pi 4 and a some LEGO parts (see here) I had this idea in mind. As Pi, microscope and display can be powered from an ordinary power bank it is possible to take and run the device outdoors.

Nice snow is very rare at Berlin. But due to a very unusual weather constallation today (Feb 7th, 2021) we had -6°C and snow. Lots of cold clean white flakes which allowed me to try making my first own microscopic snowflake images.

What you see here are the results of my very first experiments and there is a lot of room for improvements.

Technical Layout:

The layout of the microscope is as described previously in an instructable:
RaspberryPi HQ camera, microscopic lens, Raspberry Pi 4, a Pimoroni Touch pHAT, a 1 meter camera cable, four M2 nuts and bolts, and an assortment of used LEGO parts. Relatively inexpensive and simple to assemble, flexible in shape and size, a water resistant plastic rig.

I used a portable monitor as display and a power bank to power both display and Raspberry Pi. To protect display and Raspberry from snow they were placed inside of a large IKEA Samla box (see image).

The microscope itself was placed on top of the box. Snowflakes were captured on a flat black LEGO plate and placed under the microscope. For some images I used a LED torch to improve illumination.

The Program:

The program is plain Python. You will need to install few libraries, e. g. for camera and the Touch pHAT.
I run it in Thonny IDE. Modify as you like. I attached the code in the last step, more information can be found in the microscope instructable.

In start mode, the program displays a preview the total captured area, allowing to place the objects of interest in the central area of the image and focus.

Pressing the "enter" field of the Touch pHAT, the area in the center is enlarged in preview for 10 seconds, allowing to optimize placement and focus. Touching field "A" takes a photo of the total area and touching field "B" a photo of the central area with maximum resolution. "C" and ""D" take photos in special formats, "back" stops the camera.

To focus, turn the lens. At -6°C turning takes a bit more force than usual. I was wearing biking gloves for better manual control.

The Procedure:

  • Start the program
  • Catch some nice flakes on the plastic plate and place it under the microscope.
  • Find the best flakes and place them in the middle of the capture area, focus.
  • Make a ROI preview (touch "enter"), if required refocus and adjust placement. optimize illumination.
  • Take an image of the ROI (touch "B"), and an image of the total area (touch "A")
  • Repeat with next snowflakes
  • Stop camera (touch "back")
  • Stop program

Take care to keep Raspberry Pi and display dry!

Conclusions from the first try:

  • Images are not too bad for a first shot with such an inexpensive solution.
  • Illumination requires to be improved.
  • A table would be helpful.
  • The Touch pHAT should placed away from the Raspberry Pi, so the box with display and Pi can be keep closed. Integrate Touchphat into lid of the box? Use a non-physical solution to control the program?
  • The microscope rig might be placed in a smaller, seperate box to avoid flakes to be blown away and camera to get wet.
  • Pi, display and power pack might be kept in a heated/isolated box to avoid disfunction and damage if used at very low temperatures.
  • Wearing long underwear might had been a good idea at -6°C

Supplies

Microscope: as described before

Total costs are approx. 150 €, not counting the LEGO parts.
45 € for the Pi, 50 € for the HQ camera module 25 € for the lens, 9 € for the Touch pHAT,
plus SD card, camera cable, ...

Display: 15.6 '' portable external monitor, Uperfect (199 € at Amazon)

Power pack: Xiaomi RedMi 20000mAh Fast Charge Power Bank (20€ at Xiaomi Germany)

Plastic box: IKEA Samla box large (130 ltr) with lid (19 € at IKEA Germany)

Step 1: Some More Images

Here you find some more images.
If required, please zoom into the images on your browser or download them.

Not every snowflake is perfect and their shape do reflect their individual history.
They grow, melt, regrow, cluster with other flakes, crush, mature.
A lot depends on temperature and humidity conditions in the whole process.

I heard -15°C shall be the perfect condition, at least for snowflakes.

Have fun and try yourself and present us your images.

Step 2: The Script

Attached you find the script.
Below the code for yore reference.

'''
A module "picamera"-based script for the Raspberry Pi HQ camera microscope,
controlled by a Pimoroni Touchphat

Install required libraries before use
'''

from time import sleep
import picamera
import datetime as dt
import touchphat
import os

w_dir = os.getcwd()
print ('working directory: ', w_dir)

camera = picamera.PiCamera() #turns camera on

#General camera settings: loads of parameters to play with
camera.rotation= 180 # 90,180,270
#camera.hflip = True # default: False
#camera.vflip = True # default: False

camera.meter_mode = 'spot' # optimize illumination,
# alternative options: 'average', 'backlit', 'matrix', 'spot'

camera. awb_mode = 'auto' # automatic whithe balance.. You may play with setting fitting best to your setting
# alternatives: off, auto, sunlight, cloudy, shade, tungsten, fluorescent, incandelescent, flash, horizon, greyworld

#camera.iso =100 # 200,400, 800
#camera.contrast = 0.5 #
camera.brightness = 50 # 0 ...100, default 50
camera.sharpness = 0 #-100 to 100, default 0

# set annotation format in images
camera.annotate_background = picamera.Color ('blue') # sets background color for text
#camera.annotate_foreground = picamera.Color (Y=1, U=0, V=0) # sets brightness for text Y=0..1

#select effects to apply while capturing image
effect1 = 'negative'     #Touchpad "C"
effect2 = 'colorbalance'   #Touchpad "D"
'''
#further options: negative, solarize, sketch, denoise, emboss, oilpaint, hatch, pastel, watercolor, film, blur, saturation, colorswap, washedout, posterize, colorpoint, colorbalance, cartoon, deinterlace1, deinterlace2 
'''
#preview settings
preview_time_1 = 8 # duration for displaying preview in sec:
#for total view: optimize object placing & focus
preview_time_2 = 8 # 
#for total view  emboss: fine tuning
preview_time_3 = 8
#for ROI-view: fine tuning

#image capture settings
delay_time = 2 # delay between images taken, in sec

#video length settings

vga_time = 30 # for 640x480 video
hires_time = 20 # for 1080p video

# define position and size of preview window
preview_xy = (800,300) #position of upper left corner of preview window on screen
preview_size = (1014, 760) # size of preview window, here: 1/4 of max resolution HQ camera

#Zeitpunkt = "" # set as general variable for timestamp text

#check for type of camera installed
camera_version = camera.revision
print ('camera type: ', camera_version)
if (camera_version == 'imx477'):
    print ('HQ camera detected')
    print ('Modus 1: 2028 x 1080, Modus 2: 2028x1520, Modus 3: 4056x3040, Modus 4: 1012x760')
    modus = 3
    camera.sensor_mode = modus
    print ('Activated modus: ', modus)
    camera.resolution = (1014,760) # defines image size
    print ('Image size: ', camera.resolution)
    camera.framerate = 15 # 30
    print ('Framerate set to: ', camera.framerate, 'fps')
    
elif (camera.version == 'ov5647'):
    print ('version 1 camera detected')
    print ('modus 1: 1080p (30 fps), modi 2/3: 2592x1944, modus 4: 1296x972, modus 5: 1296x730, modi 6/7: VGA (60/90 fps)')
    print ('code may require some adaptation')
    camera.resolution = (1014,760) # defines image size
    print ('Image size: ', camera.resolution)
    camera.framerate = 30

elif (camera.version == 'imx219'):
    print ('version 2 camera detected')
    print ('Settings may require some adjustments')
    print ('modus 1: 1080p (30 fps), modi 2/3: 3280x2464 (15), modus 4: 1640x1232 (40), modus 5: 1640x922, modus 6: 720p (90), modus 7: VGA (90)')
    print ('code may require some adaptation')
    camera.resolution = (1014,760) # defines image size
    print ('Image size: ', camera.resolution)
    camera.framerate = 30

else:    
    print ('No HQ camera! Please modify settings manually') # code to be optimized for version 2 camera

# determine timestamp
def get_zeitpunkt():
    Time_info = dt.datetime.now().strftime('%Y-%m-%d %H:%M')
    print ('Timestamp: ', Time_info)
    return Time_info

#Routines for preview and taking images, evoked by touckphat

@touchphat.on_release('Enter') # button 'Enter' evokes Preview total for previewtime 1 seconds
def preview_ROI_none():
    touchphat.led_on(6)
    camera.zoom = (0.375, 0.375, 0.25, 0.25) #restrict to ROI: central 25 %, equals maximal resolution
    camera.annotate_text = 'Preview ROI'
    sleep (preview_time_3)
    camera.zoom = (0,0,0,0)
    camera.annotate_text ='Preview total'
    touchphat.led_off(6)
    
''' #not used here
def preview_Total_emboss():
    touchphat.led_on(1)
    camera.image_effect =('emboss') # 'none, 'cartoon', 'negative','sketch', 'emboss' ...
    camera.annotate_text = "Preview - emboss"
    sleep (preview_time_2)
    camera.image_effect =('none')# back to standard
    touchphat.led_off(1)
'''
@touchphat.on_release('A') # button 'A' evokes taing image of total area, no effects
def take_Total_none():  # take image of tolal view without applying any effect
    touchphat.led_on(2)    
    Zeitpunkt = get_zeitpunkt()
    Image_text = Zeitpunkt + ' - Total-none'
    camera.annotate_text = Image_text
    camera.capture (Image_text + '.jpg') # get total image, store as jpg
    print ('Image taken: ',Image_text)
    camera.annotate_text ='Preview total area'
    touchphat.led_off(2)
    
@touchphat.on_release('B') # button 'B' evokes taing image of ROI, no effects
def take_ROI_none():   #take image of ROI applying no effect
    # zoom to ROI
    camera.zoom = (0.375, 0.375, 0.25, 0.25) #restrict to ROI: central 25 %, equals maximal resolution
    sleep (2)
    Zeitpunkt = get_zeitpunkt()
    Image_text = Zeitpunkt + ' - ROI-none' # for unmodified image
    camera.annotate_text = Image_text
    camera.capture (Image_text +'.png') # store as png to reduce loss of information
    print ('Image taken: ',Image_text)
    camera.zoom = (0, 0, 0, 0)
    camera.annotate_text ='Preview total area'

@touchphat.on_release('C') # button 'C' evokes taking image of ROI, effect1 applied
def take_ROI_effect1():  #take image of ROI applying effect1
    camera.zoom = (0.375, 0.375, 0.25, 0.25) #restrict to ROI: central 25 %, equals maximal resolution       
    camera.image_effect = effect1
    sleep (1)
    Zeitpunkt = get_zeitpunkt()
    Image_text = Zeitpunkt + ' - ROI-' + effect1
    camera.annotate_text = Image_text
    camera.capture (Image_text +'.png') # store as png to reduce loss of information
    print ('Image taken: ',Image_text)
    camera.image_effect = 'none'
    camera.zoom = (0, 0, 0, 0)
    camera.annotate_text ='Preview total area'

@touchphat.on_release('D') # button 'D' evokes taking image of ROI, effect2: posterize applied
def take_ROI_effect2():  #take image of ROI applying effect = 'xxx'
    camera.zoom = (0.375, 0.375, 0.25, 0.25) #restrict to ROI: central 25 %, equals maximal resolution       
    camera.image_effect = effect2
    sleep (1)
    Zeitpunkt = get_zeitpunkt()
    Image_text = Zeitpunkt + ' - ROI-'+ effect2
    camera.annotate_text = Image_text
    camera.capture (Image_text +'.png') # store as png to reduce loss of information
    print ('Image taken: ',Image_text)
    camera.image_effect = 'none'
    camera.zoom = (0, 0, 0, 0)
    camera.annotate_text ='Preview total area'

@touchphat.on_release('Back') # button 'Back' evokes end of programm and cleaning of memory
def end_camera():   #end routine
    touchphat.led_on(1)
    camera.stop_preview()
    camera.close () # close camera, free storage
    print ("Camera closed")
    sleep (1)
    touchphat.led_off(1)
    print ('Stop script now manually!') # how to stop all processes from  tread?
    #sys.exit()
    #quit()
    #exit()

#instructions for use
print ()
print ('On Touch phat press: ')
print ('   "Enter" (right side) for a 10 sec preview of ROI (no effect applied)')
print ('   "A" to take an image of total area, no effect applied')
print ('   "B" to take an image of ROI, no effect applied')
print ('   "C" to take an image of ROI, effect1 "', effect1, '" applied')
print ('   "D" to take an image of ROI, effect2 "', effect2, '" applied')
print ('   "Back" (left side) to end program')
print ()

#start with total area preview w/o effects applied
#camera warm-up time 3 sec
touchphat.all_on() # turn all LEDs on
sleep (1)
touchphat.all_off() # turn them off

#start preview
camera.start_preview()
camera.preview.fullscreen = False # enables display of preview in defined window
camera.preview.window = (preview_xy[0], preview_xy[1], preview_size[0], preview_size[1]) # defines position of upper left corner and size of display window
camera.annotate_text = 'Preview total area'
#sleep (preview_time_1)