diff --git a/C/include/ascii.h b/C/include/ascii.h index 84e336e..3f192a4 100644 --- a/C/include/ascii.h +++ b/C/include/ascii.h @@ -7,12 +7,13 @@ #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 */ + int brightness; + int contrast; + int invert; + int color; + int edges; + int dither; + const char *charset; } ascii_opts_t; // Convert YUYV raw data to grayscale diff --git a/C/src/ascii.c b/C/src/ascii.c index cfad4c1..27cafb0 100644 --- a/C/src/ascii.c +++ b/C/src/ascii.c @@ -1,5 +1,6 @@ #include "ascii.h" +#include #include #include #include @@ -50,6 +51,31 @@ size_t ascii_out_size(int dst_w, int dst_h, int color) { } } +// Sobel edge detection (kernel convolution) +static void sobel(const uint8_t *in, uint8_t *out, int w, int h) { + static const int Gx[3][3] = {{-1, 0, 1}, {-2, 0, -2}, {-1, 0, 1}}; + static const int Gy[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}}; + + for (int y = 1; y < h - 1; y++) { + for (int x = 1; x < w - 1; x++) { + int gx = 0, gy = 0; + for (int ky = -1; ky <= 1; ky++) { + for (int kx = -1; kx <= 1; kx++) { + int p = in[(y + ky) * w + (x + kx)]; + gx += Gx[ky + 1][kx + 1] * p; + gy += Gy[ky + 1][kx + 1] * p; + } + } + + // TEST: test both L1 and L2 normalisations + int mag = abs(gx) + abs(gy); // L1 normalisation + // int mag = sqrt(gx * gx + gy * gy); // L2 normalisation + + out[y * w + x] = (uint8_t)(mag > 255 ? 255 : mag); + } + } +} + // 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, @@ -61,6 +87,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, int contrast = opts ? opts->contrast : 100; int invert = opts ? opts->invert : 0; int do_color = opts && opts->color && (rgb != NULL); + int do_edges = opts ? opts->edges : 0; int do_dither = opts ? opts->dither : 0; // Blocking Widht and height pixels in source pixels @@ -123,6 +150,18 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } } + // Sobel edge detection + if (do_edges) { + // Temporary buffer for detected edges results + uint8_t *edge_buf = calloc(dst_w * dst_h, sizeof(uint8_t)); + if (edge_buf) { + sobel(small_g, edge_buf, dst_w, dst_h); + // overwite grayscale image with edge map + memcpy(small_g, edge_buf, dst_w * dst_h); + free(edge_buf); + } + } + // Floyd-Steinberg dithering if (do_dither) { int16_t *eb = malloc(dst_w * dst_h * sizeof(int16_t)); diff --git a/C/src/main.c b/C/src/main.c index 9cc24d1..16bbc7e 100644 --- a/C/src/main.c +++ b/C/src/main.c @@ -1,18 +1,18 @@ #include "ascii.h" #include "capture.h" -#include "timing.h" #include "thread_sharing.h" +#include "timing.h" #include +#include #include +#include #include #include #include #include #include #include -#include -#include // Defaults #define DEFAULT_ASCII_WIDTH 80 @@ -51,6 +51,7 @@ static void print_usage(const char *prog) { " -b brightness offset -128..128 (default: 0)\n" " -c contrast in percent >0; 100=none (default: 100)\n" " -i invert brightness->charset mapping\n" + " -e enable Sobel edge detection\n" " -C colour output (ANSI truecolor)\n" " -D Floyd-Steinberg dithering\n", prog, DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS, @@ -62,7 +63,7 @@ void term_raw_mode(void) { tcgetattr(STDOUT_FILENO, &orig_terminal); struct termios raw = orig_terminal; raw.c_lflag &= ~(ICANON | ECHO); // no line buffering or no echo - raw.c_cc[VMIN] = 0; // non-blocking read + raw.c_cc[VMIN] = 0; // non-blocking read raw.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); } @@ -87,13 +88,14 @@ int main(int argc, char *argv[]) { .contrast = 100, .invert = 0, .color = 0, + .edges = 0, .dither = 0, .charset = NULL, }; // CLI parsing int opt; - while ((opt = getopt(argc, argv, "d:W:H:w:h:f:b:c:iCDs:")) != -1) { + while ((opt = getopt(argc, argv, "ed:W:H:w:h:f:b:c:iCDs:")) != -1) { switch (opt) { case 'd': device = optarg; @@ -137,6 +139,9 @@ int main(int argc, char *argv[]) { case 'C': opts.color = 1; break; + case 'e': + opts.edges = 1; + break; case 'D': opts.dither = 1; break; @@ -159,8 +164,8 @@ int main(int argc, char *argv[]) { } 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" : ""); + opts.color ? " | color" : "", opts.edges ? " | edges" : "", + opts.dither ? " | dither" : "", opts.invert ? " | inverted" : ""); // Allocate pixel buffers int cam_pixels = cam.width * cam.height; diff --git a/README.md b/README.md index 44d94ea..491a0f2 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,27 @@ Ascii video output from webcam in terminal. +## Build and Run +``` +git clone https://github.com/Harshit-Dhanwalkar/AsciiCam.git +cd AsciiCam/C/ +make + +cd build/ +./webcam_ascii --help +``` + + ## TODO - [x] Adjust width and height of capturing frame. +- [x] A producer/consumer thread splitting. - [ ] Custom ASCII charset via config file - [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`. - [x] Dithering effect. -- [x] A producer/consumer thread splitting. +- [x] Sobel edge detection (kernel convolution). Algorithm reference: https://homepages.inf.ed.ac.uk/rbf/HIPR2/sobel.htm - [ ] Migrate from C to Cpp.