Add FPS rendering on top

This commit is contained in:
Harshit-Dhanwalkar 2026-05-24 17:13:03 +05:30
parent 9eaf33ea36
commit 8a79d2fece
6 changed files with 146 additions and 70 deletions

View file

@ -7,13 +7,13 @@
#define ASCII_CHARS_DEFAULT " .:-=+*#%@"
typedef struct {
int brightness;
int contrast;
int invert;
int color;
int edges;
int dither;
const char *charset;
int brightness; /* additive offset: -128..128 */
int contrast; /* percent, 100 = no change */
int invert; /* flip brightness->charset mapping */
int color; /* ANSI truecolor output */
int edges; /* Sobel edge detection */
int dither; /* Floyd-Steinberg dithering */
const char *charset; /* NULL = ASCII_CHARS_DEFAULT */
} ascii_opts_t;
// Convert YUYV raw data to grayscale
@ -28,4 +28,7 @@ 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);
// Overlay FPS box
void overlay_fps_box(int dst_w, double fps, int color_enabled);
#endif

View file

@ -1,17 +1,22 @@
#ifndef THREAD_SHARING_H
#include <pthread.h>
#include <stdint.h>
#include "ascii.h"
typedef struct {
uint8_t *buf[2]; // Duble buffer
int width, height;
int ascii_w, ascii_h;
int ready_idx;
int has_frame;
uint8_t *buf[2]; // Double buffer
int width, height;
int ascii_w, ascii_h;
int ready_idx;
int has_frame;
pthread_mutex_t lock;
pthread_cond_t cond;
volatile int stop;
ascii_opts_t opts;
pthread_cond_t cond;
volatile int stop;
ascii_opts_t opts;
} shared_frame_t;
void *capture_thread(void *arg);
void *render_thread (void *arg);
#endif

View file

@ -3,9 +3,19 @@
#include <time.h>
typedef struct {
long samples[16]; // frame duration (ns) ring buffer
int head;
int count;
} fps_counter_t;
// Initialize framerate control
void timing_init(int fps);
void timing_sleep(struct timespec *start_time);
void fps_push(fps_counter_t *fc, long elapsed_ns);
double fps_get(const fps_counter_t *fc);
#endif

View file

@ -5,6 +5,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Helpers
static inline uint8_t clamp_u8(int v) {
@ -53,8 +54,16 @@ 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}};
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++) {
@ -90,7 +99,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
int do_edges = opts ? opts->edges : 0;
int do_dither = opts ? opts->dither : 0;
// Blocking Widht and height pixels in source pixels
// Blocking width and height pixels in source pixels
double bw = (double)src_w / dst_w;
double bh = (double)src_h / dst_h;
@ -117,7 +126,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
xe = xs + 1;
long tg = 0, tr = 0, tgv = 0, tb = 0;
int cnt = 0;
int count = 0;
for (int sy = ys; sy < ye && sy < src_h; sy++) {
for (int sx = xs; sx < xe && sx < src_w; sx++) {
@ -128,14 +137,14 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
tgv += px[1];
tb += px[2];
}
cnt++;
count++;
}
}
if (cnt == 0)
cnt = 1;
if (count == 0)
count = 1;
// Brightness and contrast
int gv = (int)(tg / cnt);
int gv = (int)(tg / count);
if (contrast != 100)
gv = 128 + (gv - 128) * contrast / 100;
gv += brightness;
@ -143,16 +152,16 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
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));
out_px[0] = clamp_u8((int)(tr / count));
out_px[1] = clamp_u8((int)(tgv / count));
out_px[2] = clamp_u8((int)(tb / count));
}
}
}
// Sobel edge detection
if (do_edges) {
// Temporary buffer for detected edges results
// 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);
@ -208,9 +217,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
// Render into caller's buffer
int out_idx = 0;
// Cursor repositions without erasing
static const char HOME[] = "\033[H";
static const char HOME[] = "\033[H"; // Cursor repositions without erasing
if (out_size > sizeof(HOME)) {
memcpy(out, HOME, sizeof(HOME) - 1);
out_idx = sizeof(HOME) - 1;
@ -243,7 +250,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
if (do_color) {
int w = snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n");
if (w > 0)
if (w > 0 && (size_t)(out_idx + w) < out_size)
out_idx += w;
} else {
if ((size_t)(out_idx + 1) < out_size)
@ -251,12 +258,29 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
}
}
if ((size_t)out_idx < out_size)
out[out_idx] = '\0';
else
out[out_size - 1] = '\0';
out[(size_t)out_idx < out_size ? out_idx : out_size - 1] = '\0';
free(small_g);
free(small_rgb);
return out_idx;
}
// FPS overlay
void overlay_fps_box(int dst_w, double fps, int color_enabled) {
char buf[80];
int col = (dst_w - 13) / 2 + 1;
if (col < 1) col = 1;
int n;
if (color_enabled) {
n = snprintf(buf, sizeof(buf),
"\033[1;%dH\033[38;2;0;255;0m\033[48;2;30;30;30m"
"[ FPS: %4.1f ]\033[0m",
col, fps);
} else {
n = snprintf(buf, sizeof(buf), "\033[1;%dH[ FPS: %4.1f ]", col, fps);
}
if (n > 0 && n < (int)sizeof(buf))
write(STDOUT_FILENO, buf, (size_t)n);
}

View file

@ -15,11 +15,11 @@
#include <unistd.h>
// Defaults
#define DEFAULT_ASCII_WIDTH 80
#define DEFAULT_ASCII_HEIGHT 40
#define DEFAULT_CAPTURE_WIDTH 160
#define DEFAULT_ASCII_WIDTH 80
#define DEFAULT_ASCII_HEIGHT 40
#define DEFAULT_CAPTURE_WIDTH 160
#define DEFAULT_CAPTURE_HEIGHT 120
#define DEFAULT_FPS 20
#define DEFAULT_FPS 20
// Signal handling
volatile sig_atomic_t keep_running = 1;
@ -55,20 +55,22 @@ static void print_usage(const char *prog) {
" -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);
DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT); // FIX: undeclared identifier ASCII_CHARS_DEFAULT
}
// termios
void term_raw_mode(void) {
tcgetattr(STDOUT_FILENO, &orig_terminal);
tcgetattr(STDOUT_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(STDOUT_FILENO, TCSAFLUSH, &orig_terminal); }
void term_restore(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_terminal); }
fps_counter_t fps_calc = {0}; // Global FPS counter // FIX:
// Main
int main(int argc, char *argv[]) {
@ -77,13 +79,13 @@ int main(int argc, char *argv[]) {
// 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 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;
ascii_opts_t opts = {
ascii_opts_t opts = { // FIX: undcleared identifier 'ascii_opts_t'
.brightness = 0,
.contrast = 100,
.invert = 0,
@ -162,10 +164,12 @@ 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\n",
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" : "");
opts.color ? " | color" : "",
opts.edges ? " | edges" : "",
opts.dither ? " | dither" : "",
opts.invert ? " | inverted": "");
// Allocate pixel buffers
int cam_pixels = cam.width * cam.height;
@ -191,16 +195,27 @@ int main(int argc, char *argv[]) {
}
// Initial full clear
write(STDOUT_FILENO, "\033[2J\033[H\033[?25l", 11);
write(STDOUT_FILENO, "\033[2J\033[H\033[?25l", 13);
term_raw_mode();
// Main loop
struct timespec frame_start;
clock_gettime(CLOCK_MONOTONIC, &frame_start);
struct timespec last_frame_time = frame_start;
char input_char;
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
if (frame_diff_ns > 0)
fps_push(&fps_calc, frame_diff_ns);
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') {
@ -222,9 +237,10 @@ int main(int argc, char *argv[]) {
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, "\033[H", 3);
write(STDOUT_FILENO, out_buf, (size_t)len);
overlay_fps_box(ascii_w, current_fps, opts.color);
}
if (webcam_requeue_buffer(&cam) < 0) {
@ -237,7 +253,7 @@ int main(int argc, char *argv[]) {
// Cleanup
term_restore();
write(STDOUT_FILENO, "\033[0m\033[?25h\n", 10);
write(STDOUT_FILENO, "\033[0m\033[?25h\n", 11);
fprintf(stderr, "Stopped.\n");
free(gray);

View file

@ -2,21 +2,39 @@
#include <errno.h>
#include <time.h>
static long frame_duration_ns = 0; // nanoseconds per frame
static long frame_duration_ns = 0; // nanoseconds per frame
void timing_init(int fps) {
frame_duration_ns = 1000000000L / fps;
}
void timing_init(int fps) { frame_duration_ns = 1000000000L / fps; }
void timing_sleep(struct timespec *start_time) {
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
long elapsed_ns = (end_time.tv_sec - start_time->tv_sec) * 1000000000L +
(end_time.tv_nsec - start_time->tv_nsec);
long sleep_ns = frame_duration_ns - elapsed_ns;
if (sleep_ns > 0) {
struct timespec ts = { sleep_ns / 1000000000L, sleep_ns % 1000000000L };
while (nanosleep(&ts, &ts) == -1 && errno == EINTR);
}
long elapsed_ns = (end_time.tv_sec - start_time->tv_sec) * 1000000000L +
(end_time.tv_nsec - start_time->tv_nsec);
long sleep_ns = frame_duration_ns - elapsed_ns;
if (sleep_ns > 0) {
struct timespec ts = {sleep_ns / 1000000000L, sleep_ns % 1000000000L};
while (nanosleep(&ts, &ts) == -1 && errno == EINTR)
;
}
}
void fps_push(fps_counter_t *fc, long ns) {
fc->samples[fc->head % 16] = ns;
fc->head++;
if (fc->count < 16)
fc->count++;
}
double fps_get(const fps_counter_t *fc) {
if (fc->count == 0)
return 0;
long sum = 0;
for (int i = 0; i < fc->count; i++) {
sum += fc->samples[i];
}
return 1e9 * fc->count / sum;
}