Initial import
authorAntonio Ospite <ao2@ao2.it>
Fri, 24 Aug 2018 10:50:54 +0000 (12:50 +0200)
committerAntonio Ospite <ao2@ao2.it>
Fri, 24 Aug 2018 15:27:51 +0000 (17:27 +0200)
17 files changed:
.gitignore [new file with mode: 0644]
90-projector.rules [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
TODO [new file with mode: 0644]
contrib/99-cyclabile.rules [new file with mode: 0644]
contrib/libusb-udev.supp [new file with mode: 0644]
cyclabile.c [new file with mode: 0644]
cyclabile.service.in [new file with mode: 0644]
fps-meter.h [new file with mode: 0644]
images/Makefile [new file with mode: 0644]
images/bike_lane.png [new file with mode: 0644]
images/bike_lane.svg [new file with mode: 0644]
images/bumpy_road.png [new file with mode: 0644]
images/bumpy_road.svg [new file with mode: 0644]
projective_split.c [new file with mode: 0644]
projective_split.h [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..e7fc10e
--- /dev/null
@@ -0,0 +1,3 @@
+*.o
+cyclabile
+cyclabile.service
diff --git a/90-projector.rules b/90-projector.rules
new file mode 100644 (file)
index 0000000..2176b13
--- /dev/null
@@ -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 (file)
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 (file)
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.
+
+* <http://sgnlr.com/works/outside/safety-first2011-prague/>
+* <http://vimeo.com/23469752>
+
+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.
+
+* <https://makezine.com/bike-projector-heads-down-display-stationary/>
+* <http://www.reddit.com/r/technology/comments/1anmip/guy_builds_a_speedometer_built_from_a_raspberry/>
+* <http://www.youtube.com/watch?v=Nfk1-XMASrk>
+
+
+### Lightlane
+
+A laser bike lane, it projects a static image.
+
+* <https://www.wired.com/2009/02/lightlanes-lase/>
diff --git a/TODO b/TODO
new file mode 100644 (file)
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 (file)
index 0000000..8e92b25
--- /dev/null
@@ -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 (file)
index 0000000..a1e43c6
--- /dev/null
@@ -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 (file)
index 0000000..e52d9f3
--- /dev/null
@@ -0,0 +1,565 @@
+/*
+ * cyclabile - CYC Light Augmented Bike Light Emitter
+ *
+ * Copyright (C) 2018  Antonio Ospite <ao2@ao2.it>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <signal.h>
+#include <linux/input.h>
+#include <leptonica/allheaders.h>
+#include <turbojpeg.h>
+#include <am7xxx.h>
+
+#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 <image_file>\t\tthe image file to upload\n");
+       printf("\t-e <event device>\t\tthe event device to get input from\n");
+       printf("\t-x <X correction>\t\tthe perspective correction in the X direction, in pixels (default 200)\n");
+       printf("\t-y <Y correction>\t\tthe perspective correction in the Y direction, in pixels (default 50)\n");
+       printf("\t-Z <zoom mode>\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 (file)
index 0000000..3b79918
--- /dev/null
@@ -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 (file)
index 0000000..a669b9d
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * fps-meter - Example program about how to measure frames per seconds
+ *
+ * Copyright (C) 2013  Antonio Ospite <ospite@studenti.unina.it>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef FPS_METER_H
+#define FPS_METER_H
+
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#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 (file)
index 0000000..9729051
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..da92b08
--- /dev/null
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="5492.126"
+   height="56444.879"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="bike_lane.svg"
+   inkscape:export-filename="/home/ao2/Proj/picoProjector/bike_lane/bike_lane.png"
+   inkscape:export-xdpi="7.8658066"
+   inkscape:export-ydpi="7.8658066">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.012259747"
+     inkscape:cx="2746.063"
+     inkscape:cy="28222.44"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1280"
+     inkscape:window-height="911"
+     inkscape:window-x="0"
+     inkscape:window-y="28"
+     inkscape:window-maximized="1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     units="cm">
+    <sodipodi:guide
+       orientation="0,1"
+       position="2.8421709e-13,6377.9515"
+       id="guide4026"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="2.8421709e-13,6377.9515"
+       id="guide4028"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="2.8421709e-13,-0.001331875"
+       id="guide4030"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="2.8421709e-13,12755.905"
+       id="guide4032"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="2.8421709e-13,19133.857"
+       id="guide4046"
+       inkscape:locked="false" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="layer3"
+     inkscape:label="Background"
+     style="display:inline"
+     transform="translate(0,37311.023)"
+     sodipodi:insensitive="true">
+    <path
+       transform="translate(-164.82684,18492.735)"
+       style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.80000019;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       d="m 164.82684,-55803.758 5492.12596,0 0,56444.87909 -5492.12596,0 z"
+       id="rect4044"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+  <g
+     inkscape:label="Lane"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-164.82684,55803.758)"
+     style="display:inline">
+    <path
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#da624e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;enable-background:accumulate"
+       d="m 341.98309,-55803.758 0.009,56444.867 H 5656.9527 v -56444.867 z"
+       id="rect3094-9"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:#ffffff;fill-opacity:1"
+       d="m 2272.2056,-6840.0532 c -5.1298,-2.1448 -14.9229,-6.2312 -21.7624,-9.0879 -21.9357,-9.164 -56.1021,-33.8957 -79.1762,-57.3135 -8.6038,-8.7319 -16.3435,-16.5171 -17.1985,-17.3015 -1.6952,-1.5575 -9.0658,-9.9621 -13.9897,-15.9605 -13.4218,-16.3511 -54.776,-79.1655 -65.3846,-99.315 -7.1968,-13.6692 -13.7906,-24.8534 -14.6536,-24.8534 -0.8631,0 -1.5678,-1.6136 -1.5678,-3.5899 0,-1.9763 -2.2112,-7.4344 -4.9127,-12.1326 -14.602,-25.396 -57.1752,-130.1824 -72.9084,-179.4524 -16.7515,-52.4596 -36.0205,-121.1738 -47.1255,-168.0574 -10.526,-44.4385 -30.4968,-141.1461 -30.4968,-147.6811 0,-3.0181 -0.5259,-6.1674 -1.1643,-7.0069 -1.5422,-2.0223 -16.1443,-93.0216 -21.9735,-136.9433 -4.0803,-30.7291 -11.5407,-95.267 -15.5638,-134.6023 -6.0851,-59.491 -14.8763,-196.202 -16.2638,-252.9225 l -0.6996,-28.6686 48.4304,1.2103 48.4305,1.2103 0.3574,16.0188 c 2.9414,129.3934 23.547,312.6953 49.8939,443.9308 4.8054,23.9127 9.1062,44.5074 9.5674,45.7663 0.4596,1.2562 3.6258,14.6046 7.0351,29.6571 15.7459,69.5441 49.0626,175.0316 73.9159,234.0328 23.7353,56.3458 63.1668,126.1636 88.5001,156.6976 4.7032,5.6674 9.9489,12.0457 11.6585,14.1742 6.4508,8.0292 13.1099,14.8737 13.9393,14.3278 0.5106,-0.3064 2.778,2.2929 5.1185,5.7885 7.4776,11.1601 50.8018,40.4741 68.7672,46.5297 15.5255,5.2333 13.9898,-11.1199 13.9898,148.9792 0,131.9847 -0.2043,141.5291 -2.7168,141.0144 -1.4962,-0.3064 -6.9169,-2.3083 -12.046,-4.448 z m 86.27,-136.8764 c 0,-78.0309 0.2551,-141.8753 0.5105,-141.8753 2.3235,0 34.7415,-14.5684 41.4634,-18.6339 20.8394,-12.6059 29.578,-19.7522 49.7412,-40.6748 21.9625,-22.7892 20.1224,-20.5111 42.7467,-52.9422 57.1501,-81.9247 104.8804,-201.5951 143.2409,-359.1393 16.0371,-65.8626 27.6084,-127.5148 42.9979,-229.087 5.6986,-37.614 19.027,-167.6958 20.3716,-198.8257 0.2043,-5.0352 1.195,-23.57 2.1805,-41.1897 0.9806,-17.6201 2.1857,-41.8192 2.6811,-53.7751 l 0.8988,-21.7386 h 48.0923 c 44.7699,0 48.0909,0.5515 48.079,8.0088 -0.012,8.9434 -3.8453,93.8137 -6.1464,136.1538 -3.6308,66.768 -14.193,182.5711 -21.7121,237.9834 -12.0008,88.4475 -21.6448,149.5731 -33.2144,210.5247 -9.2211,48.5774 -38.1037,172.6237 -43.6899,187.641 -0.9345,2.5176 -4.4632,14.8738 -7.8373,27.4592 -17.4685,65.163 -63.155,189.3018 -85.5761,232.5295 -3.4163,6.5933 -9.0659,17.7258 -12.5466,24.7394 -10.1995,20.5503 -27.954,51.5506 -47.2987,82.5871 -7.8893,12.6574 -43.8466,60.1993 -47.41,62.6836 -0.8579,0.5975 -3.5236,3.4624 -5.9305,6.364 -2.4104,2.9057 -5.0403,5.2799 -5.8497,5.2799 -0.8069,0 -2.6861,1.9456 -4.1721,4.3202 -7.5922,12.1524 -60.7441,51.4056 -80.4204,59.3899 -6.8394,2.7781 -17.4659,7.082 -23.6143,9.5694 -19.8263,8.0217 -17.5778,25.5923 -17.5778,-137.3523 z m 1250.5339,136.8376 c -5.1296,-2.1601 -15.2721,-6.2521 -22.5395,-9.0879 -7.2667,-2.8342 -15.3108,-6.9277 -17.8754,-9.0955 -2.5687,-2.1704 -6.9503,-5.071 -9.7435,-6.458 -2.7934,-1.3839 -8.7407,-5.4365 -13.2126,-9.0046 -4.4735,-3.5645 -12.3292,-9.789 -17.4593,-13.8259 -11.3885,-8.9612 -24.6603,-22.1192 -38.0832,-37.7574 -5.5572,-6.4738 -12.9016,-14.9917 -16.3215,-18.9285 -10.6965,-12.3127 -52.4086,-75.4851 -62.1772,-94.165 -9.2109,-17.6155 -21.1989,-41.0543 -28.7564,-56.2247 -22.167,-44.4982 -68.1876,-170.0071 -87.1642,-237.7199 -30.7182,-109.6041 -55.8503,-229.8367 -74.9413,-358.5209 -4.4886,-30.2582 -13.5536,-102.9776 -13.5491,-108.6951 0,-3.9832 3.0436,-5.7205 10.024,-5.7205 5.5101,0 18.6256,-1.9456 29.145,-4.3152 10.5198,-2.3695 28.3835,-5.4672 39.6979,-6.8787 l 20.5709,-2.5686 8.6701,53.7899 c 7.5573,46.8922 17.9929,102.3162 25.5768,135.8422 1.2818,5.669 4.2846,18.9668 6.6705,29.5494 19.9095,88.3071 50.3295,181.9946 82.7591,254.8764 24.0903,54.1402 63.8378,120.658 88.5522,148.1933 5.1297,5.7154 11.4253,13.0537 13.9897,16.3072 14.3739,18.234 55.2607,47.3049 76.9443,54.7076 4.7033,1.6035 10.1229,4.0598 12.0466,5.4478 l 3.4982,2.5278 v 141.379 141.3785 l -3.4982,-0.5465 c -1.9252,-0.3064 -7.6948,-2.3184 -12.8239,-4.4837 z m 86.2706,-136.5572 v -141.5945 l 6.6061,-2.8035 c 3.6359,-1.5423 9.055,-3.4624 12.0465,-4.2693 9.6972,-2.6095 33.6048,-17.3439 50.5192,-31.1288 49.158,-40.0632 92.9076,-106.1178 135.2345,-204.1817 13.8928,-32.1865 50.396,-139.8092 61.5648,-181.511 33.3882,-124.6688 59.8245,-282.1431 71.0679,-423.3361 6.8558,-86.0984 9.5975,-130.372 9.5975,-154.9888 v -14.3463 h 48.3197 48.3195 l -1.0467,35.4686 c -1.9202,64.8934 -9.4486,189.5616 -13.2015,218.5335 -0.9805,7.5508 -1.6495,13.7298 -1.4911,13.7298 0.5668,0 -6.5141,69.1121 -9.6184,93.8198 -11.0648,88.0676 -22.3103,161.2747 -33.3938,217.3896 -2.9824,15.1026 -5.815,31.0636 -6.2941,35.4686 -0.5106,4.4071 -1.4859,8.0094 -2.2418,8.0094 -0.7558,0 -1.3737,2.2571 -1.3737,5.0148 0,2.7575 -1.578,11.4681 -3.4981,19.3564 -1.9252,7.8878 -4.1977,17.8382 -5.0505,22.1125 -3.2887,16.4446 -18.3407,79.5976 -20.0086,83.95 -0.9651,2.5176 -4.8514,16.9339 -8.6414,32.0364 -3.7893,15.1031 -18.8274,62.4713 -33.4204,105.2623 -28.7277,84.2411 -50.0875,134.6703 -85.7436,202.4286 -10.9931,20.8914 -45.0757,73.3178 -61.2951,94.2855 -27.4489,35.4844 -64.1432,69.5037 -94.0031,87.1514 -17.6421,10.4258 -53.2852,25.7413 -59.9087,25.7413 -2.9568,0 -3.0436,-4.0343 -3.0436,-141.595 z m -623.0219,-1058.873 -14.4039,-42.5852 11.2035,-98.8873 11.2036,-98.8875 17.9981,-2.7014 c 32.4004,-4.8719 74.8662,-11.9753 111.2626,-18.6124 19.664,-3.585 50.0922,-8.7141 67.6183,-11.3973 17.5257,-2.6809 43.4066,-6.8481 57.5132,-9.2584 14.1068,-2.4103 39.6378,-6.5259 56.7365,-9.1481 46.3857,-7.1117 82.6224,-13.1181 125.9082,-20.869 67.2006,-12.032 85.2483,-15.8098 85.9917,-17.9991 l -310.0629,-1706.5471 c -3.7177,-19.813 -6.7316,-40.408 -6.696,-45.766 0.03,-5.357 3.0794,-36.513 6.7696,-69.236 l 6.7056,-59.496 51.521,-1.21 c 28.3365,-0.664 51.5208,-0.618 51.5208,0.102 0,0.72 -2.7932,24.885 -6.2138,53.702 -3.4164,28.817 -5.8032,56.197 -5.3022,60.845 0.5106,4.647 3.544,22.214 6.7597,39.036 l 361.8679,1990.1701 -28.9428,6.2497 c -15.918,3.4367 -29.9723,5.6377 -31.231,4.8871 -1.2613,-0.7508 -17.3475,1.8485 -35.752,5.77 -18.4041,3.9219 -47.8022,9.3054 -65.3285,11.9634 -17.5257,2.6555 -43.4064,6.7955 -57.5132,9.1947 -14.1067,2.4001 -39.2878,6.5069 -55.9597,9.1277 -63.6799,10.0111 -88.9678,14.2385 -123.5765,20.6571 -19.6633,3.6411 -49.3921,8.7615 -66.0628,11.3665 -16.6713,2.6095 -42.5525,6.7321 -57.5138,9.1706 -14.9609,2.4359 -40.8421,6.5815 -57.5135,9.2068 -16.6714,2.6248 -40.8041,6.8047 -53.6275,9.2855 -12.824,2.4767 -27.1774,4.4939 -31.8965,4.4787 l -8.5803,-0.023 -14.4034,-42.5847 z m -758.5572,-12.6396 c -15.9339,-9.237 -27.2334,-36.1411 -33.9441,-80.8145 -5.5941,-37.2376 -4.9587,-64.9388 3.2222,-140.8459 l 251.0668,-2359.2418 c 16.5768,-164.558 27.4363,-228.428 49.802,-292.904 11.7464,-33.862 33.9238,-81.638 46.8493,-100.926 14.1042,-21.045 42.6548,-46.89 58.0939,-52.587 3.4214,-1.261 10.4146,-4.295 15.5443,-6.734 14.3793,-6.847 42.8421,-5.62 50.3403,2.17 10.9681,11.393 19.7757,32.556 25.3063,60.806 4.7951,24.492 5.1541,29.216 5.1495,67.524 -0.012,46.521 -3.3908,73.851 -12.4532,100.532 -9.1415,26.915 -17.9699,35.735 -39.5862,39.547 -29.1607,5.142 -43.5876,14.191 -58.9001,36.944 -21.1141,31.376 -34.675,87.737 -46.0523,191.394 l -258.8299,2421.0271 c -4.8257,48.0351 -13.6063,83.9025 -24.529,100.1698 -5.0964,7.5891 -20.4252,19.6435 -23.2988,18.3203 -0.6026,-0.3064 -4.1006,-2.2469 -7.7805,-4.3815 z m 663.0394,-282.4086 c -14.3243,-44.6192 -41.2544,-127.8932 -59.8453,-185.053 -86.9314,-267.281 -110.0361,-338.4889 -131.5313,-405.3713 -12.7151,-39.5627 -31.9765,-98.7048 -42.8027,-131.4276 -10.8273,-32.7231 -30.847,-94.5066 -44.4897,-137.2986 -13.643,-42.7906 -34.3256,-106.6341 -45.9607,-141.8742 -45.9306,-139.1031 -61.2712,-190.2584 -60.903,-203.0813 l 32.5133,-299.0335 33.3115,102.1417 c 18.3218,56.1777 43.8053,134.1636 56.6292,173.302 12.8234,39.1376 43.6015,133.9527 68.3941,210.6992 24.7934,76.7476 55.2662,170.4326 67.7184,208.1894 12.4516,37.7569 33.436,102.5814 46.6322,144.0534 13.1968,41.4724 28.2447,87.8112 33.4408,102.9739 5.1955,15.1634 20.8721,63.6097 34.8358,107.6594 13.9643,44.0505 34.7087,107.8946 46.0998,141.8755 25.3905,75.7477 25.7607,77.2167 23.7915,94.3549 -2.8801,25.0305 -11.9118,109.3519 -13.6752,127.6108 -0.9702,10.0684 -3.3194,32.7226 -5.2169,50.3424 -1.8948,17.62 -3.488,33.0658 -3.5288,34.3246 -0.031,1.2563 -0.8682,8.4664 -1.8282,16.0182 -0.96,7.5513 -2.5993,22.9975 -3.6412,34.3252 -1.0416,11.3261 -2.349,24.1491 -2.9055,28.4949 -0.7815,6.1725 -6.6965,-9.8406 -27.0448,-73.226 z m -1128.7039,-35.2961 c 1.4093,-49.5186 1.9967,-62.5126 5.1607,-114.4794 15.1649,-249.0516 48.623,-463.4165 103.1202,-660.696 17.4256,-63.0799 57.7153,-176.515 77.851,-219.1882 2.6811,-5.6801 5.9345,-12.9724 7.2316,-16.205 3.7331,-9.2998 39.2855,-77.5559 46.6059,-89.474 7.9939,-13.0149 43.3052,-64.7687 44.1907,-64.7687 0.5108,0 3.8863,-4.0648 7.5111,-9.0363 22.2829,-30.5575 81.5998,-80.8676 106.0721,-89.9658 2.5687,-0.9549 8.5104,-3.6309 13.2127,-5.9575 25.6667,-12.6804 92.302,-18.3948 95.7607,-8.2116 0.5874,1.7211 0,11.9269 -1.292,22.6809 -1.297,10.7537 -3.1406,27.7902 -4.0956,37.8591 -0.9548,10.0689 -3.7942,37.8713 -6.3083,61.7844 -2.5125,23.9126 -7.2198,68.7055 -10.457,99.5408 l -5.8855,56.0639 h -10.8297 c -22.569,0 -69.3867,20.1592 -90.9814,39.1754 -4.642,4.0906 -9.5378,8.1346 -10.8809,8.993 -17.7596,11.3542 -69.708,79.8897 -87.7933,115.827 -4.0087,7.9711 -7.6131,14.4923 -8.0053,14.4923 -0.8273,0 -29.0622,59.5794 -30.2209,63.7685 -0.4597,1.5473 -5.3233,13.4009 -10.8809,26.3444 -11.1443,25.958 -36.2979,100.6969 -45.9143,136.4296 -3.3858,12.5853 -6.5948,23.9126 -7.1269,25.1709 -0.531,1.2562 -2.6402,9.4969 -4.6776,18.3064 -2.0428,8.8108 -6.6757,27.7723 -10.3013,42.1385 -3.6259,14.3661 -6.5923,27.135 -6.5923,28.3749 0,1.2459 -2.0325,11.1096 -4.5093,21.9337 -5.9369,25.9089 -23.55,129.6208 -29.3803,172.9966 -11.5768,86.1428 -20.3435,189.2226 -23.3524,274.5968 l -1.0469,29.7477 -48.3871,1.2102 -48.3874,1.2104 z m 856.2887,-5.7848 c -3.304,-86.0851 -14.3693,-213.7097 -24.8927,-287.1823 -7.8014,-54.4681 -13.4204,-88.71 -20.2765,-123.5683 -2.9722,-15.1031 -5.8187,-31.064 -6.3288,-35.4691 -0.5108,-4.4071 -1.5474,-8.0089 -2.3032,-8.0089 -0.7557,0 -1.3737,-2.1704 -1.3737,-4.8207 0,-6.0075 -14.8334,-69.2351 -26.2489,-111.8858 -11.462,-42.8247 -49.6785,-154.1538 -63.3894,-184.6617 l -11.0019,-24.48 2.2827,-21.2853 c 1.2562,-11.7075 2.9517,-28.4943 3.7738,-37.3049 2.921,-31.3322 4.1875,-43.9252 8.0405,-80.0903 2.1448,-20.1369 6.3758,-60.0298 9.3989,-88.6509 3.0231,-28.621 5.7572,-52.8037 6.0749,-53.7393 0.7762,-2.2929 19.2404,34.2358 34.5402,68.3374 59.134,131.8028 108.8759,308.0131 139.7597,495.101 16.1953,98.1074 22.2329,143.8981 32.5863,247.1371 2.0171,20.1368 4.4939,47.9403 5.5066,61.7844 1.006,13.8444 2.4461,33.4091 3.1968,43.4781 2.3592,31.7647 7.8152,145.3238 7.8152,162.6565 0,8.71 -1.3737,8.9658 -48.0755,8.9658 h -48.0749 z m 1337.5914,11.9697 c 0,-26.012 -7.8261,-144.0019 -12.5563,-189.3131 -4.2743,-40.9399 -9.2855,-84.5373 -11.5722,-100.6857 -23.5511,-166.2838 -48.3619,-275.3424 -89.5885,-393.797 -24.7879,-71.2231 -40.8975,-108.8803 -66.8226,-156.2041 -11.4181,-20.843 -48.1664,-76.0464 -54.2168,-81.4448 -2.1141,-1.8894 -5.9457,-6.1306 -8.5109,-9.4279 -17.2071,-22.1203 -48.1091,-46.4944 -77.7214,-61.3039 -28.36,-14.1833 -72.7327,-17.3204 -98.7057,-6.9788 -6.2757,2.5023 -7.1633,1.6903 -8.6325,-7.87 -0.8987,-5.8599 -6.7123,-39.15 -12.9149,-73.9786 -29.1015,-163.4017 -31.5103,-178.3158 -29.4364,-182.1888 1.1132,-2.0782 3.4163,-4.0393 5.1286,-4.361 5.7925,-1.1082 12.4205,-3.7176 15.8951,-6.275 7.8439,-5.7702 52.2278,-16.052 69.5068,-16.102 19.7747,-0.1018 47.5269,6.0932 65.3765,14.4887 5.985,2.8137 12.9796,5.8297 15.5448,6.6994 9.7256,3.299 42.1338,25.8814 59.0676,41.1615 11.0713,9.9909 31.8989,31.9103 40.4149,42.5348 1.7108,2.1295 7.6559,9.4796 13.2125,16.3255 20.0387,24.6853 64.6319,97.8025 78.0131,127.9131 2.298,5.1717 7.5445,16.5944 11.6586,25.3855 26.1672,55.9128 67.6525,171.8262 86.8241,242.5943 10.3135,38.0705 24.8502,95.557 29.5111,116.7038 3.3857,15.3487 3.1252,14.0792 14.1665,69.6259 33.862,170.3471 56.6168,378.7587 61.9789,567.6667 l 0.9396,33.1802 h -48.2812 -48.2813 z m -935.7636,-134.5448 c 0,-2.0987 4.4581,-43.0857 9.8998,-91.0773 5.4447,-47.9929 14.6194,-130.5089 20.388,-183.3684 5.769,-52.86 11.8428,-107.4356 13.4979,-121.2799 1.6547,-13.8443 3.2735,-28.261 3.5951,-32.0369 0.3576,-3.7739 7.1729,-65.5599 15.2247,-137.2983 8.0516,-71.7384 19.4096,-173.6823 25.2392,-226.5423 5.8294,-52.8594 18.7641,-169.2201 28.7427,-258.5782 9.9784,-89.3587 21.0967,-189.126 24.7073,-221.7053 3.6103,-32.5798 6.9306,-60.9806 7.3781,-63.1127 0.4595,-2.1346 4.4888,17.3148 8.9836,43.2171 4.4939,25.9021 12.4006,70.7788 17.5706,99.7255 5.1695,28.9474 14.0884,79.4042 19.8191,112.1274 5.7304,32.7226 11.6495,65.6742 13.1524,73.2259 1.8026,9.047 2.3134,16.8511 1.4962,22.8827 -1.3481,9.98 -9.8835,89.5234 -15.4762,144.1636 -1.9356,18.8778 -5.7554,54.9189 -8.4951,80.0903 -2.7423,25.1715 -6.5632,61.2125 -8.4945,80.0911 -1.9303,18.8783 -4.1364,39.4732 -4.8974,45.7662 -0.766,6.2924 -1.7004,15.56 -2.0784,20.5948 -0.4085,5.0351 -1.8026,18.168 -3.1509,29.1877 -1.3531,11.0188 -2.1396,20.96 -1.7514,22.0916 0.4085,1.1337 3.5695,-9.9438 7.0788,-24.6117 15.961,-66.7139 40.1295,-147.1843 58.4069,-194.4692 16.5278,-42.7589 12.8372,-49.2939 31.1876,55.2212 6.8935,39.265 16.2726,92.4217 20.8425,118.1272 l 8.3082,46.7355 -14.6793,47.0842 c -13.2994,42.6589 -25.7573,87.4692 -32.812,118.0226 -1.4555,6.2924 -4.7646,20.2673 -7.3558,31.0538 -2.589,10.7868 -4.7135,20.4661 -4.7135,21.5094 0,1.0417 -2.0273,10.7526 -4.5041,21.5768 -4.167,18.2121 -12.5926,66.0633 -22.3969,127.2303 -2.2164,13.8442 -7.6678,57.2976 -12.1085,96.5642 l -8.0737,71.3915 -28.9193,3.1969 c -38.8813,4.2947 -82.3735,11.0912 -105.0867,16.4236 -37.2518,8.7432 -50.5194,10.5249 -50.5194,6.7813 z m -473.388,-1434.6166 29.8526,-286.038 561.5139,-25.777 -31.0647,278.4693 z m 579.8661,-579.052 c -17.474,-27.005 -25.6754,-66.958 -25.6039,-124.727 0.03,-30.966 0.6639,-38.732 5.0505,-62.694 5.7471,-31.387 16.6964,-58.461 26.6161,-65.814 l 296.9177,-48.722 c 17.9543,16.2 29.6287,53.744 33.6221,108.12 4.2181,57.418 -6.078,116.487 -25.9941,149.156 z"
+       id="path4022"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="sssssssssssscsssccccscsssscssscssssssssssscssssssssssccscccssscscccssssscssscssssssssccccsscssssssscccscssccssssssssssssccccsssssscccscssssccscsssssssssccccssccsssssscsssccscsssssssccssssssssssscsssscssccsssssccssscssssssssccsscssccccsssssssscsssssssssssscsssssssssscscssssssssccsssscccssssscsssscssssssssssscsssscsccsssscsssccccccssccscc" />
+    <path
+       style="fill:none;stroke:#ffffff;stroke-width:354.33071899;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1062.99212598, 1062.99212598;stroke-dashoffset:2125.98364258;stroke-opacity:1"
+       d="m 341.9922,463.95706 -1e-5,-56090.55306"
+       id="path3971-2-7"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.80000019;marker:none;enable-background:accumulate"
+       d="m 2713.3232,-21676.405 -438.7443,127.24 718.3815,-2258.527 718.3809,2258.527 -439.9157,-116.636 v 2110.078 h -558.1024 z"
+       id="rect3973"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccccc" />
+    <path
+       style="fill:none;stroke:#ffffff;stroke-width:351.35113525;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:2125.98364258;stroke-opacity:1"
+       d="M 5479.7875,463.95706 V -55626.596"
+       id="path3971-2-7-4-4"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+  </g>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="Viewport"
+     style="display:none"
+     transform="translate(0,37311.023)">
+    <path
+       transform="translate(-164.82684,18492.735)"
+       style="color:#000000;fill:#000000;fill-opacity:0.5078125;stroke:none;stroke-width:354.33071899;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       d="m 164.82684,-55803.758 5492.12596,0 0,3295.276 -5492.12596,0 z"
+       id="rect2997-5"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <rect
+       style="color:#000000;fill:#000000;fill-opacity:0.5078125;stroke:none;stroke-width:354.33071899;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       id="rect2997"
+       width="5492.126"
+       height="3295.2756"
+       x="-2.2737368e-13"
+       y="15838.582" />
+  </g>
+</svg>
diff --git a/images/bumpy_road.png b/images/bumpy_road.png
new file mode 100644 (file)
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 (file)
index 0000000..6daa87d
--- /dev/null
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="5492.126"
+   height="56444.879"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="bumpy_road.svg"
+   inkscape:export-filename="/home/ao2/WIP/pygame-experiments/road.png"
+   inkscape:export-xdpi="13.11"
+   inkscape:export-ydpi="13.11">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.012259747"
+     inkscape:cx="2746.063"
+     inkscape:cy="28222.441"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1280"
+     inkscape:window-height="911"
+     inkscape:window-x="0"
+     inkscape:window-y="28"
+     inkscape:window-maximized="1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     units="cm" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="layer3"
+     inkscape:label="Background"
+     style="display:inline"
+     transform="translate(0,37311.023)"
+     sodipodi:insensitive="true">
+    <path
+       transform="translate(-164.82684,18492.735)"
+       style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.80000019;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       d="m 164.82684,-55803.758 5492.12596,0 0,56444.87909 -5492.12596,0 z"
+       id="rect4044"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+  <g
+     inkscape:label="Lane"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-164.82684,55803.758)"
+     style="display:inline">
+    <path
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#555753;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;enable-background:accumulate"
+       d="m 164.82698,-55803.746 0.009,56444.86691 H 5656.9527 V -55803.746 Z"
+       id="rect3094-9"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:none;stroke:#ffffff;stroke-width:354.33071899;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1062.99212598, 1062.99212598;stroke-dashoffset:2125.98364258;stroke-opacity:1"
+       d="m 341.9922,463.95706 -1e-5,-56090.55306"
+       id="path3971-2-7"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="display:inline;fill:#555753;stroke:#ffffff;stroke-width:354.33071899;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1062.99212598, 1062.99212598;stroke-dashoffset:2125.98364258;stroke-opacity:1"
+       d="M 5479.7875,463.95555 V -55626.597"
+       id="path3971-2-7-7"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="display:inline;fill:none;stroke:#ffffff;stroke-width:354.33099365;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:2125.98535156;stroke-opacity:1"
+       d="m 2910.8898,463.95555 c 0,0 0,-2337.10635 0,-3505.65955 0,-1172.9171 1081.0261,-2144.5229 1100,-3305.6595 21.0522,-1288.3203 -928.9006,-2428.5779 -1100,-3705.6595 -199.4852,-1488.953 83.3323,-3003.496 100,-4505.66 9.2668,-835.168 0,-1670.439 0,-2505.659 0,-735.22 7.6483,-1470.48 0,-2205.66 -16.6676,-1602.146 -369.004,-3226.169 -100,-4805.659 191.5606,-1124.771 1169.9172,-2065.089 1200,-3205.66 35.0701,-1329.66 -910.0065,-2507.533 -1200,-3805.659 -271.7431,-1216.43 -416.4789,-2462.048 -500,-3705.66 -73.8364,-1099.41 -144.3288,-2213.266 0,-3305.659 235.3611,-1781.396 1579.0946,-3417.731 1400,-5205.66 -67.0289,-669.159 -846.253,-1135.302 -900,-1805.659 -89.4577,-1115.757 974.5796,-2086.611 1000,-3205.66 29.7871,-1311.278 -1000,-2494.043 -1000,-3805.659 0,-1212.78 0,-3505.66 0,-3505.66"
+       id="path3971-2-7-7-2"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="csaaaaaaaaaaaaasc" />
+    <rect
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ef2929;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:354.33099365;marker:none;enable-background:accumulate"
+       id="rect2991"
+       width="5492.126"
+       height="7.0866141"
+       x="164.82684"
+       y="-1010.0585"
+       inkscape:export-xdpi="13.11"
+       inkscape:export-ydpi="13.11" />
+    <rect
+       transform="translate(164.82684,-18492.735)"
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ef2929;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:354.33099365;marker:none;enable-background:accumulate"
+       id="rect2991-2"
+       width="5492.126"
+       height="7.0866141"
+       x="-8.2812494e-06"
+       y="-35666.93"
+       inkscape:export-xdpi="13.11"
+       inkscape:export-ydpi="13.11" />
+  </g>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="Viewport"
+     style="display:none"
+     transform="translate(0,37311.023)">
+    <path
+       transform="translate(-164.82684,18492.735)"
+       style="color:#000000;fill:#000000;fill-opacity:0.5078125;stroke:none;stroke-width:354.33071899;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       d="m 164.82684,-55803.758 5492.12596,0 0,3295.276 -5492.12596,0 z"
+       id="rect2997-5"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <rect
+       style="color:#000000;fill:#000000;fill-opacity:0.5078125;stroke:none;stroke-width:354.33071899;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       id="rect2997"
+       width="5492.126"
+       height="3295.2756"
+       x="-2.2737368e-13"
+       y="15838.582" />
+  </g>
+</svg>
diff --git a/projective_split.c b/projective_split.c
new file mode 100644 (file)
index 0000000..b20a418
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * Separate calculating projected coordinates and transforming the pixels.
+ *
+ * Copyright (C) 2018  Antonio Ospite <ao2@ao2.it>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * 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 <arm_neon.h>
+
+/*
+ * 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 (file)
index 0000000..261220c
--- /dev/null
@@ -0,0 +1,50 @@
+/* 
+ * Separate calculating projected coordinates and transforming the pixels.
+ *
+ * Copyright (C) 2018  Antonio Ospite <ao2@ao2.it>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef PROJECTIVE_SPLIT_H
+#define PROJECTIVE_SPLIT_H
+
+#include <leptonica/allheaders.h>
+
+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 */