HaloPi: a Raspberry Pi Halometer for Measuring Red Blood Cells

by DaniloR29 in Teachers > University+

2758 Views, 25 Favorites, 0 Comments

HaloPi: a Raspberry Pi Halometer for Measuring Red Blood Cells

Halometer.png

Measuring Red Blood Cells by Diffraction


Diffraction-based methods provide a simple and effective way to estimate the average diameter of red blood cells (RBCs) by analyzing the light patterns they produce when illuminated by a coherent source.

When a laser beam (typically in the red or green range, e.g., 532–650 nm) passes through a thin blood smear on a microscope slide, each red blood cell acts as a microscopic diffracting object. Instead of forming a simple shadow, the transmitted light produces a pattern of concentric bright and dark rings on a distant screen. These rings—often called diffraction halos—contain quantitative information about the size of the cells.

The key idea is straightforward: larger cells create smaller diffraction rings while smaller cells produce larger ones. By measuring the radius of the first diffraction ring on the screen, you can estimate the average cell diameter. The relationship can be written as:


d = (n × λ × L) / r

where:

- (d) is the average red blood cell diameter

- (λ) is the wavelength of the laser light

- (L) is the distance between the sample and the screen

- (r) is the radius of the measured diffraction ring

- (n) is a numerical factor that depends on which ring is measured and on the optical model

In many practical cases, using the first minimum of an Airy-like pattern (n ~ 1.22).


This approach is closely related to the method proposed by Adianus Pijper in 1929, which used diffraction spectra to estimate erythrocyte size. It offered a quick alternative to direct microscopic measurements and was particularly helpful for spotting abnormalities in red blood cell size distributions.

Diffraction methods have several advantages: they’re fast and relatively simple, require low-cost instrumentation, and there’s a direct link between the optical pattern and cell size. However, they have limitations as they provide an average size rather than individual cell measurements. This means results depend on sample preparation and alignment, and red blood cells aren’t perfectly circular apertures, introducing approximations.

Despite these limitations, this method remains a beautiful example of applying physical optics to biological measurement. Modern tools allow for a fully automated revisit. In this project, I present HaloPi, a contemporary reinterpretation of the historical halometer. It combines laser diffraction 3D printing and Raspberry Pi image analysis.

Supplies

The basic setup consists of:

  1. A low-power laser source (a green or red laser pointer) with a 3D printed support
  2. A prepared and diluted blood smear on a glass slide
  3. A 3D printed slide support that allows X-Y translations
  4. A screen placed at a known distance (L)
  5. a beam stopper
  6. A camera (in this project, a Raspberry Pi camera) to record the pattern
  7. Fixing M4 and M2 screw
  8. Two perforated steel plates to support the equipment.

You need a 3D printer; we have used a Creality Ender 5 Pro with PLA 1.4 mm filament.

Build the Microscope Slides Support

MicroSclidesScannerV1.1.png
  1. To create two platforms for holding the slide support and laser, use two perforated plates. Refer to my project “The Magic-Sand Slicer” for instructions on setting up the platforms.
  2. First, print the STL files MSS_Base, MSS_HorSlider and MSS_slideHolder. Attach the MSS_Base to the perforated steel plate. Then add the hexagonal connection screw (the silver bolt in the figure) and a 2-3cm long M4 screw to regulate the vertical position of the slide.
  3. Next, print the MSS_slideHolder. I’ve used 75x25mm slides but if you have a different size you can modify the slide holder using the OpenScad file below to suit your slide dimensions.
  4. Finally, print the MSS_HorSlider. Test the MSS_slideHolder sliding smoothly into the MSS_HorSlider slot. If it doesn’t slide smoothly, use a file and sandpaper to help with insertion and sliding.
  5. Prepare a blood smear on a microscope slide. Begin by taking a drop of blood and disinfecting your finger with a suitable antiseptic. Then prick your finger with a sterile needle and add the blood drop to the slide. Spread it across the surface using a cover slip. Allow it to dry and insert the slide into the MSS_slideHolder.


module MSlideSupport() {
difference(){
union(){
// Slide notch
cube([79, 29, 6], center=true);
translate([-50,0,0]) cube ([55,5.9,5.9],center=true);
translate ([0,14.5,0]) rotate ([45,0,0]) cube ([79,4.25,4.25],center=true);
translate ([0,-14.5,0]) rotate ([45,0,0]) cube ([79,4.25,4.25],center=true);
}
translate ([0,0,2]) cube([75.75, 25.75, 4], center=true);
translate ([0,0,1]) cube([73.5, 23.5, 10], center=true);
translate ([10,10,4]) cube ([10,10,4],center=true);
}
}

Laser Support

LaserSupport.png
IMG_1789.JPG
IMG_1783 copy.JPG
  1. Print the three STL files. Then add the M4 nut to the LaserBase.stl file. Use a vice to force the bolt into the hexagonal cavity.
  2. Next, add a 5-6 cm long M4 screw to the bottom of the laser holder (LaserBottom.stl file) and secure it with a screw.
  3. Insert the laser into the cylindrical cavity. We used a common stylus-type laser pointer with a diameter of about 14 mm and secured it with two M2 screws. Rotating the laser cylinder should allow you to turn it on and off.
  4. Finally, mount the laser on the platform in front of the microscope slide holder using two M2 screws and nuts as shown in the last figure.

Projection Screen and Raspberry Pi Camera

IMG_1778 copy.JPG
Presentation1.png

The laser diffraction projection was projected onto the round screen using the KaleidoPhoneScope project. This project also includes the files for the Pi camera holder.

For the Raspberry Pi, we have used the very cheap 1.3 model, and the support is designed for this model. If you do not have a Raspberry Pi, you can still use a smarthphone that provide a better quel

Finally, add a beamer stop. You can build one by gluing a small circular piece of black cardboard to the top using a suitable length of wire and supporting it with the 3D-printed wire holder from the Kaleidoscope project.

The final schematic image shows the different components that can be used to identify the part from the previous project.

Visualization Program for Raspberry Pi

Screenshot 2026-05-07 at 19.21.17.png

The HaloPi software is a preliminary experimental version developed to visualize and analyze diffraction halos produced by blood smears illuminated with a laser. The program provides live camera acquisition, interactive alignment, radial diffraction profile (RDF) analysis, image enhancement, and photo capture capabilities using a Raspberry Pi and Pi Camera. The figure shows the interface of the program, with the image filtered to display only the green channel.

The software was designed as a modern digital reinterpretation of historical halometry techniques and is intended primarily for educational, experimental, and demonstrative purposes. Future versions may include automatic ring detection, quantitative red blood cell diameter estimation, and advanced diffraction analysis tools.

For simplicity, the version of the software provided with this Instructable is a streamlined experimental release focused on the essential functions needed to visualize and record diffraction halos. More advanced versions currently under development include additional image-processing tools, automated diffraction analysis, and quantitative estimation of red blood cell diameters.

The attached Python program is provided in the accompanying TXT file so that readers can easily inspect, modify, and adapt the code for their own experimental setups.


#!/usr/bin/env python3

import os
import re
import time
import threading
import numpy as np
import cv2
from tkinter import *
from picamera import PiCamera
from picamera.array import PiRGBArray
from PIL import Image, ImageTk, ImageDraw

# =====================================================
# CONFIGURATION
# =====================================================

PHOTO_FOLDER = "/home/pi/photos"
PREVIEW_RES = (640, 480)
FULL_RES = (1920, 1080)

os.makedirs(PHOTO_FOLDER, exist_ok=True)

# =====================================================
# AUTO FILE NUMBERING
# =====================================================

def get_next_index():
files = os.listdir(PHOTO_FOLDER)
numbers = []

for f in files:
m = re.match(r"photo_(\d+)\.jpg", f)
if m:
numbers.append(int(m.group(1)))

return max(numbers)+1 if numbers else 1

photo_index = get_next_index()

# =====================================================
# CAMERA SETUP
# =====================================================

camera = PiCamera()
camera.resolution = PREVIEW_RES
camera.framerate = 24
camera.rotation = 270

camera.iso = 100
camera.brightness = 70
camera.contrast = 80
camera.shutter_speed = 1000
camera.exposure_mode = "off"

raw_capture = PiRGBArray(camera, size=PREVIEW_RES)

time.sleep(2)

# =====================================================
# GLOBAL VARIABLES
# =====================================================

latest_image = None
rdf_profile = None
sharpness_value = 0
lock = threading.Lock()

center_x = PREVIEW_RES[0] // 2
center_y = PREVIEW_RES[1] // 2

# =====================================================
# CAPTURE PHOTO
# =====================================================

def capture_photo():

global photo_index

filename = os.path.join(
PHOTO_FOLDER,
f"photo_{photo_index:03d}.jpg"
)

photo_index += 1

camera.capture(filename)

print("Saved:", filename)

# =====================================================
# RDF CALCULATION
# =====================================================

def radial_profile(data, center):

y, x = np.indices((data.shape))

r = np.sqrt((x - center[0])**2 + (y - center[1])**2)
r = r.astype(np.int32)

tbin = np.bincount(r.ravel(), data.ravel())
nr = np.bincount(r.ravel())

radialprofile = tbin / np.maximum(nr, 1)

return radialprofile

# =====================================================
# CAMERA LOOP
# =====================================================

def camera_loop():

global latest_image
global rdf_profile
global sharpness_value

for frame in camera.capture_continuous(
raw_capture,
format="bgr",
use_video_port=True):

img = frame.array

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Sharpness
sharpness_value = cv2.Laplacian(gray, cv2.CV_64F).var()

# RDF
rdf = radial_profile(gray, (center_x, center_y))

rdf = rdf[:300]

rdf = rdf.astype(np.float32)

rdf -= rdf.min()

if rdf.max() > 0:
rdf /= rdf.max()

rdf_profile = rdf

# Convert image
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

pil_img = Image.fromarray(img_rgb)

draw = ImageDraw.Draw(pil_img)

# Crosshair
draw.line((center_x, 0, center_x, PREVIEW_RES[1]), fill=(0,255,0))
draw.line((0, center_y, PREVIEW_RES[0], center_y), fill=(0,255,0))

# Small center marker
draw.ellipse(
(
center_x-4,
center_y-4,
center_x+4,
center_y+4
),
outline=(255,0,0),
width=2
)

# Overlay horizontal intensity profile
try:

line_data = gray[center_y, center_x:]

kernel = np.ones(5) / 5

line_data = np.convolve(line_data, kernel, mode='same')

prof = line_data.astype(np.float32)

prof -= prof.min()

if prof.max() > 0:
prof /= prof.max()

base_y = 40
scale_h = 60

prev_x = center_x
prev_y = base_y

for i in range(len(prof)):

x = center_x + i

if x >= PREVIEW_RES[0]:
break

y = int(base_y + scale_h - prof[i] * scale_h)

draw.line(
(prev_x, prev_y, x, y),
fill=(255,255,0),
width=2
)

prev_x = x
prev_y = y

except:
pass

with lock:
latest_image = pil_img.copy()

raw_capture.truncate(0)

# =====================================================
# GUI
# =====================================================

root = Tk()
root.title("HaloPi - Simplified Halometer")

# =====================================================
# IMAGE DISPLAY
# =====================================================

video_label = Label(root)
video_label.grid(row=0, column=0, padx=10, pady=10)

# =====================================================
# RDF DISPLAY
# =====================================================

rdf_canvas = Canvas(root, width=640, height=200, bg="black")
rdf_canvas.grid(row=1, column=0, padx=10, pady=10)

# =====================================================
# CONTROLS
# =====================================================

control_frame = Frame(root)
control_frame.grid(row=0, column=1, sticky="n")

# ISO
Label(control_frame, text="ISO").pack()

iso_slider = Scale(
control_frame,
from_=100,
to=800,
orient=HORIZONTAL,
length=200
)

iso_slider.set(100)
iso_slider.pack()

# Exposure
Label(control_frame, text="Exposure (µs)").pack()

exposure_slider = Scale(
control_frame,
from_=100,
to=10000,
orient=HORIZONTAL,
length=200
)

exposure_slider.set(1000)
exposure_slider.pack()

# Brightness
Label(control_frame, text="Brightness").pack()

brightness_slider = Scale(
control_frame,
from_=0,
to=100,
orient=HORIZONTAL,
length=200
)

brightness_slider.set(70)
brightness_slider.pack()

# Contrast
Label(control_frame, text="Contrast").pack()

contrast_slider = Scale(
control_frame,
from_=-100,
to=100,
orient=HORIZONTAL,
length=200
)

contrast_slider.set(80)
contrast_slider.pack()

# =====================================================
# UPDATE CAMERA SETTINGS
# =====================================================

def update_camera_settings():

camera.iso = iso_slider.get()
camera.shutter_speed = exposure_slider.get()
camera.brightness = brightness_slider.get()
camera.contrast = contrast_slider.get()

root.after(200, update_camera_settings)

# =====================================================
# MOUSE CLICK
# =====================================================

def select_center(event):

global center_x
global center_y

center_x = int(event.x)
center_y = int(event.y)

print(f"Selected center: ({center_x}, {center_y})")

video_label.bind("<Button-1>", select_center)

# =====================================================
# CAPTURE BUTTON
# =====================================================

capture_button = Button(
control_frame,
text="Capture Photo",
command=capture_photo,
bg="green",
fg="white",
width=20,
height=2
)

capture_button.pack(pady=20)

# =====================================================
# UPDATE GUI
# =====================================================

def update_gui():

with lock:
img = latest_image

if img is not None:

imgtk = ImageTk.PhotoImage(img)

video_label.imgtk = imgtk
video_label.configure(image=imgtk)

# Draw RDF

rdf_canvas.delete("all")

if rdf_profile is not None:

h = 200
w = 640

prev_x = 0
prev_y = h

for i in range(min(len(rdf_profile), w)):

val = rdf_profile[i]

y = h - val * h

rdf_canvas.create_line(
prev_x,
prev_y,
i,
y,
fill="lime",
width=2
)

prev_x = i
prev_y = y

root.title(f"HaloPi - Sharpness: {sharpness_value:.1f}")

root.after(50, update_gui)

# =====================================================
# START
# =====================================================

threading.Thread(target=camera_loop, daemon=True).start()

update_gui()
update_camera_settings()

root.mainloop()

camera.close()

Diffraction Experiment

Presentation1.png
ApparatusTop.png

WARNING: Inexpensive laser pointers, especially higher-power models, can cause serious eye damage if viewed directly or even through accidental reflections. Always use caution when performing these experiments and avoid exposing the eyes to the laser beam or specular reflections.


The apparatus should be assembled as shown in Figure 1, aligning the laser pointer, beam stop screen, and camera. Carefully measure the distance L (second figure) as it’s used in the formula provided in the introduction for calculating the red cell diameter.

Then start the Raspberry Pi and run the provided programme from the command line. Move the microscope slide horizontally and vertically using the adjustment screws to locate a region of the blood smear that produces a clean and approximately circular diffraction pattern on the screen.

Adjust the camera parameters to obtain the best contrast and visibility of the diffraction rings. The RDF (Radial Diffraction Function) plot can help identify the alternating bright and dark diffraction regions corresponding to the halo structure.

Preliminary Results

IMG_1785.JPG
photo_004.jpg

The first figure shows a snapshot of the diffraction pattern produced by the blood smear. The second image shows an example of a photograph saved by the program. The RDF (Radial Diffraction Function) is displayed at the bottom using a logarithmic scale, while the yellow curve overlaid on the image represents the same intensity profile to provide a more intuitive visualization of the diffraction structure.

The sharpness and clarity of the diffraction rings can be affected by several experimental factors, including the quality of the blood smear, the alignment of the optical components, the beam stop geometry, the projection distance, and the characteristics of the laser source itself.

In this preliminary version of the experiment, a low-cost green laser pointer was used because of its availability and brightness. However, inexpensive green lasers often produce noticeable speckle patterns, internal reflections, and beam irregularities that can reduce the sharpness and symmetry of the diffraction halos. In some cases, these effects may generate grainy structures or asymmetrical intensity distributions in the observed rings. Different light sources can also be explored. Red diode lasers often produce smoother and more stable diffraction patterns with reduced speckle noise.

Also, the blood smear thickness plays an important role. Very thick smears tend to produce diffuse scattering rather than clean diffraction patterns, whereas thinner, more homogeneous smears generally provide sharper, more regular halos. Dust particles, imperfections on the microscope slide, and vibrations of the optical setup may further degrade the image quality.

Despite these limitations, the experiment demonstrates that historical halometry techniques can still be reproduced using inexpensive modern components, digital imaging, and simple computational analysis.


I hope you enjoyed this project and feel inspired to reproduce it, improve it, and explore the fascinating principles of diffraction, optics, and historical scientific instrumentation through your own experiments.