The HQ Camera for Raspberry Pi offers a resolution of 12.3MP as well as a C-mount adapter that allows the use of professional lenses dedicated to image analysis. For this microscope project, we will use parts of a CNC 3018 to build a motorized stand. The camera is installed on a vertical axis printed in 3D. The vertical axis is motorized by a Nema 17 motor driven from an A4988 controller in Python using the RpiMotorLib library from Gavin Lyons. By adding extension tubes, it is possible to achieve greater magnification to go up to the supplemental microscope.
Before getting started, you can start by reading this article which explains how to drive a Nema 17 engine using the Python RpiMotorLib library.
Components used for the support (stand)
Here are the recycled components from a CNC 3018 used for the stand:
- x2 aluminum profiles 2020 (square section 20x20mm) of length 220mm approximately
- x2 2020 aluminum profiles (square section 20x20mm) approximately 350mm long
- x4 30mm brackets
- T-nuts for aluminum profile 2020
You can also 3D print your 2020 profiles using this project available on Thingiverse.
And the elements for the motorized vertical axis
- x2 rods ø10mm length 400mm mini
- x2 linear guides ø10mm
- x4 supports for rod ø10mm
- x1 Nema motor board
- x1 T8 screw of minimum length 350mm
- x1 direct screw transmission element
- x1 flexible connection ø8mm
You will also find all the necessary mechanical components in this article
HQ camera, lens, extension tube
- HQ 12.3MP v1.0 2018 Camera with C mount lens mount
- 50mm C mount lens
- Neopixel 16 RGB LED ring light
- Extension tube kit
You will find all the necessary components in this article
How to choose the length of the extension tube?
The stand presented in this project is equipped with a 50mm lens with a 40mm extension tube which allows a satisfactory macro mode for electronics.
The extension ring (or extension tube) is screwed between the HQ camera and the lens. This allows the focal length to be changed and thus to obtain a higher magnification. It is an economical solution to obtain a macro mode without investing in a dedicated lens.
Pentax / Cosmicar C5028-M lens installed on the HQ Camera of the Raspberry Pi via a via 50mm extension tube (40 + 10).
You can also print your extension rings using these STL files. The screw pitch being very fine, print with a fine resolution of 0.1mm ideally.
Be careful, however, because the larger the extension ring, the shorter the depth of field. It will not be possible to sharpen the entire image when there is relief. That is, it will be impossible to clearly see the circuit and the electronic components.
To estimate the focusing distance you can read this article published by Nikon Passion.
Here are some magnifications and focusing distances for a 50mm lens depending on the extension ring used. The distance is calculated when the focus is at infinity.
Extension ring (mm) | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | 110 | 120 |
Magnification | 0.20 | 0.40 | 0.60 | 0.80 | 1.00 | 1.20 | 1.40 | 1.60 | 1.80 | 2.00 | 2.20 | 2.40 |
Object distance (mm) * | 300 | 175 | 133 | 113 | 100 | 92 | 86 | 81 | 78 | 75 | 73 | 71 |
(*) approximate theoretical distance
How to calculate (estimate) optical magnification
It is quite easy to estimate the optical magnification from a known measurement compared to what you get on the screen.
Here, for example, we can read about 9 graduations of a caliper on the screen which represents 9mm. On my screen, I measure around 165mm, which gives us a magnification of 165/9 ∼ x18.
Electric circuit
Components recovered on a CNC 3018
- x1 Nema 17HS4401 motor or equivalent
- x1 driver for A4988 or DRV8825 stepper motor
- x1 4-pin cable for Nema motor
- x1 mini multi-voltage power supply module delivering at least 5V and 12V
- x1 LED ring ø32mm ( x16 RGB LEDs )
- Raspberry Pi. The support is compatible with models 3 and 4
Here is the pinpointing of the circuit
Stepper Motor Driver A4988 | |
12V power supply | Pin A4988 |
+ 12V from battery or mains power | VMOT |
GND from battery or mains power | GND |
Raspberry Pi GPIO (any version) | |
5V from Raspberry Pi | VDD |
Raspberry Pi GND | GND |
GPIO 21 | STP |
GPIO 20 | DIR |
To stepper motor Usually the motor is delivered with a cable fitted with a 2.54mm pitch connector. If the displacement is reversed, reverse the connector or modify the Python code | 1A, 1B, 2A, 2B |
GPIO 14 | MS1 |
GPIO 15 | MS2 |
GPIO 18 | MS3 |
Connect RESET and SLEEP pins together | |
Neopixel 16 RGB LED ring light | |
GND of the Raspberry Pi connected to the GND of the external power supply | GND |
+ 5V external power supply | WIN |
GPIO 12 | In |
Notes
The Adafruit Neopixel library only supports pins 10, 12, 18 and 21 of the Raspberry Pi. To be able to use pin 10, the SPI bus must be activated from the Raspberry Pi OS system settings.
All the grounds (GND) must be connected together.
Parts to 3D print
Here are the parts to be 3D printed. STL files as well as Fusion 360 models are available on Thingiverse, Cults3D and GitHub.
HQ v1.0 camera support The support is designed for the elements of a CNC 3018 (ball guide, screw transmission) | |
Support for Raspberry Pi to be fixed on the base of the stand | |
Light ring mount (recommended) compatible with any C-mount lens | |
Extension ring for C-mount. The thread is very fine, print with the finest resolution (0.1mm in general) offered by your printer. STL file to download on Thingiverse |
Assembly of the stand
Here are some photos showing the assembly of the 2020 aluminum profiles.
The support for the Raspberry Pi (any model) is fixed using a T-nut to the right or to the left of the vertical axis.
The HQ camera is installed on the 3D printed stand. The length of the ribbon supplied as standard is sufficient if the Raspberry Pi is installed vertically on the proposed support.
Install the Python libraries
The project requires the installation of the following Python libraries
- RpiMotorLib
- PiCamera
- Adafruit NeoPixel
To verify that Python version 3 is installed correctly (which is always the case on Raspberry Pi OS normally), run the following command in a Terminal.
If Python3 is properly installed, you should get the version number in response.
python3 --version
Python 3.7.3
To install the Python libraries, we will need the pip3 script. To verify that it is correctly installed, run this command which should send you the path to the script
pip3 --version
If pip3 is not installed, run
sudo apt install python3-pip
Now we can install the libraries
sudo pip3 install rpi_ws281x adafruit-circuitpython-neopixel
sudo python3 -m pip install --force-reinstall adafruit-blinka
sudo pip3 install rpimotorlib
sudo apt install python3-picamera
Activate the camera, the SPI and I2C buses
Access to the GPIO and some GPIO pins require activation. You will also need to enable access to the CSI port before you can use the camera.
From the Preferences menu, open Configuration
Enable I2C, SPI and camera
Python code of the project. HQ camera video stream broadcast on an HTML control interface
The source code that allows you to retrieve the video stream from the camera is taken directly from the Web Streaming example available in the online documentation of the PiCamera library.
It would have been much easier to develop the interface with Flask (which I tried), but the video stream obtained is much less fluid: cry: Until we find a more efficient technical solution, we will use the HTTP functions Python standards.
The interface uses the Bootstrap 4 style sheet which allows to obtain a professional and responsive rendering whatever the size of the screen, including on a smartphone.
The HTML interface is accessible on the local network. The Raspberry Pi offers enough power for multiple users to view the video stream simultaneously. Convenient for a classroom.
Several parameters can be customized before running the script
- GPIO_pins pins MS1, MS2 and MS3 to communicate with the A4988 controller
- spindle direction to control the direction of the Nema 17 motor
- step spindle to control the advance of the Nema 17 motor
- step_per_mm Nema motor calibration. Default 72 steps per millimeter
- distance 72 steps to cover by default, ie 1mm.
- LED_COUNT number of LEDs of the Neopixel light ring
- LED_PIN pin to drive the WS2812B controller of the Neopixel light ring. Be careful, pin 10 does not work (on my Raspberry Pi 3). This is the only Raspberry Pi PWM pin available, the others (D18 and D21) are used to drive the A4988 controller
- LED_BRIGHTNESS brightness level. Unlike the Arduino version, there is no function to modify the brightness level after initialization of the Neopixel Adafruit library for the Python language.
- LED_ORDER order of the LEDs. Default RGB. Modify if the color obtained does not conform to what is requested from the HTML interface.
# Source code based on the Web streaming example from the official PiCamera package # http://picamera.readthedocs.io/en/latest/recipes2.html#web-streaming import io, picamera, logging, socketserver, os from statistics import mean from threading import Condition, Thread from http import server import RPi.GPIO as GPIO from RpiMotorLib import RpiMotorLib import board, time, neopixel #define GPIO pins GPIO_pins = (14, 15, 18) # Microstep Resolution MS1-MS3 -> GPIO Pin direction= 20 # Direction Pin step = 21 # Step Pin step_per_mm = 72 # Step by millimeter | stepper per millimeter distance = 72 # By default move 1mm => 72 steps per mm stepper = "1mm" debug = False # LED strip configuration LED_COUNT = 16 # Number of LED ring. | Nombre de LED LED_PIN = board.D12 # GPIO pin. Don't use D10 | Ne fonctionne pas sur la broche D10 LED_BRIGHTNESS = 1 # LED brightness, from 0 to 1 | Niveau de luminosité, compris entre 0 à 1 LED_ORDER = neopixel.GRB # Order of LED colours. May also be RGB, GRBW, or RGBW | Type de LED ring_status = False # LED Ring status ring_status_label = "Off" # LED Ring status for user | Etat de l'anneau de LED affiché sur la page Web color = "white" # Default color | Couleur d'éclairage par défaut (blanc) camera_rotation = 270 # Image rotation in degrees (0,90,180,270) | rotation de l'image en degrées (0,90,180,270) # Create instance for the Stepper Motor mymotortest = RpiMotorLib.A4988Nema(direction, step, GPIO_pins, "A4988") print("A4988 initialized") # Neopixel Ring object # auto_write must be set to False to change color | l'option auto_write doit être False pour pouvoir changer de couleur ring = neopixel.NeoPixel(board.D12, LED_COUNT, brightness = LED_BRIGHTNESS, auto_write=False, pixel_order = LED_ORDER) ring.fill((0,0,0)) ring.show() class StreamingOutput(object): def __init__(self): self.frame = None self.buffer = io.BytesIO() self.condition = Condition() def write(self, buf): if buf.startswith(b'\xff\xd8'): # New frame, copy the existing buffer's content and notify all # clients it's available self.buffer.truncate() with self.condition: self.frame = self.buffer.getvalue() self.condition.notify_all() self.buffer.seek(0) return self.buffer.write(buf) class StreamingHandler(server.BaseHTTPRequestHandler): def getPage(self): PAGE=""" <html> <head> <meta charset="utf-8"> <!-- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=yes"> --> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <title>Raspberry Pi HQ Camera Magnifying</title> </head> <body> <div class="container"> <div class="row"> <div class="col-sm-9" style="padding:10"> <img src="stream.mjpg" style="width:100%"> </div> <div class="col-sm-3" style="padding:10"> <div class="row"> <div class="col-4"> <form method="POST" action="up"> <button type="submit" class="btn btn-primary">Up</button> </form> </div> <div class="col-4"> <form method="POST" action="down" class="center-block"> <button type="submit" class="btn btn-primary">Down</button> </form> </div> </div> <div></div> <div class="row"> <div class="col-3"> <form method="POST" action="switchonoffring"> <button name="ringcolor" value="light" type="submit" style="font-size=40" class="btn btn-primary" >{button_label}</button> </form> </div> <div class="col-2"> <form method="POST" action="changecolor"> <button name="ringcolor" value="light" type="submit" style="font-size=40" class="btn btn-light" > W </button> </form> </div> <div class="col-2"> <form method="POST" action="changecolor" > <button name="ringcolor" value="red" type="submit" class="btn btn-danger"> R </button> </form> </div> <div class="col-2"> <form method="POST" action="changecolor" > <button name="ringcolor" value="yellow" type="submit" class="btn btn-warning"> Y </button> </form> </div> <div class="col-2"> <form method="POST" action="changecolor" > <button name="ringcolor" value="green" type="submit" class="btn btn-success"> G </button> </form> </div> </div> <form method="POST" action="setdistance"> <div class="btn-group" role="group" aria-label="Basic example" > <button name="distance" type="submit" class="btn btn-secondary" value="7">0.1 mm</button> <button name="distance" type="submit" class="btn btn-secondary" value="73">1 mm</button> <button name="distance" type="submit" class="btn btn-secondary" value="727">10 mm</button> </div> </form> <h3>Info</h3> <p>Step: {step}</p> <p>LED: {ring_status}</p> <p>Color: {ring_color}</p> </div> </div> </body> </html> """.format(ring_status=ring_status_label, ring_color=color,button_label= "OFF" if ring_status else "ON", step=stepper) return PAGE def changeLedColor(self): global ring_status, color if ring_status == True: print("color changed to ", color) if color == "red": print('red') ring.fill((255,0,0)) ring.show() elif color == "yellow": print('yellow') ring.fill((255,255,0)) ring.show() elif color == "green": print('green') ring.fill((70,245,10)) ring.show() else: print('white') ring.fill((255,255,255)) ring.show() else: print("Ring if OFF") def do_POST(self): global distance, ring_status, color, stepper, ring_status_label, step_per_mm, distance content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) if self.path == '/up': print("Go Up to ", distance) mymotortest.motor_go(False, "Full" , distance, 0.001 , False, .05) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == '/down': print("Go Down to ", distance) mymotortest.motor_go(True, "Full" , distance, 0.001 , False, .05) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == "/changecolor": color = post_data[10:].decode('utf-8') self.changeLedColor() self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == "/switchonoffring": try: if ring_status == False: ring_status = True ring_status_label = "On" self.changeLedColor() print("Switch ON LED") else: ring.fill((0, 0, 0)) ring.show() ring_status_label = 'Off' ring_status = False print("Switch OFF LED") except Exception as e: logging.warning('Neopixel error: %s', str(e)) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == '/setdistance': distance = int(post_data[9:]) stepper = str( round(distance / step_per_mm, 1) ) + "mm" print("Set stepper to ", distance) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() else: self.send_error(404) self.end_headers() def do_GET(self): if self.path == '/': self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == '/index.html': content = self.getPage().encode('utf-8') self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', len(content)) self.end_headers() self.wfile.write(content) elif self.path == '/stream.mjpg': self.send_response(200) self.send_header('Age', 0) self.send_header('Cache-Control', 'no-cache, private') self.send_header('Pragma', 'no-cache') self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME') self.end_headers() try: while True: with output.condition: output.condition.wait() frame = output.frame self.wfile.write(b'--FRAME\r\n') self.send_header('Content-Type', 'image/jpeg') self.send_header('Content-Length', len(frame)) self.end_headers() self.wfile.write(frame) self.wfile.write(b'\r\n') except Exception as e: logging.warning( 'Removed streaming client %s: %s', self.client_address, str(e)) else: self.send_error(404) self.end_headers() class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer): allow_reuse_address = True daemon_threads = True with picamera.PiCamera(resolution='1296x972', framerate=24) as camera: output = StreamingOutput() #Uncomment the next line to change your Pi's Camera rotation (in degrees) camera.rotation = camera_rotation camera.start_recording(output, format='mjpeg') try: #print("try to get distance") address = ('', 8000) server = StreamingServer(address, StreamingHandler) server.serve_forever() #finally: # print("Stop camera stream") # camera.stop_recording() except KeyboardInterrupt: print("Shtdown HTTP server") server.shutdown() print("Close camera") camera.stop_recording() print("Switch off LED and cleanup GPIO") ring.fill((0,0,0)) ring.show() time.sleep(0.1) ring.deinit() GPIO.cleanup() os._exit(0)
Save the Python script
Explanation of the code
We will detail here only the most important points of the source code.
The Python Adafruit NeoPixels library allows you to independently control each LED driven by the WS2812B circuit. Unlike the Arduino version, it is not possible to change the brightness level except to free the resource and re-create a new NeoPixel object. To be able to change the color, you must initialize the auto_write parameter to False.
The D10 pin (SPI bus) is problematic on my Raspberry Pi (unless it comes from the library). There is only pin D12 available for this project.
ring = neopixel.NeoPixel(board.D12, LED_COUNT, brightness = LED_BRIGHTNESS, auto_write=False, pixel_order = LED_ORDER)
The fill() method allows you to change the color of all the LEDs in the ring. We apply the change by executing the show() method.
ring.fill((255,0,0))
ring.show()
The HTML interface is handled by a classic Python HTTP server. The HTML interface is using the Bootstrap theme already used with Flask.
Unlike Flask, data binding (automatic state update) is not available. It is therefore necessary to send an updated version of the interface for each update. As this is stored in a character string, you just need to use the format method to substitute the value of each variable.
PAGE=""" ... <p>Step: {step}</p> <p>LED: {ring_status}</p> <p>Color: {ring_color}</p> </div> ... """.format(ring_status=ring_status_label, ring_color=color,button_label= "OFF" if ring_status else "ON", step=stepper)
The StreamingHandler() method is used to intercept all POST and GET requests sent to the HTTP server from the browser. For example here, we intercept the UP command to move the vertical axis of the microscope using the Nema 17 motor. The right column shows the writing difference compared to Flask .
Standard Python HTTP Server | With Flask |
|
|
To stop the script, we intercept the key combination Ctrl + C so as to
- server.shutdown() stop the HTTP server
- camera.stop_recording() stop the video stream and release the camera
- ring.fill((0,0,0)) turn off the light ring
- ring.deinit() free the resources of the NeoPixel object
- GPIO.cleanup() free the GPIO
- os._exit(0) quit the Python script
Start the microscope script in administrator mode
To be able to drive the Neopixel LED ring from the Raspberry Pi GPIO, you must start the script in administrator mode by preceding the command with a sudo.
Open the Terminal and go to the directory of the Python script, for example the Document folder
cd /home/pi/Documents
Then run the script by preceding the command with a sudo
sudo python3 nom_du_script.py
The HTML interface of the microscope
The HTML interface displays the video feed from the HQ camera. On the side of the image, there are the UP and DOWN commands to move the Z axis of the stand up and down.
A selector allows you to choose the movement to be made. 0.1mm 1mm or 10mm with each movement. You can modify the source code to change this pre-selection.
The ON / OFF command turns on the light ring. It is possible to change the color of the lighting. Default in white, red, yellow and green.
The settings are displayed below the selector
Examples of magnification obtained with different lighting
By varying the color of the LED lighting, it is possible to highlight certain circuit details (components, tracks, inscriptions, etc.).
Here is a video demonstration
Photos of an ESP01 at different magnifications by changing the extension tube
Here are some shots at different magnifications of an ESP8266EX installed on an ESP01 development board.
White lighting with 100% brightness.
Extension tube 30m | Extension tube 40mm | 50mm extension tube |
Magnification x18 | Magnification x26 | Magnification x33 |
Observation of an RGB LED
Just for fun, here are two shots of an RGB LED of the Neopixel ring used for lighting. The integrated circuit, the connections and each LED source can be viewed very clearly for the color Red, Green and Blue.
Onion epidermis observation test
Here is a last test carried out with an 85mm extension tube. I replaced the methylene blue with food coloring to highlight the cells in the epidermis. To obtain the cliché, the lighting was placed on the table.
We can distinguish the cells very well but not the nucleus at all (certainly because of the dye used and the magnification too low).
This assembly is still limited for scientific observations.
Display the interface in full screen, kiosk mode
All internet browsers offer a version suitable for display screens. Compared to the F11 key , the Kiosk mode offers the following advantages:
- User cannot see desktop or operating system details
- The X (close) button is hidden
- The F11 key is disabled
- Menu bars, toolbars are not visible
- The status bar at the bottom is not visible
- Right click context menu does not work
- Destination links are not visible when hovering over links
Add a tab to Terminal or open a new Terminal then run the following command
chromium-browser -kiosk 127.0.0.1:8000
Chromium opens in full screen without the address bar and elevators. Super practical for welding under a microscope!
To learn more about Chromium’s kiosk mode, read this tutorial
- Motorized microscope with HQ camera for Raspberry Pi and HTML interface (Python)
- Which camera to choose for the Raspberry Pi 3 or Pi Zero W
- Six 3D printing boxes to make a DIY surveillance camera with a Raspberry Pi Zero W
- Raspberry Pi Zero W and Node-RED MQTT Surveillance Camera for Home Assistant
- #Test: unpacking the Panoramic Camera Xiaomi Mijia 1080p 360 °