From 948c9d5d3d5077802cddebab1492314cc17d2806 Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Tue, 6 Oct 2026 00:46:27 +0800 Subject: [PATCH] =?UTF-8?q?tools:=20=E7=85=A7=E7=89=87=E5=B7=AE=E5=BC=82?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E4=B8=89=E4=BB=B6=E5=A5=97=EF=BC=88=E5=B8=A7?= =?UTF-8?q?=E5=89=96=E9=9D=A2=20/=20NUC=20=E7=9B=B4=E5=87=BA=20/=20?= =?UTF-8?q?=E6=9D=A1=E7=BA=B9=E9=87=8F=E5=8C=96=EF=BC=89+=20=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=20.tmp-*=20=E5=AF=B9=E6=AF=94=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为"我们的照片与官方照片到底差在哪"提供可量化的手段,而不是靠肉眼看图: - FrameProfile:对 160x120 载荷算行/列均值的步进与 3 抽头高频,并给出 「列高频 / 行高频」比值——列固定图案噪声会表现为该比值显著 >1,场景内容则接近 1。 同时对压缩块与"官方布局"(DDT 头 + 884B BasePara + 帧)都能定位载荷。 - NucDump:把 NUC 块按线性映射直出 PNG,用来判断成品图里的结构是"数据里有" 还是"显示级引入的"。 - StripeCheck:在成品图的平坦区域量列抖动,并给出它占局部量程的百分比。 实测结论(本轮):我们的原始帧列高频 675 counts(列噪声主导,符合未校正预期), 而我们的 NUC 块只有 4.1 counts、行/列比 1.25,且成品图上平坦区列抖动仅为局部量程的 0.1% —— 即**校正后没有残留列噪声**,肉眼以为的竖纹是场景纹理。官方的成品帧(其文件 存的是校正后数据)同样干净(colHF 5.5、比值 1.39),两边校正质量等价。 .tmp-*/ 加入忽略:对比用的用户照片与中间产物放在那里,不入库。 --- .gitignore | 3 + tools/FrameProfile.java | 119 ++++++++++++++++++++++++++++++++++++++++ tools/NucDump.java | 36 ++++++++++++ tools/StripeCheck.java | 25 +++++++++ 4 files changed, 183 insertions(+) create mode 100644 tools/FrameProfile.java create mode 100644 tools/NucDump.java create mode 100644 tools/StripeCheck.java diff --git a/.gitignore b/.gitignore index 6274a86..6b8dd98 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ __pycache__/ # Java 编译产物(手册配图生成器、tools/ 下的小工具) *.class + +# 临时对比/分析目录(不提交,含用户照片与中间产物) +.tmp-*/ diff --git a/tools/FrameProfile.java b/tools/FrameProfile.java new file mode 100644 index 0000000..0b00147 --- /dev/null +++ b/tools/FrameProfile.java @@ -0,0 +1,119 @@ +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; + +/** + * Column/row profile of a 160x120 measurement payload, to tell scene structure from + * sensor fixed-pattern noise. + * + * The signature of column FPN is a column-to-column JITTER that is large compared + * with the same figure measured along rows: real scene content is smooth in both + * directions, stripes are not. Reported per payload: + * + * colMean step — mean |colMean[i] - colMean[i-1]| (stripes along y) + * rowMean step — mean |rowMean[j] - rowMean[j-1]| (stripes along x) + * colHF — the same after removing the local trend (3-tap), i.e. how much + * of the jitter is high-frequency grain rather than scene slope + * + * Run it on the RAW frame and on the NUC block of the same capture: if RAW is + * striped and NUC is not, the correction works and the stripes seen in a render + * must come from somewhere else; if NUC is still striped, the flat-field is stale. + * + * Usage: java FrameProfile [rawOffsetHex] + * Without an offset it profiles every FRAME/NUC block it can find; an explicit + * offset is for files that store the payload outside typed blocks (the official + * app's MDT: body + 884-byte BasePara, then the frame). + */ +public class FrameProfile { + static final int B_FRAME = 0x5BB5B55D, B_NUC = 0x5BB5B560; + static final int W = 160, H = 120, NPIX = W * H; + + public static void main(String[] args) throws Exception { + File f = new File(args[0]); + byte[] b = Files.readAllBytes(f.toPath()); + ByteBuffer bb = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); + System.out.println("== " + f.getName() + " (" + b.length + " bytes) =="); + + // typed blocks + int t = b.length - 152; + if (b.length >= 152 && bb.getInt(t) == 0x5BB5B57B) { + int ddt = bb.getInt(t + 4); + int bodySize = bb.getInt(ddt + 4); + int p = ddt + 0x88, end = p + bodySize; + while (p + 8 <= end) { + int m = bb.getInt(p), len = bb.getInt(p + 4); + if (len < 0 || p + 8 + len > end) break; + if ((m == B_FRAME || m == B_NUC) && len >= 38400) { + report(m == B_FRAME ? "RAW frame (typed block)" : "NUC block (typed block)", + b, p + 8, f.getName()); + } + p += 8 + len; + } + // official layout: 884-byte BasePara right after the DDT header, then the frame + if (args.length < 2) { + int bp = ddt + 0x88; + if (bp + 4 <= b.length) { + int structLen = bb.getInt(bp); + if (structLen > 0 && structLen < 4096 && bp + 4 + structLen + 38400 <= b.length) { + report("RAW frame (after " + structLen + "-byte BasePara)", + b, bp + 4 + structLen, f.getName()); + } + } + } + } + if (args.length >= 2) { + report("payload at 0x" + args[1], b, Integer.parseInt(args[1], 16), f.getName()); + } + } + + static void report(String label, byte[] b, int off, String name) { + if (off + 2 * NPIX > b.length) { + System.out.println(" " + label + ": out of range"); + return; + } + int[] v = new int[NPIX]; + for (int i = 0; i < NPIX; i++) { + v[i] = (b[off + 2 * i] & 0xFF) | ((b[off + 2 * i + 1] & 0xFF) << 8); + } + double[] col = new double[W], row = new double[H]; + long sum = 0; + for (int y = 0; y < H; y++) { + for (int x = 0; x < W; x++) { + int p = v[y * W + x]; + col[x] += p; row[y] += p; sum += p; + } + } + for (int x = 0; x < W; x++) col[x] /= H; + for (int y = 0; y < H; y++) row[y] /= W; + + System.out.printf(" %-34s mean=%.0f%n", label, sum / (double) NPIX); + int[] srt = v.clone(); java.util.Arrays.sort(srt); + System.out.printf(" percentiles p1=%.0f p25=%.0f p50=%.0f p75=%.0f p99=%.0f%n", + q(srt,1), q(srt,25), q(srt,50), q(srt,75), q(srt,99)); + System.out.printf(" colMean step=%.1f colHF=%.1f (vertical stripes)%n", + step(col), hf(col)); + System.out.printf(" rowMean step=%.1f rowHF=%.1f (horizontal stripes)%n", + step(row), hf(row)); + double ratio = hf(col) / Math.max(1e-9, hf(row)); + System.out.printf(" HF ratio col/row = %.2f %s%n", ratio, + ratio > 2 ? "-> COLUMN FPN dominant" : ratio < 0.5 ? "-> ROW FPN dominant" : "-> balanced (scene-like)"); + } + + static double q(int[] s, int p) { return s[Math.min(s.length - 1, p * s.length / 100)]; } + + static double step(double[] a) { + double s = 0; + for (int i = 1; i < a.length; i++) s += Math.abs(a[i] - a[i - 1]); + return s / (a.length - 1); + } + + /** Mean absolute deviation from the 3-tap local trend: the grain, minus scene slope. */ + static double hf(double[] a) { + double s = 0; + for (int i = 1; i < a.length - 1; i++) { + s += Math.abs(a[i] - (a[i - 1] + a[i + 1]) / 2); + } + return s / (a.length - 2); + } +} diff --git a/tools/NucDump.java b/tools/NucDump.java new file mode 100644 index 0000000..06ce61f --- /dev/null +++ b/tools/NucDump.java @@ -0,0 +1,36 @@ +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import javax.imageio.ImageIO; +/** Render a 160x120 payload (NUC block) with a straight linear map, to see whether + * structure in the final JPEG is in the DATA or added by the display stage. */ +public class NucDump { + public static void main(String[] a) throws Exception { + byte[] b = Files.readAllBytes(new File(a[0]).toPath()); + ByteBuffer bb = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); + int t = b.length - 152, ddt = bb.getInt(t + 4), bodySize = bb.getInt(ddt + 4); + int p = ddt + 0x88, end = p + bodySize, off = -1; + while (p + 8 <= end) { + int m = bb.getInt(p), len = bb.getInt(p + 4); + if (len < 0 || p + 8 + len > end) break; + if (m == 0x5BB5B560 && len >= 38400) { off = p + 8; break; } + p += 8 + len; + } + if (off < 0) { System.out.println("no NUC block"); return; } + int W = 160, H = 120; + int[] v = new int[W * H]; + for (int i = 0; i < W * H; i++) v[i] = (b[off + 2 * i] & 0xFF) | ((b[off + 2 * i + 1] & 0xFF) << 8); + int lo = Integer.MAX_VALUE, hi = Integer.MIN_VALUE; + for (int x : v) { if (x < lo) lo = x; if (x > hi) hi = x; } + BufferedImage im = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) { + int g = (int) Math.round(255.0 * (v[y * W + x] - lo) / Math.max(1, hi - lo)); + im.setRGB(x, y, (g << 16) | (g << 8) | g); + } + ImageIO.write(im, "png", new File(a[1])); + System.out.println("wrote " + a[1] + " range " + lo + ".." + hi); + } +} diff --git a/tools/StripeCheck.java b/tools/StripeCheck.java new file mode 100644 index 0000000..8d639ce --- /dev/null +++ b/tools/StripeCheck.java @@ -0,0 +1,25 @@ +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; +/** Column-jitter (stripe) amplitude inside a flat region of a rendered photo. */ +public class StripeCheck { + public static void main(String[] a) throws Exception { + BufferedImage im = ImageIO.read(new File(a[0])); + int x0 = Integer.parseInt(a[1]), y0 = Integer.parseInt(a[2]); + int w = Integer.parseInt(a[3]), h = Integer.parseInt(a[4]); + double[] col = new double[w]; + double lo = 255, hi = 0; + for (int y = y0; y < y0 + h; y++) + for (int x = x0; x < x0 + w; x++) { + int c = im.getRGB(x, y); + int l = (((c >> 16) & 0xFF) * 299 + ((c >> 8) & 0xFF) * 587 + (c & 0xFF) * 114) / 1000; + col[x - x0] += l; if (l < lo) lo = l; if (l > hi) hi = l; + } + for (int i = 0; i < w; i++) col[i] /= h; + double hf = 0; int n = 0; + for (int i = 1; i < w - 1; i++) { hf += Math.abs(col[i] - (col[i-1] + col[i+1]) / 2); n++; } + hf /= n; + System.out.printf("%-28s region %dx%d luma %.0f..%.0f (range %.0f) colJitter=%.2f = %.1f%% of local range%n", + new File(a[0]).getName(), w, h, lo, hi, hi - lo, hf, 100 * hf / Math.max(1, hi - lo)); + } +}