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

261 lines
8.7 KiB
C

// imgStats: text summary of a single PPM (P6) or PGM (P5).
// Reports dimensions, non-black coverage, luminance mean/histogram,
// and per-row/column ink density. Useful for asking "did anything draw?"
// and "where on the screen?" without ever Read'ing the PNG.
//
// Usage: imgStats img.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 int readToken(FILE *f, char *buf, size_t size);
static void reportStats(const ImageT *img);
static int skipWhitespaceAndComments(FILE *f);
static int loadImage(const char *path, ImageT *out) {
FILE *f = fopen(path, "rb");
if (f == NULL) {
fprintf(stderr, "imgStats: 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, "imgStats: %s: unsupported magic '%s'\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, "imgStats: %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);
return -1;
}
if (fread(pixels, 1, need, f) != need) {
fprintf(stderr, "imgStats: %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 reportStats(const ImageT *img) {
int w = img->width;
int h = img->height;
int ch = img->channels;
size_t total = (size_t)w * (size_t)h;
size_t nonBlack = 0;
uint64_t sumLum = 0;
// 8-bin luminance histogram
size_t lumHist[8] = {0};
// Per-row and per-column ink (non-black) counts
int *rowInk = calloc((size_t)h, sizeof(int));
int *colInk = calloc((size_t)w, sizeof(int));
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
const uint8_t *p = img->pixels + ((size_t)y * w + x) * ch;
int lum;
if (ch == 3) {
lum = (p[0] + p[1] + p[2]) / 3;
} else {
lum = p[0];
}
sumLum += (uint64_t)lum;
lumHist[lum >> 5]++;
if (lum > 16) {
nonBlack++;
rowInk[y]++;
colInk[x]++;
}
}
}
double meanLum = (double)sumLum / (double)total;
double pct = (100.0 * (double)nonBlack) / (double)total;
printf("dims: %dx%d (%zu px, %d ch)\n", w, h, total, ch);
printf("non_black_px: %zu (%.2f%%)\n", nonBlack, pct);
printf("mean_lum: %.2f\n", meanLum);
printf("lum_hist (0-31, 32-63, ...): ");
for (int i = 0; i < 8; i++) {
printf("%zu%s", lumHist[i], i == 7 ? "\n" : " ");
}
// Row density: fold rows into 24 buckets
const int rowBuckets = 24;
int rowB[rowBuckets];
memset(rowB, 0, sizeof(rowB));
for (int y = 0; y < h; y++) {
rowB[(y * rowBuckets) / h] += rowInk[y];
}
int rowPeak = 0;
for (int i = 0; i < rowBuckets; i++) {
if (rowB[i] > rowPeak) {
rowPeak = rowB[i];
}
}
printf("row_ink (top->bottom, 24 buckets, '#'=peak=%d):\n ", rowPeak);
for (int i = 0; i < rowBuckets; i++) {
int v = rowB[i];
char g;
if (v == 0) {
g = '.';
} else if (rowPeak == 0) {
g = '.';
} else if (v == rowPeak) {
g = '#';
} else {
int level = (v * 9 + rowPeak - 1) / rowPeak;
if (level < 1) { level = 1; }
if (level > 9) { level = 9; }
g = (char)('0' + level);
}
putchar(g);
}
putchar('\n');
// Column density: fold cols into 40 buckets
const int colBuckets = 40;
int colB[colBuckets];
memset(colB, 0, sizeof(colB));
for (int x = 0; x < w; x++) {
colB[(x * colBuckets) / w] += colInk[x];
}
int colPeak = 0;
for (int i = 0; i < colBuckets; i++) {
if (colB[i] > colPeak) {
colPeak = colB[i];
}
}
printf("col_ink (left->right, 40 buckets, '#'=peak=%d):\n ", colPeak);
for (int i = 0; i < colBuckets; i++) {
int v = colB[i];
char g;
if (v == 0) {
g = '.';
} else if (colPeak == 0) {
g = '.';
} else if (v == colPeak) {
g = '#';
} else {
int level = (v * 9 + colPeak - 1) / colPeak;
if (level < 1) { level = 1; }
if (level > 9) { level = 9; }
g = (char)('0' + level);
}
putchar(g);
}
putchar('\n');
// Horizon detection: row with biggest jump in cumulative ink
// (heuristic for sky/ground transition)
int bestRow = -1;
long bestJump = 0;
long window = 8;
for (int y = (int)window; y < h - (int)window; y++) {
long up = 0;
long down = 0;
for (int k = 1; k <= (int)window; k++) {
up += rowInk[y - k];
down += rowInk[y + k];
}
long jump = labs(down - up);
if (jump > bestJump) {
bestJump = jump;
bestRow = y;
}
}
if (bestRow >= 0) {
printf("horizon_row_guess: %d (delta_ink=%ld over +-%ld rows)\n",
bestRow, bestJump, window);
}
free(rowInk);
free(colInk);
}
static int skipWhitespaceAndComments(FILE *f) {
int c;
while ((c = fgetc(f)) != EOF) {
if (c == '#') {
while ((c = fgetc(f)) != EOF && c != '\n') {
// discard
}
} else if (!isspace(c)) {
ungetc(c, f);
return 0;
}
}
return -1;
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s img.ppm\n", argv[0]);
return 2;
}
ImageT img = {0};
if (loadImage(argv[1], &img) < 0) {
return 1;
}
reportStats(&img);
free(img.pixels);
return 0;
}