Motorized microscope with HQ camera for Raspberry Pi and HTML interface (Python)

raspberry pi hq camera microscope cnc 3018 parts python interface neopixel ring led
Table of Contents

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.

Read Also
Drive a Nema 17 stepper motor with the RpiMotorLib Python library for A4988

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

Read Also
Components for DIY 3D printers and CNC. Profiles, T-nut, Nema 17, A4899 ...

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

Read Also
Cameras for Raspberry Pi. 5MP, 8MP, NoIR, HQ 12MP C-mount lens

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 C5028-M lens installed on the HQ Camera of the Raspberry Pi via a via 50mm extension tube (40 + 10)

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.

caliber tube extension 30mm magnification 18 raspberry pi hq camera

Electric circuit

Components recovered on a CNC 3018

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
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
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


Tripod HQ Camera Nema 17 A4988 Neopixel Ring 16


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)

Raspberry Pi Microscope support camera hq xe vertical
Support for Raspberry Pi to be fixed on the base of the stand Raspberry Pi Microscope support a4988 profile aluminium 2020
Light ring mount (recommended) compatible with any C-mount lens Raspberry Pi Microscope adapter led ring rgb
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

C-mount extender tube hq camera raspberry pi

Assembly of the stand

Here are some photos showing the assembly of the 2020 aluminum profiles.

raspberry pi hq camera microscope top view top view

Top view

raspberry pi hq camera microscope nema 17 stepper motor

Left rear view. Fixing the vertical axis and the Nema 17 motor on its support.

raspberry pi hq camera microscope nema 17 stepper motor right

Right rear view. Fixing the T8 screw using a flexible coupling on the output shaft of the Nema 17 stepper motor

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.

raspberry pi hq camera support a4988

The Raspberry Pi is screwed to the 3D printed vertical support on the 2020 aluminum profile using a T-nut

raspberry pi hq camera microscope circuit

Connection to the GPIO

raspberry pi hq camera microscope circuit a4988 power supply nema neopixel

Connection of the Nema 17 motor to the A4988 controller. 3V / 5V / 12V power supply for Nema 17 motor and Neopixel LED ring.

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.

raspberry pi hq camera microscope support cam 3d printed

50mm lens installed on the 3D printed support

raspberry pi hq camera microscope neopixel led ring 50mm optical cosmicar

Neopixel LED ring installed in the 3D printed holder then inserted on the lens.

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

raspberry pi os activate i2c spi 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.

Attention, the script only works in Python 3.

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.
GitHub logo

Source Code

# Source code based on the Web streaming example from the official PiCamera package

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)

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
            with self.condition:
                self.frame = self.buffer.getvalue()
        return self.buffer.write(buf)

class StreamingHandler(server.BaseHTTPRequestHandler):
    def getPage(self):
                    <meta charset="utf-8">
                    <!-- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=yes"> -->

                    <!-- Bootstrap CSS -->
                    <link rel="stylesheet" href="" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

                    <title>Raspberry Pi HQ Camera Magnifying</title>
                <div class="container">
                    <div class="row">
                    <div class="col-sm-9" style="padding:10">
                        <img src="stream.mjpg" style="width:100%">
                    <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>
                            <div class="col-4">
                                <form method="POST" action="down" class="center-block">
                                    <button type="submit" class="btn btn-primary">Down</button>
                        <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>
                            <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>
                            <div class="col-2">     
                                <form method="POST" action="changecolor" >
                                    <button name="ringcolor" value="red" type="submit" class="btn btn-danger"> R </button>
                            <div class="col-2">     
                                <form method="POST" action="changecolor" >
                                    <button name="ringcolor" value="yellow" type="submit" class="btn btn-warning"> Y </button>
                            <div class="col-2">     
                                <form method="POST" action="changecolor" >
                                    <button name="ringcolor" value="green" type="submit" class="btn btn-success"> G </button>
                        <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>
                        <p>Step: {step}</p>
                        <p>LED: {ring_status}</p>
                        <p>Color: {ring_color}</p>
            """.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":
          elif color == "yellow":
          elif color == "green":
           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 = 
        if self.path == '/up':
            print("Go Up to ", distance)
            mymotortest.motor_go(False, "Full" , distance, 0.001 , False, .05)
            self.send_header('Location', '/index.html')
        elif self.path == '/down':
            print("Go Down to ", distance)
            mymotortest.motor_go(True, "Full" , distance, 0.001 , False, .05)
            self.send_header('Location', '/index.html')
        elif self.path == "/changecolor":
            color = post_data[10:].decode('utf-8')
            self.send_header('Location', '/index.html')
        elif self.path == "/switchonoffring":          
                if ring_status == False:
                  ring_status = True
                  ring_status_label = "On"
                  print("Switch ON LED")
                  ring.fill((0, 0, 0))
                  ring_status_label = 'Off'
                  ring_status = False
                  print("Switch OFF LED")
            except Exception as e:
                logging.warning('Neopixel error: %s', str(e))
            self.send_header('Location', '/index.html')
        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_header('Location', '/index.html')
    def do_GET(self):
        if self.path == '/':
            self.send_header('Location', '/index.html')
        elif self.path == '/index.html':
            content = self.getPage().encode('utf-8')
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
        elif self.path == '/stream.mjpg':
            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')
                while True:
                    with output.condition:
                        frame = output.frame
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
            except Exception as e:
                    'Removed streaming client %s: %s',
                    self.client_address, str(e))

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')
        #print("try to get distance")

        address = ('', 8000)
        server = StreamingServer(address, StreamingHandler)

    #    print("Stop camera stream")
    #    camera.stop_recording()
    except KeyboardInterrupt:
        print("Shtdown HTTP server")
        print("Close camera")
        print("Switch off LED and cleanup GPIO")

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.


The HTML interface is handled by a classic Python HTTP server. The HTML interface is using the Bootstrap theme already used with Flask.

Read Also
Flask + Bootstrap. HTML interface for effortless Python projects

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.

     <p>Step: {step}</p>
     <p>LED: {ring_status}</p>
     <p>Color: {ring_color}</p>
   """.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
if self.path == '/up': 
    mymotortest.motor_go(False, "Full" , distance, 0.001 , False, .05) 
    self.send_header('Location', '/index.html') 
@app.route("/up", methods=["POST"]) 
def up(): 
    global distance print("Move up,", distance, "steps") 
    mymotortest.motor_go(False, "Full" , distance, 0.01 , False, .05) 
    return redirect(request.referrer)

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

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

raspberry pi hq camera microscope magnifier python http mjpeg stream nema17 a4988

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.).

esp8266ex microscope raspberry pi hq camera python white


esp8266ex microscope raspberry pi hq camera python red ring led


esp8266ex microscope raspberry pi hq camera python green


esp8266ex microscope raspberry pi hq camera python yellow


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
50mm lens 30mm tube 50mm lens 40mm tube 50mm lens 50mm tube
Magnification x18 Magnification x26 Magnification x33
caliber tube extension 30mm magnification 18 raspberry pi hq camera caliber tube extension 40mm magnification 26 raspberry pi hq camera caliber tube extension 50mm magnification 33 raspberry pi hq camera

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.

Light On Light off
microscope raspberry pi hq camera led rgb zoom x33 on microscope raspberry pi hq camera led rgb zoom x33 off

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.

microscope raspberry pi hq camera cell onion cell

Observation of the epidermis of an onion.

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

Chromium opens in full screen without the address bar and elevators. Super practical for welding under a microscope!

raspberry pi hq camera microscope tactile display http server

To learn more about Chromium’s kiosk mode, read this tutorial

Read Also
Open an HTML page when starting Raspberry Pi OS with Chromium Browser in full screen (kiosk mode)

Click to rate this post!
[Total: 0 Average: 0]

Thanks for your reading

Did you like this project ? Don't miss any more projects by subscribing to our weekly newsletter!

Are you having a problem with this topic?

Maybe someone has already found the solution, visit the forum before asking your question

1 Comment
  1. I’ve been looking for this tutorial for a long time, thanks.
    I just ask for a small change to the program.
    Would it be possible to extend the program with input from the end switches, at each end of the stroke?
    Another thing I miss is the signal – enable. The stepper motor, which is still active, heats up quite dangerous.

    Thanks again for the great tutorial.

Leave a Reply

Read more
DIY Projects
DIY Projects
%d bloggers like this: