diff --git a/keogram.cpp b/keogram.cpp index 6b1c269b6..3459c715d 100644 --- a/keogram.cpp +++ b/keogram.cpp @@ -4,19 +4,20 @@ // Rotation added by Agustin Nunez @agnunez // SPDX-License-Identifier: MIT -#include +#include #include +#include +#include +#include #include #include -#include #include -#include #ifdef OPENCV_C_HEADERS #include #include -#include #include +#include #endif #include @@ -34,175 +35,259 @@ //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- +int loglevel = 0; -int main(int argc, char *argv[]) -{ - if (argc < 4) - { - std::cout << KRED << "You need to pass 3 arguments: source directory, " - "image extension, output file" - << std::endl; - std::cout << "Optionally you can pass after them: " << std::endl; - std::cout << " -no-label - Disable hour labels" << std::endl; - std::cout << " -fontname = Font Name - Default = 0 - Font Types " - "(0-7), Ex. 0 = simplex, 4 = triplex, 7 = script" - << std::endl; - std::cout << " -fontcolor = Font Color - Default = 255 0 0 - Text " - "blue (BRG)" - << std::endl; - std::cout << " -fonttype = Font Type - Default = 8 - Font Line " - "Type,(0-2), 0 = AA, 1 = 8, 2 = 4" - << std::endl; - std::cout << " -fontsize - Default = 2.0 - Text Font Size" << std::endl; - std::cout << " -fontline - Default = 3 - Text Font " - "Line Thickness" - << std::endl; - std::cout << " -rotate - Default = 0 - Rotation angle anticlockwise (deg)" << std::endl; - std::cout << " ex: keogram ../images/current/ jpg keogram.jpg -fontsize 2" << std::endl; - std::cout << " ex: keogram . png /home/pi/allsky/keogram.jpg -no-label" << KNRM << std::endl; - return 3; - } +void usage_and_exit(int x) { + std::cout + << "Usage:\tkeogram -d -e -o []" + << std::endl; + if (x) + std::cout + << KRED + << "Source directory, image extension, and output file are required" + << std::endl; - std::string directory = argv[1]; - std::string extension = argv[2]; - std::string outputfile = argv[3]; - - bool labelsEnabled = true; - int fontFace = cv::FONT_HERSHEY_SCRIPT_SIMPLEX; - double fontScale = 2; - int fontType = 8; - int thickness = 3; - unsigned char fontColor[3] = { 255, 0, 0 }; - double angle = 0; - - // Handle optional parameters - for (int a = 4; a < argc; ++a) - { - if (!strcmp(argv[a], "-no-label")) - { - labelsEnabled = false; - } - else if (!strcmp(argv[a], "-fontname")) - { - fontFace = atoi(argv[++a]); - } - else if (!strcmp(argv[a], "-fonttype")) - { - fontType = atoi(argv[++a]); - } - else if (!strcmp(argv[a], "-fontsize")) - { - fontScale = atof(argv[++a]); - } - else if (!strcmp(argv[a], "-fontline")) - { - thickness = atoi(argv[++a]); - } - else if (!strcmp(argv[a], "-fontcolor")) - { - fontColor[0] = atoi(argv[++a]); - fontColor[1] = atoi(argv[++a]); - fontColor[2] = atoi(argv[++a]); - } - else if (!strcmp(argv[a], "-rotate")) - { - angle = atoi(argv[++a]); - } - } + std::cout << KNRM << std::endl; + std::cout << "Arguments:" << std::endl; + std::cout << "-d | --directory : directory from which to load images " + "(required)" + << std::endl; + std::cout << "-e | --extension : image extension to process (required)" + << std::endl; + std::cout << "-o | --output-file : name of output file (required)" + << std::endl; + std::cout << "-r | --rotate : number of degrees to rotate image, " + "counterclockwise (0)" + << std::endl; + std::cout << "-h | --help : display this help message" << std::endl; + std::cout << "-v | --verbose : Increase logging verbosity" << std::endl; + std::cout << "-n | --no-label : Disable hour labels" << std::endl; + std::cout + << "-C | --font-color : label font color, in HTML format (0000ff)" + << std::endl; + std::cout << "-L | --font-line : font line thickness (3)" << std::endl; + std::cout << "-N | --font-name : font name (simplex)" << std::endl; + std::cout << "-S | --font-side : font size (2.0)" << std::endl; + std::cout << "-T | --font-type : font line type (1)" << std::endl; - glob_t files; - std::string wildcard = directory + "/*." + extension; - glob(wildcard.c_str(), 0, NULL, &files); - if (files.gl_pathc == 0) - { - globfree(&files); - std::cout << "No images found, exiting." << std::endl; - return 0; + std::cout << KNRM << std::endl; + std::cout + << "Font name is one of these OpenCV font names:\n\tSimplex, Plain, " + "Duplex, Complex, Triplex, ComplexSmall, ScriptSimplex, ScriptComplex" + << std::endl; + std::cout << "Font Type is an OpenCV line type: 0=antialias, 1=8-connected, " + "2=4-connected" + << std::endl; + std::cout << KNRM << std::endl; + std::cout << " ex: keogram --directory ../images/current/ --extension jpg " + "--output-file keogram.jpg --font-size 2" + << std::endl; + std::cout << " ex: keogram -d . -e png -o /home/pi/allsky/keogram.jpg -n" + << KNRM << std::endl; + exit(x); +} + +int get_font_by_name(char* s) { + // case insensitively check the user-specified font, and use something + // sensible in case of erroneous input + if (strcasecmp(s, "plain") == 0) + return cv::FONT_HERSHEY_PLAIN; + if (strcasecmp(s, "duplex") == 0) + return cv::FONT_HERSHEY_DUPLEX; + if (strcasecmp(s, "complex") == 0) + return cv::FONT_HERSHEY_COMPLEX; + if (strcasecmp(s, "complexsmall") == 0) + return cv::FONT_HERSHEY_COMPLEX_SMALL; + if (strcasecmp(s, "triplex") == 0) + return cv::FONT_HERSHEY_TRIPLEX; + if (strcasecmp(s, "scriptsimplex") == 0) + return cv::FONT_HERSHEY_SCRIPT_SIMPLEX; + if (strcasecmp(s, "scriptcomplex") == 0) + return cv::FONT_HERSHEY_SCRIPT_COMPLEX; + if (strcasecmp(s, "simplex")) // yes, this is intentional + std::cout << KRED << "Unknown font '" << s << "', using SIMPLEX " << KNRM + << std::endl; + return cv::FONT_HERSHEY_SIMPLEX; +} + +int main(int argc, char* argv[]) { + int c; + bool labelsEnabled = true; + int fontFace = cv::FONT_HERSHEY_SCRIPT_SIMPLEX; + double fontScale = 2; + int fontType = cv::LINE_8; + int thickness = 3; + unsigned char fontColor[3] = {255, 0, 0}; + double angle = 0; + std::string directory, extension, outputfile; + + while (1) { // getopt loop + int option_index = 0; + static struct option long_options[] = { + {"directory", required_argument, 0, 'd'}, + {"extension", required_argument, 0, 'e'}, + {"output", required_argument, 0, 'o'}, + {"font-color", required_argument, 0, 'C'}, + {"font-line", required_argument, 0, 'L'}, + {"font-name", required_argument, 0, 'N'}, + {"font-size", required_argument, 0, 'S'}, + {"font-type", required_argument, 0, 'T'}, + {"rotate", required_argument, 0, 'r'}, + {"no-label", no_argument, 0, 'n'}, + {"verbose", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0}}; + + c = getopt_long(argc, argv, "d:e:o:r:C:L:N:S:T:nvh", long_options, + &option_index); + if (c == -1) + break; + switch (c) { // option switch + int tmp; + case 'h': + usage_and_exit(0); + // NOTREACHED + case 'd': + directory = optarg; + break; + case 'e': + extension = optarg; + break; + case 'o': + outputfile = optarg; + break; + case 'n': + labelsEnabled = false; + break; + case 'r': + angle = atof(optarg); + break; + case 'v': + loglevel++; + break; + case 'C': + if (optarg[0] == '#') // skip '#' if input is like '#coffee' + optarg++; + sscanf(optarg, "%06x", &tmp); + fontColor[0] = (tmp >> 16) & 0xff; + fontColor[1] = (tmp >> 8) & 0xff; + fontColor[2] = tmp & 0xff; + break; + case 'L': + thickness = atoi(optarg); + break; + case 'N': + fontFace = get_font_by_name(optarg); + break; + case 'S': + fontScale = atof(optarg); + break; + case 'T': + tmp = atoi(optarg); + if (tmp == 2) + fontType = cv::LINE_4; + else if (tmp == 0) + fontType = cv::LINE_AA; + else + fontType = cv::LINE_8; + break; + default: + break; + } // option switch + } // getopt loop + + if (directory.empty() || extension.empty() || outputfile.empty()) + usage_and_exit(3); + + glob_t files; + std::string wildcard = directory + "/*." + extension; + glob(wildcard.c_str(), 0, NULL, &files); + if (files.gl_pathc == 0) { + globfree(&files); + std::cout << "No images found, exiting." << std::endl; + return 0; + } + + cv::Mat accumulated; + + int prevHour = -1; + + for (size_t f = 0; f < files.gl_pathc; f++) { + cv::Mat imagesrc = cv::imread(files.gl_pathv[f], cv::IMREAD_UNCHANGED); + if (!imagesrc.data) { + std::cout << "Error reading file " << basename(files.gl_pathv[f]) + << std::endl; + continue; } - cv::Mat accumulated; + if (loglevel) + std::cout << "[" << f + 1 << "/" << files.gl_pathc << "] " + << basename(files.gl_pathv[f]) << std::endl; - int prevHour = -1; + cv::Point2f center((imagesrc.cols - 1) / 2.0, (imagesrc.rows - 1) / 2.0); + cv::Mat rot = cv::getRotationMatrix2D(center, angle, 1.0); + cv::Rect2f bbox = + cv::RotatedRect(cv::Point2f(), imagesrc.size(), angle).boundingRect2f(); + rot.at(0, 2) += bbox.width / 2.0 - imagesrc.cols / 2.0; + rot.at(1, 2) += bbox.height / 2.0 - imagesrc.rows / 2.0; + cv::Mat imagedst; + cv::warpAffine(imagesrc, imagedst, rot, bbox.size()); + if (accumulated.empty()) { + accumulated.create(imagedst.rows, files.gl_pathc, imagesrc.type()); + } - for (size_t f = 0; f < files.gl_pathc; f++) - { - cv::Mat imagesrc = cv::imread(files.gl_pathv[f], cv::IMREAD_UNCHANGED); - if (!imagesrc.data) - { - std::cout << "Error reading file " << basename(files.gl_pathv[f]) << std::endl; - continue; - } + // Copy middle column to destination + imagedst.col(imagedst.cols / 2).copyTo(accumulated.col(f)); - std::cout << "[" << f + 1 << "/" << files.gl_pathc << "] " << basename(files.gl_pathv[f]) << std::endl; - - //double angle = -36; - cv::Point2f center((imagesrc.cols-1)/2.0, (imagesrc.rows-1)/2.0); - cv::Mat rot = cv::getRotationMatrix2D(center, angle, 1.0); - cv::Rect2f bbox = cv::RotatedRect(cv::Point2f(), imagesrc.size(), angle).boundingRect2f(); - rot.at(0,2) += bbox.width/2.0 - imagesrc.cols/2.0; - rot.at(1,2) += bbox.height/2.0 - imagesrc.rows/2.0; - cv::Mat imagedst; - cv::warpAffine(imagesrc, imagedst, rot, bbox.size()); - if (accumulated.empty()) - { - accumulated.create(imagedst.rows, files.gl_pathc, imagesrc.type()); - } + if (labelsEnabled) { + struct stat s; + stat(files.gl_pathv[f], &s); - // Copy middle column to destination - imagedst.col(imagedst.cols / 2).copyTo(accumulated.col(f)); - - if (labelsEnabled) - { - struct stat s; - stat(files.gl_pathv[f], &s); - - struct tm *t = localtime(&s.st_mtime); - if (t->tm_hour != prevHour) - { - if (prevHour != -1) - { - // Draw a dashed line and label for hour - cv::LineIterator it(accumulated, cv::Point(f, 0), cv::Point(f, accumulated.rows)); - for (int i = 0; i < it.count; i++, ++it) - { - // 4 pixel dashed line - if (i & 4) - { - uchar *p = *it; - for (int c = 0; c < it.elemSize; c++) - { - *p = ~(*p); - p++; - } - } - } - - // Draw text label to the left of the dash - char hour[3]; - snprintf(hour, 3, "%02d", t->tm_hour); - std::string text(hour); - int baseline = 0; - cv::Size textSize = cv::getTextSize(text, fontFace, fontScale, thickness, &baseline); - - if (f - textSize.width >= 0) - { - cv::putText(accumulated, text, - cv::Point(f - textSize.width, accumulated.rows - textSize.height), fontFace, - fontScale, cv::Scalar(fontColor[0], fontColor[1], fontColor[2]), thickness, - fontType); - } - } - prevHour = t->tm_hour; + struct tm* t = localtime(&s.st_mtime); + if (t->tm_hour != prevHour) { + if (prevHour != -1) { + // Draw a dashed line and label for hour + cv::LineIterator it(accumulated, cv::Point(f, 0), + cv::Point(f, accumulated.rows)); + for (int i = 0; i < it.count; i++, ++it) { + // 4 pixel dashed line + if (i & 4) { + uchar* p = *it; + for (int c = 0; c < it.elemSize; c++) { + *p = ~(*p); + p++; + } } + } + + // Draw text label to the left of the dash + char hour[3]; + snprintf(hour, 3, "%02d", t->tm_hour); + std::string text(hour); + int baseline = 0; + cv::Size textSize = + cv::getTextSize(text, fontFace, fontScale, thickness, &baseline); + + if (f - textSize.width >= 0) { + cv::putText(accumulated, text, + cv::Point(f - textSize.width, + accumulated.rows - textSize.height), + fontFace, fontScale, + cv::Scalar(fontColor[0], fontColor[1], fontColor[2]), + thickness, fontType); + } } + prevHour = t->tm_hour; + } } - globfree(&files); + } + globfree(&files); - std::vector compression_params; - compression_params.push_back(CV_IMWRITE_PNG_COMPRESSION); - compression_params.push_back(9); - compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); - compression_params.push_back(95); + std::vector compression_params; + compression_params.push_back(CV_IMWRITE_PNG_COMPRESSION); + compression_params.push_back(9); + compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); + compression_params.push_back(95); - cv::imwrite(outputfile, accumulated, compression_params); + cv::imwrite(outputfile, accumulated, compression_params); } diff --git a/scripts/endOfNight.sh b/scripts/endOfNight.sh index 47d80685c..4c6fc0f65 100755 --- a/scripts/endOfNight.sh +++ b/scripts/endOfNight.sh @@ -1,14 +1,14 @@ #!/bin/bash if [ $# -eq 1 ] ; then - if [ "x$1" = "x-h" ] ; then - echo "Usage: $BASH_ARGV0 [YYYYmmdd]" - exit - else - LAST_NIGHT=$1 - fi + if [ "x$1" = "x-h" ] ; then + echo "Usage: $BASH_ARGV0 [YYYYmmdd]" + exit + else + LAST_NIGHT=$1 + fi else - LAST_NIGHT=$(date -d '12 hours ago' +'%Y%m%d') + LAST_NIGHT=$(date -d '12 hours ago' +'%Y%m%d') fi source $ALLSKY_HOME/config.sh @@ -16,13 +16,13 @@ source $ALLSKY_HOME/scripts/filename.sh source $ALLSKY_HOME/scripts/ftp-settings.sh cd $ALLSKY_HOME/scripts -ME="$(basename "$BASH_ARGV0")" # Include script name in output so it's easier to find in the log file +ME="$(basename "$BASH_ARGV0")" # Include script name in output so it's easier to find in the log file # Post end of night data. This includes next twilight time if [[ $POST_END_OF_NIGHT_DATA == "true" ]]; then - echo -e "$ME: Posting next twilight time to let server know when to resume liveview\n" - ./postData.sh - echo -e "\n" + echo -e "$ME: Posting next twilight time to let server know when to resume liveview\n" + ./postData.sh + echo -e "\n" fi LAST_NIGHT_DIR="$ALLSKY_HOME/images/$LAST_NIGHT" @@ -32,8 +32,8 @@ LAST_NIGHT_DIR="$ALLSKY_HOME/images/$LAST_NIGHT" # and isn't necessary unless your system produces corrupt images which then # generate funny colors in the summary images... if [[ "$REMOVE_BAD_IMAGES" == "true" ]]; then - echo -e "$ME: Removing bad images\n" - ./removeBadImages.sh $LAST_NIGHT_DIR + echo -e "$ME: Removing bad images\n" + ./removeBadImages.sh $LAST_NIGHT_DIR fi TMP_DIR="$ALLSKY_HOME/tmp" @@ -43,75 +43,64 @@ mkdir -p "$TMP_DIR" if [[ $KEOGRAM == "true" ]]; then echo -e "$ME: Generating Keogram\n" mkdir -p $LAST_NIGHT_DIR/keogram/ - OUTPUT="$LAST_NIGHT_DIR/keogram/keogram-$LAST_NIGHT.$EXTENSION" - # The keogram command outputs one line for each of the many hundreds of files, - # and this adds needless clutter to the log file, so send output to a tmp file so we can output the - # number of images. - - TMP="$TMP_DIR/keogramTMP.txt" - ../keogram $LAST_NIGHT_DIR/ $EXTENSION $OUTPUT > ${TMP} - RETCODE=$? - if [[ $UPLOAD_KEOGRAM == "true" && $RETCODE = 0 ]] ; then - if [[ $PROTOCOL == "S3" ]] ; then - $AWS_CLI_DIR/aws s3 cp $OUTPUT s3://$S3_BUCKET$KEOGRAM_DIR --acl $S3_ACL & - elif [[ $PROTOCOL == "local" ]] ; then - cp $OUTPUT $KEOGRAM_DIR & - else - lftp "$PROTOCOL://$USER:$PASSWORD@$HOST:$KEOGRAM_DIR" \ - -e "set net:max-retries 1; put "$OUTPUT"; bye" & - fi - fi - echo -e "$ME: Processed $(wc -l < ${TMP}) keogram files\n" - # Leave ${TMP} in case the user needs to debug something. - - # Optionally copy to the local website in addition to the upload above. - if [ "$WEB_KEOGRAM_DIR" != "" ]; then - cp $OUTPUT "$WEB_KEOGRAM_DIR" - fi + OUTPUT="$LAST_NIGHT_DIR/keogram/keogram-$LAST_NIGHT.$EXTENSION" + ${ALLSKY_HOME}/keogram -d $LAST_NIGHT_DIR/ -e $EXTENSION -o $OUTPUT + RETCODE=$? + if [[ $UPLOAD_KEOGRAM == "true" && $RETCODE = 0 ]] ; then + if [[ $PROTOCOL == "S3" ]] ; then + $AWS_CLI_DIR/aws s3 cp $OUTPUT s3://$S3_BUCKET$KEOGRAM_DIR --acl $S3_ACL & + elif [[ $PROTOCOL == "local" ]] ; then + cp $OUTPUT $KEOGRAM_DIR & + else + lftp "$PROTOCOL://$USER:$PASSWORD@$HOST:$KEOGRAM_DIR" \ + -e "set net:max-retries 1; put "$OUTPUT"; bye" & + fi + fi + + # Optionally copy to the local website in addition to the upload above. + if [ "$WEB_KEOGRAM_DIR" != "" ]; then + cp $OUTPUT "$WEB_KEOGRAM_DIR" + fi fi # Generate startrails from collected images. # Threshold set to 0.1 by default in config.sh to avoid stacking over-exposed images. if [[ $STARTRAILS == "true" ]]; then - echo -e "$ME: Generating Startrails\n" - mkdir -p $LAST_NIGHT_DIR/startrails/ - OUTPUT="$LAST_NIGHT_DIR/startrails/startrails-$LAST_NIGHT.$EXTENSION" - # The startrails command outputs one line for each of the many hundreds of files, - # and this adds needless clutter to the log file, so send output to a tmp file so we can output the - # number of images. - TMP="$TMP_DIR/startrailsTMP.txt" - ../startrails $LAST_NIGHT_DIR/ $EXTENSION $BRIGHTNESS_THRESHOLD $OUTPUT > ${TMP} - RETCODE=$? - if [[ $UPLOAD_STARTRAILS == "true" && $RETCODE == 0 ]] ; then - if [[ $PROTOCOL == "S3" ]] ; then - $AWS_CLI_DIR/aws s3 cp $OUTPUT s3://$S3_BUCKET$STARTRAILS_DIR --acl $S3_ACL & - elif [[ $PROTOCOL == "local" ]] ; then - cp $OUTPUT $STARTRAILS_DIR & - else - lftp "$PROTOCOL"://"$USER":"$PASSWORD"@"$HOST":"$STARTRAILS_DIR" \ - -e "set net:max-retries 1; put $OUTPUT; bye" & - fi - fi - echo -e "$ME: Processed $(wc -l < ${TMP}) startrails files. Summary:\n" - grep "^Minimum" "${TMP}" - # Leave ${TMP} in case the user needs to debug something. - - # Optionally copy to the local website in addition to the upload above. - if [ "$WEB_STARTRAILS_DIR" != "" ]; then - cp $OUTPUT "$WEB_STARTRAILS_DIR" - fi + echo -e "$ME: Generating Startrails\n" + mkdir -p $LAST_NIGHT_DIR/startrails/ + OUTPUT="$LAST_NIGHT_DIR/startrails/startrails-$LAST_NIGHT.$EXTENSION" + # The startrails command outputs one line for each of the many hundreds of files, + # and this adds needless clutter to the log file, so send output to a tmp file so we can output the + # number of images. + ${ALLSKY_HOME}/startrails -d $LAST_NIGHT_DIR/ -e $EXTENSION -b $BRIGHTNESS_THRESHOLD -o $OUTPUT + RETCODE=$? + if [[ $UPLOAD_STARTRAILS == "true" && $RETCODE == 0 ]] ; then + if [[ $PROTOCOL == "S3" ]] ; then + $AWS_CLI_DIR/aws s3 cp $OUTPUT s3://$S3_BUCKET$STARTRAILS_DIR --acl $S3_ACL & + elif [[ $PROTOCOL == "local" ]] ; then + cp $OUTPUT $STARTRAILS_DIR & + else + lftp "$PROTOCOL"://"$USER":"$PASSWORD"@"$HOST":"$STARTRAILS_DIR" \ + -e "set net:max-retries 1; put $OUTPUT; bye" & + fi + fi + + # Optionally copy to the local website in addition to the upload above. + if [ "$WEB_STARTRAILS_DIR" != "" ]; then + cp $OUTPUT "$WEB_STARTRAILS_DIR" + fi fi # Generate timelapse from collected images if [[ $TIMELAPSE == "true" ]]; then - echo -e "$ME: Generating Timelapse\n" - ./timelapse.sh $LAST_NIGHT - echo -e "\n" - - # Optionally copy to the local website in addition to the upload above. - if [ "$WEB_MP4DIR" != "" ]; then - cp $LAST_NIGHT_DIR/allsky-$LAST_NIGHT.mp4 "$WEB_MP4DIR" - fi + echo -e "$ME: Generating Timelapse\n" + ./timelapse.sh $LAST_NIGHT + echo -e "\n" + + # Optionally copy to the local website in addition to the upload above. + if [ "$WEB_MP4DIR" != "" ]; then + cp $LAST_NIGHT_DIR/allsky-$LAST_NIGHT.mp4 "$WEB_MP4DIR" + fi fi # Run custom script at the end of a night. This is run BEFORE the automatic deletion just in case you need to do something with the files before they are removed @@ -119,8 +108,8 @@ fi # Automatically delete old images and videos if [[ $AUTO_DELETE == "true" ]]; then - del=$(date --date="$NIGHTS_TO_KEEP days ago" +%Y%m%d) - for i in `find $ALLSKY_HOME/images/ -type d -name "2*"`; do # "2*" for years >= 2000 - (($del > $(basename $i))) && rm -rf $i - done + del=$(date --date="$NIGHTS_TO_KEEP days ago" +%Y%m%d) + for i in `find $ALLSKY_HOME/images/ -type d -name "2*"`; do # "2*" for years >= 2000 + (($del > $(basename $i))) && rm -rf $i + done fi diff --git a/startrails.cpp b/startrails.cpp index 1fcd9419b..70d3ba697 100644 --- a/startrails.cpp +++ b/startrails.cpp @@ -3,22 +3,24 @@ // Based on script by Thomas Jacquin // SPDX-License-Identifier: MIT -#include +#include #include -#include +#include +#include +#include #include +#include #include -#include #ifdef OPENCV_C_HEADERS #include #include -#include #include +#include #endif -#include #include +#include #define KNRM "\x1B[0m" #define KRED "\x1B[31m" @@ -32,112 +34,169 @@ //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- -int main(int argc, char *argv[]) -{ - if (argc != 5) - { - std::cout << KRED - << "You need to pass 4 arguments: source directory, file extension, brightness treshold, output file" - << KNRM << std::endl; - std::cout << " ex: startrails ../images/20180208/ jpg 0.07 startrails.jpg" << std::endl; - std::cout << " brightness ranges from 0 (black) to 1 (white)" << std::endl; - std::cout << " A moonless sky is around 0.05 while full moon can be as high as 0.4" << std::endl; - return 3; - } - std::string directory = argv[1]; - std::string extension = argv[2]; - double threshold = atof(argv[3]); - std::string outputfile = argv[4]; - - // Find files - glob_t files; - std::string wildcard = directory + "/*." + extension; - glob(wildcard.c_str(), 0, NULL, &files); - if (files.gl_pathc == 0) - { - globfree(&files); - std::cout << "No images found, exiting." << std::endl; - return 0; +void usage_and_exit(int x) { + std::cout << "Usage: startrails [-v] -d -e [-b -o " + " | -s]" + << std::endl; + if (x) { + std::cout << KRED + << "Source directory and file extension are always required." + << std::endl; + std::cout << "brightness threshold and output file are required to render " + "startrails" + << KNRM << std::endl; + } + + std::cout << std::endl << "Arguments:" << std::endl; + std::cout << "-h : display this help, then exit" << std::endl; + std::cout << "-v : increase log verbosity" << std::endl; + std::cout << "-s : print image directory statistics without producing image." + << std::endl; + std::cout << "-d : directory from which to read images" << std::endl; + std::cout << "-e : filter images to just this extension" << std::endl; + std::cout << "-o : output image filename" << std::endl; + std::cout << "-b : ranges from 0 (black) to 1 (white)" << std::endl; + std::cout << "\tA moonless sky may be as low as 0.05 while full moon can be " + "as high as 0.4" + << std::endl; + std::cout << std::endl + << "ex: startrails -b 0.07 -d ../images/20180208/ -e jpg -o " + "startrails.jpg" + << std::endl; + exit(x); +} + +int main(int argc, char* argv[]) { + std::string directory, extension, outputfile; + double threshold = -1; + int verbose = 0, stats_only = 0; + char c; + + while ((c = getopt(argc, argv, "hvsb:d:e:o:")) != -1) { + switch (c) { + case 'h': + usage_and_exit(0); + // NOTREACHED + break; + case 'v': + verbose++; + break; + case 's': + stats_only = 1; + break; + case 'b': + double tf; + tf = atof(optarg); + if (tf >= 0 && tf <= 1.0) + threshold = tf; + break; + case 'd': + directory = optarg; + break; + case 'e': + extension = optarg; + break; + case 'o': + outputfile = optarg; + break; + default: + break; } + } + + if (stats_only) { + threshold = 0; + outputfile = "/dev/null"; + } + + if (directory.empty() || extension.empty() || outputfile.empty() || + threshold < 0) + usage_and_exit(3); + + // Find files + glob_t files; + std::string wildcard = directory + "/*." + extension; + glob(wildcard.c_str(), 0, NULL, &files); + if (files.gl_pathc == 0) { + globfree(&files); + std::cout << "No images found, exiting." << std::endl; + return 0; + } + + cv::Mat accumulated; + + // Create space for statistics + cv::Mat stats; + stats.create(1, files.gl_pathc, CV_64F); - cv::Mat accumulated; - - // Create space for statistics - cv::Mat stats; - stats.create(1, files.gl_pathc, CV_64F); - - for (size_t f = 0; f < files.gl_pathc; f++) - { - cv::Mat image = cv::imread(files.gl_pathv[f], cv::IMREAD_UNCHANGED); - if (!image.data) - { - std::cout << "Error reading file " << basename(files.gl_pathv[f]) << std::endl; - stats.col(f) = 1.0; // mark as invalid - continue; - } - - cv::Scalar mean_scalar = cv::mean(image); - double mean; - switch (image.channels()) - { - default: // mono case - mean = mean_scalar.val[0]; - break; - case 3: // for color choose maximum channel - case 4: - mean = cv::max(mean_scalar[0], cv::max(mean_scalar[1], mean_scalar[2])); - break; - } - // Scale to 0-1 range - switch (image.depth()) - { - case CV_8U: - mean /= 255.0; - break; - case CV_16U: - mean /= 65535.0; - break; - } - std::cout << "[" << f + 1 << "/" << files.gl_pathc << "] " << basename(files.gl_pathv[f]) << " " << mean - << std::endl; - - stats.col(f) = mean; - - if (mean <= threshold) - { - if (accumulated.empty()) - { - image.copyTo(accumulated); - } - else - { - accumulated = cv::max(accumulated, image); - } - } + for (size_t f = 0; f < files.gl_pathc; f++) { + cv::Mat image = cv::imread(files.gl_pathv[f], cv::IMREAD_UNCHANGED); + if (!image.data) { + std::cout << "Error reading file " << basename(files.gl_pathv[f]) + << std::endl; + stats.col(f) = 1.0; // mark as invalid + continue; } - // Calculate some statistics - double min_mean, max_mean; - cv::Point min_loc; - cv::minMaxLoc(stats, &min_mean, &max_mean, &min_loc); - double mean_mean = cv::mean(stats)[0]; - - // For median, do partial sort and take middle value - std::vector vec; - stats.copyTo(vec); - std::nth_element(vec.begin(), vec.begin() + (vec.size() / 2), vec.end()); - double median_mean = vec[vec.size() / 2]; - - std::cout << "Minimum: " << min_mean << " maximum: " << max_mean << " mean: " << mean_mean - << " median: " << median_mean << std::endl; - - // If we still don't have an image (no images below threshold), copy the minimum mean image so we see why - if (accumulated.empty()) - { - std::cout << "No images below threshold, writing the minimum image only" << std::endl; - accumulated = cv::imread(files.gl_pathv[min_loc.x], cv::IMREAD_UNCHANGED); + cv::Scalar mean_scalar = cv::mean(image); + double mean; + switch (image.channels()) { + default: // mono case + mean = mean_scalar.val[0]; + break; + case 3: // for color choose maximum channel + case 4: + mean = cv::max(mean_scalar[0], cv::max(mean_scalar[1], mean_scalar[2])); + break; + } + // Scale to 0-1 range + switch (image.depth()) { + case CV_8U: + mean /= 255.0; + break; + case CV_16U: + mean /= 65535.0; + break; + } + if (verbose) + std::cout << "[" << f + 1 << "/" << files.gl_pathc << "] " + << basename(files.gl_pathv[f]) << " " << mean << std::endl; + + stats.col(f) = mean; + + if (mean <= threshold) { + if (accumulated.empty()) { + image.copyTo(accumulated); + } else { + accumulated = cv::max(accumulated, image); + } + } + } + + // Calculate some statistics + double min_mean, max_mean; + cv::Point min_loc; + cv::minMaxLoc(stats, &min_mean, &max_mean, &min_loc); + double mean_mean = cv::mean(stats)[0]; + + // For median, do partial sort and take middle value + std::vector vec; + stats.copyTo(vec); + std::nth_element(vec.begin(), vec.begin() + (vec.size() / 2), vec.end()); + double median_mean = vec[vec.size() / 2]; + + std::cout << "Minimum: " << min_mean << " maximum: " << max_mean + << " mean: " << mean_mean << " median: " << median_mean + << std::endl; + + // If we still don't have an image (no images below threshold), copy the + // minimum mean image so we see why + if (!stats_only) { + if (accumulated.empty()) { + std::cout << "No images below threshold, writing the minimum image only" + << std::endl; + accumulated = cv::imread(files.gl_pathv[min_loc.x], cv::IMREAD_UNCHANGED); } - globfree(&files); std::vector compression_params; compression_params.push_back(CV_IMWRITE_PNG_COMPRESSION); @@ -146,5 +205,7 @@ int main(int argc, char *argv[]) compression_params.push_back(95); cv::imwrite(outputfile, accumulated, compression_params); - return 0; + } + globfree(&files); + return 0; }