close
Skip to content

foxis/PySpectrometer3

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

305 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PySpectrometer3

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.


Why a Total Rewrite?

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.


Features

Operating Modes

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

Spectrum Extraction

  • 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

Calibration

  • 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

Spectral sensitivity correction

  • 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.toml and 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 Sensitivity column (and waterfall / REC add # Sensitivity: … comma values like dark/white). Comment headers document correction: Sensitivity_correction_applied, Sensitivity_curve (datasheet_CMOS vs user_calibrated), Sensitivity_calibration_reference (illuminant used for CRR, or n/a). With S off, there is no sensitivity column or # Sensitivity: line; headers still record that correction was not applied. [sensitivity].calibration_reference in 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
Loading

Processing Pipeline

  • 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

Capture

  • 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

Display & Export

  • 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 set ctx.min_wavelength in on_start to trim.

Control bar (icon buttons)

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.

  • Implementationsrc/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 caseicon_type="playback" is handled in Button (live red circle / frozen gray square / capture progress pie); it is not in the generic icon registry.
  • Labelslabel on each ButtonDefinition is 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
Loading

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

Quick Start

# 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

Keyboard Shortcuts (common)

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

Project Structure

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
Loading

See docs/ARCHITECTURE.md for the full design, mode specs, and implementation status.


Hardware

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.


Raspberry Pi Zero 2 W Setup (Waveshare 3.5" + OV9281)

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.

Prerequisites

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

Quick Setup (Makefile)

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 reboot

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

Manual Setup

If you prefer to configure manually or the Makefile fails:

  1. Download overlays3.5inch DPI LCD DTBO, extract, and copy .dtbo files to /boot/firmware/overlays/.

  2. 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,arducam for Arducam modules; use ov9281 for generic. gpio=22=op,dl enables LED control.)

  3. Rebootsudo reboot

Display Rotation (Trixie/Bookworm)

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.

Safe Shutdown (Read-Only Root)

For portable/field use, configure the Pi to survive unsafe power-off:

  • Logs to RAM/var/log on 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.

References


Dependencies

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


License

Open Source. See LICENSE for details.

If you find value in projects like this, consider supporting the original author: PayPal — Les Wright.

About

The Third Incarnation of the Spectrometer project!

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Python 98.2%
  • Shell 1.3%
  • Makefile 0.5%