diff --git a/README.md b/README.md index 8a33eca..a4fca74 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,24 @@ These are the sections describing the sensors. * `rate=15` the refresh rate in fps to use for the sensor * `fmt=BGGR8` sets the pixel and bus formats used when capturing from the sensor, only BGGR8 is fully supported * `rotate=90` the rotation angle to make the sensor match the screen + +# Post processing + +Megapixels only captures raw frames and stores .dng files. It captures a 5 frame burst and saves it to a temporary +location. Then the postprocessing script is run which will generate the final .jpg file and writes it into the +pictures directory. Megapixels looks for the post processing script in the following locations: + +* ./postprocess.sh +* $XDG_CONFIG_DIR/megapixels/postprocess.sh +* ~/.config/megapixels/postprocess.sh +* /etc/megapixels/postprocess.sh +* /usr/share/megapixels/postprocess.sh + +The bundled postprocess.sh script will copy the first frame of the burst into the picture directory as an DNG +file and if dcraw and imagemagick are installed it will generate a JPG and also write that to the picture +directory. It supports either the full dcraw or dcraw_emu from libraw. + +It is possible to write your own post processing pipeline my providing your own `postprocess.sh` script at +one of the above locations. The first argument to the script is the directory containing the temporary +burst files and the second argument is the final path for the image without an extension. For more details +see postprocess.sh in this repository. diff --git a/main.c b/main.c index 24d62d3..ad3ccf9 100644 --- a/main.c +++ b/main.c @@ -86,6 +86,9 @@ static int preview_height = -1; static char *last_path = NULL; static int auto_exposure = 1; static int auto_gain = 1; +static int burst_length = 5; +static char burst_dir[20]; +static char processing_script[512]; // Widgets GtkWidget *preview; @@ -404,6 +407,8 @@ process_image(const int *p, int size) struct tm tim; uint8_t *pixels; char fname[255]; + char fname_target[255]; + char command[1024]; char timestamp[30]; char uniquecameramodel[255]; GdkPixbuf *pixbuf; @@ -453,7 +458,9 @@ process_image(const int *p, int size) tim = *(localtime(&rawtime)); strftime(timestamp, 30, "%Y%m%d%H%M%S", &tim); strftime(datetime, 20, "%Y:%m:%d %H:%M:%S", &tim); - sprintf(fname, "%s/Pictures/IMG%s-%d.dng", getenv("HOME"), timestamp, capture); + + sprintf(fname_target, "%s/Pictures/IMG%s", getenv("HOME"), timestamp); + sprintf(fname, "%s/%d.dng", burst_dir, burst_length - capture); if(!(tif = TIFFOpen(fname, "w"))) { printf("Could not open tiff\n"); @@ -516,7 +523,7 @@ process_image(const int *p, int size) TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 1, ¤t.blacklevel); } TIFFCheckpointDirectory(tif); - printf("Writing frame\n"); + printf("Writing frame to %s\n", fname); unsigned char *pLine = (unsigned char*)malloc(current.width); for(int row = 0; row < current.height; row++){ @@ -551,8 +558,8 @@ process_image(const int *p, int size) TIFFClose(tif); - // Update the thumbnail if this is the last frame if (capture == 0) { + // Update the thumbnail if this is the last frame pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, current.width / (skip*2), current.height / (skip*2)); pixels = gdk_pixbuf_get_pixels(pixbuf); quick_debayer_bggr8((const uint8_t *)p, pixels, current.width, current.height, skip); @@ -573,6 +580,12 @@ process_image(const int *p, int size) g_printerr("%s\n", error->message); g_clear_error(&error); } + + // Start post-processing the captured burst + g_printerr("Post process %s to %s.ext\n", burst_dir, fname_target); + sprintf(command, "%s %s %s &", processing_script, burst_dir, fname_target); + system(command); + } } } @@ -959,7 +972,18 @@ on_open_directory_clicked(GtkWidget *widget, gpointer user_data) void on_shutter_clicked(GtkWidget *widget, gpointer user_data) { - capture = 5; + char template[] = "/tmp/megapixels.XXXXXX"; + char *tempdir; + tempdir = mkdtemp(template); + + if (tempdir == NULL) { + g_printerr("Could not make capture directory %s\n", template); + exit (EXIT_FAILURE); + } + + strcpy(burst_dir, tempdir); + + capture = burst_length; } void @@ -1065,12 +1089,69 @@ find_config(char *conffile) return -1; } +int +find_processor(char *script) +{ + char *xdg_config_home; + char filename[] = "postprocess.sh"; + wordexp_t exp_result; + + // Resolve XDG stuff + if ((xdg_config_home = getenv("XDG_CONFIG_HOME")) == NULL) { + xdg_config_home = "~/.config"; + } + wordexp(xdg_config_home, &exp_result, 0); + xdg_config_home = strdup(exp_result.we_wordv[0]); + wordfree(&exp_result); + + // Check postprocess.h in the current working directory + sprintf(script, "%s", filename); + if(access(script, F_OK) != -1) { + sprintf(script, "./%s", filename); + printf("Found postprocessor script at %s\n", script); + return 0; + } + + // Check for a script in XDG_CONFIG_HOME + sprintf(script, "%s/megapixels/%s", xdg_config_home, filename); + if(access(script, F_OK) != -1) { + printf("Found postprocessor script at %s\n", script); + return 0; + } + + // Check user overridden /etc/megapixels/postprocessor.sh + sprintf(script, "%s/megapixels/%s", SYSCONFDIR, filename); + if(access(script, F_OK) != -1) { + printf("Found postprocessor script at %s\n", script); + return 0; + } + + // Check packaged /usr/share/megapixels/postprocessor.sh + sprintf(script, "%s/megapixels/%s", DATADIR, filename); + if(access(script, F_OK) != -1) { + printf("Found postprocessor script at %s\n", script); + return 0; + } + + return -1; +} + int main(int argc, char *argv[]) { + int ret; char conffile[512]; - find_config(conffile); + ret = find_config(conffile); + if (ret) { + g_printerr("Could not find any config file\n"); + return ret; + } + ret = find_processor(processing_script); + if (ret) { + g_printerr("Could not find any post-process script\n"); + return ret; + } TIFFSetTagExtender(register_custom_tiff_tags); diff --git a/meson.build b/meson.build index e9a1bf4..b0376de 100644 --- a/meson.build +++ b/meson.build @@ -31,3 +31,7 @@ install_data([ 'config/pine64,pinetab.ini', ], install_dir : get_option('datadir') / 'megapixels/config/') + +install_data(['postprocess.sh'], + install_dir : get_option('datadir') / 'megapixels/', + install_mode: 'rwxr-xr-x') diff --git a/postprocess.sh b/postprocess.sh new file mode 100755 index 0000000..dc9ad98 --- /dev/null +++ b/postprocess.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +# The post-processing script gets called after taking a burst of +# pictures into a temporary directory. The first argument is the +# directory containing the raw files in the burst. The contents +# are 1.dng, 2.dng.... up to the number of photos in the burst. +# +# The second argument is the filename for the final photo without +# the extension, like "/home/user/Pictures/IMG202104031234" +# +# The post-processing script is responsible for cleaning up +# temporary directory for the burst. + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 [burst-dir] [target-name]" + exit 2 +fi + +BURST_DIR="$1" +TARGET_NAME="$2" + +# Copy the first frame of the burst as the raw photo +cp "$BURST_DIR"/1.dng "$TARGET_NAME.dng" + +# Create a .jpg if raw processing tools are installed +DCRAW="" +if command -v "dcraw_emu" &> /dev/null +then + DCRAW=dcraw_emu +fi +if command -v "dcraw" &> /dev/null +then + DCRAW=dcraw +fi + +if [ -n "$DCRAW" ]; then + # +M use embedded color matrix + # -H 4 Recover highlights by rebuilding them + # -o 1 Output in sRGB colorspace + # -q 3 Debayer with AHD algorithm + # -T Output TIFF + # -fbdd 1 Raw denoising with FBDD + $DCRAW +M -H 4 -o 1 -q 3 -T -fbdd 1 $BURST_DIR/1.dng + + if command -v convert &> /dev/null + then + convert "$BURST_DIR"/1.dng.tiff "$TARGET_NAME.jpg" + else + cp "$BURST_DIR"/1.dng.tiff "$TARGET_NAME.tiff" + fi +fi + +# Clean up the temp dir containing the burst +rm -rf "$BURST_DIR"