From: Antonio Ospite Date: Fri, 24 Aug 2018 10:50:54 +0000 (+0200) Subject: Initial import X-Git-Url: https://git.ao2.it/experiments/cyclabile.git/commitdiff_plain/eca851302e73a9b4a4d3609c14ade3efe90c8441?ds=sidebyside Initial import --- eca851302e73a9b4a4d3609c14ade3efe90c8441 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7fc10e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.o +cyclabile +cyclabile.service diff --git a/90-projector.rules b/90-projector.rules new file mode 100644 index 0000000..2176b13 --- /dev/null +++ b/90-projector.rules @@ -0,0 +1,3 @@ +# Add a /dev/projector alias for the Acer C110 in display mode. +# This makes it easier to execute systemd units when the device shows up. +ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="1de1", ATTRS{idProduct}=="c101", TAG+="systemd", ENV{SYSTEMD_ALIAS}="/dev/projector" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6118bcc --- /dev/null +++ b/Makefile @@ -0,0 +1,87 @@ +CFLAGS = -std=c99 -pedantic -pedantic-errors -Wall -g3 -O2 -D_ANSI_SOURCE_ +CFLAGS += -fno-common \ + -Wall \ + -Wdeclaration-after-statement \ + -Wextra \ + -Wformat=2 \ + -Winit-self \ + -Winline \ + -Wpacked \ + -Wp,-D_FORTIFY_SOURCE=2 \ + -Wpointer-arith \ + -Wlarger-than-65500 \ + -Wmissing-declarations \ + -Wmissing-format-attribute \ + -Wmissing-noreturn \ + -Wmissing-prototypes \ + -Wnested-externs \ + -Wold-style-definition \ + -Wredundant-decls \ + -Wsign-compare \ + -Wstrict-aliasing=2 \ + -Wstrict-prototypes \ + -Wundef \ + -Wunreachable-code \ + -Wunsafe-loop-optimizations \ + -Wunused-but-set-variable \ + -Wwrite-strings + +LDLIBS = -llept -lturbojpeg -lam7xxx + +# For clock_nanosleep() +CFLAGS += -D_POSIX_C_SOURCE=200112L + +# for clock_gettime() +LDLIBS += -lrt + +# Some compiler optimizations +CFLAGS += -O3 \ + -fno-strict-aliasing \ + -ftree-vectorize \ + -ffast-math \ + -funroll-loops \ + -funsafe-math-optimizations \ + -fsingle-precision-constant + +# NEON optimizations +ifeq ($(NEON), 1) + CFLAGS += -march=armv7-a \ + -mtune=cortex-a8 \ + -mfpu=neon \ + -mfloat-abi=hard \ + -DUSE_NEON +endif + +# Use the BBB eQEP unit +ifeq ($(EQEP), 1) + CFLAGS += -DUSE_EQEP +endif + + +cyclabile: cyclabile.o projective_split.o + +clean: + rm -rf *~ *.o cyclabile cyclabile.service + +run: + ./cyclabile -P 2 -f images/bike_lane.png -e /dev/input/event1 + +install_service: + cp 90-projector.rules $(DESTDIR)/lib/udev/rules.d/ + sed -e 's!@@CYCLABILE_SOURCE_PATH@@!$(shell pwd)!' < cyclabile.service.in > cyclabile.service + cp cyclabile.service $(DESTIR)/lib/systemd/system/ + +uninstall_service: + systemctl enable cyclabile.service + rm $(DESTIR)/lib/systemd/system/cyclabile.service + rm $(DESTDIR)/lib/udev/rules.d/90-projector.rules + udevadm control --reload + +enable_service: install_service + udevadm control --reload + systemctl enable cyclabile.service + +test: cyclabile + valgrind --suppressions=contrib/libusb-udev.supp \ + --leak-check=full --show-reachable=yes \ + ./cyclabile -f images/bumpy_road.png -e /dev/input/event1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..91a991d --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Cyclabile, a light augmented bike lane emitter + +Cyclabile is an experiment about projecting a moving bike lane on the road: the +lane moves when the bike moves, producing the effect of riding on an actual bike +lane, even when there isn't one. + +Dependencies: + +* `libam7xxx0.1` - a library to use USB pico-projector based on the am7xxx chip. +* `libturbojpeg0` - a library for fast JPEG encoding +* `liblept5` - a library for image processing, used to calculate a perspective + projection. + +## Limitations + +The effectiveness of projecting on the paving instead of signaling the presence +with a traditional light can be argued. + +For now the project purpose is more about making a statement (there should be +more bike lanes) than to build an actual product. + +## Instruction + +The prototype has been developed on a BeagleBone Black. + +A keypad is needed to adjust the perspective projection parameters, see +http://ao2.it/137 for instructions about how a device-tree overlay can be used +for that. + +A precise rotary encoder is needed to detect movement at very low speed, +something like the one at https://ao2.it/135 can be used. + +The instructions at https://ao2.it/138 show an example of how to build a support +to mount a magnet ring on the front wheel of the bike. + +The device-tree overlay at https://git.ao2.it/experiments/bbb-eqep2b-ao2.git/ +enables the BBB hardware support for rotary encoders. + +To build the project for the BeagleBone Black execute the following command: + + $ make NEON=1 EQEP=1 + +And install the systemd unit to launch the program automatically when the USB +projector gets connected: + + $ sudo make enable_service + +The software can also be tested on a normal desktop PC, emulating the encoder +with the mouse wheel, with the following command: + + $ make && sudo make run + + +## Similar projects + +### Safety First + +"*Safety First*" is an art installation by Vladimír Turner and Ondřej Mladý very +much in the spirit of Cyclabile. + +* +* + +The difference is that Cyclabile is more portable and more interactive, the bike +lane moves depending on the bike movement while "*Safety First*" used a video +recording in a fixed loop. + + +### Bike Projector Headlight + +This is more generic, it's used to display info on the road, not specifically +a bike lane. + +* +* +* + + +### Lightlane + +A laser bike lane, it projects a static image. + +* diff --git a/TODO b/TODO new file mode 100644 index 0000000..a29000b --- /dev/null +++ b/TODO @@ -0,0 +1,7 @@ +- Initialize the output device before the input image and derive the viewport + height from the output device native resolution. + +- Check if the image height is at least two times the viewport height. + +- For width, check if the image width less than or equal to the viewport + width. diff --git a/contrib/99-cyclabile.rules b/contrib/99-cyclabile.rules new file mode 100644 index 0000000..8e92b25 --- /dev/null +++ b/contrib/99-cyclabile.rules @@ -0,0 +1,6 @@ +# NOTE: this script is provided just as a template for systems without systemd. + +# Execute cyclabile when the projector is plugged in, +ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="1de1", ATTRS{idProduct}=="c101", MODE="0660", GROUP="plugdev", RUN+="/usr/bin/make -C /home/debian/cyclabile run" +# Kill it when the projector is unplugged. +ACTION=="remove", SUBSYSTEM=="usb", ENV{ID_VENDOR_ID}=="1de1", ENV{ID_MODEL_ID}=="c101", RUN+="killall cyclabile" diff --git a/contrib/libusb-udev.supp b/contrib/libusb-udev.supp new file mode 100644 index 0000000..a1e43c6 --- /dev/null +++ b/contrib/libusb-udev.supp @@ -0,0 +1,28 @@ +{ + Suppress a false positive about the udev allocation cache, see https://github.com/libusb/libusb/issues/231 + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + obj:/lib/x86_64-linux-gnu/libusb-1.0.so.0.1.* + obj:/lib/x86_64-linux-gnu/libusb-1.0.so.0.1.* + fun:libusb_init +} + +{ + Suppress a false positive about the udev allocation cache, see https://github.com/libusb/libusb/issues/231 + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + obj:/lib/x86_64-linux-gnu/libudev.so.1.6.* + fun:udev_enumerate_scan_devices + obj:/lib/x86_64-linux-gnu/libusb-1.0.so.0.1.* + obj:/lib/x86_64-linux-gnu/libusb-1.0.so.0.1.* + fun:libusb_init +} diff --git a/cyclabile.c b/cyclabile.c new file mode 100644 index 0000000..e52d9f3 --- /dev/null +++ b/cyclabile.c @@ -0,0 +1,565 @@ +/* + * cyclabile - CYC Light Augmented Bike Light Emitter + * + * Copyright (C) 2018 Antonio Ospite + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "projective_split.h" + +#include "fps-meter.h" + +#define M_PI 3.14159265358979323846 /* pi */ + +/* Rotary encoder properties. */ +#define PULSES_PER_REVOLUTION 120 + +/* Video projector property. */ +#define HEIGHT_IN_PIXELS 480 + +/* Bike setup properties */ +#define WHEEL_RADIUS_IN_METERS 0.33 +#define HEIGHT_IN_METERS 0.92 + +#define PIXELS_PER_METER (HEIGHT_IN_PIXELS / HEIGHT_IN_METERS) + + +static unsigned int run = 1; + +static struct { + char *image_file; + char *event_device; + unsigned int width; + unsigned int height; + unsigned int x_correction; + unsigned int y_correction; + unsigned int zoom_mode; + unsigned int power_mode; +} config = { + .image_file = NULL, + .event_device = NULL, + .width = 800, + .height = 480, + .x_correction = 200, + .y_correction = 0, + .zoom_mode = AM7XXX_ZOOM_ORIGINAL, + .power_mode = AM7XXX_POWER_LOW, +}; + +#ifdef USE_EQEP +#define POSITION_FILE "/sys/devices/platform/ocp/48304000.epwmss/48304180.eqep/position" +static int read_position(int *position) +{ + FILE *file; + int ret = 0; + + file = fopen(POSITION_FILE, "r"); + if (file == NULL) { + fprintf(stderr, "Cannot open %s\n", POSITION_FILE); + return -errno; + } + + ret = fscanf(file, "%d", position); + if (ret != 1) + ret = -EINVAL; + else + ret = 0; + + fclose(file); + return ret; +} +#else +static int read_position(int *position) +{ + (void)position; + return 0; +} +#endif + +static int mod(int a, int b) +{ + int c = a % b; + return (c < 0) ? c + b : c; +} + +static int projector_device_init(am7xxx_context **ctx, am7xxx_device **dev, unsigned int device_index) +{ + int log_level = AM7XXX_LOG_INFO; + int ret; + + ret = am7xxx_init(ctx); + if (ret < 0) { + perror("am7xxx_init"); + goto err; + } + + am7xxx_set_log_level(*ctx, log_level); + + ret = am7xxx_open_device(*ctx, dev, device_index); + if (ret < 0) { + perror("am7xxx_open_device"); + goto err; + } + + ret = am7xxx_set_zoom_mode(*dev, config.zoom_mode); + if (ret < 0) { + perror("am7xxx_set_zoom_mode"); + goto err_close_device; + } + + ret = am7xxx_set_power_mode(*dev, config.power_mode); + if (ret < 0) { + perror("am7xxx_set_power_mode"); + goto err_close_device; + } + + return 0; + +err_close_device: + am7xxx_close_device(*dev); +err: + am7xxx_shutdown(*ctx); + return ret; +} + +static void unset_run(int signo) +{ + (void) signo; + run = 0; +} + +static int set_signal_handler(int signum, void (*signal_handler)(int)) +{ + struct sigaction new_action; + struct sigaction old_action; + int ret; + + new_action.sa_handler = signal_handler; + sigemptyset(&new_action.sa_mask); + new_action.sa_flags = 0; + + ret = sigaction(signum, NULL, &old_action); + if (ret < 0) { + perror("sigaction on old_action"); + goto out; + } + + if (old_action.sa_handler != SIG_IGN) { + ret = sigaction(signum, &new_action, NULL); + if (ret < 0) { + perror("sigaction on new_action"); + goto out; + } + } + +out: + return ret; +} + +static struct p *calc_perspective_map(void) +{ + PTA *src; + PTA *dst; + BOX *viewport; + l_float32 *transform_coeffs; + struct p *perspective_map = NULL; + int ret; + + src = ptaCreate(4); + if (src == NULL) + return NULL; + + ptaAddPt(src, 0, 0); + ptaAddPt(src, config.width, 0); + ptaAddPt(src, config.width, config.height); + ptaAddPt(src, 0, config.height); + + dst = ptaCreate(4); + if (dst == NULL) + goto out_destroy_pta_src; + + ptaAddPt(dst, config.x_correction, config.y_correction); + ptaAddPt(dst, config.width - config.x_correction, config.y_correction); + ptaAddPt(dst, config.width, config.height); + ptaAddPt(dst, 0, config.height); + + transform_coeffs = NULL; + ret = getProjectiveXformCoeffs(dst, src, &transform_coeffs); + if (ret != 0) + goto out_destroy_pta_dst; + + viewport = boxCreateValid(0, 0, config.width, config.height); + if (viewport == NULL) + goto out_free_transform_coeffs; + + perspective_map = _pixProjectiveSampled_precalc_map(viewport, transform_coeffs); + + boxDestroy(&viewport); + +out_free_transform_coeffs: + LEPT_FREE(transform_coeffs); +out_destroy_pta_dst: + ptaDestroy(&dst); +out_destroy_pta_src: + ptaDestroy(&src); + + return perspective_map; +} + +static void usage(char *name) +{ + printf("usage: %s [OPTIONS]\n\n", name); + printf("OPTIONS:\n"); + printf("\t-f \t\tthe image file to upload\n"); + printf("\t-e \t\tthe event device to get input from\n"); + printf("\t-x \t\tthe perspective correction in the X direction, in pixels (default 200)\n"); + printf("\t-y \t\tthe perspective correction in the Y direction, in pixels (default 50)\n"); + printf("\t-Z \t\tthe display zoom mode, between %d (original) and %d (test)\n", + AM7XXX_ZOOM_ORIGINAL, AM7XXX_ZOOM_TEST); + printf("\t-h \t\t\tthis help message\n"); + printf("\n\nEXAMPLES OF USE:\n"); + printf("\t%s -f road.png -e /dev/input/event5\n", name); + printf("\t%s -f narrow-road.png -e /dev/input/event0 -x 130 -Z 1\n", name); +} + +int main(int argc, char *argv[]) +{ + PIX *image; + PIX *transformed_image; + BOX *viewport; + struct p *perspective_map; + + tjhandle jpeg_compressor; + unsigned char *out_buf; + unsigned long out_buf_size; + + am7xxx_context *projector_ctx = NULL; + am7xxx_device *projector_dev = NULL; + + int input_fd; + struct input_event ev[64]; + unsigned int i; + + int position = 0; + int old_position = 0; + int position_delta; + float distance; + + struct fps_meter_stats fps_stats; + + int y = 0; + int lastframe_limit; + + int ret; + int opt; + + while ((opt = getopt(argc, argv, "e:f:x:y:P:Z:h")) != -1) { + switch (opt) { + case 'e': + if (config.event_device != NULL) + fprintf(stderr, "Warning: event device already specified\n"); + config.event_device = optarg; + break; + case 'f': + if (config.image_file != NULL) + fprintf(stderr, "Warning: image file already specified\n"); + config.image_file = optarg; + break; + case 'x': + config.x_correction = atoi(optarg); /* atoi() is as nasty as it is easy */ + break; + case 'y': + config.y_correction = atoi(optarg); /* atoi() is as nasty as it is easy */ + break; + case 'Z': + config.zoom_mode = atoi(optarg); /* atoi() is as nasty as it is easy */ + switch(config.zoom_mode) { + case AM7XXX_ZOOM_ORIGINAL: + case AM7XXX_ZOOM_H: + case AM7XXX_ZOOM_H_V: + case AM7XXX_ZOOM_TEST: + break; + default: + fprintf(stderr, "Invalid zoom mode value, must be between %d and %d\n", + AM7XXX_ZOOM_ORIGINAL, AM7XXX_ZOOM_TEST); + ret = -EINVAL; + goto out; + } + break; + case 'P': + config.power_mode = atoi(optarg); /* atoi() is as nasty as it is easy */ + switch(config.power_mode) { + case AM7XXX_POWER_OFF: + case AM7XXX_POWER_LOW: + case AM7XXX_POWER_MIDDLE: + case AM7XXX_POWER_HIGH: + case AM7XXX_POWER_TURBO: + break; + default: + fprintf(stderr, "Invalid power mode value, must be between %d and %d\n", + AM7XXX_POWER_OFF, AM7XXX_POWER_TURBO); + ret = -EINVAL; + goto out; + } + break; + case 'h': + usage(argv[0]); + ret = 0; + goto out; + default: /* '?' */ + usage(argv[0]); + ret = -EINVAL; + goto out; + } + } + + if (config.image_file == NULL) { + fprintf(stderr, "An image file MUST be specified with the -f option.\n\n"); + usage(argv[0]); + ret = -EINVAL; + goto out; + } + + if (config.event_device == NULL) { + fprintf(stderr, "An event device MUST be specified with the -e option.\n\n"); + usage(argv[0]); + ret = -EINVAL; + goto out; + } + + ret = set_signal_handler(SIGINT, unset_run); + if (ret < 0) { + perror("setting SIGINT"); + goto out; + } + + ret = set_signal_handler(SIGTERM, unset_run); + if (ret < 0) { + perror("setting SIGTERM"); + goto out; + } + + image = pixRead(config.image_file); + if (image == NULL) { + fprintf(stderr, "Cannot load image\n"); + ret = -EINVAL; + goto out; + } + lastframe_limit = (pixGetHeight(image) - config.height); + + config.width = pixGetWidth(image); + + transformed_image = pixCreate(config.width, config.height, pixGetDepth(image)); + if (transformed_image == NULL) { + fprintf(stderr, "Cannot create image\n"); + ret = -EINVAL; + goto out_destroy_image; + } + + /* Transform only a part of the whole image */ + viewport = boxCreateValid(0, y, config.width, config.height); + if (viewport == NULL) { + ret = -EINVAL; + goto out_destroy_transformed_image; + } + + perspective_map = calc_perspective_map(); + if (perspective_map == NULL) { + ret = -ENOMEM; + goto out_destroy_viewport; + } + + /* jpeg encoder init */ + jpeg_compressor = tjInitCompress(); + if (jpeg_compressor == NULL) { + fprintf(stderr, "tjInitCompress failed: %s\n", tjGetErrorStr()); + ret = -ENOMEM; + goto out_free_perspective_map; + } + + out_buf_size = tjBufSize(config.width, config.height, TJSAMP_420); + ret = (int) out_buf_size; + if (ret < 0) { + fprintf(stderr, "tjBufSize failed: %s\n", tjGetErrorStr()); + goto out_jpeg_destroy; + } + out_buf = malloc(out_buf_size); + if (out_buf == NULL) { + ret = -ENOMEM; + goto out_jpeg_destroy; + } + + /* output init */ + ret = projector_device_init(&projector_ctx, &projector_dev, 0); + if (ret < 0) + goto out_free_out_buf; + + /* input event init */ + input_fd = open(config.event_device, O_RDONLY | O_NONBLOCK); + if (input_fd < 0) { + perror("open"); + ret = input_fd; + goto out_am7xxx_shutdown; + } + + ret = read_position(&old_position); + if (ret < 0) + goto out_cleanup; + + + fps_meter_init(&fps_stats); + while (run) { + + fps_meter_update(&fps_stats); + + errno = 0; + ret = read(input_fd, ev, sizeof(ev)); + if (ret < (int)sizeof(ev[0]) && errno != EAGAIN && errno != EWOULDBLOCK) { + perror("read"); + ret = -errno; + goto out_cleanup; + } + + /* + * Handle keypad input to adjust the perspective map. + * This can go in a preliminar "setup" stage instead of the + * main loop. + */ + if (ret >= (int)sizeof(ev[0])) { + for (i = 0; i < ret / sizeof(ev[0]); i++) { + /* + * This is for testing the software with the + * mouse wheel... + */ + if (ev[i].type == EV_REL && ev[i].code == REL_WHEEL) { + position += ev[i].value; + } + else if (ev[i].type == EV_KEY && ev[i].value > 0) { + switch(ev[i].code) { + case KEY_UP: + if (config.y_correction < config.height) + config.y_correction += 1; + break; + case KEY_DOWN: + if (config.y_correction > 0) + config.y_correction -= 1; + break; + case KEY_LEFT: + if (config.x_correction > 0) + config.x_correction -= 1; + break; + case KEY_RIGHT: + if (config.x_correction < config.width) + config.x_correction += 1; + break; + default: + break; + + } + free(perspective_map); + perspective_map = calc_perspective_map(); + if (perspective_map == NULL) { + ret = -ENOMEM; + goto out_cleanup; + } + pixClearAll(transformed_image); + break; + } + } + } + + ret = read_position(&position); + if (ret < 0) + break; + position_delta = old_position - position; + old_position = position; + + /* distance in meters */ + distance = 2 * WHEEL_RADIUS_IN_METERS * M_PI / PULSES_PER_REVOLUTION * position_delta; +#ifdef DEBUG + fprintf(stderr, "position_delta: %d distance: %f pixels: %d\n", position_delta, distance, (int) (distance * PIXELS_PER_METER)); +#endif + + /* convert distance to pixels */ + y += distance * PIXELS_PER_METER; + y = mod(y, lastframe_limit); + + boxSetGeometry(viewport, -1, y, -1, -1); + + /* Apply the perspective transformation */ + transformed_image = _pixProjectiveSampled_apply_map_dest_roi(image, + perspective_map, + L_BRING_IN_BLACK, + transformed_image, + viewport); + ret = tjCompress2(jpeg_compressor, (unsigned char *)pixGetData(transformed_image), + config.width, 0, config.height, TJPF_XBGR, &out_buf, &out_buf_size, + TJSAMP_420, 70, TJFLAG_NOREALLOC | TJFLAG_FASTDCT); + if (ret < 0) { + tjGetErrorStr(); + fprintf(stderr, "tjCompress2 failed: %s\n", tjGetErrorStr()); + break; + } + + ret = am7xxx_send_image_async(projector_dev, + AM7XXX_IMAGE_FORMAT_JPEG, + config.width, + config.height, + out_buf, + out_buf_size); + + if (ret < 0) { + perror("am7xxx_send_image"); + break; + } + + } + + ret = 0; + +out_cleanup: + close(input_fd); +out_am7xxx_shutdown: + am7xxx_set_zoom_mode(projector_dev, AM7XXX_ZOOM_TEST); + am7xxx_set_power_mode(projector_dev, AM7XXX_POWER_OFF); + am7xxx_close_device(projector_dev); + am7xxx_shutdown(projector_ctx); +out_free_out_buf: + free(out_buf); +out_jpeg_destroy: + tjDestroy(jpeg_compressor); +out_free_perspective_map: + free(perspective_map); +out_destroy_viewport: + boxDestroy(&viewport); +out_destroy_transformed_image: + pixDestroy(&transformed_image); +out_destroy_image: + pixDestroy(&image); +out: + return ret; +} diff --git a/cyclabile.service.in b/cyclabile.service.in new file mode 100644 index 0000000..3b79918 --- /dev/null +++ b/cyclabile.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=Start CYCLABILE + +BindsTo=dev-projector.device +After=dev-projector.device + +[Service] +# For the time being run the program from its source directory +ExecStart=/usr/bin/make -C "@@CYCLABILE_SOURCE_PATH@@" run + +[Install] +# This ensures that the unit is run when the device shows up +WantedBy=dev-projector.device diff --git a/fps-meter.h b/fps-meter.h new file mode 100644 index 0000000..a669b9d --- /dev/null +++ b/fps-meter.h @@ -0,0 +1,96 @@ +/* + * fps-meter - Example program about how to measure frames per seconds + * + * Copyright (C) 2013 Antonio Ospite + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FPS_METER_H +#define FPS_METER_H + +#include +#include +#include + +#ifdef DEBUG +#define dbg(...) \ + do { \ + printf(__VA_ARGS__); \ + printf("\n"); \ + fflush(stdout); \ + } while(0) +#else +#define dbg(...) do {} while(0) +#endif + +#define NSEC_PER_SEC 1000000000 + +#define timespecsub(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_nsec = (a)->tv_nsec - (b)->tv_nsec; \ + if ((result)->tv_nsec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_nsec += 1000000000; \ + } \ + } while(0) + +struct fps_meter_stats { + struct timespec time_start; + struct timespec time_end; + + unsigned int frames; + double nsecs; +}; + +static void fps_meter_init(struct fps_meter_stats *stats) +{ + memset(stats, 0, sizeof(*stats)); + + clock_gettime(CLOCK_MONOTONIC, &stats->time_start); + dbg("Init time: s: %ld, ns: %ld", stats->time_start.tv_sec, stats->time_start.tv_nsec); +} + +static void fps_meter_update(struct fps_meter_stats *stats) +{ + struct timespec elapsed; + + dbg("Start time: s: %ld, ns: %ld", stats->time_start.tv_sec, stats->time_start.tv_nsec); + + clock_gettime(CLOCK_MONOTONIC, &stats->time_end); + dbg("End time: s: %ld, ns: %ld", stats->time_end.tv_sec, stats->time_end.tv_nsec); + + timespecsub(&stats->time_end, &stats->time_start, &elapsed); + dbg("Elapsed s: %ld ns: %ld", elapsed.tv_sec, elapsed.tv_nsec); + + stats->frames++; + stats->nsecs += (elapsed.tv_sec * NSEC_PER_SEC + elapsed.tv_nsec); + if (stats->nsecs >= NSEC_PER_SEC) { + /* + * if we were garanteed that each frame took less than + * a second, then just printing 'frames' would be enough here, + * but if we want to cover the case when a frame may take more + * than a second, some calculations have to be done. + */ + float fps = stats->frames / (stats->nsecs / NSEC_PER_SEC); + printf("(frames: %d, nsecs: %f) FPS: %.2f\n", stats->frames, stats->nsecs, fps); + stats->nsecs -= NSEC_PER_SEC; + stats->frames = 0; + } + + /* update the stats for the next iteration */ + clock_gettime(CLOCK_MONOTONIC, &stats->time_start); +} +#endif /* FPS_METER_H */ diff --git a/images/Makefile b/images/Makefile new file mode 100644 index 0000000..9729051 --- /dev/null +++ b/images/Makefile @@ -0,0 +1,12 @@ +WIDTH ?= 800 + +SOURCES := $(wildcard *.svg) +IMAGE_FILES := $(SOURCES:.svg=.png) + +all: $(IMAGE_FILES) + +clean: + rm -f $(IMAGE_FILES) + +%.png: %.svg + inkscape --export-png=$@ --export-width=$(WIDTH) $< diff --git a/images/bike_lane.png b/images/bike_lane.png new file mode 100644 index 0000000..d82fdd0 Binary files /dev/null and b/images/bike_lane.png differ diff --git a/images/bike_lane.svg b/images/bike_lane.svg new file mode 100644 index 0000000..da92b08 --- /dev/null +++ b/images/bike_lane.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/images/bumpy_road.png b/images/bumpy_road.png new file mode 100644 index 0000000..21fe586 Binary files /dev/null and b/images/bumpy_road.png differ diff --git a/images/bumpy_road.svg b/images/bumpy_road.svg new file mode 100644 index 0000000..6daa87d --- /dev/null +++ b/images/bumpy_road.svg @@ -0,0 +1,146 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/projective_split.c b/projective_split.c new file mode 100644 index 0000000..b20a418 --- /dev/null +++ b/projective_split.c @@ -0,0 +1,247 @@ +/* + * Separate calculating projected coordinates and transforming the pixels. + * + * Copyright (C) 2018 Antonio Ospite + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * The idea behind these functions is that in some use cases the perspective + * deformation does not change, it's only the projected data which changes, so + * there's no need to recalculate the projection parameters every time. + * + * It is enough to apply the pre-calculated projection to the new data, + * possibly using a region-of-interest, to avoid clipping the image in case + * it's larger than the viewport. + */ + +#include "projective_split.h" + +/* + * Pre-calculate the transformation map. + * + * This can be useful if the transform mapping does not change but we want to + * apply the same transform to different data. + */ +struct p * +_pixProjectiveSampled_precalc_map(BOX *viewport, + l_float32 *vc) +{ + l_int32 i, j, x, y; + struct p *map; + + if (!vc) + return (struct p *)ERROR_PTR("vc not defined", __func__, NULL); + + map = malloc(viewport->w * viewport->h * sizeof(*map)); + if (map == NULL) + return (struct p *)ERROR_PTR("cannot allocate map", __func__, NULL); + + for (i = 0; i < viewport->h; i++) { + for (j = 0; j < viewport->w; j++) { + /* XXX: transform i and j by considering the viewport origin */ + projectiveXformSampledPt(vc, j, i, &x, &y); + + /* XXX: This is a hack to avoid conditionals when + * applying the map. + * Basically we set all out of bound pixels to be the same + * as the pixel at 0,0. + * + * When applying the map make sure that the pixel at + * 0,0 is set to the desired "background" color. + */ + if (x < 0 || y < 0 || x >= viewport->w || y >= viewport->h) { + x = 0; + y = 0; + } + + map[i * viewport->w + j].x = x; + map[i * viewport->w + j].y = y; + } + } + + return map; +} + +#ifdef USE_NEON + +#include + +/* + * Apply the map pre-calculated transformation map to an image, but only to + * a region of interest. + * + * This is a NEON optimized version, however the speedup is not that + * impressive because moving pixels around always means accessing uncontiguous + * memory. + */ +PIX * +_pixProjectiveSampled_apply_map_dest_roi(PIX *pixs, + struct p *map, + l_int32 incolor, + PIX *pixd, + BOX *roi) +{ + int32_t i, j, w, h, d, wpls, wpld; + uint32_t *datas, *datad, *lined; + l_uint32 pixel0; + + uint32_t stride; + uint32x4x2_t point; + uint32x4_t lines; + uint32x4_t roi_x; + uint32x4_t roi_y; + uint32x4_t x_offset; + uint32x4_t y_offset; + uint32x4_t wpls_v; + + PROCNAME("pixProjectiveSampled_neon"); + + if (!pixs) + return (PIX *)ERROR_PTR("pixs not defined", procName, NULL); + if (!map) + return (PIX *)ERROR_PTR("map not defined", procName, NULL); + if (incolor != L_BRING_IN_WHITE && incolor != L_BRING_IN_BLACK) + return (PIX *)ERROR_PTR("invalid incolor", procName, NULL); + pixGetDimensions(pixs, &w, &h, &d); + if (d != 32) + return (PIX *)ERROR_PTR("depth not 32", procName, NULL); + if (!pixd) + return (PIX *)ERROR_PTR("pixd not defined", procName, NULL); + + /* Scan over the dest pixels */ + datas = pixGetData(pixs); + wpls = pixGetWpl(pixs); + datad = pixGetData(pixd); + wpld = pixGetWpl(pixd); + + roi_x = vmovq_n_u32(roi->x); + roi_y = vmovq_n_u32(roi->y); + wpls_v = vmovq_n_u32(wpls); + + /* + * Save the value of the pixel at 0,0 in the destination image, + * and set it to black, because the map uses the pixel at 0,0 for out + * of bounds pixels. + */ + pixGetPixel(pixs, roi->x, roi->y, &pixel0); + pixSetPixel(pixs, roi->x, roi->y, 0); + + for (i = 0; i < roi->h; i++) { + lined = datad + i * wpld; + stride = i * roi->w; + for (j = 0; j < roi->w; j += 4) { + unsigned int map_index = stride + j; + + point = vld2q_u32((uint32_t *)(map + map_index)); + + x_offset = vaddq_u32(point.val[0], roi_x); + + y_offset = vaddq_u32(point.val[1], roi_y); + + lines = vmlaq_u32(x_offset, y_offset, wpls_v); + + lined[j + 0] = *(datas + lines[0]); + lined[j + 1] = *(datas + lines[1]); + lined[j + 2] = *(datas + lines[2]); + lined[j + 3] = *(datas + lines[3]); + } + } + + /* restore the previous value */ + pixSetPixel(pixs, roi->x, roi->y, pixel0); + + return pixd; +} + +#else + +/* + * Apply the map pre-calculated transformation map to an image, but only to + * a region of interest. + */ +PIX * +_pixProjectiveSampled_apply_map_dest_roi(PIX *pixs, + struct p *map, + l_int32 incolor, + PIX *pixd, + BOX *roi) +{ + l_int32 i, j, w, h, d, wpls, wpld; + l_uint32 *datas, *datad, *lined; + l_uint32 datas_offset; + l_uint32 stride; + l_uint32 pixel0; + struct p *point; + + PROCNAME("pixProjectiveSampled"); + + if (!pixs) + return (PIX *)ERROR_PTR("pixs not defined", procName, NULL); + if (!map) + return (PIX *)ERROR_PTR("map not defined", procName, NULL); + if (incolor != L_BRING_IN_WHITE && incolor != L_BRING_IN_BLACK) + return (PIX *)ERROR_PTR("invalid incolor", procName, NULL); + + pixGetDimensions(pixs, &w, &h, &d); + if (d != 32) + return (PIX *)ERROR_PTR("depth not 32", procName, NULL); + if (!pixd) + return (PIX *)ERROR_PTR("pixd not defined", procName, NULL); + + /* Scan over the dest pixels */ + datas = pixGetData(pixs); + wpls = pixGetWpl(pixs); + datad = pixGetData(pixd); + wpld = pixGetWpl(pixd); + + /* + * Save the value of the pixel at 0,0 in the destination image, + * and set it to black, because the map uses the pixel at 0,0 for out + * of bounds pixels. + * + * This is just a dirty trick to avoid a conditional deep in the loop + * below. + * + * A more general algorithm would check for out of bounds explicitly, + * with something like: + * + * if (point->x >= 0 && point->y >= 0 && point->x < roi->w && point->y < roi->h) { + * datas_offset = (point->x + roi->x) + (point->y + roi->y) * wpls; + * *(lined + j) = *(datas + datas_offset); + * } + */ + pixGetPixel(pixs, roi->x, roi->y, &pixel0); + pixSetPixel(pixs, roi->x, roi->y, 0); + + for (i = 0; i < roi->h; i++) { + lined = datad + i * wpld; + stride = i * roi->w; + for (j = 0; j < roi->w; j++) { + point = map + stride + j; + + datas_offset = (point->x + roi->x) + (point->y + roi->y) * wpls; + *(lined + j) = *(datas + datas_offset); + } + } + + /* restore the previous value */ + pixSetPixel(pixs, roi->x, roi->y, pixel0); + + return pixd; +} + + +#endif diff --git a/projective_split.h b/projective_split.h new file mode 100644 index 0000000..261220c --- /dev/null +++ b/projective_split.h @@ -0,0 +1,50 @@ +/* + * Separate calculating projected coordinates and transforming the pixels. + * + * Copyright (C) 2018 Antonio Ospite + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PROJECTIVE_SPLIT_H +#define PROJECTIVE_SPLIT_H + +#include + +struct p { + l_int32 x; + l_int32 y; +}; + +struct p * +_pixProjectiveSampled_precalc_map(BOX *viewport, + l_float32 *vc); + +PIX * +_pixProjectiveSampled_apply_map_dest_roi(PIX *pixs, + struct p *map, + l_int32 incolor, + PIX *pixd, + BOX *roi); + +#ifdef USE_NEON +PIX * +_pixProjectiveSampled_apply_map_dest_roi_neon(PIX *pixs, + struct p *map, + l_int32 incolor, + PIX *pixd, + BOX *roi); +#endif + +#endif /* PROJECTIVE_SPLIT_H */