Feature request, HDRMerge batch

First of all, thank you to the developers of HDRMerge! I was previously using Lightroom classics HDR functionality to create DNG’s out of my bracketed raw images but while it generally produced good results there would often be heavy noise in areas of extreme luminance difference making the results unusable. With HDRMerge set to 32 bits per sample I get perfect results 99% of the time even without any manual masking.

I generally shoot my HDR brackets in sets of 3 or 5 in manual mode. When using the current batch mode (time interval between shots) it sometimes misses a few of the images, get different sets mixed up or fails to merge certain sets completely. It would be great to as an option be able to specify the number of images each exposure set contains. This is how PTGui Pro’s Batch Builder “Detect Panoramas” function works for instance. As long as all images in a specified directory are captured using the same number of brackets this should produce predicable results every time.

Is there already some way of doing this? Perhaps by using 3rd party software or scripts?

Try using the -B and -g gap switches. See:

Thank you afre, this is worth experimenting with to see if I can get better results!

It seems that this is still a time interval based grouping. If I increase the default gap from 3 seconds to let’s say 30 seconds I run the risk of including images from following bracket sets in there. When out shooting I sometimes need to wait for several minutes to capture certain brackets because people get in front of the camera or I have to recapture certain exposures. Because of this variable time interval the current mode for creating batch sets based on the time gap between shots will not always produce predictable results for me. A batch mode based on a preset number of images to include in each merge would deliver the correct results every time.

If your groupings are that irregular, you may want to write a script that groups files together then passes the result to HDRMerge.

Yes, this sounds like what I would need but I would need to teach myself how to code first.

This patch adds a new option “-i” which accepts the number per bracket set, so in your case 3. Or 5.
I do not (yet) check if that number divides the number of images passed.

diff --git a/src/Launcher.cpp b/src/Launcher.cpp
index b188d79..0539b46 100644
--- a/src/Launcher.cpp
+++ b/src/Launcher.cpp
@@ -22,6 +22,7 @@
 
 #include <iostream>
 #include <iomanip>
+#include <iterator>
 #include <string>
 #include <QApplication>
 #include <QTranslator>
@@ -72,27 +73,42 @@ struct CoutProgressIndicator : public ProgressIndicator {
 
 list<LoadOptions> Launcher::getBracketedSets() {
     list<LoadOptions> result;
-    list<pair<ImageIO::QDateInterval, QString>> dateNames;
-    for (QString & name : generalOptions.fileNames) {
-        ImageIO::QDateInterval interval = ImageIO::getImageCreationInterval(name);
-        if (interval.start.isValid()) {
-            dateNames.emplace_back(interval, name);
-        } else {
-            // We cannot get time information, process it alone
-            result.push_back(generalOptions);
-            result.back().fileNames.clear();
-            result.back().fileNames.push_back(name);
+    if(generalOptions.imagesPerBracket > 0) {
+        cout << "creating sets with " << generalOptions.imagesPerBracket << " images per set." << endl;
+        while(!generalOptions.fileNames.empty()) {
+            // for the moment assume that filenames.size() % imagesPerBracket ==0
+            LoadOptions opt = generalOptions;
+            auto oIt = opt.fileNames.begin();
+            auto goIt = generalOptions.fileNames.begin();
+            std::advance(oIt, generalOptions.imagesPerBracket);
+            std::advance(goIt, generalOptions.imagesPerBracket);
+            opt.fileNames.erase(oIt, opt.fileNames.end());
+            generalOptions.fileNames.erase(generalOptions.fileNames.begin(), goIt);
+            result.push_back(opt);
         }
-    }
-    dateNames.sort();
-    ImageIO::QDateInterval lastInterval;
-    for (auto & dateName : dateNames) {
-        if (lastInterval.start.isNull() || lastInterval.difference(dateName.first) > generalOptions.batchGap) {
-            result.push_back(generalOptions);
-            result.back().fileNames.clear();
+    } else {
+        list<pair<ImageIO::QDateInterval, QString>> dateNames;
+        for (QString & name : generalOptions.fileNames) {
+            ImageIO::QDateInterval interval = ImageIO::getImageCreationInterval(name);
+            if (interval.start.isValid()) {
+                dateNames.emplace_back(interval, name);
+            } else {
+                // We cannot get time information, process it alone
+                result.push_back(generalOptions);
+                result.back().fileNames.clear();
+                result.back().fileNames.push_back(name);
+            }
+        }
+        dateNames.sort();
+        ImageIO::QDateInterval lastInterval;
+        for (auto & dateName : dateNames) {
+            if (lastInterval.start.isNull() || lastInterval.difference(dateName.first) > generalOptions.batchGap) {
+                result.push_back(generalOptions);
+                result.back().fileNames.clear();
+            }
+            result.back().fileNames.push_back(dateName.second);
+            lastInterval = dateName.first;
         }
-        result.back().fileNames.push_back(dateName.second);
-        lastInterval = dateName.first;
     }
     int setNum = 0;
     for (auto & i : result) {
@@ -174,6 +190,15 @@ void Launcher::parseCommandLine() {
             generalOptions.crop = false;
         } else if (string("--batch") == argv[i] || string("-B") == argv[i]) {
             generalOptions.batch = true;
+        } else if (string("-i") == argv[i]) {
+            if(++i < argc) {
+                try {
+                    int value = stoi(argv[i]);
+                    generalOptions.imagesPerBracket = value;
+                } catch(std::invalid_argument & e) {
+                    cerr << tr("Invalid %1 parameter, falling back to interval-based bracketing set creation.").arg(argv[i - 1]) << endl;
+                }
+            }
         } else if (string("--single") == argv[i]) {
             generalOptions.withSingles = true;
         } else if (string("--help") == argv[i]) {
@@ -257,6 +282,8 @@ void Launcher::showHelp() {
     cout << "    " << "-a            " << tr("Calculates the output file name as") << " %id[-1]/%iF[0]-%in[-1].dng." << endl;
     cout << "    " << "-B|--batch    " << tr("Batch mode: Input images are automatically grouped into bracketed sets,") << endl;
     cout << "    " << "              " << tr("by comparing the creation time. Implies -a if no output file name is given.") << endl;
+    cout << "    " << "-i NUM_IMAGES " << tr("Fixed number of images per bracket set. use together with -B.") << endl;
+    cout << "    " << "              " << tr("Creation time will be ignored.") << endl;
     cout << "    " << "-g gap        " << tr("Batch gap, maximum difference in seconds between two images of the same set.") << endl;
     cout << "    " << "--single      " << tr("Include single images in batch mode (the default is to skip them.)") << endl;
     cout << "    " << "-b BPS        " << tr("Bits per sample, can be 16, 24 or 32.") << endl;
diff --git a/src/LoadSaveOptions.hpp b/src/LoadSaveOptions.hpp
index 5c3db3d..7d6fce0 100644
--- a/src/LoadSaveOptions.hpp
+++ b/src/LoadSaveOptions.hpp
@@ -35,9 +35,10 @@ struct LoadOptions {
     bool useCustomWl;
     uint16_t customWl;
     bool batch;
+    int imagesPerBracket;
     double batchGap;
     bool withSingles;
-    LoadOptions() : align(true), crop(true), useCustomWl(false), customWl(16383), batch(false), batchGap(2.0),
+    LoadOptions() : align(true), crop(true), useCustomWl(false), customWl(16383), batch(false), imagesPerBracket(-1), batchGap(2.0),
         withSingles(false) {}
 };
 

If the feature is of general interest I can open a pull request for a proper review.

3 Likes

Please do.

Did the pull request happen yet?