Files
MAG160C/csdk/src/mag160c_display.c
T

557 lines
20 KiB
C

/*
* MAG160C display pipeline - pure C, no OS/USB dependencies.
*
* Implements the pipeline validated on hardware (Windows demo v3) and
* informed by open-source thermal SDKs (SeekThermal/OpenThermal, FLIR
* Lepton AGC, MLX90640):
*
* frame -> bad pixel correction -> diff/NUC -> AGC -> LUT -> RGB
*
* 1. Bad pixel detection (Seek-style histogram peak deviation + temporal
* min/max) with topological-order 4-neighbour fill.
* 2. AGC: percentile stretch (FLIR LINEAR clip, 2%/98%) or adaptive diff
* stretch with deadband.
* 3. Ironbow pseudo-color LUT (FLIR-style anchors).
* 4. FFC scheduler replicating the official demo cadence recovered from
* analysis/captures/libusb0_trace.txt: FFC(1) after the ~10th frame
* (stream -> type=0), then every ffc_period frames send FFC(0) and
* ffc_gap frames later FFC(1). Verified: 1400+ type=0 frames, 15 fps,
* zero stalls.
* 5. Two-point linear temperature calibration (counts -> degC).
*/
#include "mag160c_internal.h"
#include "mag160c/mag160c_display.h"
#include <stdlib.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* bad pixel map */
void mag160c_display_badmap_init(mag160c_display_badmap_t *m, uint32_t w,
uint32_t h) {
if (m == NULL) {
return;
}
memset(m, 0, sizeof(*m));
m->width = w;
m->height = h;
m->pixels = w * h;
m->temporal_thr = 400; /* min/max fluctuation (counts) */
m->hist_min_dev = 200; /* histogram peak min deviation (counts) */
m->bad = (uint8_t *)calloc(m->pixels, 1);
m->order_x = (uint32_t *)malloc(m->pixels * sizeof(uint32_t));
m->order_y = (uint32_t *)malloc(m->pixels * sizeof(uint32_t));
m->ref_sum = (uint64_t *)calloc(m->pixels, sizeof(uint64_t));
m->ref_min = (uint16_t *)malloc(m->pixels * sizeof(uint16_t));
m->ref_max = (uint16_t *)malloc(m->pixels * sizeof(uint16_t));
m->ref = (uint16_t *)calloc(m->pixels, sizeof(uint16_t));
}
void mag160c_display_badmap_destroy(mag160c_display_badmap_t *m) {
if (m == NULL) {
return;
}
free(m->bad);
free(m->order_x);
free(m->order_y);
free(m->ref_sum);
free(m->ref_min);
free(m->ref_max);
free(m->ref);
memset(m, 0, sizeof(*m));
}
static int has_valid_neighbor(const uint8_t *bad, uint32_t w, uint32_t h,
uint32_t x, uint32_t y) {
return (x > 0 && !bad[y * w + x - 1]) ||
(x + 1 < w && !bad[y * w + x + 1]) ||
(y > 0 && !bad[(y - 1) * w + x]) ||
(y + 1 < h && !bad[(y + 1) * w + x]);
}
/* neighbour mean of frame at (x,y), skipping bad pixels; 0 when none. */
static uint32_t neighbor_mean(const uint16_t *fr, const uint8_t *bad,
uint32_t w, uint32_t h, uint32_t x, uint32_t y) {
uint64_t sum = 0;
uint32_t n = 0;
if (x > 0 && !bad[y * w + x - 1]) { sum += fr[y * w + x - 1]; n++; }
if (x + 1 < w && !bad[y * w + x + 1]) { sum += fr[y * w + x + 1]; n++; }
if (y > 0 && !bad[(y - 1) * w + x]) { sum += fr[(y - 1) * w + x]; n++; }
if (y + 1 < h && !bad[(y + 1) * w + x]) { sum += fr[(y + 1) * w + x]; n++; }
return n ? (uint32_t)(sum / n) : 0;
}
/* topological fill order so clusters are filled edge-first */
static void build_fill_order(mag160c_display_badmap_t *m) {
const uint32_t w = m->width, h = m->height, n = m->pixels;
uint8_t *work = (uint8_t *)malloc(n);
if (work == NULL) {
return;
}
memcpy(work, m->bad, n);
uint32_t remain = 0;
for (uint32_t i = 0; i < n; ++i) {
if (work[i]) remain++;
}
uint32_t len = 0;
while (remain > 0) {
uint32_t progress = 0;
for (uint32_t y = 0; y < h; ++y) {
for (uint32_t x = 0; x < w; ++x) {
if (!work[y * w + x]) continue;
if (!has_valid_neighbor(work, w, h, x, y)) continue;
m->order_x[len] = x;
m->order_y[len] = y;
len++;
work[y * w + x] = 0;
remain--;
progress++;
}
}
if (progress == 0) { /* fully isolated: force-fill in order */
for (uint32_t y = 0; y < h && progress == 0; ++y) {
for (uint32_t x = 0; x < w && progress == 0; ++x) {
if (!work[y * w + x]) continue;
m->order_x[len] = x;
m->order_y[len] = y;
len++;
work[y * w + x] = 0;
remain--;
progress++;
}
}
}
}
m->order_len = len;
free(work);
}
/* threshold = histPeak - (frameMax - histPeak) (Seek method).
* Guarded: a pixel is only flagged when it is also at least hist_min_dev
* counts above the scene peak, so a lone extreme outlier cannot push the
* threshold below the whole background. */
static uint32_t histogram_peak_threshold(const uint16_t *fr, uint32_t n,
uint32_t min_dev) {
uint32_t *hist = (uint32_t *)calloc(65536, sizeof(uint32_t));
if (hist == NULL) {
return 0xffffu;
}
uint32_t peakv = 0, peakc = 0, maxv = 0;
for (uint32_t i = 0; i < n; ++i) {
uint32_t v = fr[i];
if (++hist[v] > peakc) {
peakc = hist[v];
peakv = v;
}
if (v > maxv) maxv = v;
}
free(hist);
if (maxv <= peakv) {
return 0xffffu;
}
int64_t t = (int64_t)peakv - ((int64_t)maxv - (int64_t)peakv);
uint64_t guard = (uint64_t)peakv + min_dev;
return t > (int64_t)guard ? (uint32_t)t : (uint32_t)guard;
}
/* Feed frames for reference statistics (call ~30 times at idle scene). */
mag160c_error_t mag160c_display_badmap_feed(mag160c_display_badmap_t *m,
const uint16_t *frame) {
if (m == NULL || frame == NULL || m->bad == NULL) {
mag160c_set_error("mag160c_display_badmap_feed: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
const uint32_t n = m->pixels;
for (uint32_t i = 0; i < n; ++i) {
uint16_t v = frame[i];
m->ref_sum[i] += v;
if (m->feed_count == 0) {
m->ref_min[i] = m->ref_max[i] = v;
} else {
if (v < m->ref_min[i]) m->ref_min[i] = v;
if (v > m->ref_max[i]) m->ref_max[i] = v;
}
}
m->feed_count++;
return MAG160C_OK;
}
/* Finalize: averages, temporal + histogram bad pixel detection, fill. */
mag160c_error_t mag160c_display_badmap_finalize(mag160c_display_badmap_t *m) {
if (m == NULL || m->bad == NULL) {
mag160c_set_error("mag160c_display_badmap_finalize: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
if (m->feed_count == 0) {
mag160c_set_error("mag160c_display_badmap_finalize: no frames fed");
return MAG160C_ERR_NOT_READY;
}
const uint32_t n = m->pixels;
for (uint32_t i = 0; i < n; ++i) {
m->ref[i] = (uint16_t)(m->ref_sum[i] / m->feed_count);
m->bad[i] = 0;
}
uint32_t nb = 0;
/* temporal detection */
for (uint32_t i = 0; i < n; ++i) {
if ((uint32_t)m->ref_max[i] - (uint32_t)m->ref_min[i] > m->temporal_thr) {
m->bad[i] = 1;
nb++;
}
}
/* histogram peak deviation (bright defects) */
uint32_t thr = histogram_peak_threshold(m->ref, n, m->hist_min_dev);
for (uint32_t i = 0; i < n; ++i) {
if (!m->bad[i] && (uint32_t)m->ref[i] > thr) {
m->bad[i] = 1;
nb++;
}
}
m->bad_count = nb;
build_fill_order(m);
/* fill the reference image itself */
uint8_t *work = (uint8_t *)malloc(n);
if (work == NULL) {
return MAG160C_ERR_NO_MEMORY;
}
memcpy(work, m->bad, n);
for (uint32_t i = 0; i < m->order_len; ++i) {
uint32_t x = m->order_x[i], y = m->order_y[i];
uint32_t v = neighbor_mean(m->ref, work, m->width, m->height, x, y);
if (v > 0) {
m->ref[y * m->width + x] = (uint16_t)v;
work[y * m->width + x] = 0;
}
}
free(work);
m->ready = 1;
mag160c_clear_error();
return MAG160C_OK;
}
/* Correct one frame in place using the stored topological order. */
mag160c_error_t mag160c_display_badmap_correct(const mag160c_display_badmap_t *m,
uint16_t *frame) {
if (m == NULL || frame == NULL || !m->ready) {
mag160c_set_error("mag160c_display_badmap_correct: not ready");
return MAG160C_ERR_NOT_READY;
}
for (uint32_t i = 0; i < m->order_len; ++i) {
uint32_t x = m->order_x[i], y = m->order_y[i];
uint32_t v = neighbor_mean(frame, m->bad, m->width, m->height, x, y);
if (v > 0) frame[y * m->width + x] = (uint16_t)v;
}
mag160c_clear_error();
return MAG160C_OK;
}
/* MOG-style per-pixel reference tracking (anti-ghost):
* - |d| < thr : background pixel, slowly track drift (ref += d/alpha)
* - |d| >= thr : foreground (object), reference FROZEN - a static object
* is never absorbed, so moving it away leaves no ghost.
* Pass ref=null to just classify (not needed by callers today). */
void mag160c_display_ref_track(uint16_t *ref, const uint16_t *live,
uint32_t n, int32_t thr, int32_t alpha) {
if (ref == NULL || live == NULL || thr <= 0 || alpha <= 0) {
return;
}
for (uint32_t i = 0; i < n; ++i) {
int32_t d = (int32_t)live[i] - (int32_t)ref[i];
if (d > -thr && d < thr) {
ref[i] = (uint16_t)((int32_t)ref[i] + d / alpha);
}
}
}
/* MOG tracking with self-healing (anti-ghost + anti-startup-noise):
* fg_count[] counts consecutive foreground frames per pixel. A pixel stuck
* as *isolated* foreground (fewer than min_nbr foreground 8-neighbors) for
* more than heal_frames is a reference error (startup noise baked into the
* reference, bad pixel) rather than a real object (objects are contiguous)
* - it is reset to the live value so noise cannot persist forever. */
void mag160c_display_ref_track_heal(uint16_t *ref, const uint16_t *live,
uint32_t w, uint32_t h,
int32_t thr, int32_t alpha,
uint8_t *fg_count, uint32_t heal_frames,
uint32_t min_nbr) {
if (ref == NULL || live == NULL || w == 0 || h == 0 ||
thr <= 0 || alpha <= 0 || fg_count == NULL) {
return;
}
if (heal_frames == 0) heal_frames = 1;
const uint32_t n = w * h;
for (uint32_t i = 0; i < n; ++i) {
int32_t d = (int32_t)live[i] - (int32_t)ref[i];
if (d > -thr && d < thr) {
ref[i] = (uint16_t)((int32_t)ref[i] + d / alpha);
fg_count[i] = 0;
} else {
if (fg_count[i] < 255) fg_count[i]++;
if (fg_count[i] >= heal_frames) {
uint32_t x = i % w, y = i / w;
uint32_t nbr_fg = 0;
for (int32_t dy = -1; dy <= 1; ++dy) {
for (int32_t dx = -1; dx <= 1; ++dx) {
if (dx == 0 && dy == 0) continue;
int32_t xx = (int32_t)x + dx, yy = (int32_t)y + dy;
if (xx < 0 || (uint32_t)xx >= w ||
yy < 0 || (uint32_t)yy >= h) {
continue;
}
uint32_t nbr = (uint32_t)yy * w + (uint32_t)xx;
int32_t dn = (int32_t)live[nbr] - (int32_t)ref[nbr];
if (dn > thr || dn < -thr) nbr_fg++;
}
}
if (nbr_fg < min_nbr) {
ref[i] = live[i]; /* heal reference error */
fg_count[i] = 0;
}
}
}
}
}
/* Flat-field (NUC) correction: out[i] = live[i] - (ref[i] - mean(ref)).
* Removes the fixed sensor mura (measured row span 15704 -> 401 counts on
* the MAG160C), so the absolute temperature image is smooth. The reference
* is the fixed pattern only - it must NOT track the scene (see
* mag160c_display_ref_track_heal for the freeze/self-heal model and the
* object-free guard at capture time), otherwise objects ghost. */
mag160c_error_t mag160c_display_nuc(const uint16_t *live, const uint16_t *ref,
uint32_t n, uint16_t *out) {
if (live == NULL || ref == NULL || out == NULL || n == 0) {
mag160c_set_error("mag160c_display_nuc: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
uint64_t s = 0;
for (uint32_t i = 0; i < n; ++i) s += ref[i];
const int32_t mean = (int32_t)(s / n);
for (uint32_t i = 0; i < n; ++i) {
int64_t v = (int64_t)live[i] - ((int64_t)ref[i] - mean);
if (v < 0) v = 0;
if (v > 65535) v = 65535;
out[i] = (uint16_t)v;
}
mag160c_clear_error();
return MAG160C_OK;
}
/* Re-align the reference after FFC(1): the type=0 baseline shifts (measured
* up to +1000 counts). Applies the median of (live-ref) clamped to
* max_shift as a global offset, robust to a small object in the scene. */
mag160c_error_t mag160c_display_ref_rebase(uint16_t *ref, const uint16_t *live,
uint32_t n, int32_t max_shift) {
if (ref == NULL || live == NULL || n == 0) {
mag160c_set_error("mag160c_display_ref_rebase: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
int32_t *d = (int32_t *)malloc(n * sizeof(int32_t));
if (d == NULL) {
return MAG160C_ERR_NO_MEMORY;
}
for (uint32_t i = 0; i < n; ++i) {
int32_t v = (int32_t)live[i] - (int32_t)ref[i];
if (v > 4000) v = 4000;
if (v < -4000) v = -4000;
d[i] = v;
}
/* median */
for (uint32_t a = 1; a < n; ++a) {
int32_t key = d[a];
uint32_t b = a;
while (b > 0 && d[b - 1] > key) {
d[b] = d[b - 1];
b--;
}
d[b] = key;
}
int32_t off = d[n / 2];
if (off > max_shift) off = max_shift;
if (off < -max_shift) off = -max_shift;
free(d);
if (off != 0) {
for (uint32_t i = 0; i < n; ++i) {
int64_t v = (int64_t)ref[i] + off;
if (v < 0) v = 0;
if (v > 65535) v = 65535;
ref[i] = (uint16_t)v;
}
}
mag160c_clear_error();
return MAG160C_OK;
}
/* ------------------------------------------------------------------ */
/* AGC */
/* Percentile stretch: find [lo_pct, hi_pct] value range of frame. */
mag160c_error_t mag160c_display_agc_range(const uint16_t *frame, uint32_t n,
uint32_t lo_pct, uint32_t hi_pct,
uint32_t *out_lo, uint32_t *out_hi) {
if (frame == NULL || out_lo == NULL || out_hi == NULL) {
mag160c_set_error("mag160c_display_agc_range: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
if (lo_pct >= hi_pct || hi_pct > 100) {
mag160c_set_error("mag160c_display_agc_range: bad percentiles");
return MAG160C_ERR_INVALID_ARGUMENT;
}
uint32_t hist[65536];
memset(hist, 0, sizeof(hist));
uint32_t nz = 0;
for (uint32_t i = 0; i < n; ++i) {
if (frame[i] == 0) continue;
hist[frame[i]]++;
nz++;
}
if (nz == 0) {
*out_lo = 0;
*out_hi = 65535;
return MAG160C_OK;
}
uint64_t acc = 0, lo = (uint64_t)nz * lo_pct / 100, hi = (uint64_t)nz * hi_pct / 100;
uint32_t mn = 0, mx = 0;
for (uint32_t k = 0; k < 65536; ++k) {
acc += hist[k];
if (acc >= lo && mn == 0) mn = k;
if (acc >= hi) {
mx = k;
break;
}
}
if (mx <= mn + 50) {
mn = 0;
mx = 65535;
}
*out_lo = mn;
*out_hi = mx;
mag160c_clear_error();
return MAG160C_OK;
}
/* Adaptive diff AGC: span = clamp(2*mean|d|, min_span, max_span). */
mag160c_error_t mag160c_display_diff_span(const int32_t *diff, uint32_t n,
uint32_t min_span, uint32_t max_span,
uint32_t *out_span) {
if (diff == NULL || out_span == NULL) {
mag160c_set_error("mag160c_display_diff_span: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
uint64_t sum = 0;
uint32_t cnt = 0;
for (uint32_t i = 0; i < n; ++i) {
int32_t d = diff[i];
if (d < 0) d = -d;
if (d > 2) {
sum += (uint64_t)d;
cnt++;
}
}
uint64_t span = cnt ? 2 * sum / cnt : 0;
if (span < min_span) span = min_span;
if (span > max_span) span = max_span;
*out_span = (uint32_t)span;
mag160c_clear_error();
return MAG160C_OK;
}
/* ------------------------------------------------------------------ */
/* pseudo color */
/* FLIR-style ironbow anchors, v in [0,255] */
void mag160c_display_ironbow(uint32_t v, uint8_t *r, uint8_t *g, uint8_t *b) {
static const uint8_t anchors[7][3] = {
{0, 0, 0}, {23, 0, 60}, {93, 0, 128}, {170, 31, 83},
{226, 117, 29}, {249, 196, 66}, {255, 255, 220}};
double f = (v & 0xff) / 255.0 * 6.0;
uint32_t i = (uint32_t)f;
if (i > 5) i = 5;
double t = f - i;
*r = (uint8_t)(anchors[i][0] + t * (anchors[i + 1][0] - anchors[i][0]));
*g = (uint8_t)(anchors[i][1] + t * (anchors[i + 1][1] - anchors[i][1]));
*b = (uint8_t)(anchors[i][2] + t * (anchors[i + 1][2] - anchors[i][2]));
}
/* ------------------------------------------------------------------ */
/* FFC scheduler (official demo cadence) */
void mag160c_ffc_scheduler_init(mag160c_ffc_scheduler_t *s, uint32_t period,
uint32_t gap) {
if (s == NULL) {
return;
}
memset(s, 0, sizeof(*s));
s->period = period ? period : 400;
s->gap = gap ? gap : 9;
}
/* Call once per complete frame. Returns:
* -1 : nothing to send
* >=0: FFC param to send (0 or 1), then call again after the response. */
int32_t mag160c_ffc_scheduler_tick(mag160c_ffc_scheduler_t *s) {
if (s == NULL) {
return -1;
}
s->frames++;
if (!s->started) {
/* official: FFC(1) after ~10 complete frames switches to type=0 */
if (s->frames >= 10) {
s->started = 1;
s->frames = 0;
return 1;
}
return -1;
}
if (s->wait1 && s->frames >= s->gap) {
s->wait1 = 0;
s->frames = 0;
return 1;
}
if (!s->wait1 && s->frames >= s->period) {
s->wait1 = 1;
s->frames = 0;
return 0;
}
return -1;
}
/* Force an immediate FFC pair: returns 0 (send FFC(0) now); the following
* tick() calls will then return 1 (send FFC(1)) after `gap` frames. This
* is the official manual-FFC behavior (FFC(0) then 9 frames later FFC(1)). */
int32_t mag160c_ffc_scheduler_trigger(mag160c_ffc_scheduler_t *s) {
if (s == NULL) {
return -1;
}
s->started = 1;
s->wait1 = 1;
s->frames = 0;
return 0;
}
/* ------------------------------------------------------------------ */
/* two-point temperature calibration */
/* counts = a * temp + b ; solve from two known points */
mag160c_error_t mag160c_temp_calibrate_linear(double counts0, double temp0,
double counts1, double temp1,
double *out_a, double *out_b) {
if (out_a == NULL || out_b == NULL) {
mag160c_set_error("mag160c_temp_calibrate_linear: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
if (counts1 == counts0) {
mag160c_set_error("mag160c_temp_calibrate_linear: identical counts");
return MAG160C_ERR_INVALID_ARGUMENT;
}
*out_a = (temp1 - temp0) / (counts1 - counts0);
*out_b = temp0 - *out_a * counts0;
mag160c_clear_error();
return MAG160C_OK;
}
double mag160c_temp_apply_linear(double counts, double a, double b) {
return a * counts + b;
}