Add soble edge detection

This commit is contained in:
Harshit-Dhanwalkar 2026-05-20 17:54:48 +05:30
parent c1f0298dae
commit 9eaf33ea36
4 changed files with 71 additions and 14 deletions

View file

@ -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

View file

@ -1,5 +1,6 @@
#include "ascii.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@ -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));

View file

@ -1,18 +1,18 @@
#include "ascii.h"
#include "capture.h"
#include "timing.h"
#include "thread_sharing.h"
#include "timing.h"
#include <getopt.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <stdint.h>
// Defaults
#define DEFAULT_ASCII_WIDTH 80
@ -51,6 +51,7 @@ static void print_usage(const char *prog) {
" -b <val> brightness offset -128..128 (default: 0)\n"
" -c <val> 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;

View file

@ -3,15 +3,27 @@ Ascii video output from webcam in terminal.
<img src="assets/demo.gif" width="325">
## 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.