1420 lines
61 KiB
C
1420 lines
61 KiB
C
/* 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 <windows.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <math.h>
|
|
#include <libusb.h>
|
|
#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;
|
|
}
|