/* MAG160C Windows Demo v4 - OFFICIAL display pipeline, pixel-verified. * * Reverse-engineered from CoreSDKLib.dll (Windows) with live same-frame * capture against ThermalSDK (analysis/pairs_*, 2026-08-11): * * 1. NUC : nuc[i] = (counts[i] - ref[i]) * 3 + comp, clamp [0, 65535] * (ref = per-pixel reference frame captured at FFC; comp = * global offset calibrated at reference capture so the NUC * output centers on the official level ~9804 counts) * 2. WINDOW (per frame, on NUC output): lo = max(min, mean - 312), * hi = min(max, mean + 312) (X=624, verified stable) * 3. GRAY: idx = (nuc[i] - lo) * (0xFFC00000 / (hi-lo)) >> 22 (u32) * gray = LUT1024[dev+0x4284c][idx] (official curve, exported) * 4. COLOR: rgb = palette[gray], 256x4 BGR at dev+0xb18 (exported) * 5. ENLARGE: 2x bilinear for the 320x240 output (StretchBlt) * * Verified: gray reconstruction from counts = 100.0% pixel-exact vs the * official same-frame gray (analysis/pairs_win); palette render = 100.0% * exact vs official RGB24. Temperature: temp_mC = T2E[3*nuc - C], * C ~= 5797 (session-adaptive; mean abs error 1.3 mC over 12 frames). * * Frame decode fix vs demo2: pixels start at data[0] of the second bulk * read (probe-verified: read1 = 28B header, read2 = 38400 px + 28B tail). */ #define _WIN32_WINNT 0x0601 #include #include #include #include #include #include #include "mag160c/mag160c.h" #include "mag160c/mag160c_display.h" #include "mag160c_official_palette256.h" #include "mag160c_official_lut1024.h" #include "mag160c_official_t2e.h" #define W 160 #define H 120 #define NPIX (W * H) #define FFC_PERIOD 1800 /* frames between FFC pairs - official * [0x2d90] = 1800 measured (120 s) */ #define FFC_GAP 9 /* frames between FFC(0) and FFC(1) (official) */ /* user-proposed reference mode: no frozen scene reference. Every pixel * of the reference is the current global minimum temperature value, so * live - ref is >= 0 everywhere: a moving object can never go negative * (black ghost) and its old position reads as plain background. Trades * the mura flat-fielding of a captured reference for zero ghosting. */ #define CONSTANT_REF 1 /* official window half-span on NUC counts (X/2 = 312, measured) */ #define WIN_HALF 312 /* official NUC gain (measured, session-stable) */ #define NUC_GAIN 19 /* official NUC slope 0.19 (matches measured nuc vs d2 response) */ /* NUC output center targeted at reference capture (official level) */ #define NUC_CENTER 9804 /* temperature conversion constant C: temp_mC = T2E[3*nuc - C] */ #define TEMP_C 5797 #define SBNUC_ENABLE 1 /* post-motion negative-residual self-heal only */ #define BADMAP_ENABLE 0 /* diagnostic: official uses a preset blind * pixel table; dynamic scene-derived bad maps * can stamp fixed screen-space shadows */ #define SB_HOT_T 120 /* freeze true hot-object pixels */ #define SB_HOT_SEED 48 /* remember mildly warm pixels so their tail * can be erased quickly after they leave */ #define SB_HOT_EXIT 24 /* leave the warm state only near background */ #define SB_HOT_REENTER 48 /* hysteresis: cancel release above this */ #define SB_HOT_HOLD 24 /* recent-hot memory in frames */ #define SB_HOT_WARM 1 #define SB_HOT_RELEASE 2 #define SB_INNOV_VAR_T 2500 /* roughly (50 counts)^2 */ #define SB_INNOV_HOLD 6 /* keep a motion gate after an edge */ #define SB_RECOVER_T 16 /* negative residual evidence threshold */ #define SB_RECOVER_RUN 2 /* require persistence before ref recovery */ #define OFFICIAL_NUC_SEGS 3 #define OFFICIAL_NUC_THR_PER_PIXEL (OFFICIAL_NUC_SEGS - 1) /* reference model (OpenCV-MOG-style background model): * REF_T : |live-ref| below this -> pixel is background, update slowly * REF_ALPHA: background learn rate (ref += d/ALPHA per frame) * REF_FREEZE: |d| above -> foreground, ref frozen (never absorbs objects, * which is what caused the ghosting) * REF_SKIP : frames to skip rendering right after FFC(1) (baseline * settles; the official app freezes the image here too) */ #define REF_T 60 #define REF_ALPHA 32 #define REF_SKIP 45 /* frames to skip right after FFC(1) so the * type=0 baseline fully settles (measured: * baseline takes ~3-4 s to recover after FFC) */ #define REF_INIT_N 12 /* reference frames (median; short window so a * transient object is rarely absorbed) */ #define REF_QUIET 25 /* max mean |frame delta| to accept an init frame */ #define REF_SELFHEAL_N 30 /* frames a pixel must be isolated-foreground * before it is healed back into the ref * (kills startup-noise ghosts; real objects * are contiguous so they never qualify) */ #define REF_REINIT_AFTER_FFC 0 /* FFC: do NOT re-collect the reference - * official behavior (verified 2026-08-12): * ref only shifts globally after FFC * (-145/-214 counts), spatial structure kept. * Re-collection absorbs scene objects into * the frozen reference and ghosts them * black. Rebase the global offset instead. */ static libusb_context *g_ctx; static libusb_device_handle *g_h; static unsigned char g_frame[40000]; static unsigned char g_bmp[54 + W * H * 3]; static char g_title[256]; static unsigned short g_reference[NPIX]; static unsigned short g_ref_mura[NPIX]; /* flat-field (mura) reference */ static unsigned short g_official_gain[NPIX * OFFICIAL_NUC_SEGS * 2]; static short g_official_thr[NPIX * OFFICIAL_NUC_THR_PER_PIXEL]; static int g_official_nuc_ready; static int g_official_ref_bias; static unsigned g_ref_mura_min; static int g_ref_mura_ready; static int g_sb_med; static int g_sb_accept; static int g_sb_shift; static int g_sb_gmin; static double g_sb_mura_dmean; static double g_sb_mura_dstd; static short g_sb_prev_d[NPIX]; static unsigned char g_sb_still[NPIX]; static unsigned short g_sb_innov_var[NPIX]; static unsigned char g_sb_cold_run[NPIX]; static unsigned char g_hot_hist[NPIX]; /* release countdown */ static unsigned char g_sb_hot_state[NPIX]; /* warm or post-hot release */ static unsigned char g_disp_hot[NPIX]; static unsigned char g_disp_hot2[NPIX]; static unsigned char g_motion_hist[NPIX]; static unsigned short g_disp_prev_live[NPIX]; static int g_disp_prev_ready; static int g_disp_cur_hot; static int g_disp_ghost; static int g_disp_repl; static unsigned short g_live[NPIX]; /* decoded + bad-pixel-corrected frame */ static unsigned short g_prev_frame[NPIX]; static int g_has_reference; static unsigned short g_ref_samples[NPIX][REF_INIT_N]; static int g_ref_n; static unsigned short g_ref_min[NPIX]; static unsigned short g_ref_max[NPIX]; static int g_ref_phase; /* 0 idle, 1 collecting init frames, * 2 collecting post-FFC frames */ static int g_comp; /* official NUC global offset (calibrated) */ static int g_skip_ffc; /* frames to skip rendering after FFC(1) */ static int g_rebase; /* 1 = re-align reference after FFC(1) */ static double g_fps; static unsigned g_fcount; static int g_probe_x = -1, g_probe_y = -1; static int g_max_x = -1, g_max_y = -1; static unsigned char g_bad[NPIX]; static int g_bad_done; static int g_bad_count; static int g_bad_order[NPIX][2]; /* fill order: bad pixels, edge-first */ static int g_bad_order_len; static double g_max_temp = -100; static double g_center_temp = -100; static HWND g_hwnd; static HFONT g_font; static volatile int g_manual_ffc; static volatile int g_ema_reset; static int sendcmd(unsigned magic, unsigned param, int len) { unsigned char cmd[8] = {0}; cmd[0] = (unsigned char)(magic); cmd[1] = (unsigned char)(magic >> 8); cmd[2] = (unsigned char)(magic >> 16); cmd[3] = (unsigned char)(magic >> 24); if (len >= 8) { cmd[4] = (unsigned char)(param); cmd[5] = (unsigned char)(param >> 8); cmd[6] = (unsigned char)(param >> 16); cmd[7] = (unsigned char)(param >> 24); } int xfer = 0; if (libusb_bulk_transfer(g_h, 0x03, cmd, len, &xfer, 2000)) return -1; unsigned char resp[0x1000]; if (libusb_bulk_transfer(g_h, 0x82, resp, sizeof(resp), &xfer, 2000)) return -1; return 0; } /* Official temperature conversion (recovered from live same-frame pairs): * temp_mC = T2E[3*nuc - C], C ~= 5797 session-adaptive. * Verified: mean |err| = 1.3 mC over 12 frames vs official MAG_GetTemperatureData_Raw. */ #define MAG_T2E_OFFSET 0x249f0 static int counts_to_temp_mc(int counts) { int64_t x = (int64_t)counts * 3 - TEMP_C; if (x < 0) x = 0; /* binary search: largest i with T2E[i] <= x */ int lo = 0, hi = 645; while (lo < hi) { int mid = (lo + hi + 1) >> 1; if (mag160c_official_t2e[mid] <= x) lo = mid; else hi = mid - 1; } int i = lo; if (i > 644) i = 644; int64_t diff = x - mag160c_official_t2e[i]; int64_t t2 = mag160c_official_t2e[i + 1] - mag160c_official_t2e[i]; int64_t slope = t2 ? (0x1000000 + t2 / 2) / t2 : 0; int64_t temp = ((slope * diff) >> 12) + ((int64_t)i << 12) - MAG_T2E_OFFSET; return (int)temp; /* millidegrees C */ } static double counts_to_c(double v) { return counts_to_temp_mc((int)v) / 1000.0; } /* The official NUC table is captured from CoreSDKLib once for this sensor. * gain.bin is laid out as three segment-major planes of (gain, offset) pairs; * thr.bin contains the first two signed thresholds for every pixel. */ static int load_official_nuc_tables(void) { FILE *f = fopen("mag160c_official_nuc_gain.bin", "rb"); if (!f) return 0; if (fread(g_official_gain, 1, sizeof(g_official_gain), f) != sizeof(g_official_gain)) { fclose(f); return 0; } fclose(f); f = fopen("mag160c_official_nuc_thr.bin", "rb"); if (!f) return 0; if (fread(g_official_thr, 1, sizeof(g_official_thr), f) != sizeof(g_official_thr)) { fclose(f); return 0; } fclose(f); g_official_nuc_ready = 1; return 1; } static int official_nuc_value(int i, int live, int ref) { int d2 = (live - ref) >> 1; int seg = 0; for (int e = 0; e < OFFICIAL_NUC_THR_PER_PIXEL; ++e) { if (d2 <= g_official_thr[i * OFFICIAL_NUC_THR_PER_PIXEL + e]) break; seg++; } int p = (seg * NPIX + i) * 2; int64_t v = ((int64_t)g_official_gain[p] * d2) >> 12; v += g_official_gain[p + 1]; if (v < 0) v = 0; if (v > 65535) v = 65535; return (int)v; } static int official_nuc_median_for_bias(int bias) { static unsigned hist[65536]; memset(hist, 0, sizeof(hist)); int n = 0; for (int i = 0; i < NPIX; i += 4) { int ref = (int)g_ref_mura[i] + bias; if (ref < 0) ref = 0; if (ref > 65535) ref = 65535; hist[official_nuc_value(i, g_live[i], ref)]++; n++; } unsigned acc = 0; for (int k = 0; k < 65536; ++k) { acc += hist[k]; if (acc > (unsigned)n / 2) return k; } return NUC_CENTER; } /* Select the scalar ref offset in the official output domain. This keeps * the fixed per-pixel flat field while following global sensor drift without * learning the spatial footprint of a moving object. */ static int calibrate_official_ref_bias(void) { int lo = -8000, hi = 8000; for (int k = 0; k < 15; ++k) { int mid = (lo + hi) >> 1; if (official_nuc_median_for_bias(mid) > NUC_CENTER) lo = mid + 1; else hi = mid; } return lo; } /* ---------- bad pixel pipeline (Seek-style) ---------- */ /* 4-neighbour mean of frame at (x,y), skipping bad pixels; returns 0 when * no valid neighbour exists. */ static int neighbor_mean(const unsigned short *fr, const unsigned char *bad, int x, int y) { long sum = 0; int n = 0; if (x > 0 && !bad[y * W + x - 1]) { sum += fr[y * W + x - 1]; n++; } if (x < W - 1 && !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 < H - 1 && !bad[(y + 1) * W + x]) { sum += fr[(y + 1) * W + x]; n++; } return n ? (int)(sum / n) : 0; } /* Topological fill: repeatedly replace bad pixels that have >=1 valid * neighbour, so bad clusters are filled from the edge inward. The fill * order is stored so the live frame can be corrected the same way. */ static int has_valid_neighbor(const unsigned char *bad, int x, int y) { return (x > 0 && !bad[y * W + x - 1]) || (x < W - 1 && !bad[y * W + x + 1]) || (y > 0 && !bad[(y - 1) * W + x]) || (y < H - 1 && !bad[(y + 1) * W + x]); } static void build_fill_order(unsigned char *bad, int (*order)[2], int *order_len) { int remain = 0; for (int i = 0; i < NPIX; ++i) if (bad[i]) remain++; int len = 0; while (remain > 0) { int progress = 0; for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { if (!bad[y * W + x]) continue; if (!has_valid_neighbor(bad, x, y)) continue; order[len][0] = x; order[len][1] = y; len++; bad[y * W + x] = 0; remain--; progress++; } } if (!progress) { /* isolated bad with no valid neighbours: force */ for (int y = 0; y < H && progress == 0; ++y) for (int x = 0; x < W && progress == 0; ++x) if (bad[y * W + x]) { order[len][0] = x; order[len][1] = y; len++; bad[y * W + x] = 0; remain--; progress++; } } } *order_len = len; } /* correct frame in place: fill bad pixels in stored topological order. * The bad mask is copied to a work buffer whose bits are cleared as pixels * are filled, so cluster interiors can use already-filled neighbours. * Returns the number of corrected pixels. */ static int correct_frame(unsigned short *fr) { static unsigned char work[NPIX]; if (!g_bad_order_len) return 0; memcpy(work, g_bad, sizeof(work)); int n = 0; for (int i = 0; i < g_bad_order_len; ++i) { int x = g_bad_order[i][0], y = g_bad_order[i][1]; int v = neighbor_mean(fr, work, x, y); if (v > 0) { fr[y * W + x] = (unsigned short)v; work[y * W + x] = 0; n++; } } return n; } static void correct_zero_pixels(unsigned short *fr) { unsigned char bad[NPIX]; int any = 0; for (int i = 0; i < NPIX; ++i) { bad[i] = (fr[i] == 0); if (bad[i]) any = 1; } if (!any) return; for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { int i = y * W + x; if (!bad[i]) continue; int v = neighbor_mean(fr, bad, x, y); if (v > 0) { fr[i] = (unsigned short)v; bad[i] = 0; } } } } /* ---------- color LUTs ---------- */ /* ironbow anchors (FLIR-style), value 0..255 */ /* ---------- render ---------- */ /* decode + correct the live frame into g_live (shared with the reference * model so both use the same corrected values). Pixels start at data[0] * of the second bulk read (probe-verified: read1 = 28B header only). */ static void decode_live(const unsigned char *data) { for (int i = 0; i < NPIX; ++i) g_live[i] = (unsigned short)(data[i * 2] | ((unsigned)data[i * 2 + 1] << 8)); if (g_bad_done) correct_frame(g_live); correct_zero_pixels(g_live); } /* Official display pipeline (pixel-verified vs CoreSDKLib): * live -> 3x3 weighted filter (official sm=1: no temporal averaging) * ref -> same 3x3 filter at capture (so the mura patterns match) * nuc[i] = clamp((live_f[i] - ref_f[i]) * NUC_GAIN + comp, 0, 65535) * lo = max(frame_min, frame_mean - WIN_HALF) * hi = min(frame_max, frame_mean + WIN_HALF) * idx = (nuc[i] - lo) * (0xFFC00000 / (hi - lo)) >> 22 (u32 math) * gray = LUT1024[idx]; rgb = palette256[gray] (BGR) */ /* official-style temporal EMA: acc = (acc+live)/2; |d|>thr means scene * change (object), take it as-is to avoid ghost trails */ static void temporal_ema(unsigned short *acc, const unsigned short *live, int n, int thr) { for (int i = 0; i < n; ++i) { int d = (int)live[i] - (int)acc[i]; if (d > thr || d < -thr) { acc[i] = live[i]; } else if (d != 0) { acc[i] = (unsigned short)(acc[i] + (d >> 1)); } } } static int neighbor_mean_u16(const unsigned short *fr, const unsigned char *mask, int x, int y) { int sum = 0, n = 0; for (int dy = -1; dy <= 1; ++dy) { int yy = y + dy; if (yy < 0 || yy >= H) continue; for (int dx = -1; dx <= 1; ++dx) { int xx = x + dx; if (xx < 0 || xx >= W || (dx == 0 && dy == 0)) continue; int j = yy * W + xx; if (mask && mask[j]) continue; sum += fr[j]; n++; } } return n ? (sum / n) : 0; } static void deghost_nuc(unsigned short *fr, unsigned mean) { unsigned char mask[NPIX]; unsigned char workmask[NPIX]; unsigned char curmask[NPIX]; int cur_hot_n = 0, ghost_n = 0, repl_n = 0; unsigned long long lsum = 0; for (int i = 0; i < NPIX; ++i) lsum += g_live[i]; unsigned live_mean = (unsigned)(lsum / NPIX); unsigned live_hot_thr = live_mean + 3000; unsigned hot_thr = mean + 180; unsigned ghost_thr = mean + 35; (void)hot_thr; (void)ghost_thr; memset(curmask, 0, sizeof(curmask)); for (int i = 0; i < NPIX; ++i) { int cur_hot = g_live[i] > live_hot_thr; int motion_cur = 0; if (g_disp_prev_ready) { int dm = (int)g_live[i] - (int)g_disp_prev_live[i]; if (dm < 0) dm = -dm; motion_cur = dm > 180; } if (cur_hot) cur_hot_n++; if (motion_cur) g_motion_hist[i] = 60; else if (g_motion_hist[i]) g_motion_hist[i]--; if (cur_hot || motion_cur) curmask[i] = 1; } /* dilate current-hot mask one pixel so object edges/trails are covered */ memset(g_disp_hot2, 0, sizeof(g_disp_hot2)); for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { int i = y * W + x; if (!curmask[i]) continue; for (int dy = -1; dy <= 1; ++dy) { int yy = y + dy; if (yy < 0 || yy >= H) continue; for (int dx = -1; dx <= 1; ++dx) { int xx = x + dx; if (xx < 0 || xx >= W) continue; g_disp_hot2[yy * W + xx] = 1; } } } } for (int i = 0; i < NPIX; ++i) { int cur_hot = g_disp_hot2[i] != 0; if (cur_hot) g_disp_hot[i] = 60; else if (g_disp_hot[i]) g_disp_hot[i]--; mask[i] = cur_hot || ((g_disp_hot[i] || g_motion_hist[i]) && !cur_hot); if ((g_disp_hot[i] || g_motion_hist[i]) && !cur_hot) ghost_n++; } memcpy(workmask, mask, sizeof(workmask)); for (int ghost_fill_pass = 0; ghost_fill_pass < 3; ++ghost_fill_pass) { int pass_n = 0; for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { int i = y * W + x; if (!workmask[i] || g_disp_hot2[i]) continue; int bg = neighbor_mean_u16(fr, workmask, x, y); if (bg > 0) { fr[i] = (unsigned short)bg; workmask[i] = 0; repl_n++; pass_n++; } } } if (!pass_n) break; } g_disp_cur_hot = cur_hot_n; g_disp_ghost = ghost_n; g_disp_repl = repl_n; memcpy(g_disp_prev_live, g_live, sizeof(g_disp_prev_live)); g_disp_prev_ready = 1; } /* official-style 3x3 weighted average (center 4, cross 2, corners 1, /16) */ static void filter3x3(const unsigned short *src, unsigned short *dst, int w, int h) { for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { long s = 0; int n = 0; for (int dy = -1; dy <= 1; ++dy) { for (int dx = -1; dx <= 1; ++dx) { int xx = x + dx, yy = y + dy; if (xx < 0 || xx >= w || yy < 0 || yy >= h) continue; int wgt = (dx == 0 && dy == 0) ? 4 : ((dx == 0 || dy == 0) ? 2 : 1); s += (long)src[yy * w + xx] * wgt; n += wgt; } } dst[y * w + x] = (unsigned short)(s / n); } } } static void render(unsigned mn, unsigned mx) { static unsigned short nuc[NPIX]; /* NUC-corrected (official form) */ unsigned short *nuc_f = nuc; static FILE *dbg; unsigned char *px = g_bmp + 54; unsigned hotv = 0; int hot = 0; unsigned csum = 0; unsigned long long ssum = 0; unsigned fmin = 0xffff, fmax = 0; (void)mn; (void)mx; if (g_has_reference) { if (g_official_nuc_ready && g_ref_mura_ready) { for (int i = 0; i < NPIX; ++i) { int ref = (int)g_ref_mura[i] + g_official_ref_bias; if (ref < 0) ref = 0; if (ref > 65535) ref = 65535; nuc[i] = (unsigned short)official_nuc_value(i, g_live[i], ref); } } else { for (int i = 0; i < NPIX; ++i) { int64_t v = ((int64_t)g_live[i] - g_reference[i]) * NUC_GAIN / 100 + g_comp; if (v < 0) v = 0; if (v > 65535) v = 65535; nuc[i] = (unsigned short)v; } } } else { for (int i = 0; i < NPIX; ++i) nuc_f[i] = 0; } for (int i = 0; i < NPIX; ++i) { unsigned v = nuc_f[i]; ssum += v; if (v < fmin) fmin = v; if (v > fmax) fmax = v; } unsigned mean = (unsigned)(ssum / NPIX); /* Display-layer deghost experiments were too aggressive for low-contrast * motion and could hide real scene detail. Keep this disabled; the * stable fixes are SBNUC, no dynamic badmap, zero-pixel repair, and * saturated color mapping. */ /* deghost_nuc(nuc_f, mean); */ ssum = 0; fmin = 0xffff; fmax = 0; for (int i = 0; i < NPIX; ++i) { unsigned v = nuc_f[i]; ssum += v; if (v < fmin) fmin = v; if (v > fmax) fmax = v; } mean = (unsigned)(ssum / NPIX); /* adaptive window: the official pipeline (0x4202c/0x42030) runs a * minimum span of 624 counts on a static scene and widens it when a * hot object is present (measured 1381 with a hand). Follow the * scene percentiles P2..P98 with that minimum span so a hand widens * the window instead of saturating to a broken image. */ static unsigned short hist[65536]; memset(hist, 0, sizeof(hist)); for (int i = 0; i < NPIX; ++i) hist[nuc_f[i]]++; unsigned long long acc = 0; unsigned p2 = 0, p98 = 0, p98t = (unsigned)((unsigned long long)NPIX * 98 / 100); for (unsigned k = 0; k < 65536; ++k) { acc += hist[k]; if (p2 == 0 && acc >= (unsigned long long)NPIX * 2 / 100) p2 = k; if (acc >= p98t) { p98 = k; break; } } unsigned span = p98 - p2; unsigned half = WIN_HALF; if (span > half * 2) half = span / 2; unsigned lo = mean > half ? mean - half : 0; unsigned hi = mean + half; if (hi > 65535) hi = 65535; if (lo > fmax) lo = fmax; if (hi < fmin) hi = fmin; if (hi <= lo) { lo = fmin; hi = fmax; if (hi <= lo) hi = lo + 1; } unsigned S = 0xFFC00000u / (hi - lo); /* diagnostics: one line per frame */ if (!dbg) dbg = fopen("demo3_diag.txt", "w"); if (dbg) { unsigned long long rsum = 0, lsum = 0; unsigned rmin = 0xffff, rmax = 0, lmin = 0xffff, lmax = 0; long long dsum = 0; unsigned long long d2sum = 0; int nz = 0; for (int i = 0; i < NPIX; ++i) { unsigned r = g_reference[i], l = g_live[i]; rsum += r; lsum += l; if (r < rmin) rmin = r; if (r > rmax) rmax = r; if (l < lmin) lmin = l; if (l > lmax) lmax = l; long long d = (long long)l - r; dsum += d; d2sum += (unsigned long long)(d * d); if (l == 0 || r == 0) nz++; } double dmean = (double)dsum / NPIX; double dstd = sqrt((double)d2sum / NPIX - dmean * dmean); fprintf(dbg, "ph=%d ref=%d bad=%d comp=%d ref=%u..%u(%.0f) live=%u..%u(%.0f) " "d=(%.0f+-%.0f) mura_d=(%.0f+-%.0f) sb_med=%d sb_acc=%d/%d shift=%d gmin=%d " "nuc=%u..%u(%.0f) win=[%u,%u] disp=%d/%d/%d nz=%d\n", g_ref_phase, g_has_reference, g_bad_done, g_comp, rmin, rmax, (double)rsum / NPIX, lmin, lmax, (double)lsum / NPIX, dmean, dstd, g_sb_mura_dmean, g_sb_mura_dstd, g_sb_med, g_sb_accept, NPIX, g_sb_shift, g_sb_gmin, fmin, fmax, (double)ssum / NPIX, lo, hi, g_disp_cur_hot, g_disp_ghost, g_disp_repl, nz); fflush(dbg); } for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { int i = y * W + x; unsigned v = nuc_f[i]; if (x >= 70 && x < 90 && y >= 50 && y < 70) csum += v; if (v > hotv && v != 0) { hotv = v; hot = i; } unsigned idx; if (v <= lo) idx = 0; else if (v >= hi) idx = 1023; else idx = ((v - lo) * S) >> 22; /* u32, truncates like official */ if (idx > 1023) idx = 1023; unsigned char gv = mag160c_official_lut1024[idx]; unsigned char r = mag160c_official_palette256[gv][2]; unsigned char g = mag160c_official_palette256[gv][1]; unsigned char b = mag160c_official_palette256[gv][0]; int dst = (119 - y) * W * 3 + x * 3; px[dst + 0] = b; px[dst + 1] = g; px[dst + 2] = r; } } if (hot >= 0) { g_max_x = hot % W; g_max_y = hot / W; g_max_temp = counts_to_c((double)hotv); int mx2 = g_max_x, my2 = 119 - g_max_y; for (int k = -2; k <= 2; ++k) { if (mx2 + k >= 0 && mx2 + k < W) { int d2 = my2 * W * 3 + (mx2 + k) * 3; px[d2] = 255; px[d2 + 1] = 255; px[d2 + 2] = 255; } if (my2 + k >= 0 && my2 + k < H) { int d2 = (my2 + k) * W * 3 + mx2 * 3; px[d2] = 255; px[d2 + 1] = 255; px[d2 + 2] = 255; } } } g_center_temp = counts_to_c((double)(csum / 400)); } static LRESULT CALLBACK wndproc(HWND hw, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_PAINT: { PAINTSTRUCT ps; HDC dc = BeginPaint(hw, &ps); HDC mem = CreateCompatibleDC(dc); HBITMAP bm = CreateCompatibleBitmap(dc, W, H); HGDIOBJ old = SelectObject(mem, bm); SetDIBitsToDevice(mem, 0, 0, W, H, 0, 0, 0, H, g_bmp + 54, (BITMAPINFO *)(g_bmp + 14), DIB_RGB_COLORS); StretchBlt(dc, 10, 10, 640, 480, mem, 0, 0, W, H, SRCCOPY); SelectObject(mem, old); DeleteObject(bm); DeleteDC(mem); SelectObject(dc, g_font); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, RGB(220, 220, 220)); int y = 20; char line[256]; snprintf(line, sizeof(line), "frame : %u", g_fcount); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; snprintf(line, sizeof(line), "fps : %.1f", g_fps); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; if (g_probe_x >= 0) { int v = g_live[g_probe_y * W + g_probe_x]; double t = counts_to_c((double)v); snprintf(line, sizeof(line), "probe : (%d,%d) %.2f C", g_probe_x, g_probe_y, t); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; } if (g_max_x >= 0) { snprintf(line, sizeof(line), "max : (%d,%d) %.2f C", g_max_x, g_max_y, g_max_temp); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; } snprintf(line, sizeof(line), "center: %.2f C", g_center_temp); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; snprintf(line, sizeof(line), "pipeline: official (NUCx3 + LUT + palette)"); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; if (g_bad_done) { snprintf(line, sizeof(line), "badpx : %d (fill %d)", g_bad_count, g_bad_order_len); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; } EndPaint(hw, &ps); break; } case WM_LBUTTONDOWN: { int x = LOWORD(lp), y = HIWORD(lp); if (x >= 10 && x < 650 && y >= 10 && y < 490) { g_probe_x = (x - 10) * W / 640; g_probe_y = 119 - (y - 10) * H / 480; InvalidateRect(hw, NULL, TRUE); } break; } case WM_COMMAND: switch (LOWORD(wp)) { case 1001: /* FFC - queue a manual pair through the scheduler; * the main loop issues FFC(0) after the next complete * frame and FFC(1) FFC_GAP frames later (official * cadence). Non-blocking: no Sleep() in the UI thread. */ g_manual_ffc = 1; SetWindowTextA(hw, "FFC queued (0 -> 1)"); break; case 1002: { char path[MAX_PATH]; SYSTEMTIME st; GetLocalTime(&st); snprintf(path, sizeof(path), "thermal_%04d%02d%02d_%02d%02d%02d.bmp", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); FILE *f = fopen(path, "wb"); if (f) { fwrite(g_bmp, 1, sizeof(g_bmp), f); fclose(f); } SetWindowTextA(hw, "saved"); break; } case 1004: /* recenter global bias without absorbing the scene */ if (g_official_nuc_ready && g_ref_mura_ready) { g_official_ref_bias = calibrate_official_ref_bias(); g_sb_shift = g_official_ref_bias; SetWindowTextA(hw, "bias recentered (flat field kept)"); } else { /* Fallback path for builds without the captured official * table: preserve the historical manual-reference action. */ for (int i = 0; i < NPIX; ++i) g_ref_mura[i] = g_live[i]; g_ref_mura_min = 0xffff; for (int i = 0; i < NPIX; ++i) if (g_ref_mura[i] < g_ref_mura_min) g_ref_mura_min = g_ref_mura[i]; g_ref_mura_ready = 1; g_has_reference = 1; g_comp = (int)NUC_CENTER; SetWindowTextA(hw, "reference set (fallback)"); } break; case 1005: g_probe_x = -1; InvalidateRect(hw, NULL, TRUE); break; } break; case WM_ERASEBKGND: return 1; case WM_KEYDOWN: if (wp == VK_ESCAPE) { DestroyWindow(hw); return 0; } break; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hw, msg, wp, lp); } int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd, int show) { (void)prev; (void)cmd; (void)show; libusb_init(&g_ctx); load_official_nuc_tables(); int ok = 0; for (int attempt = 0; attempt < 4 && !ok; ++attempt) { if (attempt > 0) { /* previous session may have left the unit streaming: reset it */ if (g_h) libusb_reset_device(g_h); if (g_h) { libusb_close(g_h); g_h = NULL; } Sleep(2500); } libusb_device **list = NULL; ssize_t cnt = libusb_get_device_list(g_ctx, &list); for (ssize_t i = 0; i < cnt && !g_h; ++i) { struct libusb_device_descriptor d; libusb_get_device_descriptor(list[i], &d); if (d.idVendor == 0x833c) libusb_open(list[i], &g_h); } libusb_free_device_list(list, 1); if (!g_h) continue; libusb_set_configuration(g_h, 2); libusb_set_configuration(g_h, 1); if (libusb_claim_interface(g_h, 0) != 0) { libusb_close(g_h); g_h = NULL; continue; } if (sendcmd(0x6bb6b66b, 0, 4)) continue; if (sendcmd(0x6bb6b66c, 0, 4)) continue; if (sendcmd(0x6bb6b66f, 0, 4)) continue; if (sendcmd(0x6bb6b672, 0, 8)) continue; Sleep(100); if (sendcmd(0x6bb6b672, 0, 8)) continue; Sleep(300); if (sendcmd(0x6bb6b673, 0, 4)) continue; Sleep(700); ok = 1; } if (!ok) { MessageBoxA(NULL, "camera init failed (retried 4x)", "MAG160C Demo", MB_ICONERROR); return 1; } WNDCLASSA wc = {0}; wc.lpfnWndProc = wndproc; wc.hInstance = inst; wc.lpszClassName = "Mag160cDemoFinal"; wc.hCursor = LoadCursor(NULL, IDC_CROSS); wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); RegisterClassA(&wc); g_hwnd = CreateWindowA("Mag160cDemoFinal", "MAG160C Thermal Demo v3", WS_OVERLAPPEDWINDOW, 60, 40, 1000, 620, NULL, NULL, inst, NULL); g_font = CreateFontA(18, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, "Consolas"); CreateWindowA("BUTTON", "FFC", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 670, 160, 120, 32, g_hwnd, (HMENU)1001, inst, NULL); CreateWindowA("BUTTON", "Save BMP", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 800, 160, 120, 32, g_hwnd, (HMENU)1002, inst, NULL); CreateWindowA("BUTTON", "Recenter bias", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 670, 200, 140, 32, g_hwnd, (HMENU)1004, inst, NULL); CreateWindowA("BUTTON", "Clear probe", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 820, 200, 100, 32, g_hwnd, (HMENU)1005, inst, NULL); unsigned sz = 54 + W * H * 3; g_bmp[0] = 'B'; g_bmp[1] = 'M'; g_bmp[2] = (unsigned char)sz; g_bmp[3] = (unsigned char)(sz >> 8); g_bmp[4] = (unsigned char)(sz >> 16); g_bmp[5] = (unsigned char)(sz >> 24); g_bmp[10] = 54; g_bmp[14] = 40; g_bmp[18] = W; g_bmp[19] = 0; g_bmp[22] = H; g_bmp[23] = 0; g_bmp[26] = 1; g_bmp[28] = 24; ShowWindow(g_hwnd, SW_SHOW); SetWindowTextA(g_hwnd, "starting - auto reference in ~10s"); /* ==== verified viewer core loop + official FFC cadence (csdk scheduler) */ int nread = 0; unsigned prev2 = 0xffffffff; mag160c_ffc_scheduler_t ffc; mag160c_ffc_scheduler_init(&ffc, FFC_PERIOD, FFC_GAP); int ffc_stall = 0; DWORD t0 = GetTickCount(); DWORD lfps_t = t0; unsigned lfps_c = 0; DWORD last_frame_t = t0; DWORD last_reset_t = t0; for (;;) { MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) goto done; TranslateMessage(&msg); DispatchMessage(&msg); } unsigned char hdr[64]; int xfer = 0; if (libusb_bulk_transfer(g_h, 0x81, hdr, sizeof(hdr), &xfer, 200) && xfer < 28) { /* no frame this round: watchdog wakes a stalled unit */ DWORD now2 = GetTickCount(); if (now2 - last_frame_t > 3000 && now2 - last_reset_t > 5000) { sendcmd(0x6bb6b672, 1, 8); last_reset_t = now2; ffc_stall++; } continue; } if (xfer < 28) continue; unsigned m = (unsigned)hdr[0] | ((unsigned)hdr[1] << 8) | ((unsigned)hdr[2] << 16) | ((unsigned)hdr[3] << 24); if (m != 0x1bb1b11b) continue; unsigned c = (unsigned)hdr[4] | ((unsigned)hdr[5] << 8) | ((unsigned)hdr[6] << 16) | ((unsigned)hdr[7] << 24); if (c == prev2) continue; prev2 = c; if (libusb_bulk_transfer(g_h, 0x81, g_frame, sizeof(g_frame), &xfer, 200) || xfer < 38400) continue; nread++; last_frame_t = GetTickCount(); /* manual FFC queued from the button: issue FFC(0) now (after a * complete frame, as in the official trace); the scheduler then * emits FFC(1) FFC_GAP frames later. */ if (g_manual_ffc) { g_manual_ffc = 0; if (mag160c_ffc_scheduler_trigger(&ffc) >= 0) { sendcmd(0x6bb6b672, 0, 8); SetWindowTextA(g_hwnd, "FFC(0) sent - warming"); } } /* official cadence (csdk scheduler): FFC(1) after ~10th frame, * then FFC(0) every FFC_PERIOD frames with FFC(1) FFC_GAP later. * FFC is only issued after a complete frame (as in the trace). */ { static FILE *ffcdbg; int32_t ffc_param = mag160c_ffc_scheduler_tick(&ffc); if (ffc_param >= 0) { sendcmd(0x6bb6b672, (unsigned)ffc_param, 8); if (!ffcdbg) ffcdbg = fopen("demo3_ffc.txt", "a"); if (ffcdbg) { fprintf(ffcdbg, "frame=%u nread=%u ffc=%d type=%u rebase=%d\n", c, nread, ffc_param, (unsigned)hdr[12], g_rebase); fflush(ffcdbg); } } if (ffc_param == 1) g_rebase = 1; /* FFC(1): re-align reference */ } /* FFC calibration window: the unit streams type=1 frames between * FFC(0) and FFC(1) (~9 frames). Do not render those (they are * raw/response data with a very different range - the official app * freezes the image instead of showing the red/yellow flash). */ if (hdr[12] != 0) { continue; } /* decode + correct once; everything below uses g_live */ decode_live(g_frame); /* after FFC(1) the type=0 baseline shifts and the unit has just * recalibrated: re-collect the reference from fresh quiet frames * (replaces the startup-collected one and any stale baseline), * then skip a few frames while the stream settles. */ if (g_rebase) { { static FILE *rdbg; if (!rdbg) rdbg = fopen("demo3_rebase.txt", "a"); if (rdbg) { fprintf(rdbg, "frame=%u type=%u hasref=%d\n", c, (unsigned)hdr[12], g_has_reference); fflush(rdbg); } } g_rebase = 0; g_skip_ffc = REF_SKIP; if (g_has_reference) { if (REF_REINIT_AFTER_FFC) { g_ref_phase = 2; /* post-FFC re-collection */ g_ref_n = 0; SetWindowTextA(g_hwnd, "re-collecting reference after FFC"); } else { mag160c_display_ref_rebase(g_ref_mura, g_live, NPIX, 2000); g_ref_mura_min = 0xffff; for (int i = 0; i < NPIX; ++i) if (g_ref_mura[i] < g_ref_mura_min) g_ref_mura_min = g_ref_mura[i]; } } } if (g_skip_ffc > 0) { g_skip_ffc--; continue; } /* first reference collection: starts after the startup FFC(1) has * switched the stream to type=0 AND the baseline has recovered. * Measured: the type=0 baseline after the first FFC(1) takes ~4 s * (60+ frames) to settle; collecting before that bakes a wrong * baseline into the reference (nuc then explodes to 50k counts). */ if (g_ref_phase == 0 && !g_ref_mura_ready && nread >= 60) { g_ref_phase = 1; g_ref_n = 0; } /* reference collection (init phase 1 or post-FFC phase 2): median * of REF_INIT_N frames. The median is naturally robust to a moving * object or noise (a transient object appears in <50% of the window * and is excluded), and the captured reference is used as the NUC * flat field: live - (ref - mean(ref)) removes the fixed sensor * mura (measured: spatial std 3560 -> 29). No quiet-gate: a strict * stillness requirement meant the reference never built while the * user was watching, so the NUC never activated and the raw mura * was displayed. */ if (g_ref_phase == 1 || g_ref_phase == 2) { /* motion gate (startup collection only): a moving scene * contaminates the reference (a hot object absorbed into ref * ghosts black once it moves away). Restart the window on * motion; a 12-frame still run then captures a clean ref. * Phase 2 (post-motion refresh) already waited for stillness, * so it does not gate. */ if (g_ref_phase == 1 && g_ref_n > 0) { unsigned long long msum = 0; unsigned moved = 0; for (int i = 0; i < NPIX; i += 4) { int d = (int)g_live[i] - (int)g_prev_frame[i]; if (d < 0) d = -d; msum += d; if (d > 60) moved++; } unsigned mmean = (unsigned)(msum / (NPIX / 4)); if (mmean > 30 || moved > NPIX / 25) { g_ref_n = 0; } } for (int i = 0; i < NPIX; ++i) { g_ref_samples[i][g_ref_n] = g_live[i]; if (g_ref_n == 0) { g_ref_min[i] = g_ref_max[i] = g_live[i]; } else { if (g_live[i] < g_ref_min[i]) g_ref_min[i] = g_live[i]; if (g_live[i] > g_ref_max[i]) g_ref_max[i] = g_live[i]; } } g_ref_n++; if (g_ref_n == REF_INIT_N) { int nb = 0; /* per-pixel median over the window */ static unsigned short tmp[REF_INIT_N]; for (int i = 0; i < NPIX; ++i) { for (int k = 0; k < REF_INIT_N; ++k) tmp[k] = g_ref_samples[i][k]; for (int a = 1; a < REF_INIT_N; ++a) { /* insertion sort */ unsigned short key = tmp[a]; int b = a - 1; while (b >= 0 && tmp[b] > key) { tmp[b + 1] = tmp[b]; b--; } tmp[b + 1] = key; } /* trimmed median (average of the middle 50%): * P15-per-pixel biased each pixel by a different * offset (measured: d spatial std 820 vs 390 for * median) which showed up as a haze over the whole * frame. The motion gate guarantees the collection * window is still, so a plain median is safe and * leaves no spatial bias. */ { long s = 0; int n = 0; for (int k = REF_INIT_N / 4; k < REF_INIT_N * 3 / 4; ++k) { s += tmp[k]; n++; } g_ref_mura[i] = (unsigned short)(s / n); } g_bad[i] = 0; } /* bad pixel detection only on the first collection; * post-FFC re-collection keeps the existing bad map */ if (BADMAP_ENABLE && g_ref_phase == 1) { /* temporal detection: min-max fluctuation */ for (int i = 0; i < NPIX; ++i) { if ((int)g_ref_max[i] - (int)g_ref_min[i] > 400) { g_bad[i] = 1; nb++; } } /* histogram-peak-deviation detection (Seek method): * bad if value > histPeak - (frameMax - histPeak), * guarded to at least histPeak + 200 */ { static unsigned hist[65536]; unsigned peakv = 0, peakc = 0, maxv = 0; for (int i = 0; i < 65536; ++i) hist[i] = 0; for (int i = 0; i < NPIX; ++i) { unsigned v = g_ref_mura[i]; if (++hist[v] > peakc) { peakc = hist[v]; peakv = v; } if (v > maxv) maxv = v; } long thr = (long)peakv - ((long)maxv - (long)peakv); if (thr < (long)peakv + 200) thr = (long)peakv + 200; for (int i = 0; i < NPIX; ++i) { if (!g_bad[i] && (long)g_ref_mura[i] > thr) { g_bad[i] = 1; nb++; } } /* dead pixels: far below the scene peak (or zero); * temporal min-max misses non-fluctuating deads */ for (int i = 0; i < NPIX; ++i) { if (!g_bad[i] && (long)g_ref_mura[i] < (long)peakv - 6000) { g_bad[i] = 1; nb++; } } } g_bad_count = nb; /* topological fill order: copy of the bad mask evolves * as pixels are filled (edge-first for clusters) */ unsigned char w[NPIX]; for (int i = 0; i < NPIX; ++i) w[i] = g_bad[i]; g_bad_order_len = 0; build_fill_order(w, (int (*)[2])g_bad_order, &g_bad_order_len); /* fill reference values in the stored order */ for (int i = 0; i < NPIX; ++i) w[i] = g_bad[i]; for (int i = 0; i < g_bad_order_len; ++i) { int x = g_bad_order[i][0], y = g_bad_order[i][1]; int v = neighbor_mean(g_ref_mura, w, x, y); if (v > 0) { g_ref_mura[y * W + x] = (unsigned short)v; w[y * W + x] = 0; } } } else if (BADMAP_ENABLE) { /* post-FFC re-collection: correct the new reference * with the existing bad map (work mask, bits cleared * as filled so cluster interiors fill too) */ static unsigned char w2[NPIX]; for (int i = 0; i < NPIX; ++i) w2[i] = g_bad[i]; for (int i = 0; i < g_bad_order_len; ++i) { int x = g_bad_order[i][0], y = g_bad_order[i][1]; int v = neighbor_mean(g_ref_mura, w2, x, y); if (v > 0) { g_ref_mura[y * W + x] = (unsigned short)v; w2[y * W + x] = 0; } } } g_has_reference = 1; g_bad_done = 1; g_ema_reset = 1; /* EMA must restart on the new baseline */ /* Official 0x180017330 is blind-pixel compensation, not a * full-frame 3x3 blur. Keep the flat-field in the same * unblurred domain as live. */ g_ref_mura_min = 0xffff; for (int i = 0; i < NPIX; ++i) if (g_ref_mura[i] < g_ref_mura_min) g_ref_mura_min = g_ref_mura[i]; g_ref_mura_ready = 1; /* calibrate the official NUC offset: with live==ref the * NUC output equals comp, so comp places the NUC output * on the official absolute level (NUC_CENTER). */ g_comp = (int)NUC_CENTER; int was = g_ref_phase; g_ref_phase = 0; char st[96]; snprintf(st, sizeof(st), "reference captured (phase %d) - bad: %d", was, nb); SetWindowTextA(g_hwnd, st); } } /* constant-reference mode: reference = global minimum temperature * this frame (see CONSTANT_REF). live - ref never negative. */ if (CONSTANT_REF) { unsigned gmin = 0xffff; for (int i = 0; i < NPIX; ++i) { if (g_live[i] < gmin) gmin = g_live[i]; } if (g_ref_mura_ready) { /* scene-based NUC with conditional updating (the standard * ghost-suppression approach: Scribner neural-net NUC 1993, * Hardie Kalman SBNUC 1998; used in FLIR/ULIS adaptive-FFC * patents). Protect only positive residuals (hot object * pixels). Negative residuals are the black-ghost failure * mode after a hot object moves away, so they must be allowed * to re-absorb quickly instead of being frozen by |dd|. */ static int hist[32768]; memset(hist, 0, sizeof(hist)); for (int i = 0; i < NPIX; ++i) { int d = (int)g_live[i] - (int)g_ref_mura[i] + 8192; if (d < 0) d = 0; if (d >= 32768) d = 32767; hist[d]++; } long half = NPIX / 2, acc = 0; int med = 0; for (int k = 0; k < 32768; ++k) { acc += hist[k]; if (acc >= half) { med = k - 8192; break; } } int accepted = 0; for (int i = 0; i < NPIX; ++i) { int rawd = (int)g_live[i] - (int)g_ref_mura[i]; int dd = rawd - med; #if SBNUC_ENABLE int state = g_sb_hot_state[i]; int prevdd = (int)g_sb_prev_d[i]; int fast_release = 0; int frame_step = (int)g_live[i] - (int)g_prev_frame[i]; int resid_step = dd - prevdd; int innov = frame_step < 0 ? -frame_step : frame_step; if (resid_step < 0) resid_step = -resid_step; if (resid_step > innov) innov = resid_step; if (innov > 255) innov = 255; int innov_sq = innov * innov; int innov_var = g_sb_innov_var[i]; if (innov_sq > innov_var) innov_var += (innov_sq - innov_var + 3) >> 2; else innov_var -= (innov_var - innov_sq + 3) >> 2; if (innov_var < 0) innov_var = 0; if (innov_var > 65535) innov_var = 65535; g_sb_innov_var[i] = (unsigned short)innov_var; if (innov_var > SB_INNOV_VAR_T) g_sb_still[i] = SB_INNOV_HOLD; else if (g_sb_still[i]) g_sb_still[i]--; int motion_gate = g_sb_still[i] != 0; if (motion_gate) { /* Do not rewrite the reference while an edge is * still moving through this pixel. Recovery starts * only after the innovation hold has expired. */ g_sb_cold_run[i] = 0; } else if (dd < -SB_RECOVER_T) { if (g_sb_cold_run[i] < SB_RECOVER_RUN) g_sb_cold_run[i]++; } else if (dd >= 0) { g_sb_cold_run[i] = 0; } int persistent_cold = g_sb_cold_run[i] >= SB_RECOVER_RUN; /* Keep the old strong-hot protection, but remember the * pixel until it is close to the background. The old * countdown started at dd=48, so a slowly cooling small * target could spend all 24 frames before becoming a * negative ghost. */ if (dd > SB_HOT_T) { state = SB_HOT_WARM; g_hot_hist[i] = 0; } else { if (state == 0 && dd > SB_HOT_SEED) state = SB_HOT_WARM; /* A sharp positive-to-negative crossing is also a * valid release trigger. This covers a target that * leaves between two samples without weakening the * normal background update gate. */ if (state == 0 && prevdd > SB_HOT_SEED && prevdd - dd > SB_HOT_SEED) { state = SB_HOT_RELEASE; g_hot_hist[i] = SB_HOT_HOLD; } if (state == SB_HOT_WARM && dd <= SB_HOT_EXIT) { state = SB_HOT_RELEASE; g_hot_hist[i] = SB_HOT_HOLD; } if (state == SB_HOT_RELEASE) { if (dd > SB_HOT_REENTER) { /* The object returned before the release * completed; do not absorb it as background. */ state = SB_HOT_WARM; g_hot_hist[i] = 0; } else if (dd <= SB_HOT_EXIT) { /* A small positive rebound pauses, rather * than spends, the release budget. */ if (g_hot_hist[i]) { fast_release = (dd < 0); g_hot_hist[i]--; } else { state = 0; } } } /* The official ref is frozen during normal frames. * Keep the adaptive part one-sided as well: a hot * target must never raise the spatial reference just * because its residual is below SB_HOT_T. Global * warming is already handled by `shift` below; only * colder residuals may lower this per-pixel ref. */ if (dd < 0 && persistent_cold && state == SB_HOT_RELEASE) { accepted++; /* Only a pixel that was previously warm may * self-heal after turning cold. A cold object * that remains stationary must stay out of the * reference, otherwise it becomes a bright ghost * when it leaves. */ int denom = (dd < -48) ? 1 : 2; int v = (int)g_ref_mura[i] + (rawd / denom); if (v < 0) v = 0; if (v > 65535) v = 65535; g_ref_mura[i] = (unsigned short)v; } } g_sb_hot_state[i] = (unsigned char)state; if (dd < -32768) g_sb_prev_d[i] = -32768; else if (dd > 32767) g_sb_prev_d[i] = 32767; else g_sb_prev_d[i] = (short)dd; #else (void)dd; (void)rawd; #endif } long long mdsum = 0; unsigned long long md2sum = 0; for (int i = 0; i < NPIX; ++i) { long long md = (long long)g_live[i] - (long long)g_ref_mura[i]; mdsum += md; md2sum += (unsigned long long)(md * md); } double mdmean = (double)mdsum / NPIX; g_sb_mura_dmean = mdmean; g_sb_mura_dstd = sqrt((double)md2sum / NPIX - mdmean * mdmean); g_sb_med = med; g_sb_accept = accepted; memset(hist, 0, sizeof(hist)); for (int i = 0; i < NPIX; ++i) { int d = (int)g_live[i] - (int)g_ref_mura[i] + 8192; if (d < 0) d = 0; if (d >= 32768) d = 32767; hist[d]++; } acc = 0; int shift = 0; for (int k = 0; k < 32768; ++k) { acc += hist[k]; if (acc >= half) { shift = k - 8192; break; } } g_ref_mura_min = 0xffff; for (int i = 0; i < NPIX; ++i) if (g_ref_mura[i] < g_ref_mura_min) g_ref_mura_min = g_ref_mura[i]; g_sb_shift = shift; g_sb_gmin = (int)gmin; for (int i = 0; i < NPIX; ++i) { int r = (int)g_ref_mura[i] + shift; if (r < 0) r = 0; if (r > 65535) r = 65535; g_reference[i] = (unsigned short)r; } } else { for (int i = 0; i < NPIX; ++i) g_reference[i] = (unsigned short)gmin; } g_has_reference = 1; g_bad_done = 1; } if (!CONSTANT_REF) { /* adaptive reference refresh: a frozen reference ghosts moving * objects into black trails. The official pipeline has no scene * detector (FFC fires on a 1800-frame timer or a sensor-temp jump * >250), so a ghost can linger for minutes. Track frame-to-frame * motion; when a motion episode ends and the scene has been still * for a while, re-collect the reference (phase-2 collection keeps * the bad map) so the ghost is erased. A still scene never * re-collects, so there is no periodic freeze. */ { unsigned long long msum = 0; unsigned moved = 0; for (int i = 0; i < NPIX; i += 4) { int d = (int)g_live[i] - (int)g_prev_frame[i]; if (d < 0) d = -d; msum += d; if (d > 60) moved++; } static int still_count = 0; static int g_had_motion = 0; unsigned mmean = (unsigned)(msum / (NPIX / 4)); if (mmean > 30 || moved > NPIX / 25) { still_count = 0; g_had_motion = 1; } else if (g_had_motion && g_has_reference && g_ref_phase == 0) { if (++still_count > 45) { g_ref_phase = 2; /* re-collect: clears absorbed ghosts */ g_ref_n = 0; still_count = 0; g_had_motion = 0; SetWindowTextA(g_hwnd, "scene settled - refreshing reference"); } } } } /* !CONSTANT_REF */ /* keep the previous frame for the init quiet-gate */ memcpy(g_prev_frame, g_live, sizeof(g_live)); /* freeze the display while a reference is being collected (startup * or post-FFC): the old reference does not match the current * baseline, so rendering now would flash garbage. The official app * freezes the image during FFC too. */ if (g_ref_phase != 0) { SetWindowTextA(g_hwnd, g_ref_phase == 1 ? "warming up - collecting reference..." : "FFC - re-collecting reference..."); InvalidateRect(g_hwnd, NULL, FALSE); UpdateWindow(g_hwnd); continue; } if (g_official_nuc_ready && g_ref_mura_ready) { g_official_ref_bias = calibrate_official_ref_bias(); g_sb_shift = g_official_ref_bias; for (int i = 0; i < NPIX; ++i) { int r = (int)g_ref_mura[i] + g_official_ref_bias; if (r < 0) r = 0; if (r > 65535) r = 65535; g_reference[i] = (unsigned short)r; } } render(0, 0); g_fcount++; DWORD now = GetTickCount(); if (now - lfps_t >= 1000) { g_fps = (g_fcount - lfps_c) * 1000.0 / (now - lfps_t); lfps_t = now; lfps_c = g_fcount; } snprintf(g_title, sizeof(g_title), "MAG160C Demo v4 (official pipeline) - frame %u type=%u ffc=%d", c, (unsigned)hdr[12], ffc_stall); SetWindowTextA(g_hwnd, g_title); /* auto-save for FFC diagnosis: every 30 frames, up to 40 shots */ if (g_fcount % 30 == 0 && g_fcount / 30 < 40) { char pth[MAX_PATH]; snprintf(pth, sizeof(pth), "demo3_auto_%03u.bmp", g_fcount / 30); FILE *f = fopen(pth, "wb"); if (f) { fwrite(g_bmp, 1, sizeof(g_bmp), f); fclose(f); } } InvalidateRect(g_hwnd, NULL, FALSE); UpdateWindow(g_hwnd); } done: Sleep(300); libusb_clear_halt(g_h, 0x03); libusb_clear_halt(g_h, 0x82); sendcmd(0x6bb6b674, 0, 4); libusb_close(g_h); libusb_exit(g_ctx); return 0; }