mirror of
https://github.com/Harshit-Dhanwalkar/AsciiCam.git
synced 2026-06-09 10:25:12 +02:00
Add FPS rendering on top
This commit is contained in:
parent
9eaf33ea36
commit
8a79d2fece
6 changed files with 146 additions and 70 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
58
C/src/main.c
58
C/src/main.c
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue