261 lines
8.7 KiB
C
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;
|
|
}
|