From 3677eb2c9470f605cb680c077f7047e012ae1a5b Mon Sep 17 00:00:00 2001 From: Harshit-Dhanwalkar Date: Tue, 26 May 2026 17:47:36 +0530 Subject: [PATCH] Add plugins thresholds change, and panel for values changes --- C/filters/edge_detect.c | 24 +++- C/filters/invert.c | 16 ++- C/filters/threshold.c | 13 ++- C/include/plugins.h | 1 + C/src/main.c | 238 +++++++++++++++++++++++++++++----------- C/src/plugins.c | 11 +- README.md | 5 + 7 files changed, 226 insertions(+), 82 deletions(-) diff --git a/C/filters/edge_detect.c b/C/filters/edge_detect.c index 3179120..5669b77 100644 --- a/C/filters/edge_detect.c +++ b/C/filters/edge_detect.c @@ -5,7 +5,8 @@ static inline int clamp(int v) { return v < 0 ? 0 : v > 255 ? 255 : v; } static void do_edge_boost(uint8_t *gray, int w, int h, void *ctx) { - (void)ctx; + int strength = ctx ? *(int *)ctx : 128; + // Unsharp mask: sharpened = original + (original - blurred) * strength uint8_t *tmp = malloc(w * h); if (!tmp) @@ -22,12 +23,23 @@ static void do_edge_boost(uint8_t *gray, int w, int h, void *ctx) { } } - // Sharpen: original + 0.5 * (original - blur) - for (int i = w + 1; i < w * (h - 1) - 1; i++) - gray[i] = (uint8_t)clamp(gray[i] + (gray[i] - tmp[i]) / 2); + // // Sharpen: original + 0.5 * (original - blur) + // for (int i = w + 1; i < w * (h - 1) - 1; i++) + // gray[i] = (uint8_t)clamp(gray[i] + (gray[i] - tmp[i]) / 2); + + // Sharpen: output = original + strength/255 * (original - blur) + for (int i = w + 1; i < w * (h - 1) - 1; i++) { + int diff = gray[i] - tmp[i]; // high-freq detail + gray[i] = (uint8_t)clamp(gray[i] + diff * strength / 255); + } free(tmp); } -static filter_plugin_t self = {do_edge_boost, "edge_boost"}; -filter_plugin_t *plugin_get(void) { return &self; } +static filter_plugin_t self = { + do_edge_boost, + "edge_boost" +}; +filter_plugin_t *plugin_get(void) { + return &self; +} diff --git a/C/filters/invert.c b/C/filters/invert.c index 3bc4b4e..de3ed23 100644 --- a/C/filters/invert.c +++ b/C/filters/invert.c @@ -2,13 +2,19 @@ #include static void do_invert(uint8_t *gray, int w, int h, void *ctx) { - (void)ctx; + int strength = ctx ? *(int *)ctx : 255; int total_pixels = w * h; for (int i = 0; i < total_pixels; i++) { - gray[i] = 255 - gray[i]; // Flip brightness + int inverted = 255 - gray[i]; + // Linear blend: output = original + strength/255 * (inverted - original) + gray[i] = (uint8_t)(gray[i] + (inverted - gray[i]) * strength / 255); } } -static filter_plugin_t self = {.process = do_invert, .name = "Invert"}; - -filter_plugin_t *plugin_get(void) { return &self; } +static filter_plugin_t self = { + .process = do_invert, + .name = "invert" +}; +filter_plugin_t *plugin_get(void) { + return &self; +} diff --git a/C/filters/threshold.c b/C/filters/threshold.c index 6e7edf4..8caaafb 100644 --- a/C/filters/threshold.c +++ b/C/filters/threshold.c @@ -1,12 +1,13 @@ #include "plugins.h" #include -#define DEFAULT_THRESH 128 +#define DEFAULT_THRESH 35 static void thresh_process(uint8_t *gray, int w, int h, void *ctx) { - uint8_t thresh = (uint8_t)(ctx ? *(int *)ctx : DEFAULT_THRESH); - int total = w * h; - for (int i = 0; i < total; i++) + // uint8_t thresh = (uint8_t)(ctx ? *(int *)ctx : DEFAULT_THRESH); + int thresh = ctx ? *(int *)ctx : DEFAULT_THRESH; // reads &plugin_param from main + int total_pixels = w * h; + for (int i = 0; i < total_pixels; i++) gray[i] = (gray[i] > thresh) ? 255 : 0; } @@ -15,4 +16,6 @@ static filter_plugin_t self = { .name = "threshold", }; -filter_plugin_t *plugin_get(void) { return &self; } +filter_plugin_t *plugin_get(void) { + return &self; +} diff --git a/C/include/plugins.h b/C/include/plugins.h index bae4985..ef1ee3a 100644 --- a/C/include/plugins.h +++ b/C/include/plugins.h @@ -13,6 +13,7 @@ typedef struct { filter_plugin_t *plugin; // resolved plugin vtable char path[256]; // absolute path to .so char tmp_path[280]; // temp copy path used for current dlopen // HACK: + char status_msg[128]; // last load/swap message int inotify_fd; // inotify instance fd int inotify_wd; // watch descriptor } plugin_loader_t; diff --git a/C/src/main.c b/C/src/main.c index d9e48d4..9ab2a2b 100644 --- a/C/src/main.c +++ b/C/src/main.c @@ -1,8 +1,8 @@ #include "ascii.h" #include "capture.h" +#include "plugins.h" #include "thread_sharing.h" #include "timing.h" -#include "plugins.h" #include #include @@ -12,8 +12,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -24,6 +24,7 @@ #define DEFAULT_CAPTURE_WIDTH 160 #define DEFAULT_CAPTURE_HEIGHT 120 #define DEFAULT_FPS 20 +#define MAX_PLUGINS 8 // Signal handling volatile sig_atomic_t keep_running = 1; @@ -66,16 +67,90 @@ static void print_usage(const char *prog) { // termios void term_raw_mode(void) { - tcgetattr(STDIN_FILENO, &orig_terminal); // save stdin state + tcgetattr(STDIN_FILENO, &orig_terminal); // save stdin state 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_lflag &= ~(ICANON | ECHO); // no line buffering or no echo + raw.c_cc[VMIN] = 0; // non-blocking read raw.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); } void term_restore(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_terminal); } +static void overlay_panel(int ascii_h, double fps, + plugin_loader_t *plugins, int *plugin_params, + int count, int selected, int color) { + char buf[1024]; + int n, base_row = ascii_h + 1; // 1-indexed panel row + + // const char *name = pl->plugin ? pl->plugin->name : "none"; + // const char *status = pl->status_msg[0] ? pl->status_msg : "ok"; + // + // if (color) { + // n = snprintf(buf, sizeof(buf), + // "\033[%d;1H\033[38;2;180;180;0m\033[48;2;18;18;18m" + // " plugin: %-14s | %s | param: %3d ([ ] ±1 { } ±10 r=reset) " + // "\033[0m\033[K", + // row, name, status, plugin_param); + // } else { + // n = snprintf(buf, sizeof(buf), + // "\033[%d;1H" + // " plugin: %-14s | %s | param: %3d ([ ] ±1 { } ±10 r=reset)" + // "\033[K", + // row, name, status, plugin_param); + // } + // if (n > 0 && n < (int)sizeof(buf)) + // (void)write(STDOUT_FILENO, buf, (size_t)n); + // FPS + hint bar + if (color) { + n = snprintf(buf, sizeof(buf), + "\033[%d;1H\033[38;2;0;220;0m\033[48;2;18;18;18m" + " FPS: %4.1f │ ↑↓ select [ ] ±1 { } ±10 r reset q quit " + "\033[0m\033[K", base_row, fps); + } else { + n = snprintf(buf, sizeof(buf), + "\033[%d;1H FPS: %4.1f | up/dn select [ ] +-1 { } +-10 r reset q quit\033[K", + base_row, fps); + } + if (n > 0 && n < (int)sizeof(buf)) + (void)write(STDOUT_FILENO, buf, (size_t)n); + + n = snprintf(buf, sizeof(buf), "\033[%d;1H\033[K", base_row + 1); + if (n > 0) (void)write(STDOUT_FILENO, buf, (size_t)n); + + // Plugin cells + if (count == 0) { + const char *msg = color + ? "\033[38;2;120;120;120m no plugins loaded \033[0m" + : " no plugins loaded"; + (void)write(STDOUT_FILENO, msg, strlen(msg)); + return; + } + + for (int i = 0; i < count; i++) { + const char *name = plugins[i].plugin ? plugins[i].plugin->name : "???"; + int param = plugin_params[i]; + int is_sel = (i == selected); + + if (color) { + // Selected: bright yellow text on dark blue bg; others: dim + if (is_sel) + n = snprintf(buf, sizeof(buf), + "\033[38;2;255;220;0m\033[48;2;0;40;80m" + " ▶ %s [%3d] \033[0m ", name, param); + else + n = snprintf(buf, sizeof(buf), + "\033[38;2;140;140;140m\033[48;2;18;18;18m" + " %s [%3d] \033[0m ", name, param); + } else { + n = snprintf(buf, sizeof(buf), + is_sel ? " *%s[%3d] " : " %s[%3d] ", name, param); + } + if (n > 0 && n < (int)sizeof(buf)) + (void)write(STDOUT_FILENO, buf, (size_t)n); + } +} + fps_counter_t fps_calc = {0}; // Main @@ -93,18 +168,17 @@ int main(int argc, char *argv[]) { ascii_opts_t opts = { .brightness = 0, - .contrast = 100, - .invert = 0, - .color = 0, - .edges = 0, - .dither = 0, - .charset = NULL, + .contrast = 100, + .invert = 0, + .color = 0, + .edges = 0, + .dither = 0, + .charset = NULL, }; // Plugins - // plugin_loader_t pl; - // memset(&pl, 0, sizeof(plugin_loader_t)); - const char *plugin_path = NULL; + const char *plugin_paths[MAX_PLUGINS]; + int plugin_path_count = 0; // CLI parsing int opt; @@ -162,7 +236,10 @@ int main(int argc, char *argv[]) { opts.charset = optarg; break; case 'p': - plugin_path = optarg; + if (plugin_path_count < MAX_PLUGINS) + plugin_paths[plugin_path_count++] = optarg; + else + fprintf(stderr, "Warning: max %d plugins, ignoring %s\n", MAX_PLUGINS, optarg); break; default: print_usage(argv[0]); @@ -172,25 +249,25 @@ int main(int argc, char *argv[]) { timing_init(fps); - // Initialize plugin system - plugin_loader_t pl; - memset(&pl, 0, sizeof(pl)); - pl.inotify_fd = -1; - - if (plugin_path) { - plugin_load(&pl, plugin_path); - plugin_watch_init(&pl, plugin_path); - } + // Initialize plugins + plugin_loader_t plugins[MAX_PLUGINS]; + int plugin_params[MAX_PLUGINS]; + int plugin_count = 0; - // void plugin_check_reload(plugin_loader_t *pl) { - // char buf[sizeof(struct inotify_event) + 256]; - // if (read(pl->inotify_fd, buf, sizeof(buf)) > 0) { - // usleep(100000); - // if (plugin_load(pl, pl->path) == 0) { - // fprintf(stderr, "Successfully hot-swapped filter plugin: [%s]\n", pl->plugin->name); - // } - // } - // }; + for (int i = 0; i < plugin_path_count; i++) { + memset(&plugins[i], 0, sizeof(plugin_loader_t)); + plugins[i].inotify_fd = -1; + plugin_params[i] = 128; // default + + if (plugin_load(&plugins[i], plugin_paths[i]) == 0) { + plugin_watch_init(&plugins[i], plugin_paths[i]); + plugin_count++; + } else { + fprintf(stderr, "Failed to load plugin: %s\n", plugin_paths[i]); + } + } + + int selected = 0; // Open webcam webcam_t cam = {.fd = -1, .buffer = MAP_FAILED}; @@ -198,16 +275,15 @@ int main(int argc, char *argv[]) { perror("webcam_init"); return 1; } - fprintf(stderr, "Device: %s | capture %dx%d | ASCII %dx%d | %d fps%s%s%s%s\n", - device, cam.width, cam.height, ascii_w, ascii_h, fps, - opts.color ? " | color" : "", - opts.edges ? " | edges" : "", - opts.dither ? " | dither" : "", - opts.invert ? " | inverted": "", - pl.plugin ? " | plugin" : ""); + fprintf(stderr, "Device: %s | capture %dx%d | ASCII %dx%d | %d fps | %d plugin(s)%s%s%s%s\n", + device, cam.width, cam.height, ascii_w, ascii_h, fps, plugin_count, + opts.color ? " | color" : "", + opts.edges ? " | edges" : "", + opts.dither ? " | dither" : "", + opts.invert ? " | inverted" : ""); - // Allocate pixel buffers - int cam_pixels = cam.width * cam.height; + // Pixel buffers allocation + int cam_pixels = cam.width * cam.height; uint8_t *gray = malloc(cam_pixels); uint8_t *rgb = opts.color ? malloc(cam_pixels * 3) : NULL; @@ -220,7 +296,8 @@ int main(int argc, char *argv[]) { // Allocate output string buffer size_t out_size = ascii_out_size(ascii_w, ascii_h, opts.color); - char *out_buf = malloc(out_size); + char *out_buf = malloc(out_size); + if (!out_buf) { perror("malloc out_buf"); free(gray); @@ -248,39 +325,68 @@ int main(int argc, char *argv[]) { // pthread_join(tid_cap, NULL); // pthread_join(tid_render, NULL); - // Initial full clear + // Initial screen setup (void)write(STDOUT_FILENO, "\033[2J\033[H\033[?25l", 13); term_raw_mode(); - // Main loop - struct timespec frame_start; + struct timespec frame_start, last_frame_time; clock_gettime(CLOCK_MONOTONIC, &frame_start); - struct timespec last_frame_time = frame_start; - char input_char; + last_frame_time = frame_start; + // Main loop while (keep_running) { clock_gettime(CLOCK_MONOTONIC, &frame_start); long frame_diff_ns = - (frame_start.tv_sec - last_frame_time.tv_sec) * 1000000000L + // Seconds - (frame_start.tv_nsec - last_frame_time.tv_nsec); // Nano seconds + (frame_start.tv_sec - last_frame_time.tv_sec) * 1000000000L + // Seconds + (frame_start.tv_nsec - last_frame_time.tv_nsec); // Nano seconds if (frame_diff_ns > 0) fps_push(&fps_calc, frame_diff_ns); - last_frame_time = frame_start; + last_frame_time = frame_start; double current_fps = fps_get(&fps_calc); - // Check for 'q' key non-blocking - if (read(STDIN_FILENO, &input_char, 1) == 1) { - if (input_char == 'q' || input_char == 'Q') { - keep_running = 0; - break; + // Keypress handling + char ch; + while (read(STDIN_FILENO, &ch, 1) == 1) { + if (ch == '\033') { + char seq[2] = {0, 0}; + if (read(STDIN_FILENO, &seq[0], 1) == 1 && seq[0] == '[') { + if (read(STDIN_FILENO, &seq[1], 1) == 1) { + switch (seq[1]) { + case 'A': // up arrow key, previous plugin + if (plugin_count > 0) + selected = (selected - 1 + plugin_count) % plugin_count; + break; + case 'B': // down arrow key, next plugin + if (plugin_count > 0) + selected = (selected + 1) % plugin_count; + break; + } + } + } + continue; + } + + // adjust plugin param if selected + int *p = (plugin_count > 0) ? &plugin_params[selected] : NULL; + switch (ch) { + case 'q': case 'Q': keep_running = 0; break; + case ']' : if (p && *p < 255) (*p)++; break; + case '[' : if (p && *p > 0) (*p)--; break; + case '}' : if (p) *p = (*p + 10 > 255) ? 255 : *p + 10; break; + case '{' : if (p) *p = (*p - 10 < 0) ? 0 : *p - 10; break; + case 'r': case 'R': if (p) *p = 128; break; } } + if (!keep_running) + break; - if (plugin_path) - plugin_check_reload(&pl); + // Hot-reload check for all plugins + for (int i = 0; i < plugin_count; i++) + plugin_check_reload(&plugins[i]); + // Frame capture if (webcam_wait_frame(&cam, 1000) < 0) continue; // timeout, retry @@ -289,8 +395,11 @@ int main(int argc, char *argv[]) { break; } - if (pl.plugin) - pl.plugin->process(gray, cam.width, cam.height, NULL); + // Run all plugins in order + for (int i = 0; i < plugin_count; i++) { + if (plugins[i].plugin) + plugins[i].plugin->process(gray, cam.width, cam.height, &plugin_params[i]); + } if (opts.color && rgb) yuyv_to_rgb((const uint8_t *)cam.buffer, rgb, cam.width, cam.height); @@ -300,7 +409,8 @@ int main(int argc, char *argv[]) { if (len > 0) { (void)write(STDOUT_FILENO, out_buf, (size_t)len); - overlay_fps_box(ascii_w, current_fps, opts.color); + overlay_panel(ascii_h, current_fps, plugins, plugin_params, plugin_count, + selected, opts.color); } if (webcam_requeue_buffer(&cam) < 0) { @@ -313,12 +423,16 @@ int main(int argc, char *argv[]) { // Cleanup term_restore(); - (void)write(STDOUT_FILENO, "\033[0m\033[?25h\n", 11); + // \033[2J = erase screen, \033[H = cursor home, \033[?25h = show cursor + static const char CLEANUP[] = "\033[2J\033[H\033[0m\033[?25h\n"; + (void)write(STDOUT_FILENO, CLEANUP, sizeof(CLEANUP) - 1); fprintf(stderr, "Stopped.\n"); free(gray); free(rgb); free(out_buf); + for (int i = 0; i < plugin_count; i++) + plugin_cleanup(&plugins[i]); webcam_cleanup(&cam); return 0; } diff --git a/C/src/plugins.c b/C/src/plugins.c index ce3374b..ce0d67c 100644 --- a/C/src/plugins.c +++ b/C/src/plugins.c @@ -90,7 +90,8 @@ int plugin_load(plugin_loader_t *pl, const char *path) { } pl->plugin = get_plugin(); - fprintf(stderr, "[plugin] loaded: %s\n", pl->plugin->name); + snprintf(pl->status_msg, sizeof(pl->status_msg), + "loaded: %s", pl->plugin->name); return 0; } @@ -143,12 +144,14 @@ void plugin_check_reload(plugin_loader_t *pl) { if (!relevant) return; - usleep(150000); // 150ms delay for linker + usleep(100000); // 0.1s delay for linker if (plugin_load(pl, pl->path) == 0) { - fprintf(stderr, "\n[plugin] Hot-swapped: %s\n", pl->plugin->name); + snprintf(pl->status_msg, sizeof(pl->status_msg), + "hot-swapped -> %s", pl->plugin->name); } else { - fprintf(stderr, "\n[plugin] Hot-swap failed. Retaining active filter.\n"); + snprintf(pl->status_msg, sizeof(pl->status_msg), + "hot-swap FAILED - old filter retained"); } } diff --git a/README.md b/README.md index de75535..8b19614 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ cd build/ ./webcam_ascii --help ``` +Run with all or selected plugins (currently 3) +``` +./build/webcam_ascii -p build/invert.so -p build/threshold.so -p build/edge_detect.so +``` + ## TODO