- 逆向:Ghidra/IDA 全量反编译 CoreSDKLib.dll/ThermalSDK.dll/libthermalSDK.so/ libcoresdk.so(ARM64)/libmagcore.so,导出 analysis/ida/export/ - 解码官方渲染管线:DDT 校准表加载->快门端点选择->Q12 插值->ref(4x type1 帧 均值)->NUC 查表->盲元补偿->窗口->LUT1024 重建->2x 升采样->调色板 - 逐像素验证:NUC+盲元 0/19200、插值 0 误差、2x 0/76800、窗口一致 - demo3 v5:完整复刻官方管线(含 DDT 解析、FFC 状态机、快门温度驱动), 修复 load_ddt 表错位导致的零像素问题 - 鬼影根因分析写入 analysis/reverse_20260813_full.md - 心跳/恢复机制:analysis/session_state.md + tools/resume_rev.ps1 - 新增 tsdk_pair3 增强采集工具;历史工具归档 csdk/tools/legacy/; 根目录抓帧残留删除,历史文档归档 analysis/history/ - csdk/README.md 完整使用文档;.gitignore/.gitattributes 补 LFS 规则
892 lines
38 KiB
C
892 lines
38 KiB
C
/* MAG160C Windows Demo v3 - built on the verified thermal_viewer core.
|
|
*
|
|
* Changes vs demo2:
|
|
* - Bad pixel: Seek-style histogram-peak-deviation detection (threshold =
|
|
* histPeak - (frameMax - histPeak)) combined with temporal min-max;
|
|
* topological-order 4-neighbor fill (bad clusters shrink from the edge
|
|
* inward), applied to the live frame AND the reference.
|
|
* - Contrast: adaptive percentile AGC - diff span = P1..P99 of |diff|
|
|
* (min 120, max 4000) with deadband = span/20; absolute mode uses the
|
|
* same percentile stretch + ironbow LUT (FLIR-style anchors).
|
|
* - FFC: replicates the official demo cadence from libusb0_trace.txt:
|
|
* FFC(1) after the ~10th frame (switch to type=0), then every FFC_PERIOD
|
|
* frames a pair FFC(0) followed exactly 9 frames later by FFC(1). In the
|
|
* official trace this kept the type=0 stream alive for 10519+ frames.
|
|
* - Temperature: two-point linear calibration (counts vs known °C) with
|
|
* fallback to the old 147 counts/C assumption.
|
|
*/
|
|
#define _WIN32_WINNT 0x0601
|
|
#include <windows.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <libusb.h>
|
|
#include "mag160c/mag160c.h"
|
|
#include "mag160c/mag160c_display.h"
|
|
#include "mag160c_official_palette.h"
|
|
#include "mag160c_official_t2e.h"
|
|
|
|
#define W 160
|
|
#define H 120
|
|
#define NPIX (W * H)
|
|
|
|
#define FFC_PERIOD 400 /* frames between FFC pairs (official: 34..2680) */
|
|
#define FFC_GAP 9 /* frames between FFC(0) and FFC(1) (official) */
|
|
|
|
/* 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 2
|
|
#define REF_INIT_N 30
|
|
#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 1 /* re-collect the reference after FFC(1)
|
|
* (replaces the one-shot median rebase) */
|
|
|
|
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 int g_diff_mode; /* 0 = absolute temperature (default, matches
|
|
* the official app; no reference, no ghost) */
|
|
static unsigned short g_reference[NPIX];
|
|
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 unsigned char g_fg_count[NPIX]; /* frames pixel stayed foreground */
|
|
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;
|
|
|
|
/* two-point calibration: temp = a * counts + b (a in C/counts) */
|
|
static double g_cal_a = 1.0 / 147.0;
|
|
static double g_cal_b = -11720.0 / 147.0 + 25.0; /* 147 c/C, 25C @ 11720 */
|
|
static int g_cal_valid;
|
|
static int g_cal_phase; /* 0 none, 1 awaiting cold, 2 awaiting hot */
|
|
static double g_cal_cold_count, g_cal_cold_temp;
|
|
static double g_cal_hot_count, g_cal_hot_temp;
|
|
|
|
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;
|
|
}
|
|
|
|
static double counts_to_c(double v) {
|
|
if (g_cal_valid) return g_cal_a * v + g_cal_b;
|
|
return g_cal_a * v + g_cal_b; /* fallback == same formula */
|
|
}
|
|
|
|
/* Official temperature conversion (recovered from CoreSDKLib 0x180016290):
|
|
* x = counts << (7 - shift) shift=6 for this unit (verified: NUC'd bg
|
|
* counts ~11224 -> 24.9 C, official probe 26.7-27.4 C)
|
|
* binary search the 646-entry T2E table, then
|
|
* temp = slope[i]*diff>>12 + (i<<12) - 0x249f0 (millidegree C / 1000)
|
|
*/
|
|
#define MAG_T2E_SHIFT 6
|
|
#define MAG_T2E_OFFSET 0x249f0
|
|
|
|
static int counts_to_temp_mc(int counts) {
|
|
int64_t x = (int64_t)counts << (7 - MAG_T2E_SHIFT);
|
|
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 x 0.001 -> /1000 = degC */
|
|
}
|
|
|
|
/* ---------- 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.
|
|
* Returns the number of corrected pixels. */
|
|
static int correct_frame(unsigned short *fr) {
|
|
if (!g_bad_order_len) return 0;
|
|
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, g_bad, x, y);
|
|
if (v > 0) { fr[y * W + x] = (unsigned short)v; n++; }
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* ---------- color LUTs ---------- */
|
|
|
|
/* ironbow anchors (FLIR-style), value 0..255 */
|
|
static void ironbow(unsigned v, unsigned char *r, unsigned char *g,
|
|
unsigned char *b) {
|
|
static const int 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 / 255.0 * 6.0;
|
|
int i = (int)f;
|
|
if (i > 5) i = 5;
|
|
double t = f - i;
|
|
*r = (unsigned char)(anchors[i][0] + t * (anchors[i + 1][0] - anchors[i][0]));
|
|
*g = (unsigned char)(anchors[i][1] + t * (anchors[i + 1][1] - anchors[i][1]));
|
|
*b = (unsigned char)(anchors[i][2] + t * (anchors[i + 1][2] - anchors[i][2]));
|
|
}
|
|
|
|
/* ---------- render ---------- */
|
|
|
|
/* decode + correct the live frame into g_live (shared with the reference
|
|
* model so both use the same corrected values) */
|
|
static void decode_live(const unsigned char *data) {
|
|
for (int i = 0; i < NPIX; ++i)
|
|
g_live[i] = (unsigned short)(data[i * 2 + 28] | ((unsigned)data[i * 2 + 29] << 8));
|
|
if (g_bad_done) correct_frame(g_live);
|
|
}
|
|
|
|
/* 3x3 median filter on counts: cuts the ~245-count temporal noise ~3x so
|
|
* the absolute temperature image is smooth (noise would otherwise render
|
|
* as red/blue speckle - the "startup noise image" the user saw). Real
|
|
* thermal structure (hand, mura gradient) survives. */
|
|
static void median3_counts(unsigned short *src, unsigned short *dst) {
|
|
unsigned short win[9];
|
|
for (int y = 0; y < H; ++y) {
|
|
for (int x = 0; x < W; ++x) {
|
|
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;
|
|
win[n++] = src[yy * W + xx];
|
|
}
|
|
}
|
|
/* insertion sort, take middle */
|
|
for (int i = 1; i < n; ++i) {
|
|
unsigned short k = win[i];
|
|
int j = i - 1;
|
|
while (j >= 0 && win[j] > k) { win[j + 1] = win[j]; j--; }
|
|
win[j + 1] = k;
|
|
}
|
|
dst[y * W + x] = win[n / 2];
|
|
}
|
|
}
|
|
}
|
|
|
|
static void render(unsigned mn, unsigned mx) {
|
|
static unsigned short sm[NPIX]; /* median-smoothed */
|
|
static unsigned short nuc[NPIX]; /* flat-field corrected */
|
|
static unsigned char nuc_hist[65536];
|
|
unsigned char *px = g_bmp + 54;
|
|
unsigned hotv = 0;
|
|
int hot = 0;
|
|
unsigned csum = 0;
|
|
long sum_abs = 0;
|
|
unsigned cnt_abs = 0;
|
|
|
|
/* flat-field (NUC) correction: nuc = live - (ref - mean(ref)).
|
|
* Removes the fixed sensor mura (measured spatial std 3560 -> 29). */
|
|
if (g_has_reference) {
|
|
mag160c_display_nuc(g_live, g_reference, NPIX, nuc);
|
|
median3_counts(g_live, sm); /* for the optional diff path */
|
|
} else {
|
|
for (int i = 0; i < NPIX; ++i) nuc[i] = g_live[i];
|
|
median3_counts(g_live, sm);
|
|
}
|
|
|
|
/* ==== official display pipeline (recovered from CoreSDKLib) ====
|
|
* counts -> NUC -> T2E temperature -> gray -> official palette.
|
|
* The vendor renders a magenta-family palette (background lands mid-
|
|
* ramp, hot objects shift toward red/purple ends). Gray is a fixed
|
|
* temperature window; we use a narrow window around the measured
|
|
* background so the scene matches the vendor look. */
|
|
{
|
|
static int temp_mc[NPIX];
|
|
int tmin = 30000, tmax = 44000; /* 30..44 C in millidegrees */
|
|
for (int i = 0; i < NPIX; ++i) {
|
|
temp_mc[i] = counts_to_temp_mc((int)nuc[i]);
|
|
}
|
|
/* center the window on the frame's temperature median so the
|
|
* background lands mid-palette (magenta) like the official app */
|
|
{
|
|
static int hist[65536];
|
|
memset(hist, 0, sizeof(hist));
|
|
for (int i = 0; i < NPIX; ++i) {
|
|
int t = temp_mc[i] / 1000;
|
|
if (t >= 0 && t < 65536) hist[t]++;
|
|
}
|
|
int acc = 0, med_t = 0;
|
|
int mid_target = NPIX / 2;
|
|
for (int k = 0; k < 65536; ++k) {
|
|
acc += hist[k];
|
|
if (acc >= mid_target) { med_t = k; break; }
|
|
}
|
|
tmin = (med_t - 2) * 1000; /* +- 2 C around background */
|
|
tmax = (med_t + 2) * 1000;
|
|
}
|
|
for (int y = 0; y < H; ++y) {
|
|
for (int x = 0; x < W; ++x) {
|
|
int i = y * W + x;
|
|
int v = nuc[i];
|
|
if (x >= 70 && x < 90 && y >= 50 && y < 70) csum += v;
|
|
if (v > (int)hotv && v != 0) { hotv = (unsigned)v; hot = i; }
|
|
unsigned char r, g, b;
|
|
if (g_diff_mode && g_has_reference) {
|
|
int d = (int)sm[i] - (int)g_reference[i];
|
|
if (d > 32767) d = 32767;
|
|
if (d < -32768) d = -32768;
|
|
unsigned char ri, gi, bi;
|
|
int span2 = 2000;
|
|
if (d > 30) {
|
|
unsigned idx = (unsigned)(d * 255 / span2);
|
|
if (idx > 255) idx = 255;
|
|
ironbow(128 + idx / 2, &ri, &gi, &bi);
|
|
} else if (d < -30) {
|
|
unsigned idx = (unsigned)(-d * 255 / span2);
|
|
if (idx > 255) idx = 255;
|
|
ironbow(128 - idx / 2, &ri, &gi, &bi);
|
|
} else {
|
|
ironbow(128, &ri, &gi, &bi);
|
|
}
|
|
r = ri; g = gi; b = bi;
|
|
} else {
|
|
int t = temp_mc[i];
|
|
int idx;
|
|
if (t <= tmin) idx = 0;
|
|
else if (t >= tmax) idx = 255;
|
|
else idx = (t - tmin) * 255 / (tmax - tmin);
|
|
r = mag160c_official_palette[idx][0];
|
|
g = mag160c_official_palette[idx][1];
|
|
b = mag160c_official_palette[idx][2];
|
|
}
|
|
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), "mode : %s", g_diff_mode ? "DIFF (ref avg)" : "absolute");
|
|
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;
|
|
}
|
|
if (g_cal_valid) {
|
|
snprintf(line, sizeof(line), "cal : %.5f C/cnt + %.1f", g_cal_a, g_cal_b);
|
|
TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24;
|
|
} else {
|
|
snprintf(line, sizeof(line), "cal : 147 counts/C (assumed)");
|
|
TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24;
|
|
}
|
|
if (g_cal_phase == 1) {
|
|
snprintf(line, sizeof(line), "CAL : aim at COLD object, press Cal-Cold");
|
|
TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24;
|
|
} else if (g_cal_phase == 2) {
|
|
snprintf(line, sizeof(line), "CAL : aim at HOT object, press Cal-Hot");
|
|
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 1003:
|
|
g_diff_mode = !g_diff_mode;
|
|
break;
|
|
case 1004: /* set reference (from the corrected live frame) */
|
|
for (int i = 0; i < NPIX; ++i)
|
|
g_reference[i] = g_live[i];
|
|
g_has_reference = 1;
|
|
g_diff_mode = 1;
|
|
SetWindowTextA(hw, "reference set");
|
|
break;
|
|
case 1005:
|
|
g_probe_x = -1;
|
|
InvalidateRect(hw, NULL, TRUE);
|
|
break;
|
|
case 1006: /* calibration step 1: cold */
|
|
if (g_has_reference) {
|
|
long s = 0;
|
|
for (int i = 0; i < NPIX; ++i)
|
|
s += (int)g_live[i];
|
|
g_cal_cold_count = (double)s / NPIX;
|
|
g_cal_cold_temp = 0.0;
|
|
g_cal_phase = 2;
|
|
SetWindowTextA(hw, "cold point captured (0C) - now aim hot");
|
|
}
|
|
break;
|
|
case 1007: /* calibration step 2: hot */
|
|
if (g_cal_phase == 2) {
|
|
long s = 0;
|
|
for (int i = 0; i < NPIX; ++i)
|
|
s += (int)g_live[i];
|
|
g_cal_hot_count = (double)s / NPIX;
|
|
g_cal_hot_temp = 90.0;
|
|
g_cal_a = (g_cal_hot_temp - g_cal_cold_temp) /
|
|
(g_cal_hot_count - g_cal_cold_count);
|
|
g_cal_b = g_cal_cold_temp - g_cal_a * g_cal_cold_count;
|
|
g_cal_valid = 1;
|
|
g_cal_phase = 0;
|
|
char st[160];
|
|
snprintf(st, sizeof(st), "calibrated: %.3f C/cnt, offset %.1f",
|
|
g_cal_a, g_cal_b);
|
|
SetWindowTextA(hw, st);
|
|
}
|
|
break;
|
|
case 1008: /* reset calibration to default */
|
|
g_cal_a = 1.0 / 147.0;
|
|
g_cal_b = -11720.0 / 147.0 + 25.0;
|
|
g_cal_valid = 0;
|
|
g_cal_phase = 0;
|
|
SetWindowTextA(hw, "calibration reset to 147 c/C");
|
|
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);
|
|
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", "Diff mode", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
|
|
670, 200, 140, 28, g_hwnd, (HMENU)1003, inst, NULL);
|
|
CreateWindowA("BUTTON", "Set reference", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
|
670, 240, 140, 32, g_hwnd, (HMENU)1004, inst, NULL);
|
|
CreateWindowA("BUTTON", "Clear probe", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
|
820, 240, 100, 32, g_hwnd, (HMENU)1005, inst, NULL);
|
|
CreateWindowA("BUTTON", "Cal Cold (0C)", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
|
670, 290, 140, 32, g_hwnd, (HMENU)1006, inst, NULL);
|
|
CreateWindowA("BUTTON", "Cal Hot (90C)", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
|
820, 290, 120, 32, g_hwnd, (HMENU)1007, inst, NULL);
|
|
CreateWindowA("BUTTON", "Reset cal", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
|
670, 330, 120, 32, g_hwnd, (HMENU)1008, 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). */
|
|
{
|
|
int32_t ffc_param = mag160c_ffc_scheduler_tick(&ffc);
|
|
if (ffc_param >= 0) sendcmd(0x6bb6b672, (unsigned)ffc_param, 8);
|
|
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) {
|
|
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_reference, g_live, NPIX, 2000);
|
|
}
|
|
}
|
|
}
|
|
if (g_skip_ffc > 0) {
|
|
g_skip_ffc--;
|
|
continue;
|
|
}
|
|
|
|
/* first reference collection: starts right after the startup FFC(1)
|
|
* has switched the stream to type=0 and the transition frames have
|
|
* been skipped (nread >= 40), so the startup "noise image" is never
|
|
* baked into the reference. */
|
|
if (g_ref_phase == 0 && !g_has_reference && nread >= 40) {
|
|
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) {
|
|
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% (drops
|
|
* top/bottom 25% outliers - a transient object or a
|
|
* dead pixel spike cannot shift the flat field) */
|
|
{
|
|
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_reference[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 (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_reference[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_reference[i] > thr) { 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_reference, w, x, y);
|
|
if (v > 0) {
|
|
g_reference[y * W + x] = (unsigned short)v;
|
|
w[y * W + x] = 0;
|
|
}
|
|
}
|
|
} else {
|
|
/* post-FFC re-collection: correct the new reference
|
|
* with the existing bad map */
|
|
for (int i = 0; i < NPIX; ++i)
|
|
g_bad[i] = g_bad[i]; /* keep map */
|
|
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_reference, g_bad, x, y);
|
|
if (v > 0) g_reference[y * W + x] = (unsigned short)v;
|
|
}
|
|
}
|
|
g_has_reference = 1;
|
|
g_bad_done = 1;
|
|
int was = g_ref_phase;
|
|
g_ref_phase = 0;
|
|
memset(g_fg_count, 0, sizeof(g_fg_count));
|
|
char st[96];
|
|
snprintf(st, sizeof(st), "reference captured (phase %d) - bad: %d",
|
|
was, nb);
|
|
SetWindowTextA(g_hwnd, st);
|
|
}
|
|
}
|
|
else if (g_has_reference && g_skip_ffc <= 0 && g_diff_mode) {
|
|
/* MOG-style per-pixel background model (csdk) with self-healing:
|
|
* only used for the optional diff display. The NUC flat-field
|
|
* reference must stay FROZEN (re-collected after each FFC only):
|
|
* updating it here would absorb the background into the
|
|
* reference, killing the NUC and re-baking the mura in. */
|
|
mag160c_display_ref_track_heal(g_reference, g_live, W, H,
|
|
REF_T, REF_ALPHA, g_fg_count,
|
|
REF_SELFHEAL_N, 2);
|
|
}
|
|
/* keep the previous frame for the init quiet-gate */
|
|
memcpy(g_prev_frame, g_live, sizeof(g_live));
|
|
unsigned hist[65536] = {0};
|
|
unsigned nz = 0;
|
|
for (int i = 0; i < NPIX; ++i) {
|
|
unsigned v = g_live[i];
|
|
if (v == 0) continue;
|
|
hist[v]++;
|
|
nz++;
|
|
}
|
|
/* absolute mode window: computed on the NUC output (flat-field
|
|
* corrected) values, which cluster tightly around the reference
|
|
* mean (measured std ~29 after NUC). A narrow median +- span
|
|
* window gives the high contrast the official app gets from its
|
|
* temperature window, while the NUC removed the mura. */
|
|
unsigned mn = 0, mx = 0, acc = 0;
|
|
unsigned mid_target = (unsigned)(nz / 2);
|
|
for (unsigned k = 0; k < 65536; ++k) {
|
|
acc += hist[k];
|
|
if (acc >= mid_target) { mx = k; break; }
|
|
}
|
|
{
|
|
unsigned med = mx;
|
|
unsigned lo2 = med > 1500 ? med - 1500 : 0;
|
|
unsigned hi2 = med + 1500;
|
|
if (hi2 > 65535) hi2 = 65535;
|
|
mn = lo2;
|
|
mx = hi2;
|
|
}
|
|
render(mn, mx);
|
|
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 v3 - frame %u type=%u range [%u..%u] ffc=%d",
|
|
c, (unsigned)hdr[12], mn, mx, ffc_stall);
|
|
SetWindowTextA(g_hwnd, g_title);
|
|
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;
|
|
}
|