From 1c9834483a029462027191701ac183baf9186e79 Mon Sep 17 00:00:00 2001 From: Harshit-Dhanwalkar Date: Tue, 19 May 2026 17:28:19 +0530 Subject: [PATCH] Add features: Brightness control, inverse colors, Dithering effect, and ANSII colors support. --- .gitignore | 3 + C/Makefile | 9 +- C/include/ascii.h | 21 +++- C/src/ascii.c | 260 +++++++++++++++++++++++++++++++++++++-------- C/src/main.c | 264 ++++++++++++++++++++++++++++++++-------------- README.md | 9 +- 6 files changed, 432 insertions(+), 134 deletions(-) diff --git a/.gitignore b/.gitignore index 845cda6..641ad9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Build dir +build/ + # Prerequisites *.d diff --git a/C/Makefile b/C/Makefile index 291c17b..4c85aa3 100644 --- a/C/Makefile +++ b/C/Makefile @@ -1,15 +1,16 @@ CC = gcc CFLAGS = -Wall -Wextra -O2 -Iinclude LDFLAGS = -lm +# LDFLAGS += -lpthread SRCDIR = src INCDIR = include -OBJDIR = obj -BINDIR = . +BUILDDIR = build +OBJDIR = $(BUILDDIR)/obj SOURCES = $(wildcard $(SRCDIR)/*.c) OBJECTS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES)) -TARGET = $(BINDIR)/webcam_ascii +TARGET = $(BUILDDIR)/webcam_ascii .PHONY: all clean @@ -25,4 +26,4 @@ $(OBJDIR): mkdir -p $(OBJDIR) clean: - rm -rf $(OBJDIR) $(TARGET) + rm -rf $(BUILDDIR) diff --git a/C/include/ascii.h b/C/include/ascii.h index b51edc8..84e336e 100644 --- a/C/include/ascii.h +++ b/C/include/ascii.h @@ -1,15 +1,30 @@ #ifndef ASCII_H #define ASCII_H +#include #include -#define ASCII_CHARS " .:-=+*#%@" +#define ASCII_CHARS_DEFAULT " .:-=+*#%@" + +typedef struct { + int brightness; /* additive offset applied to gray: -128..128 */ + int contrast; /* multiplier in percent; 100 = no change */ + int invert; /* non-zero: flip brightness -> charset mapping */ + int color; /* non-zero: emit ANSI truecolor escape codes */ + int dither; /* non-zero: apply Floyd-Steinberg dithering */ + const char *charset; /* custom charset string; NULL -> ASCII_CHARS_DEFAULT */ +} ascii_opts_t; // Convert YUYV raw data to grayscale void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height); +void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height); + +// Output buffer sizing +size_t ascii_out_size(int dst_w, int dst_h, int color); // grayscale to ASCII output grid -char *grayscale_to_ascii(const uint8_t *gray, int src_w, int src_h, - int dst_w, int dst_h); +int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, + int src_h, int dst_w, int dst_h, char *out, + size_t out_size, const ascii_opts_t *opts); #endif diff --git a/C/src/ascii.c b/C/src/ascii.c index 6f0394b..cfad4c1 100644 --- a/C/src/ascii.c +++ b/C/src/ascii.c @@ -1,49 +1,223 @@ #include "ascii.h" + #include +#include #include #include -#define CHECK(x, msg) if ((x) < 0) { perror(msg); goto cleanup; } - -typedef struct { - uint8_t *raw_frame; - uint8_t *gray; - char *ascii; -} frame_t; - -char *grayscale_to_ascii(const uint8_t *gray, int src_w, int src_h, - int dst_w, int dst_h) { - // Allocate output string: each row has dst_w chars + newline, plus null terminator - char *output = malloc(dst_w * dst_h + dst_h + 1); - if (!output) return NULL; - - double block_w = (double)src_w / dst_w; - double block_h = (double)src_h / dst_h; - const char *ascii = ASCII_CHARS; - int ascii_len = strlen(ascii); - - int out_idx = 0; - for (int y = 0; y < dst_h; y++) { - for (int x = 0; x < dst_w; x++) { - long total = 0; - int count = 0; - int y_start = (int)(y * block_h); - int y_end = (int)((y + 1) * block_h); - int x_start = (int)(x * block_w); - int x_end = (int)((x + 1) * block_w); - - for (int sy = y_start; sy < y_end && sy < src_h; sy++) { - for (int sx = x_start; sx < x_end && sx < src_w; sx++) { - total += gray[sy * src_w + sx]; - count++; - } - } - unsigned char avg = (count > 0) ? (total / count) : 0; - int idx = avg * (ascii_len - 1) / 255; - output[out_idx++] = ascii[idx]; - } - output[out_idx++] = '\n'; - } - output[out_idx] = '\0'; - return output; +// Helpers +static inline uint8_t clamp_u8(int v) { + return (v < 0) ? 0 : (v > 255) ? 255 : (uint8_t)v; +} + +// Image conversion +void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height) { + int n = width * height; + for (int i = 0; i < n; i++) + gray[i] = yuyv[i * 2]; // Y sample at even bytes in YUYV format +} + +void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) { + int pairs = (width * height) / 2; + for (int i = 0; i < pairs; i++) { + int y0 = yuyv[i * 4 + 0]; + int u = yuyv[i * 4 + 1]; + int y1 = yuyv[i * 4 + 2]; + int v = yuyv[i * 4 + 3]; + int d = u - 128; + int e = v - 128; + + for (int p = 0; p < 2; p++) { + int c = ((p == 0) ? y0 : y1) - 16; + uint8_t *px = rgb + (i * 2 + p) * 3; + px[0] = clamp_u8((298 * c + 409 * e + 128) >> 8); // R + px[1] = clamp_u8((298 * c - 100 * d - 208 * e + 128) >> 8); // G + px[2] = clamp_u8((298 * c + 516 * d + 128) >> 8); // B + } + } +} + +// Buffer sizing +size_t ascii_out_size(int dst_w, int dst_h, int color) { + /* prefix: 3 bytes + 1 Char */ + if (color) { + /* Per cell: "\033[38;2;255;255;255m" (20) + char (1) = 21 + * Per row end: "\033[0m\n" (6) */ + return 3 + (size_t)dst_h * ((size_t)dst_w * 21 + 6) + 1; + } else { + /* Per cell: 1 byte; per row: + newline */ + return 3 + (size_t)dst_h * ((size_t)dst_w + 1) + 1; + } +} + +// Grayscale to ascii +int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, + int src_h, int dst_w, int dst_h, char *out, + size_t out_size, const ascii_opts_t *opts) { + const char *charset = + (opts && opts->charset) ? opts->charset : ASCII_CHARS_DEFAULT; + int nchars = (int)strlen(charset); + int brightness = opts ? opts->brightness : 0; + int contrast = opts ? opts->contrast : 100; + int invert = opts ? opts->invert : 0; + int do_color = opts && opts->color && (rgb != NULL); + int do_dither = opts ? opts->dither : 0; + + // Blocking Widht and height pixels in source pixels + double bw = (double)src_w / dst_w; + double bh = (double)src_h / dst_h; + + // Downsample to (dst_w x dst_h) + uint8_t *small_g = malloc(dst_w * dst_h); + uint8_t *small_rgb = do_color ? malloc(dst_w * dst_h * 3) : NULL; + + if (!small_g || (do_color && !small_rgb)) { + free(small_g); + free(small_rgb); + return -1; + } + + for (int y = 0; y < dst_h; y++) { + int ys = (int)(y * bh); + int ye = (int)((y + 1) * bh); + if (ye <= ys) + ye = ys + 1; + + for (int x = 0; x < dst_w; x++) { + int xs = (int)(x * bw); + int xe = (int)((x + 1) * bw); + if (xe <= xs) + xe = xs + 1; + + long tg = 0, tr = 0, tgv = 0, tb = 0; + int cnt = 0; + + for (int sy = ys; sy < ye && sy < src_h; sy++) { + for (int sx = xs; sx < xe && sx < src_w; sx++) { + tg += gray[sy * src_w + sx]; + if (do_color) { + const uint8_t *px = rgb + (sy * src_w + sx) * 3; + tr += px[0]; + tgv += px[1]; + tb += px[2]; + } + cnt++; + } + } + if (cnt == 0) + cnt = 1; + + // Brightness and contrast + int gv = (int)(tg / cnt); + if (contrast != 100) + gv = 128 + (gv - 128) * contrast / 100; + gv += brightness; + small_g[y * dst_w + x] = clamp_u8(gv); + + if (do_color) { + uint8_t *out_px = small_rgb + (y * dst_w + x) * 3; + out_px[0] = clamp_u8((int)(tr / cnt)); + out_px[1] = clamp_u8((int)(tgv / cnt)); + out_px[2] = clamp_u8((int)(tb / cnt)); + } + } + } + + // Floyd-Steinberg dithering + if (do_dither) { + int16_t *eb = malloc(dst_w * dst_h * sizeof(int16_t)); + if (eb) { + for (int i = 0; i < dst_w * dst_h; i++) + eb[i] = (int16_t)small_g[i]; + + for (int y = 0; y < dst_h; y++) { + for (int x = 0; x < dst_w; x++) { + int old_v = eb[y * dst_w + x]; + + // Quantise to nearest charset level + int qi = old_v * (nchars - 1) / 255; + if (qi < 0) + qi = 0; + if (qi >= nchars) + qi = nchars - 1; + int new_v = qi * 255 / (nchars - 1); + + eb[y * dst_w + x] = (int16_t)new_v; + int err = old_v - new_v; + +#define FS_ADD(DY, DX, NUM) \ + do { \ + int ny = y + (DY), nx = x + (DX); \ + if (nx >= 0 && nx < dst_w && ny >= 0 && ny < dst_h) \ + eb[ny * dst_w + nx] += (int16_t)(err * (NUM) / 16); \ + } while (0) + FS_ADD(0, 1, 7); + FS_ADD(1, -1, 3); + FS_ADD(1, 0, 5); + FS_ADD(1, 1, 1); +#undef FS_ADD + } + } + + for (int i = 0; i < dst_w * dst_h; i++) + small_g[i] = clamp_u8(eb[i]); + + free(eb); + } + /* HACK: If malloc fails silently fall back to no dithering */ + } + + // Render into caller's buffer + int out_idx = 0; + + // Cursor repositions without erasing + static const char HOME[] = "\033[H"; + if (out_size > sizeof(HOME)) { + memcpy(out, HOME, sizeof(HOME) - 1); + out_idx = sizeof(HOME) - 1; + } + + for (int y = 0; y < dst_h; y++) { + for (int x = 0; x < dst_w; x++) { + int gv = small_g[y * dst_w + x]; + int idx = gv * (nchars - 1) / 255; + if (idx < 0) + idx = 0; + if (idx >= nchars) + idx = nchars - 1; + if (invert) + idx = nchars - 1 - idx; + + char ch = charset[idx]; + + if (do_color) { + const uint8_t *px = small_rgb + (y * dst_w + x) * 3; + int w = snprintf(out + out_idx, out_size - (size_t)out_idx, + "\033[38;2;%d;%d;%dm%c", px[0], px[1], px[2], ch); + if (w > 0 && (size_t)(out_idx + w) < out_size) + out_idx += w; + } else { + if ((size_t)(out_idx + 1) < out_size) + out[out_idx++] = ch; + } + } + + if (do_color) { + int w = snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n"); + if (w > 0) + out_idx += w; + } else { + if ((size_t)(out_idx + 1) < out_size) + out[out_idx++] = '\n'; + } + } + + if ((size_t)out_idx < out_size) + out[out_idx] = '\0'; + else + out[out_size - 1] = '\0'; + + free(small_g); + free(small_rgb); + return out_idx; } diff --git a/C/src/main.c b/C/src/main.c index 8f68c79..9d8f8c4 100644 --- a/C/src/main.c +++ b/C/src/main.c @@ -1,6 +1,7 @@ -#include "capture.h" #include "ascii.h" +#include "capture.h" #include "timing.h" + #include #include #include @@ -10,101 +11,204 @@ #include // Defaults -#define DEFAULT_ASCII_WIDTH 80 +#define DEFAULT_ASCII_WIDTH 80 #define DEFAULT_ASCII_HEIGHT 40 -#define DEFAULT_CAPTURE_WIDTH 160 +#define DEFAULT_CAPTURE_WIDTH 160 #define DEFAULT_CAPTURE_HEIGHT 120 #define DEFAULT_FPS 20 +// Signal handling volatile sig_atomic_t keep_running = 1; -void handle_signal(int sig) { (void)sig; keep_running = 0; } - -void print_usage(char *prog) { - fprintf(stderr, - "Usage: %s [-d ] [-W ] [-H ] [-f ]\n", - prog); - fprintf(stderr, " -d : video device (default: /dev/video0)\n"); - fprintf(stderr, " -W : ASCII output width (default: %d)\n", DEFAULT_ASCII_WIDTH); - fprintf(stderr, " -H : ASCII output height (default: %d)\n", DEFAULT_ASCII_HEIGHT); - fprintf(stderr, " -f : target framerate (default: %d)\n", DEFAULT_FPS); +void handle_signal(int sig) { + (void)sig; + keep_running = 0; } +// Usage +static void print_usage(const char *prog) { + fprintf( + stderr, + "Usage: %s [options]\n" + "\n" + "Capture options:\n" + " -d video device (default: /dev/video0)\n" + " -w capture width (default: %d)\n" + " -h capture height (default: %d)\n" + " -f target framerate (default: %d)\n" + "\n" + "Output options:\n" + " -W ASCII output columns (default: %d)\n" + " -H ASCII output rows (default: %d)\n" + " -s custom charset string (default: \"%s\")\n" + "\n" + "Image adjustments:\n" + " -b brightness offset -128..128 (default: 0)\n" + " -c contrast in percent >0; 100=none (default: 100)\n" + " -i invert brightness->charset mapping\n" + " -C colour output (ANSI truecolor)\n" + " -D Floyd-Steinberg dithering\n", + prog, DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS, + DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT); +} + +// Main int main(int argc, char *argv[]) { - signal(SIGINT, handle_signal); + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); - // Config - char *device = "/dev/video0"; - int ascii_w = DEFAULT_ASCII_WIDTH; - int ascii_h = DEFAULT_ASCII_HEIGHT; - int fps = DEFAULT_FPS; + // Config + char *device = "/dev/video0"; + int ascii_w = DEFAULT_ASCII_WIDTH; + int ascii_h = DEFAULT_ASCII_HEIGHT; + int cap_w = DEFAULT_CAPTURE_WIDTH; + int cap_h = DEFAULT_CAPTURE_HEIGHT; + int fps = DEFAULT_FPS; - int opt; - while ((opt = getopt(argc, argv, "d:W:H:f:")) != -1) { - switch (opt) { - case 'd': device = optarg; break; - case 'W': ascii_w = atoi(optarg); if (ascii_w <= 0) ascii_w = DEFAULT_ASCII_WIDTH; break; - case 'H': ascii_h = atoi(optarg); if (ascii_h <= 0) ascii_h = DEFAULT_ASCII_HEIGHT; break; - case 'f': fps = atoi(optarg); if (fps <= 0) fps = DEFAULT_FPS; break; - default: print_usage(argv[0]); return 1; - } + ascii_opts_t opts = { + .brightness = 0, + .contrast = 100, + .invert = 0, + .color = 0, + .dither = 0, + .charset = NULL, + }; + + // CLI parsing + int opt; + while ((opt = getopt(argc, argv, "d:W:H:w:h:f:b:c:iCDs:")) != -1) { + switch (opt) { + case 'd': + device = optarg; + break; + case 'W': + ascii_w = atoi(optarg); + if (ascii_w <= 0) + ascii_w = DEFAULT_ASCII_WIDTH; + break; + case 'H': + ascii_h = atoi(optarg); + if (ascii_h <= 0) + ascii_h = DEFAULT_ASCII_HEIGHT; + break; + case 'w': + cap_w = atoi(optarg); + if (cap_w <= 0) + cap_w = DEFAULT_CAPTURE_WIDTH; + break; + case 'h': + cap_h = atoi(optarg); + if (cap_h <= 0) + cap_h = DEFAULT_CAPTURE_HEIGHT; + break; + case 'f': + fps = atoi(optarg); + if (fps <= 0) + fps = DEFAULT_FPS; + break; + case 'b': + opts.brightness = atoi(optarg); + break; + case 'c': + opts.contrast = atoi(optarg); + if (opts.contrast <= 0) + opts.contrast = 100; + break; + case 'i': + opts.invert = 1; + break; + case 'C': + opts.color = 1; + break; + case 'D': + opts.dither = 1; + break; + case 's': + opts.charset = optarg; + break; + default: + print_usage(argv[0]); + return 1; } + } - // Capture resolution (fixed for simplicity, could also be made configurable) - int cap_w = DEFAULT_CAPTURE_WIDTH; - int cap_h = DEFAULT_CAPTURE_HEIGHT; + timing_init(fps); - timing_init(fps); + // Open webcam + webcam_t cam = {.fd = -1, .buffer = MAP_FAILED}; + if (webcam_init(&cam, device, cap_w, cap_h) < 0) { + perror("webcam_init"); + return 1; + } + fprintf(stderr, "Device: %s | capture %dx%d | ASCII %dx%d | %d fps%s%s%s\n", + device, cam.width, cam.height, ascii_w, ascii_h, fps, + opts.color ? " | color" : "", opts.dither ? " | dither" : "", + opts.invert ? " | inverted" : ""); - webcam_t cam = { .fd = -1, .buffer = MAP_FAILED }; - if (webcam_init(&cam, device, cap_w, cap_h) < 0) { - perror("webcam_init"); - return 1; - } - printf("Webcam opened: %s, capture resolution %dx%d\n", device, cam.width, cam.height); + // Allocate pixel buffers + int cam_pixels = cam.width * cam.height; - uint8_t *gray = malloc(cam.width * cam.height); - if (!gray) { - perror("malloc gray"); - webcam_cleanup(&cam); - return 1; - } + uint8_t *gray = malloc(cam_pixels); + uint8_t *rgb = opts.color ? malloc(cam_pixels * 3) : NULL; - printf("Starting ASCII stream (%dx%d), target %d fps. Press Ctrl+C to stop.\n", - ascii_w, ascii_h, fps); - - struct timespec start_time; - while (keep_running) { - clock_gettime(CLOCK_MONOTONIC, &start_time); - - if (webcam_wait_frame(&cam, 1000) < 0) { - // timeout or error, just continue - continue; - } - - if (webcam_capture_frame(&cam, gray) < 0) { - perror("capture_frame"); - break; - } - - char *ascii_art = grayscale_to_ascii(gray, cam.width, cam.height, - ascii_w, ascii_h); - if (ascii_art) { - printf("\033[2J\033[H"); // clear screen, home cursor - fputs(ascii_art, stdout); - fflush(stdout); - free(ascii_art); - } - - if (webcam_requeue_buffer(&cam) < 0) { - perror("requeue_buffer"); - break; - } - - timing_sleep(&start_time); - } - - printf("\nStopping...\n"); + if (!gray || (opts.color && !rgb)) { + perror("malloc pixel buffers"); free(gray); webcam_cleanup(&cam); - return 0; + return 1; + } + + // Allocate output string buffer + size_t out_size = ascii_out_size(ascii_w, ascii_h, opts.color); + char *out_buf = malloc(out_size); + if (!out_buf) { + perror("malloc out_buf"); + free(gray); + free(rgb); + webcam_cleanup(&cam); + return 1; + } + + // Initial full clear + write(STDOUT_FILENO, "\033[2J\033[H", 7); + + // Main loop + struct timespec frame_start; + + while (keep_running) { + clock_gettime(CLOCK_MONOTONIC, &frame_start); + + if (webcam_wait_frame(&cam, 1000) < 0) + continue; // timeout, retry + + if (webcam_capture_frame(&cam, gray) < 0) { + perror("capture_frame"); + break; + } + + if (opts.color && rgb) + yuyv_to_rgb((const uint8_t *)cam.buffer, rgb, cam.width, cam.height); + + int len = grayscale_to_ascii(gray, rgb, cam.width, cam.height, ascii_w, + ascii_h, out_buf, out_size, &opts); + if (len > 0) { + write(STDOUT_FILENO, out_buf, (size_t)len); + } + + if (webcam_requeue_buffer(&cam) < 0) { + perror("requeue_buffer"); + break; + } + + timing_sleep(&frame_start); + } + + // Cleanup + write(STDOUT_FILENO, "\033[0m\033[?25h\n", 10); + fprintf(stderr, "Stopped.\n"); + + free(gray); + free(rgb); + free(out_buf); + webcam_cleanup(&cam); + return 0; } diff --git a/README.md b/README.md index 4687553..86d0be5 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ Ascii video output from your webcam in your terminal. ## TODO - [x] Adjust width and height of capturing frame. -- [ ] Brightness/contrast adjustment. - [ ] Custom ASCII charset via config file -- [ ] Reverse video - Invert brightness $\rightarrow$ charset mapping -- [ ] Color output - Extract U/V channels, map to ANSI/RGB codes +- [x] Brightness/contrast adjustment. +- [x] Reverse video - Invert brightness $\rightarrow$ charset mapping +- [x] Color output - Extract U/V channels, map to ANSI/RGB codes - [ ] Add feature to record and save it in popular video formats like `.mp4`, `.mov` and `.gif`. -- [ ] Dithering +- [x] Dithering effect. +- [ ] A producer/consumer split with pthread_mutex + pthread_cond and a double-buffer swap would decouple them: one thread talks exclusively to V4L2, the other does ASCII conversion and writes. - [ ] Migrate from C to Cpp.