A modular spectrometer application for Raspberry Pi and desktop. Calibrate, measure spectra, record waterfalls, analyze Raman scattering, and perform color science—all from one codebase.
Inspired by PySpectrometer2 by Les Wright — the project that turned a pocket spectroscope and Pi camera into a serious instrument. This project is a complete rewrite with a new architecture.
Screenshots coming soon — you can add your own to the media/ folder.
PySpectrometer2 proved that a DIY spectrometer could rival commercial units. But OS changes (Bullseye and beyond), broken dependencies, and the limitations of a single monolithic script made evolution difficult. PySpectrometer3 was built from scratch to address:
- Maintainability — Modular design, clear separation of capture → extraction → processing → display
- Extensibility — Mode-based architecture so new workflows (Raman, Color Science) don’t require rewriting core logic
- Flexibility — Pluggable camera backends (Picamera2, OpenCV, V4L, RTSP, HTTP streams)
- Testability — Unit-tested extraction, calibration, and peak detection
- Modern tooling — Poetry, Ruff, pytest, Python 3.11+
Most of it is vibe coded — built iteratively with a focus on “does it work?” and “can we extend it?” rather than perfect upfront design. The architecture doc (docs/ARCHITECTURE.md) captures the resulting structure.
| Mode | Purpose |
|---|---|
| Calibration | Wavelength calibration from known spectral lines (FL12, Hg, LED, D65) with auto peak-matching and polynomial fit |
| Measurement | General spectrum measurement with dark/white reference, overlay comparison, LED and I2C light control |
| Waterfall | Time-resolved spectrum display; stream to CSV with timestamps |
| Raman | Raman shift (cm⁻¹) display; laser wavelength config; zero cm⁻¹ auto-detect |
| Color Science | XYZ/LAB, CRI, CCT; reflectance/transmittance/illumination; xy chromaticity diagram |
- Auto-level — Adjust gain to bring peaks into target range
- Auto-center Y — Find spectrum center from intensity profile
- Extraction methods — Weighted sum (default), median (robust to hot pixels), Gaussian fit (precision)
- Rotation handling — Auto-detect and correct tilted spectrum lines (e.g. 5–15°)
- Perpendicular width — Configurable sampling width for better S/N
- 4-point minimum — 3rd order polynomial for accurate wavelength mapping
- Reference sources — Fluorescent (FL12), Mercury (Hg), white LEDs, CIE D65, CIE Illuminant A (tungsten, 2856 K — see below)
- Auto-calibrate — Match detected peaks to reference lines; save/load calibration
- Datasheet CMOS (default) — Relative spectral response from the bundled OV9281 CSV (or silicon-CMOS fallback). This is the generic sensor curve from the manufacturer/PDF.
- User curve (CRR in Calibration) — After wavelength calibration, choose a reference that matches your lamp (LED, A, D65, fluorescent, …), fill the slit with that light, then press CRR. The app forms a smoothed measured / reference ratio, scales it to the datasheet curve where the reference has signal, and blends toward the datasheet at the spectral edges (about ±35 nm fade) so ends stay stable. The result is stored under
[sensitivity]in~/.config/itohio/spectral/config.tomland loaded on next start. - CIE Illuminant A (“tungsten”) — Standard Illuminant A is defined as a Planckian (black-body) spectrum at 2856 K correlated color temperature. It is the CIE model for classic incandescent / tungsten lamps used in vision and photometry. It is not the same as photographic “3200 K tungsten” film lights (those run hotter). Real bulbs vary with voltage and glass; match your lamp as closely as you can and keep geometry stable when recording.
- S button — In Measurement, Raman, Color Science, Waterfall, S defaults on (correction applied). Turn S off to view data without dividing by the sensitivity curve. In Calibration, S defaults off so you can work on wavelength calibration on a raw(er) axis; enable S to preview the same correction as in other modes.
- CMOS button (Calibration) — Drops the user fit and returns to the datasheet-only curve; clears the saved custom table in config.
- CSV / Waterfall export — With S on, exports include the active sensitivity as a
Sensitivitycolumn (and waterfall / REC add# Sensitivity: …comma values like dark/white). Comment headers document correction:Sensitivity_correction_applied,Sensitivity_curve(datasheet_CMOSvsuser_calibrated),Sensitivity_calibration_reference(illuminant used for CRR, orn/a). With S off, there is no sensitivity column or# Sensitivity:line; headers still record that correction was not applied.[sensitivity].calibration_referencein config stores the CRR reference name.
flowchart LR
subgraph cal [Calibration mode]
R[Pick reference e.g. A or LED]
M[Live spectrum on calibrated axis]
F[CRR: smooth measured over ref]
C[(config.toml sensitivity)]
R --> M --> F --> C
end
subgraph use [Other modes]
S[S on: divide by active curve]
C --> S
end
- Dark/white correction — Normalize to references for transmission/reflectance
- Savitzky–Golay filter — Smoothing with configurable order
- Sensitivity correction — Datasheet CMOS curve and optional user-calibrated curve (see above)
- Peak detection — For calibration, overlays, and mode-specific logic
- Picamera2 — Native Raspberry Pi camera support
- OpenCV — Webcam, V4L2, RTSP, HTTP MJPEG (e.g. remote Pi stream)
- 10-bit grayscale — Pipeline expects 0–1023 for better dynamic range
- Graticule — Wavelength axis, peak labels, measurement cursors
- Waterfall — Time vs wavelength intensity
- Waveshare 3.5" — Optimized layout for touchscreen
- Fullscreen — 800×480 for benchtop setups
- CSV export — Spectrum data with metadata
- PDF report — Measurement mode and CSV viewer: multi-page report (matplotlib); saved under the same dated directory as CSV (
output/<date>/spectrum-<time>[-label].pdf), optional label like save - CSV wavelength floor — Measurement and Waterfall use
[export].min_wavelength(nm, default 300): points below that are omitted from CSV. Set to 0 for the full axis. Other modes use 0 on context; a mode can setctx.min_wavelengthinon_startto trim.
The bottom control bar uses OpenCV-drawn icons (no image assets or icon fonts) so it stays lightweight on a Raspberry Pi Zero and small displays. Each mode assigns an optional icon_type on ButtonDefinition; the bar renders square buttons when a known icon name is set, and falls back to the text label if icon_type is empty or unknown.
- Implementation —
src/pyspectrometer/gui/icons.py(draw functions + registry),src/pyspectrometer/gui/buttons.py(Button.render, auto width = height for known icons),src/pyspectrometer/gui/control_bar.py(spacer width uses the same square sizing). - Special case —
icon_type="playback"is handled inButton(live red circle / frozen gray square / capture progress pie); it is not in the generic icon registry. - Labels —
labelon eachButtonDefinitionis kept for logs and debugging; on-screen, known icons replace visible text. - Text kept as-is — Buttons whose meaning is a proper name (e.g. calibration reference sources Hg, D65, FL12, CSV viewer illuminant names) stay as short text.
flowchart LR
BD[ButtonDefinition]
IT{icon_type set?}
KN{Known in icons.py?}
SQ[Square button + draw icon]
TX[Text-sized button + putText]
BD --> IT
IT -->|no or empty| TX
IT -->|yes| KN
KN -->|yes| SQ
KN -->|no| TX
Registered icon names (add new ones in icons.py and wire them in the mode’s get_buttons()):
icon_type |
Meaning |
|---|---|
save |
Save / export to file |
pdf |
PDF report (measurement mode; same output folder as CSV) |
load |
Load from file |
quit |
Exit application |
sensitivity |
Spectral sensitivity correction (S-curve) |
avg |
Averaging |
peak_hold |
Peak hold / Max |
acc |
Accumulation |
dark |
Dark reference |
white |
White reference |
absorption |
Absorption view |
bars |
Spectrum bars |
zoom_x |
Horizontal zoom slider |
zoom_y |
Vertical zoom slider |
lamp |
Lamp / illuminant control |
peaks |
Peak detection |
snap |
Snap to peaks |
delta |
Peak delta / separation |
clear |
Clear / erase |
reference |
Reference spectrum overlay |
overlay |
Raw / stacked overlay |
gain |
Gain slider |
exposure |
Exposure |
auto_gain |
Auto gain |
auto_exposure |
Auto exposure |
eye |
Cycle preview (camera vs graph) |
calibrate |
Wavelength calibration / ruler |
level |
Auto level |
reset |
Reset calibration or sensitivity |
# Install (desktop)
poetry install
# Run (default: Measurement mode)
poetry run python -m pyspectrometer
# Calibration mode
poetry run python -m pyspectrometer --mode calibration
# Raman with 785 nm laser
poetry run python -m pyspectrometer --mode raman --laser 785
# Use webcam instead of Pi camera
poetry run python -m pyspectrometer --list-cameras # List devices
poetry run python -m pyspectrometer --camera 0 # Use device 0
# On Raspberry Pi (after make setup-packages): use system Python
python3 -m pyspectrometer --waveshare --mode measurement| Key | Action |
|---|---|
q |
Quit |
s |
Save spectrum (PNG + CSV) |
p |
Export PDF report (measurement mode; same folder as CSV) |
h |
Toggle peak hold |
m |
Toggle measure mode (wavelength cursor) |
c |
Calibration (in Calibration mode) |
e |
Cycle extraction method |
E |
Auto-detect rotation angle |
flowchart LR
subgraph Capture
PICAM[Picamera2]
OCV[OpenCV/V4L/RTSP]
end
subgraph Processing
EXT[Spectrum Extraction]
PIPE[Pipeline: filters, ref correction]
end
subgraph Modes
CAL[Calibration]
MEAS[Measurement]
WF[Waterfall]
RAM[Raman]
COL[Color Science]
end
Capture --> EXT --> PIPE --> Modes
See docs/ARCHITECTURE.md for the full design, mode specs, and implementation status.
The original PySpectrometer2 hardware design still applies:
- Standard build — Pocket spectroscope + Pi camera + zoom lens (M12)
- Mini build — Pocket spectroscope + Pi camera + 12 mm fixed lens
- Standalone — Hyperpixel 4" or Waveshare 3.5" for a compact benchtop unit
For a portable spectrometer with OV9281, prism, fiber optics, and Waveshare 3.5" display, see Building a Portable Visible Light Spectrometer (ITOHI blog).
Reference: Les Wright’s PySpectrometer and YouTube channel.
This section covers configuring Raspberry Pi OS Trixie (or Bookworm) for the Waveshare 3.5" DPI LCD and OV9281 monochrome camera. The DPI display uses GPIO pins, so we disable auto-detect and load overlays explicitly.
- Raspberry Pi Zero 2 W with Raspberry Pi OS Trixie (or Bookworm)
- Waveshare 3.5" DPI LCD (640×480, touch)
- OV9281 monochrome camera module (CSI)
From the project root on the Pi, run in order:
make setup-packages # apt (libcamera-dev) + poetry (picamera2, rpi-libcamera)
make setup-partitions # Separate writable /home (see below)
make setup-safe-shutdown # Logs to RAM, root/boot read-only
make setup-display # Waveshare + OV9281 overlays and config
sudo rebootPartitions: Root = used + 4GB (+ 256MB buffer), home = remainder. Boot from USB, run make setup-partitions (no GParted—all from command line). No swap (bad for SD wear). Logs use tmpfs (RAM).
If you prefer to configure manually or the Makefile fails:
-
Download overlays — 3.5inch DPI LCD DTBO, extract, and copy
.dtbofiles to/boot/firmware/overlays/. -
Edit config — Add to
/boot/firmware/config.txt:camera_auto_detect=0 display_auto_detect=0 dtoverlay=vc4-kms-v3d dtoverlay=waveshare-35dpi dtoverlay=waveshare-touch-35dpi max_framebuffers=2 dtoverlay=ov9281,arducam enable_uart=1 gpio=22=op,dl(
ov9281,arducamfor Arducam modules; useov9281for generic.gpio=22=op,dlenables LED control.) -
Reboot —
sudo reboot
Use Screen Configuration → Screen → DPI-1 → Orientation to rotate display and touch together. For headless/lite: add video=DPI-1:640x480M@60,rotate=90 (or 180/270) at the start of /boot/firmware/cmdline.txt.
For portable/field use, configure the Pi to survive unsafe power-off:
- Logs to RAM —
/var/logon tmpfs (no SD writes) - Root and boot read-only — No filesystem corruption on power loss
- Separate
/home— Application files and debugging live on a writable partition
Run make setup-partitions first (creates /home), then make setup-safe-shutdown. Use rw to remount for system updates, ro when done.
- Waveshare 3.5" DPI LCD Wiki — Full setup, rotation, touch calibration
- Portable Spectrometer Build — Hardware design, prism vs grating, OV9281
- Python ≥ 3.11
- NumPy, OpenCV, SciPy, colour-science
- Picamera2 (Raspberry Pi only — via apt)
Raspberry Pi: Poetry installs picamera2 and rpi-libcamera from PyPI. Apt: libcamera-dev only (C library).
Open Source. See LICENSE for details.
If you find value in projects like this, consider supporting the original author: PayPal — Les Wright.