Introduction: Snow Flake Microscopy With a Raspberry Pi Microscope
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)