Multispectral Thermal (PAG7936)¶
The PAG7936 variant of the Multispectral Thermal Camera Module pairs a 1 MP global-shutter colour sensor with a FLIR Lepton thermal core, so the OpenMV Cam can run colour-vision and thermal pipelines side by side.
For full datasheet, photos, and ordering see the Multispectral Thermal product page.
Note
Supported on the OpenMV N6 only.
Highlights¶
PAG7936: 1 MP global shutter
Accepts FLIR Lepton 1.x / 2.x / 3.x thermal cores
Simultaneous thermal + colour processing on one module
Sees in complete darkness, supports temperature measurement
Global shutter handles fast motion without rolling-shutter artifacts
Usage¶
The colour sensor and the FLIR Lepton each get their own
csi.CSI instance. The first call defaults to the primary sensor
(the PAG7936); the second binds to the Lepton by passing
cid= csi.LEPTON. Hard-reset the colour sensor with
csi.CSI.reset (hard=True) to bring the rail up, and
configure the Lepton with hard=False so its driver only
reprograms the chip without re-toggling reset.
csi.CSI.framesize ( csi.QVGA ) matches the Lepton
output to the colour camera, so each snapshot() returns a
320x240 frame. The Lepton driver internally upscales its 80x60
(1.x/2.x) or 160x120 (3.x) native frame to the requested size — at
QVGA every Lepton pixel covers a 4x4 or 2x2 cell on the colour
frame.
Two scratch buffers stay constant across the frame loop — a 256x1
alpha palette stored as an image.Image so cool Lepton pixels
become transparent and hot pixels become opaque (the quadratic
ramp suppresses background detail without crushing the mid-range),
and a Lepton frame buffer pre-allocated with image.Image so
csi.CSI.snapshot (blocking=False, image=...) can fill it in
place each iteration without reallocating:
import time
import csi
import image
import math
alpha_pal = image.Image(256, 1, image.GRAYSCALE)
for i in range(256):
alpha_pal[i] = int(math.pow((i / 255), 2) * 255)
# Setup the color camera sensor.
csi0 = csi.CSI()
csi0.reset(hard=True) # force hardware reset.
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)
csi1 = csi.CSI(cid=csi.LEPTON)
csi1.reset(hard=False) # no hardware reset - just configure lepton
csi1.pixformat(csi.GRAYSCALE)
csi1.framesize(csi.QVGA)
# Optional temperature range controls for the LEPTON.
# csi1.ioctl(csi.IOCTL_LEPTON_SET_MODE, True, False)
# csi1.ioctl(csi.IOCTL_LEPTON_SET_RANGE, 20.0, 40.0)
clock = time.clock()
img1 = image.Image(csi1.width(), csi1.height(), csi1.pixformat())
while True:
clock.tick()
img0 = csi0.snapshot()
csi1.snapshot(blocking=False, image=img1)
img0.draw_image(img1, 0, 0, color_palette=image.PALETTE_IRONBOW,
alpha_palette=alpha_pal,
hint=image.BILINEAR)
print(clock.fps())
Each iteration takes a blocking colour snapshot and a non-blocking
Lepton snapshot — the Lepton runs at 9 Hz so blocking on it would
throttle the colour pipeline.
Image.draw_image then composites the two: color_palette=
image.PALETTE_IRONBOW maps the Lepton’s grayscale to a FLIR-style
warm colour ramp, alpha_palette= blends each pixel using the
quadratic alpha map, and hint= image.BILINEAR smooths the
upscale.
Temperature measurement¶
Radiometric Leptons (Lepton 2.5 / 3.5) report calibrated per-pixel
temperature data. Enable measurement mode through csi.CSI.ioctl
with csi.IOCTL_LEPTON_SET_MODE, then clamp the temperature window
with csi.IOCTL_LEPTON_SET_RANGE (min_celsius, max_celsius).
The Lepton driver linearly maps grayscale pixel value 0 to
min_celsius and 255 to max_celsius, so each pixel becomes
a temperature lookup within the configured window. Pixels colder
than min_celsius saturate at 0, pixels hotter than
max_celsius saturate at 255.
csi.IOCTL_LEPTON_SET_MODE takes two flags. The first turns
measurement on; the second selects the sensor’s temperature range:
Low range —
(True, False)— sensor span-10 °Cto+140 °C(room-scale scenes). Clamp the window to the area of interest, e.g.(20.0, 40.0)for body-heat tracking:csi1.ioctl(csi.IOCTL_LEPTON_SET_MODE, True, False) csi1.ioctl(csi.IOCTL_LEPTON_SET_RANGE, 20.0, 40.0)
High range —
(True, True)— sensor span-10 °Cto~+450 °Ctypical (~+400 °Cat room temperature) for hot objects. Clamp to e.g.(0.0, 400.0)for furnace or hot-element tracking:csi1.ioctl(csi.IOCTL_LEPTON_SET_MODE, True, True) csi1.ioctl(csi.IOCTL_LEPTON_SET_RANGE, 0.0, 400.0)
To convert a grayscale pixel back to Celsius:
def p_to_temp(p, min_t, max_t):
return (p * (max_t - min_t)) / 255.0 + min_t
This works on individual pixels or on aggregated statistics
(e.g. stats.mean() from Image.get_statistics) inside an
ROI when locating hot/cool regions with Image.find_blobs.
GPU-accelerated alignment¶
Image.draw_image accepts a transform= argument — a 3x3
homography matrix as a 2-D ulab.numpy array. On the OpenMV N6 the
GPU runs the per-pixel projection during the same draw, so the
Lepton frame can be re-aligned against the colour camera’s
perspective without a separate warp pass. Calibrate the matrix per
camera with the
thermal-overlay-calibration tool:
import time
import csi
import image
from ulab import numpy as np
import math
# Calibration matrix from the thermal-overlay-calibration tool.
m = np.array([
[3.704807, 0.257018, 37.260564],
[0.052147, 3.609977, -7.831831],
[0.000294, 0.000552, 1.000000],
])
alpha_pal = image.Image(256, 1, image.GRAYSCALE)
for i in range(256):
alpha_pal[i] = int(math.pow((i / 255), 2) * 255)
# Setup the color camera sensor.
csi0 = csi.CSI()
csi0.reset(hard=True) # force hardware reset.
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.VGA)
csi1 = csi.CSI(cid=csi.LEPTON)
csi1.reset(hard=False) # no hardware reset - just configure lepton
csi1.pixformat(csi.GRAYSCALE)
csi1.framesize(csi.QQVGA)
# Optional temperature range controls for the LEPTON.
# csi1.ioctl(csi.IOCTL_LEPTON_SET_MODE, True, False)
# csi1.ioctl(csi.IOCTL_LEPTON_SET_RANGE, 20.0, 40.0)
clock = time.clock()
img1 = image.Image(csi1.width(), csi1.height(), csi1.pixformat())
while True:
clock.tick()
img0 = csi0.snapshot()
csi1.snapshot(blocking=False, image=img1)
img0.draw_image(img1, 0, 0, color_palette=image.PALETTE_IRONBOW,
alpha_palette=alpha_pal,
hint=image.BILINEAR,
transform=m)
print(clock.fps())
Note that this variant runs the colour camera at csi.VGA
(640x480) and the Lepton at csi.QQVGA (160x120) — the homography
projects the smaller Lepton frame into the larger colour frame as
part of the draw, so the upscale factor is baked into the matrix
itself rather than applied separately.