import java.awt.image.BufferedImage; import java.io.File; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; import javax.imageio.ImageIO; /** * Inspect MAG160C photo files — ours and the official app's — and print what is * needed to explain why two photos of the same scene differ. * * It reports three layers, from the outside in: * 1. CONTAINER — is it the vendor MDT (JPEG + DDT blocks + tail)? which blocks, * how big. A structural difference is the first thing to rule out. * 2. DATA — the raw measurement frame and the NUC block: counts min/mean/max. * If the two files disagree HERE, the difference is upstream of * rendering (sensor state, FFC, calibration), not our colour mapping. * 3. IMAGE — JPEG luma percentiles, mean and a Laplacian sharpness figure. * That is what "the photos look different" usually means. * * Usage: * javac -encoding UTF-8 MdtInspect.java * java MdtInspect ours.jpg official.jpg * (one file: prints the report; two files: also prints the comparison) */ public class MdtInspect { // ---- MDT container magics (little-endian, as stored) ---- static final int SEC_DDT = 0x5BB5B55B; static final int SEC_TAIL = 0x5BB5B57B; static final int B_INFO0 = 0x5BB5B55B; static final int B_INFO1 = 0x5BB5B55C; static final int B_FRAME = 0x5BB5B55D; static final int B_TXT = 0x5BB5B55E; static final int B_PROBES = 0x5BB5B55F; static final int B_NUC = 0x5BB5B560; static final int B_RENDER = 0x5BB5B561; static final int B_EXTREMES = 0x5BB5B562; /** Calibration file (not a photo) — worth naming explicitly when handed one. */ static final int CALI_MAGIC_3 = 0x5AA50003; static final int CALI_MAGIC_4 = 0x5AA50004; public static void main(String[] args) throws Exception { if (args.length == 0) { System.out.println("usage: java MdtInspect [second file to compare]"); return; } Report[] reports = new Report[args.length]; for (int i = 0; i < args.length; i++) { reports[i] = inspect(new File(args[i])); System.out.println(reports[i]); } if (reports.length == 2) compare(reports[0], reports[1]); } // ------------------------------------------------------------------ model static class Report { String name; long bytes; boolean isCali; boolean isMdt; int jpegW, jpegH; int tailOffset = -1; java.util.List blocks = new java.util.ArrayList<>(); String probes, text, render, extremes; int[] rawCounts; // FRAME block, 38400 B as 19200 u16 int[] nucCounts; // NUC block, same layout double p1, p25, p50, p75, p99, meanLuma, sharpness; String note = ""; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("== ").append(name).append(" (").append(bytes).append(" bytes) ==\n"); if (isCali) { sb.append(" calibration file (magic 5AA5000x) — not a photo\n"); return sb.toString(); } sb.append(" container: ").append(isMdt ? "vendor MDT" : "plain JPEG (no DDT tail)") .append(note.isEmpty() ? "" : " [" + note + "]").append('\n'); sb.append(" JPEG : ").append(jpegW).append("x").append(jpegH).append('\n'); for (String b : blocks) sb.append(" block : ").append(b).append('\n'); if (probes != null) sb.append(" probes : ").append(probes).append('\n'); if (extremes != null) sb.append(" extremes : ").append(extremes).append('\n'); if (text != null && !text.isBlank()) sb.append(" note : ").append(text).append('\n'); if (render != null) sb.append(" render : ").append(render).append('\n'); if (rawCounts != null) sb.append(" raw frame: ").append(stats(rawCounts)).append('\n'); if (nucCounts != null) sb.append(" NUC data : ").append(stats(nucCounts)).append('\n'); sb.append(String.format(" luma : p1=%.0f p25=%.0f p50=%.0f p75=%.0f p99=%.0f mean=%.1f sharpness=%.2f%n", p1, p25, p50, p75, p99, meanLuma, sharpness)); return sb.toString(); } } // ---------------------------------------------------------------- parsing static Report inspect(File f) throws Exception { Report r = new Report(); r.name = f.getName(); byte[] b = Files.readAllBytes(f.toPath()); r.bytes = b.length; if (b.length < 8) { r.note = "too small"; return r; } ByteBuffer bb = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); int magic = bb.getInt(0); if (magic == CALI_MAGIC_3 || magic == CALI_MAGIC_4) { r.isCali = true; return r; } // --- DDT tail (last 152 bytes) --- int ddt = -1; if (b.length >= 152) { int t = b.length - 152; if (bb.getInt(t) == SEC_TAIL) { int off = bb.getInt(t + 4); if (off > 0 && off + 0x88 <= b.length && bb.getInt(off) == SEC_DDT) { ddt = off; r.isMdt = true; r.tailOffset = t; } } } if (ddt < 0) { r.note = "no MDT tail — official files may store the raw frame elsewhere"; } else { int bodySize = bb.getInt(ddt + 4); int p = ddt + 0x88, end = Math.min(b.length - 152, p + bodySize); while (p + 8 <= end) { int m = bb.getInt(p), len = bb.getInt(p + 4); if (len < 0 || p + 8 + len > end) break; byte[] data = Arrays.copyOfRange(b, p + 8, p + 8 + len); switch (m) { case B_INFO0 -> r.blocks.add("INFO0 " + len + " B"); case B_INFO1 -> r.blocks.add("INFO1 " + len + " B"); case B_FRAME -> { r.blocks.add("FRAME(raw) " + len + " B"); r.rawCounts = toU16(data); } case B_TXT -> r.blocks.add("TXT " + len + " B"); case B_PROBES -> { r.blocks.add("PROBES " + len + " B"); r.probes = new String(data, StandardCharsets.UTF_8).trim(); } case B_NUC -> { r.blocks.add("NUC " + len + " B"); r.nucCounts = toU16(data); } case B_RENDER -> { int flags = len >= 8 ? bb.getInt(p + 8 + 4) : 0; int rot = len >= 12 ? bb.getInt(p + 8 + 8) : 0; r.blocks.add("RENDER v" + (len >= 12 ? 2 : 1) + " flags=" + flags + " rot=" + rot); r.render = "flipH=" + ((flags & 1) != 0) + " flipV=" + ((flags & 2) != 0) + " rot=" + rot; } case B_EXTREMES -> { r.blocks.add("EXTREMES " + len + " B"); r.extremes = new String(data, StandardCharsets.UTF_8).trim(); } default -> r.blocks.add(String.format("unknown 0x%08X %d B", m, len)); } p += 8 + len; } } // --- the JPEG itself + image statistics --- BufferedImage im = ImageIO.read(f); if (im == null) { r.note += " (JPEG not decodable)"; return r; } r.jpegW = im.getWidth(); r.jpegH = im.getHeight(); imageStats(im, r); return r; } static int[] toU16(byte[] d) { int n = d.length / 2; int[] out = new int[n]; for (int i = 0; i < n; i++) out[i] = (d[2 * i] & 0xFF) | ((d[2 * i + 1] & 0xFF) << 8); return out; } static String stats(int[] v) { if (v.length == 0) return "(empty)"; long sum = 0; int mn = Integer.MAX_VALUE, mx = Integer.MIN_VALUE; for (int x : v) { sum += x; if (x < mn) mn = x; if (x > mx) mx = x; } return String.format("%d samples min=%d mean=%.1f max=%d spread=%d", v.length, mn, sum / (double) v.length, mx, mx - mn); } static void imageStats(BufferedImage im, Report r) { int w = im.getWidth(), h = im.getHeight(); int[] lum = new int[w * h]; long sum = 0; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int c = im.getRGB(x, y); int l = (((c >> 16) & 0xFF) * 299 + ((c >> 8) & 0xFF) * 587 + (c & 0xFF) * 114) / 1000; lum[y * w + x] = l; sum += l; } } r.meanLuma = sum / (double) lum.length; int[] sorted = lum.clone(); Arrays.sort(sorted); r.p1 = pct(sorted, 1); r.p25 = pct(sorted, 25); r.p50 = pct(sorted, 50); r.p75 = pct(sorted, 75); r.p99 = pct(sorted, 99); // Laplacian energy: comparable across images of the SAME size, which is how a // "ours looks softer/sharper" claim gets settled double acc = 0; long n = 0; for (int y = 1; y < h - 1; y++) { for (int x = 1; x < w - 1; x++) { int i = y * w + x; int lap = 4 * lum[i] - lum[i - 1] - lum[i + 1] - lum[i - w] - lum[i + w]; acc += Math.abs(lap); n++; } } r.sharpness = acc / n; } static double pct(int[] sorted, int p) { return sorted[Math.min(sorted.length - 1, Math.max(0, p * sorted.length / 100))]; } // ------------------------------------------------------------ comparison static void compare(Report a, Report b) { // Nothing to compare unless BOTH are photos that actually decoded: a pair of // calibration files (or an undecodable JPEG) has no luma figures, and printing // zeroes would look like a real result. if (a.isCali || b.isCali || a.jpegW == 0 || b.jpegW == 0) { System.out.println("---- comparison skipped: not two decodable photos ----"); if (a.isCali || b.isCali) { System.out.println(" calibration files describe the SENSOR, not a scene;"); System.out.println(" compare them byte-wise or with the block report above."); } return; } System.out.println("---- comparison " + a.name + " vs " + b.name + " ----"); if (a.jpegW != b.jpegW || a.jpegH != b.jpegH) { System.out.printf(" size differs: %dx%d vs %dx%d — different framing/resolution,%n" + " so a pixel-wise diff is meaningless; compare the numbers above instead.%n", a.jpegW, a.jpegH, b.jpegW, b.jpegH); } System.out.printf(" mean luma : %.1f vs %.1f (Δ %+.1f) -> brightness%n", a.meanLuma, b.meanLuma, b.meanLuma - a.meanLuma); System.out.printf(" p25/p75 : %.0f/%.0f vs %.0f/%.0f -> contrast width %+.0f%n", a.p25, a.p75, b.p25, b.p75, (b.p75 - b.p25) - (a.p75 - a.p25)); System.out.printf(" sharpness : %.2f vs %.2f (ratio %.2f) -> detail/softness%n", a.sharpness, b.sharpness, b.sharpness / Math.max(1e-9, a.sharpness)); if (a.rawCounts != null && b.rawCounts != null) { System.out.println(" raw frame : " + stats(a.rawCounts) + "\n " + stats(b.rawCounts)); System.out.println(" ^ if these differ, the two captures saw a different sensor state/FFC,"); System.out.println(" so part of the visual gap is scene/camera, not rendering."); } else { System.out.println(" raw frame : " + (a.rawCounts == null ? a.name + " has none; " : "") + (b.rawCounts == null ? b.name + " has none" : "")); System.out.println(" ^ without the raw frame of BOTH we cannot separate rendering from capture."); } } }