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)); + } +}