271 lines
9.7 KiB
C
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;
|
|
}
|