231 lines
8.8 KiB
C
231 lines
8.8 KiB
C
/* Tests for the display pipeline module (bad map, AGC, FFC scheduler, cal). */
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "mag160c/mag160c.h"
|
|
#include "mag160c/mag160c_display.h"
|
|
|
|
static int failures = 0;
|
|
|
|
#define CHECK(cond, msg) do { \
|
|
if (!(cond)) { printf("FAIL: %s\n", msg); failures++; } \
|
|
else { printf("ok: %s\n", msg); } \
|
|
} while (0)
|
|
|
|
static void test_badmap(void) {
|
|
/* 4x3 synthetic frame with two bad pixels and a 2-pixel cluster */
|
|
enum { W = 4, H = 3 };
|
|
mag160c_display_badmap_t m;
|
|
mag160c_display_badmap_init(&m, W, H);
|
|
m.temporal_thr = 50;
|
|
|
|
/* pixel (1,0): bright defect (value 5000 vs peak 100) - every frame */
|
|
/* pixels (2,1),(3,1): temporal fluctuation cluster */
|
|
uint16_t f2[H * W];
|
|
for (int i = 0; i < 30; ++i) {
|
|
for (int p = 0; p < H * W; ++p) f2[p] = 100;
|
|
f2[0 * W + 1] = 5000;
|
|
f2[1 * W + 2] = 100 + (i % 2 ? 80 : 0);
|
|
f2[1 * W + 3] = 100 + (i % 2 ? 80 : 0);
|
|
mag160c_display_badmap_feed(&m, f2);
|
|
}
|
|
mag160c_error_t e = mag160c_display_badmap_finalize(&m);
|
|
CHECK(e == MAG160C_OK, "badmap finalize ok");
|
|
CHECK(m.ready == 1, "badmap ready");
|
|
CHECK(m.bad[0 * W + 1] == 1, "bright defect detected");
|
|
CHECK(m.bad[1 * W + 2] == 1 && m.bad[1 * W + 3] == 1, "temporal cluster detected");
|
|
CHECK(m.bad_count == 3, "bad_count == 3");
|
|
/* correction must fill bad pixels with neighbour means */
|
|
for (int p = 0; p < H * W; ++p) f2[p] = 100;
|
|
f2[0 * W + 1] = 5000;
|
|
f2[1 * W + 2] = 180;
|
|
f2[1 * W + 3] = 180;
|
|
mag160c_display_badmap_correct(&m, f2);
|
|
CHECK(f2[0 * W + 1] == 100, "bright defect corrected to neighbour mean");
|
|
CHECK(f2[1 * W + 2] == 100, "cluster pixel corrected");
|
|
CHECK(f2[1 * W + 3] == 100, "cluster pixel 2 corrected");
|
|
CHECK(m.ref[1 * W + 2] == 100, "reference cluster filled");
|
|
mag160c_display_badmap_destroy(&m);
|
|
}
|
|
|
|
static void test_agc(void) {
|
|
/* 1000 samples: 980 background 1000..1979, 10 hot outliers 40000+,
|
|
* 10 zeros. Percentile [2,98] must exclude the outliers. */
|
|
uint16_t frame[1000];
|
|
for (int i = 0; i < 980; ++i) frame[i] = (uint16_t)(1000 + i % 980);
|
|
for (int i = 0; i < 10; ++i) frame[980 + i] = (uint16_t)(40000 + i * 100);
|
|
frame[990] = 0;
|
|
frame[991] = 0;
|
|
uint32_t lo = 0, hi = 0;
|
|
mag160c_error_t e = mag160c_display_agc_range(frame, 1000, 2, 98, &lo, &hi);
|
|
CHECK(e == MAG160C_OK, "agc_range ok");
|
|
CHECK(hi < 40000, "agc excludes hot defect outliers");
|
|
CHECK(lo >= 1000 && lo <= 1100, "agc low percentile sane");
|
|
|
|
int32_t diff[8] = {0, 5, 10, -10, 100, -100, 1000, -1000};
|
|
uint32_t span = 0;
|
|
e = mag160c_display_diff_span(diff, 8, 120, 4000, &span);
|
|
CHECK(e == MAG160C_OK, "diff_span ok");
|
|
CHECK(span > 200 && span <= 4000, "diff span adaptive");
|
|
}
|
|
|
|
static void test_ffc(void) {
|
|
mag160c_ffc_scheduler_t s;
|
|
mag160c_ffc_scheduler_init(&s, 400, 9);
|
|
int sent0 = 0, sent1 = 0;
|
|
for (int f = 0; f < 900; ++f) {
|
|
int32_t p = mag160c_ffc_scheduler_tick(&s);
|
|
if (p == 0) sent0++;
|
|
if (p == 1) sent1++;
|
|
}
|
|
CHECK(sent1 >= 2, "ffc scheduler sends FFC(1) (initial + pair)");
|
|
CHECK(sent0 >= 2, "ffc scheduler sends FFC(0) periodically");
|
|
}
|
|
|
|
static void test_ffc_trigger(void) {
|
|
/* manual trigger: FFC(0) now, FFC(1) exactly `gap` frames later */
|
|
mag160c_ffc_scheduler_t s;
|
|
mag160c_ffc_scheduler_init(&s, 400, 9);
|
|
int32_t p = mag160c_ffc_scheduler_trigger(&s);
|
|
CHECK(p == 0, "trigger returns 0 (send FFC(0))");
|
|
int frames_to_1 = 0;
|
|
for (int f = 0; f < 20; ++f) {
|
|
int32_t t = mag160c_ffc_scheduler_tick(&s);
|
|
if (t == 1) {
|
|
frames_to_1 = f + 1;
|
|
break;
|
|
}
|
|
}
|
|
CHECK(frames_to_1 == 9, "FFC(1) comes exactly 9 frames after trigger");
|
|
}
|
|
|
|
static void test_cal(void) {
|
|
double a = 0, b = 0;
|
|
mag160c_error_t e = mag160c_temp_calibrate_linear(11000, 0.0, 14000, 90.0, &a, &b);
|
|
CHECK(e == MAG160C_OK, "linear cal ok");
|
|
CHECK(a > 0.029 && a < 0.031, "slope ~0.03 C/count");
|
|
CHECK(mag160c_temp_apply_linear(12500, a, b) > 44.9 &&
|
|
mag160c_temp_apply_linear(12500, a, b) < 45.1, "apply midpoint 45C");
|
|
e = mag160c_temp_calibrate_linear(11000, 0, 11000, 90, &a, &b);
|
|
CHECK(e != MAG160C_OK, "identical counts rejected");
|
|
}
|
|
|
|
static void test_ref_track(void) {
|
|
/* hot object (1200 over bg) for 4500 frames must not be absorbed */
|
|
enum { N = 8 };
|
|
uint16_t ref[N], live[N];
|
|
for (int i = 0; i < N; ++i) ref[i] = 11720;
|
|
for (int i = 0; i < N; ++i) live[i] = 11720 + 1200;
|
|
for (int f = 0; f < 4500; ++f)
|
|
mag160c_display_ref_track(ref, live, N, 60, 32);
|
|
int absorbed = 0;
|
|
for (int i = 0; i < N; ++i) if (ref[i] != 11720) absorbed++;
|
|
CHECK(absorbed == 0, "ref_track: static hot object not absorbed (no ghost)");
|
|
|
|
/* removal: diff ~0 */
|
|
for (int i = 0; i < N; ++i) live[i] = 11720;
|
|
long sum = 0;
|
|
for (int i = 0; i < N; ++i) sum += (int)live[i] - (int)ref[i];
|
|
CHECK(sum > -10 && sum < 10, "ref_track: no ghost after object removal");
|
|
|
|
/* slow drift still tracked */
|
|
for (int f = 0; f < 2000; ++f) {
|
|
for (int i = 0; i < N; ++i) live[i] = (uint16_t)(11723 + f % 10);
|
|
mag160c_display_ref_track(ref, live, N, 60, 32);
|
|
}
|
|
long drift = 0;
|
|
for (int i = 0; i < N; ++i) drift += (int)live[i] - (int)ref[i];
|
|
CHECK(drift / N < 60, "ref_track: slow drift tracked");
|
|
}
|
|
|
|
static void test_ref_heal(void) {
|
|
enum { W2 = 160, H2 = 120 };
|
|
enum { N = W2 * H2 };
|
|
static uint16_t ref[N], live[N];
|
|
static uint8_t fg[N];
|
|
for (int i = 0; i < N; ++i) { ref[i] = 11720; live[i] = 11720; }
|
|
|
|
/* 40 isolated startup-noise pixels wrongly in the reference (+800) */
|
|
int noise[40];
|
|
for (int n = 0; n < 40; ++n) {
|
|
int x = 10 + (n % 8) * 5, y = 10 + (n / 8) * 5;
|
|
noise[n] = y * W2 + x;
|
|
ref[noise[n]] = 11720 + 800;
|
|
}
|
|
for (int f = 0; f < 50; ++f)
|
|
mag160c_display_ref_track_heal(ref, live, W2, H2, 60, 32, fg, 30, 2);
|
|
int healed = 0;
|
|
for (int n = 0; n < 40; ++n) if (ref[noise[n]] == 11720) healed++;
|
|
CHECK(healed == 40, "heal: isolated startup noise pixels healed");
|
|
|
|
/* contiguous 60x40 hot object must stay frozen (not healed/absorbed) */
|
|
for (int i = 0; i < N; ++i) { ref[i] = 11720; live[i] = 11720; fg[i] = 0; }
|
|
for (int y = 40; y < 80; ++y)
|
|
for (int x = 50; x < 110; ++x) live[y * W2 + x] = 11720 + 1200;
|
|
for (int f = 0; f < 200; ++f)
|
|
mag160c_display_ref_track_heal(ref, live, W2, H2, 60, 32, fg, 30, 2);
|
|
int absorbed = 0;
|
|
for (int y = 40; y < 80; ++y)
|
|
for (int x = 50; x < 110; ++x)
|
|
if (ref[y * W2 + x] > 11720 + 100) absorbed++;
|
|
CHECK(absorbed == 0, "heal: contiguous object never absorbed/healed");
|
|
}
|
|
|
|
static void test_nuc(void) {
|
|
/* synthetic mura: background 11720 with a +5000 row-1 band and a
|
|
* -3000 col-17 band; a hand (+1200 over a region) must survive NUC */
|
|
enum { W2 = 160, H2 = 120 };
|
|
enum { N = W2 * H2 };
|
|
static uint16_t live[N], ref[N], out[N];
|
|
for (int i = 0; i < N; ++i) {
|
|
int x = i % W2, y = i / W2;
|
|
uint16_t v = 11720;
|
|
if (y == 1) v += 5000; /* mura row band */
|
|
if (x == 17) v -= 3000; /* mura col band */
|
|
ref[i] = v;
|
|
live[i] = v;
|
|
if (x >= 60 && x < 100 && y >= 40 && y < 80) live[i] += 1200; /* hand */
|
|
}
|
|
mag160c_error_t e = mag160c_display_nuc(live, ref, N, out);
|
|
CHECK(e == MAG160C_OK, "nuc ok");
|
|
/* mura removed: row1 and col17 pixels back near background */
|
|
int r1 = out[1 * W2 + 50];
|
|
int c17 = out[50 * W2 + 17];
|
|
CHECK(r1 >= 11690 && r1 <= 11770, "nuc removes row mura band");
|
|
CHECK(c17 >= 11690 && c17 <= 11770, "nuc removes col mura band");
|
|
/* hand region still visible */
|
|
int hand = out[50 * W2 + 80];
|
|
CHECK(hand >= 11700 + 1100 && hand <= 11700 + 1300, "nuc keeps object");
|
|
}
|
|
|
|
static void test_ref_rebase(void) {
|
|
enum { N = 8 };
|
|
uint16_t ref[N], live[N];
|
|
for (int i = 0; i < N; ++i) { ref[i] = 11720; live[i] = 11720 + 800; }
|
|
mag160c_error_t e = mag160c_display_ref_rebase(ref, live, N, 2000);
|
|
CHECK(e == MAG160C_OK, "ref_rebase ok");
|
|
long sum = 0;
|
|
for (int i = 0; i < N; ++i) sum += (int)live[i] - (int)ref[i];
|
|
CHECK(sum == 0, "ref_rebase removes global offset");
|
|
/* single hot pixel (outlier) does not drag the median */
|
|
live[0] = 11720 + 6000;
|
|
mag160c_display_ref_rebase(ref, live, N, 2000);
|
|
sum = 0;
|
|
for (int i = 0; i < N; ++i) sum += (int)live[i] - (int)ref[i];
|
|
CHECK(sum >= 0 && sum < 6000, "ref_rebase robust to single outlier");
|
|
}
|
|
|
|
int main(void) {
|
|
test_badmap();
|
|
test_agc();
|
|
test_ffc();
|
|
test_ffc_trigger();
|
|
test_ref_track();
|
|
test_ref_heal();
|
|
test_nuc();
|
|
test_ref_rebase();
|
|
test_cal();
|
|
printf(failures ? "\nTEST_DISPLAY_FAILED (%d)\n" : "\nTEST_DISPLAY_PASSED\n",
|
|
failures);
|
|
return failures ? 1 : 0;
|
|
}
|