tools: 照片差异定位三件套(帧剖面 / NUC 直出 / 条纹量化)+ 忽略 .tmp-* 对比目录

为"我们的照片与官方照片到底差在哪"提供可量化的手段,而不是靠肉眼看图:

- 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-*/ 加入忽略:对比用的用户照片与中间产物放在那里,不入库。
This commit is contained in:
ZXCLI
2026-10-06 00:46:27 +08:00
parent 4a60011ed9
commit 948c9d5d3d
4 changed files with 183 additions and 0 deletions
+119
View File
@@ -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 <file.mdt> [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);
}
}