// 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 #include #include #include #include #include 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; }