fs2port/port/tools/imgDiff.c
2026-05-13 21:32:05 -05:00

271 lines
9.7 KiB
C

// imgDiff: textual diff between two PPM (P6) or PGM (P5) images.
// Same dimensions required. Outputs counts, mean/max abs error per channel,
// bbox of differing pixels, and a 16x12 ASCII density heatmap.
//
// Usage: imgDiff a.ppm b.ppm [--ascii]
//
// PNGs are not supported directly; convert via ImageMagick:
// convert in.png ppm:out.ppm
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int width;
int height;
int channels;
uint8_t *pixels;
} ImageT;
static int loadImage(const char *path, ImageT *out);
static void freeImage(ImageT *img);
static int skipWhitespaceAndComments(FILE *f);
static int readToken(FILE *f, char *buf, size_t size);
static void reportDiff(const ImageT *a, const ImageT *b, bool ascii);
static void freeImage(ImageT *img) {
free(img->pixels);
img->pixels = NULL;
}
static int loadImage(const char *path, ImageT *out) {
FILE *f = fopen(path, "rb");
if (f == NULL) {
fprintf(stderr, "imgDiff: cannot open %s\n", path);
return -1;
}
char magic[8];
if (readToken(f, magic, sizeof(magic)) < 0) {
fclose(f);
return -1;
}
int channels = 0;
if (strcmp(magic, "P6") == 0) {
channels = 3;
} else if (strcmp(magic, "P5") == 0) {
channels = 1;
} else {
fprintf(stderr, "imgDiff: %s: unsupported magic '%s' (want P5/P6)\n", path, magic);
fclose(f);
return -1;
}
char tok[32];
if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; }
int width = atoi(tok);
if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; }
int height = atoi(tok);
if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; }
int maxval = atoi(tok);
if (maxval != 255) {
fprintf(stderr, "imgDiff: %s: maxval %d not supported\n", path, maxval);
fclose(f);
return -1;
}
// readToken already consumed the single whitespace separator
// following maxval, so binary pixel data starts at the cursor.
size_t need = (size_t)width * (size_t)height * (size_t)channels;
uint8_t *pixels = malloc(need);
if (pixels == NULL) {
fclose(f);
fprintf(stderr, "imgDiff: out of memory\n");
return -1;
}
if (fread(pixels, 1, need, f) != need) {
fprintf(stderr, "imgDiff: %s: short read\n", path);
free(pixels);
fclose(f);
return -1;
}
fclose(f);
out->width = width;
out->height = height;
out->channels = channels;
out->pixels = pixels;
return 0;
}
static int readToken(FILE *f, char *buf, size_t size) {
if (skipWhitespaceAndComments(f) < 0) {
return -1;
}
size_t i = 0;
int c;
while ((c = fgetc(f)) != EOF && !isspace(c)) {
if (i + 1 >= size) {
return -1;
}
buf[i++] = (char)c;
}
buf[i] = '\0';
return (int)i;
}
static void reportDiff(const ImageT *a, const ImageT *b, bool ascii) {
int w = a->width;
int h = a->height;
int ch = a->channels;
size_t total = (size_t)w * (size_t)h;
size_t differ = 0;
uint64_t sumAbs = 0;
int maxAbs = 0;
int minX = w;
int minY = h;
int maxX = -1;
int maxY = -1;
uint64_t sumLumA = 0;
uint64_t sumLumB = 0;
const int gridX = 16;
const int gridY = 12;
int *grid = calloc((size_t)gridX * (size_t)gridY, sizeof(int));
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
const uint8_t *pa = a->pixels + ((size_t)y * w + x) * ch;
const uint8_t *pb = b->pixels + ((size_t)y * w + x) * ch;
int d = 0;
for (int c = 0; c < ch; c++) {
int da = abs((int)pa[c] - (int)pb[c]);
if (da > d) {
d = da;
}
sumAbs += (uint64_t)da;
}
if (ch == 3) {
sumLumA += (uint64_t)((pa[0] + pa[1] + pa[2]) / 3);
sumLumB += (uint64_t)((pb[0] + pb[1] + pb[2]) / 3);
} else {
sumLumA += pa[0];
sumLumB += pb[0];
}
if (d != 0) {
differ++;
if (d > maxAbs) {
maxAbs = d;
}
if (x < minX) { minX = x; }
if (y < minY) { minY = y; }
if (x > maxX) { maxX = x; }
if (y > maxY) { maxY = y; }
int gx = (x * gridX) / w;
int gy = (y * gridY) / h;
grid[gy * gridX + gx]++;
}
}
}
double pct = total > 0 ? (100.0 * (double)differ / (double)total) : 0.0;
double mae = (double)sumAbs / ((double)total * (double)ch);
double lumA = (double)sumLumA / (double)total;
double lumB = (double)sumLumB / (double)total;
printf("dims: %dx%d (%zu px, %d ch)\n", w, h, total, ch);
printf("identical: %s\n", differ == 0 ? "true" : "false");
printf("pixels_differ: %zu (%.2f%%)\n", differ, pct);
printf("mean_abs_err: %.2f\n", mae);
printf("max_abs_err: %d\n", maxAbs);
if (differ > 0) {
printf("bbox_diff: (%d,%d)-(%d,%d) size=%dx%d\n",
minX, minY, maxX, maxY,
maxX - minX + 1, maxY - minY + 1);
} else {
printf("bbox_diff: <none>\n");
}
printf("luminance_mean: a=%.1f b=%.1f delta=%+.1f\n", lumA, lumB, lumB - lumA);
if (ascii && differ > 0) {
int peak = 0;
for (int i = 0; i < gridX * gridY; i++) {
if (grid[i] > peak) {
peak = grid[i];
}
}
printf("heatmap (%dx%d, '.'=0 '#'=peak=%d):\n", gridX, gridY, peak);
for (int gy = 0; gy < gridY; gy++) {
printf(" ");
for (int gx = 0; gx < gridX; gx++) {
int v = grid[gy * gridX + gx];
char glyph;
if (v == 0) {
glyph = '.';
} else {
int level = (v * 9 + peak - 1) / peak;
if (level < 1) { level = 1; }
if (level > 9) { level = 9; }
glyph = (char)('0' + level);
if (v == peak) {
glyph = '#';
}
}
putchar(glyph);
}
putchar('\n');
}
}
free(grid);
}
static int skipWhitespaceAndComments(FILE *f) {
int c;
while ((c = fgetc(f)) != EOF) {
if (c == '#') {
while ((c = fgetc(f)) != EOF && c != '\n') {
// discard comment
}
} else if (!isspace(c)) {
ungetc(c, f);
return 0;
}
}
return -1;
}
int main(int argc, char **argv) {
bool ascii = false;
const char *aPath = NULL;
const char *bPath = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--ascii") == 0) {
ascii = true;
} else if (aPath == NULL) {
aPath = argv[i];
} else if (bPath == NULL) {
bPath = argv[i];
} else {
fprintf(stderr, "imgDiff: unexpected arg '%s'\n", argv[i]);
return 2;
}
}
if (aPath == NULL || bPath == NULL) {
fprintf(stderr, "usage: %s a.ppm b.ppm [--ascii]\n", argv[0]);
return 2;
}
ImageT a = {0};
ImageT b = {0};
if (loadImage(aPath, &a) < 0) {
return 1;
}
if (loadImage(bPath, &b) < 0) {
freeImage(&a);
return 1;
}
if (a.width != b.width || a.height != b.height || a.channels != b.channels) {
fprintf(stderr, "imgDiff: dimensions differ: %dx%dx%d vs %dx%dx%d\n",
a.width, a.height, a.channels,
b.width, b.height, b.channels);
freeImage(&a);
freeImage(&b);
return 1;
}
reportDiff(&a, &b, ascii);
freeImage(&a);
freeImage(&b);
return 0;
}