Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extreme dynamic scene loses most details #113

Open
Floessie opened this issue Nov 22, 2017 · 22 comments
Open

Extreme dynamic scene loses most details #113

Floessie opened this issue Nov 22, 2017 · 22 comments
Labels
Milestone

Comments

@Floessie
Copy link
Contributor

Hi Ingo and DrSlony,

I've shot a scene using Magic Lantern's auto HDR bracketing, which correctly resulted in eight shots all 2EV apart. I naively saved them to a 16b DNG, knowing the embedded preview would be crap. Only later, when I struggled developing the DNG in RT, I learned that this DNG was only 1MB in size. 😮

Then I saved the series in 32b, this resulted in a 16MB DNG, again undevelopable. Only when I reduce the set to the "innermost" three to four shots I get a decent DNG.

What's wrong with the scene, and why does HDRMerge lose that data instead of accumulating all possible data into the DNG?

You can find the files here.

Best,
Flössie

@Beep6581
Copy link
Collaborator

Would be good to first exclude Magic Lantern as the source of the issues. I've never processed Magic Lantern raws in HDRMerge. Could you try the maximum bracketing set allowed by standard Canon firmware?

I renamed the files in order of EV, 1-8.cr2.

Worth nothing that --batch mode detects 3 sets, and the black levels and wb are not constant.
See #1 and #6 (comment)

The white balance is passed untouched to the output DNG

$ ~/programs/hdrmerge-master/hdrmerge --batch -b 16 --no-align --no-crop -p half -w 11432 -vv *.cr2
Using LibRaw 0.18.5-Release
Set 0: 4.cr2
Set 1: 3.cr2 5.cr2
Set 2: 2.cr2 6.cr2 8.cr2 1.cr2 7.cr2
Skipping single image 4.cr2
[  0%] Loading 3.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
3.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/100sec f8 EV:-12.6439
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1601 1024 2456 1024, cblack: 2048 2048 2048 2048
[ 33%] Loading 5.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
5.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/6sec f8 EV:-8.58496
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1579 1024 2411 1024, cblack: 2048 2048 2048 2048
Load files: 5.68887 seconds
Using custom white level 11432
Compute response functions: 0.496783 seconds
Generate mask: 0.948835 seconds
[100%] Done loading!
Writing result to /home/morgan/downloads/floessie/3-5.dng
Writing /home/morgan/downloads/floessie/3-5.dng, 16-bit, 5202x3465
[  0%] Rendering image
Adjusted white balance: 1.56348 1 2.39844 1
Fatten mask (SSE version): 0.0883232 seconds
Blur: 0.465352 seconds
Compose: 0.843472 seconds
[ 33%] Rendering preview
Render preview: 1.12768 seconds
[ 66%] Writing output
Write output: 7.23871 seconds
[100%] Done writing!
[  0%] Loading 2.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
2.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/400sec f8 EV:-14.6439
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1586 1024 2422 1024, cblack: 2048 2048 2048 2048
[ 16%] Loading 6.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
6.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/1.66667sec f8 EV:-6.73697
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1586 1024 2422 1024, cblack: 2049 2048 2048 2048
[ 32%] Loading 8.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
8.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/0.1sec f8 EV:-2.67807
b4b4b4b4 RGBG, sat 13584, black 2049, flip 5, wb: 1799 1024 1893 1024, cblack: 2050 2050 2049 2050
[ 48%] Loading 1.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
1.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/1600sec f8 EV:-16.6439
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 2166 1024 1584 1024, cblack: 2048 2049 2048 2048
[ 64%] Loading 7.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
7.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/0.4sec f8 EV:-4.67807
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1799 1024 1893 1024, cblack: 2049 2049 2048 2049
Load files: 14.29 seconds
Using custom white level 11432
Compute response functions: 2.16348 seconds
Generate mask: 1.04476 seconds
[100%] Done loading!
Writing result to /home/morgan/downloads/floessie/1-8.dng
Writing /home/morgan/downloads/floessie/1-8.dng, 16-bit, 5202x3465
[  0%] Rendering image
Adjusted white balance: 2.11523 1 1.54688 1
Fatten mask (SSE version): 0.076665 seconds
Blur: 0.471906 seconds
Compose: 1.0446 seconds
[ 33%] Rendering preview
Render preview: 1.11075 seconds
[ 66%] Writing output
Write output: 1.26277 seconds
[100%] Done writing!
$ ~/programs/hdrmerge-master/hdrmerge -o 1-8.dng -b 16 --no-align --no-crop -p half -w 11432 -vv *.cr2
Using LibRaw 0.18.5-Release
[  0%] Loading 1.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
1.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/1600sec f8 EV:-16.6439
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 2166 1024 1584 1024, cblack: 2048 2049 2048 2048
[ 11%] Loading 2.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
2.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/400sec f8 EV:-14.6439
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1586 1024 2422 1024, cblack: 2048 2048 2048 2048
[ 22%] Loading 3.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
3.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/100sec f8 EV:-12.6439
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1601 1024 2456 1024, cblack: 2048 2048 2048 2048
[ 33%] Loading 4.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
4.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/25sec f8 EV:-10.6439
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1584 1024 2422 1024, cblack: 2048 2048 2048 2048
[ 44%] Loading 5.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
5.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/6sec f8 EV:-8.58496
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1579 1024 2411 1024, cblack: 2048 2048 2048 2048
[ 55%] Loading 6.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
6.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/1.66667sec f8 EV:-6.73697
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1586 1024 2422 1024, cblack: 2049 2048 2048 2048
[ 66%] Loading 7.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
7.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/0.4sec f8 EV:-4.67807
b4b4b4b4 RGBG, sat 13584, black 2048, flip 5, wb: 1799 1024 1893 1024, cblack: 2049 2049 2048 2049
[ 77%] Loading 8.cr2
Number of frames : 1
Invalid aperture: 0 replaced by aperture: f8
8.cr2: 5202x3465 (5344x3516+142+51, by Canon EOS 600D, 100ISO 1/0.1sec f8 EV:-2.67807
b4b4b4b4 RGBG, sat 13584, black 2049, flip 5, wb: 1799 1024 1893 1024, cblack: 2050 2050 2049 2050
Load files: 20.7374 seconds
Using custom white level 11432
Compute response functions: 2.15621 seconds
Generate mask: 0.398727 seconds
[100%] Done loading!
Writing result to 1-8.dng
Writing 1-8.dng, 16-bit, 5202x3465
[  0%] Rendering image
Adjusted white balance: 2.11523 1 1.54688 1
Fatten mask (SSE version): 0.0238369 seconds
Blur: 0.249987 seconds
Compose: 0.407139 seconds
[ 33%] Rendering preview
Render preview: 0.396647 seconds
[ 66%] Writing output
Write output: 0.502799 seconds
[100%] Done writing!

Fixing the aperture made no difference:

exiftool -P -overwrite_original_in_place -ApertureValue="8.0" -FNumber="8.0" *.cr2

@Floessie
Copy link
Contributor Author

@Beep6581

Would be good to first exclude Magic Lantern as the source of the issues. I've never processed Magic Lantern raws in HDRMerge. Could you try the maximum bracketing set allowed by standard Canon firmware?

The Canon EOS 600D can only do three bracketed shots (max. +-2EV) with stock firmware. That's why I use ML. There's nothing special about the CR2s (apart from not having the bracketing sequence tag AFAIK), as ML only changes the shutter time the way I would do manually. So you can take the center three images and basically get what Canon bracketing would have shot. Worth to mention, I never had a problem with ML auto bracketed shots, but this here is extreme in it's dynamic range. Most of my bracketed scenes causes four to five shots 2EV apart.

Worth nothing that --batch mode detects 3 sets, and the black levels and wb are not constant.

About the three sets: The longest exposure might be longer than the default set gap of HDRMerge. Plus, I had to take the series twice, because someone was walking through the shot. Maybe I made a best-of selection, can't remember and don't have the full series here to check.

About BL and WB: I can't tell you anything about BL, but WB was set to AutoWB because I wasn't aware that this would matter. Does it?

Fixing the aperture made no difference.

This was shot with my manual Samyang 35 1.4, most probably at f/2 or maybe f/2.8. I had no problems with the combination of this camera, lens, and ML auto bracketing in the past, but the scenes weren't that extreme.

HTH,
Flössie

@Floessie
Copy link
Contributor Author

@Beep6581 In your batch run, why is the 10s exposure (1/0.1) still rated -2.67807EV? Is that normal? I would have expected at least 0 for the brightest image.

@Beep6581
Copy link
Collaborator

@Floessie I'm "under the impression" that it doesn't matter as long as the step size is right, but I will test both the EV and WB on some other shots this weekend.

@Floessie
Copy link
Contributor Author

Floessie commented Dec 7, 2017

@Beep6581 @heckflosse Any idea where to look? Could it be some cancelation problem like the one Alberto fixed for Fattal? I'd really like to develop that shot...

Best,
Flössie

@heckflosse
Copy link
Collaborator

@Floessie I have an idea where to look. Have to check that. I will tell you later today.

@heckflosse
Copy link
Collaborator

@Floessie I would start here :

void Image::computeResponseFunction(const Image & r) {

@Floessie
Copy link
Contributor Author

@heckflosse The problem is the darkest image.

diff --git a/src/Image.cpp b/src/Image.cpp
index da5dfec..5fea6e7 100644
--- a/src/Image.cpp
+++ b/src/Image.cpp
@@ -153,6 +153,7 @@ void Image::computeResponseFunction(const Image & r) {
         alglib::spline1dfitreport rep;
         alglib::spline1dfitpenalized(values, adjValues, i, 200, 3, info, response.nonLinear, rep);
         response.linear = alglib::spline1dcalc(response.nonLinear, response.threshold) / response.threshold;
+        printf("algo 1: %lf\n", response.linear);
     } else {
         response.threshold = 65535;
         // Fallback method for dark images:
@@ -171,6 +172,7 @@ void Image::computeResponseFunction(const Image & r) {
             }
         }
         response.linear = numerator / denom;
+        printf("algo 2: %lf\n", response.linear);
     }
 }

With the whole set I get:

algo 2: 0,234110
algo 2: 0,074496
algo 2: 0,017766
algo 2: 0,004300
algo 2: 0,001041
algo 2: 0,000252
algo 2: 0,000062

And most of the resulting image is black. Without 20170724-230458.cr2, everything is fine:

algo 2: 1,535183
algo 2: 0,366117
algo 2: 0,088617
algo 2: 0,021460
algo 2: 0,005202
algo 2: 0,001274

I tried this and that inside computeResponseFunction(), understanding the code only fragmentally, but without success.

@Beep6581
Copy link
Collaborator

@Floessie is this still reproducible?

@Beep6581 Beep6581 added the bug label Jun 19, 2018
@Floessie
Copy link
Contributor Author

@Beep6581 Last time I checked, it was still reproducible and there were no changes concerning computeResponseFunction(). I can reevaluate, though (if I can find the stack in question again).

@Floessie
Copy link
Contributor Author

@Beep6581 Problem still exists. Range is 0, 4, 6, 8, ..., 16 EV.

@fanckush
Copy link
Contributor

@Floessie could you re-upload the images if you still have them :) ?

@Floessie
Copy link
Contributor Author

@fanckush Sure. Here they are.

@fanckush
Copy link
Contributor

@Floessie Could you try this patch? It enhances the way the response function parameters are calucalted for dark images:

index 4b1df64..a077b46 100644
--- a/src/Image.cpp
+++ b/src/Image.cpp
@@ -165,7 +165,7 @@ void Image::computeResponseFunction(const Image & r) {
                 int pos = y * width + x;
                 double v = usePixels[pos];
                 double nv = rUsePixels[pos];
-                if (v >= nv && v < satThreshold) {
+                if (v >= nv && v < response.threshold) {
                     numerator += v * r.response(nv);
                     denom += v * v;
                 }

Reasoning: This code lives in an else block that handles dark images, the response.threshold is manually set to 65535. I'm not sure yet why but it seems that the response function needs to adapt (shift) the whole curve in case of a dark image.

changes: the if statement used to determine the numerator and denom should also relate to the hardcoded threshold and not satThreshold which is much higher.

@fanckush
Copy link
Contributor

I did some testing on a normal set of images set

Before the patch:
Screenshot 2019-05-29 at 15 25 00
After the patch:
Screenshot 2019-05-29 at 15 36 34

There is an artifacts near the clouds, it is the same as #162 (comment)

At the same time it fixes the data-loss issue with your images.
I will continue to look into it. tips/hints are welcome :)

@Beep6581
Copy link
Collaborator

@fanckush there seem to be quite a few issues with that image. The flag and water artifacts could be explained by movement between the images (I haven't checked the raw photo) but other areas are static.
artifacts

@Floessie
Copy link
Contributor Author

Floessie commented Jun 3, 2019

@fanckush As much as I liked this being solved, your patch is not yet on the spot.

With patch

bad

Without patch

good

@fanckush
Copy link
Contributor

fanckush commented Jun 3, 2019

@Floessie yup. It was a naive attempt that showed good results for one case. I'm still going through understanding what everything does and only after that I can attempt to solve this :) this may take a while, I'll came back with another patch 👍

@fanckush
Copy link
Contributor

fanckush commented Sep 14, 2019

I'm back with another patch. First I wanna say that this is much trickier than I expected.

TL;DR at the bottom.

@Floessie you are getting 1MB because most of the resulting HDR image is dark AND the small bright part is extremely bright.

Part 1 ImageStack::compose()
HDRMerge squashes everything into the range: (0 + black level) to (params.max)
the fact that the HDR image has such a huge Dynamic Range AND most of it is dark, we end up loosing almost all details because they are scaled down using mult.
What I did is scale the output HDR image to whatever bitrate the user chooses.

Part 2
The other problem is with Image::computeResponseFunction() and the way we call it from Stack::computeResponseFunctions()
We pretty much use the darkest image as the base: we scale it UP to 16-bit and then we use it to compute the response function for the brighter image, and that brighter image is used for the next and so on.
The issue is that if the darkest image is too dark then it will have more noise than data and will not be reliable for the next images.
Sa what i did is reverse it; the brightest image is used as the base for the rest downwards

TL;DR
I had to change quite a bit in ImageStack and Image. To get good results with this patch, you must however choose 24 bit or 32 because 16 is too little cover all the DR without scaling it down too much resulting in loss of details.
So please be as skeptical as you can and if possible test other normal HDR sets for artifacts and whatnot.

Patch

I am here!
diff --git a/src/Image.cpp b/src/Image.cpp
index 4b1df64..838071b 100644
--- a/src/Image.cpp
+++ b/src/Image.cpp
@@ -44,16 +44,19 @@ void Image::buildImage(uint16_t * rawImage, const RawParameters & params) {
     size_t size = width*height;
     brightness = 0.0;
     max = 0;
+    min = params.black * 100; // some inital big value
     for (size_t y = 0, ry = params.topMargin; y < height; ++y, ++ry) {
         for (size_t x = 0, rx = params.leftMargin; x < width; ++x, ++rx) {
             uint16_t v = rawImage[ry*params.rawWidth + rx];
             (*this)(x, y) = v;
             brightness += v;
             if (v > max) max = v;
+            if (v < min) min = v;
         }
     }
     brightness /= size;
-    response.setLinear(params.max == 0 ? 1.0 : 65535.0 / params.max);
+    // response.setLinear(params.max == 0 ? 1.0 : 65535.0 / params.max);
+    response.setLinear(1.0);
     subtractBlack(params);
 }
 
@@ -165,9 +168,11 @@ void Image::computeResponseFunction(const Image & r) {
                 int pos = y * width + x;
                 double v = usePixels[pos];
                 double nv = rUsePixels[pos];
-                if (v >= nv && v < satThreshold) {
-                    numerator += v * r.response(nv);
-                    denom += v * v;
+                if (v <= nv && nv < satThreshold) {
+                    if ((nv - (double)r.min) / ((double)r.max - (double)r.min) > 0.1) {
+                        numerator += v * r.response(nv);
+                        denom += v * v;
+                    }
                 }
             }
         }
diff --git a/src/Image.hpp b/src/Image.hpp
index 0bc760b..f0f4ff5 100644
--- a/src/Image.hpp
+++ b/src/Image.hpp
@@ -104,7 +104,7 @@ private:
     QString filename;
 
     std::unique_ptr<Array2D<uint16_t>[]> scaled;
-    uint16_t satThreshold, max;
+    uint16_t satThreshold, max, min;
     double brightness;
     ResponseFunction response;
     double halfLightPercent;
diff --git a/src/ImageIO.cpp b/src/ImageIO.cpp
index a80ca51..3dbdfa3 100644
--- a/src/ImageIO.cpp
+++ b/src/ImageIO.cpp
@@ -183,7 +183,7 @@ void ImageIO::save(const SaveOptions & options, ProgressIndicator & progress) {
     params.width = stack.getWidth();
     params.height = stack.getHeight();
     params.adjustWhite(stack.getImage(stack.size() - 1));
-    Array2D<float> composedImage = stack.compose(params, options.featherRadius);
+    Array2D<float> composedImage = stack.compose(params, options.featherRadius, options.bps);
 
     progress.advance(33, "Rendering preview");
     QImage preview = renderPreview(composedImage, params, stack.getMaxExposure(), options.previewSize <= 1);
diff --git a/src/ImageStack.cpp b/src/ImageStack.cpp
index 6074321..5090f10 100644
--- a/src/ImageStack.cpp
+++ b/src/ImageStack.cpp
@@ -21,6 +21,7 @@
  */
 
 #include <algorithm>
+#include <cmath>
 
 #include "BoxBlur.hpp"
 #include "ImageStack.hpp"
@@ -166,8 +167,8 @@ void ImageStack::crop() {
 
 void ImageStack::computeResponseFunctions() {
     Timer t("Compute response functions");
-    for (int i = images.size() - 2; i >= 0; --i) {
-        images[i].computeResponseFunction(images[i + 1]);
+    for (int i = 1; i < images.size(); ++i) {
+        images[i].computeResponseFunction(images[i - 1]);
     }
 }
 
@@ -394,7 +395,7 @@ static Array2D<uint8_t> fattenMask(const Array2D<uint8_t> & mask, int radius) {
 }
 #endif
 
-Array2D<float> ImageStack::compose(const RawParameters & params, int featherRadius) const {
+Array2D<float> ImageStack::compose(const RawParameters & params, int featherRadius, int bit_depth) const {
     int imageMax = images.size() - 1;
     BoxBlur map(fattenMask(mask, featherRadius));
     measureTime("Blur", [&] () {
@@ -459,7 +460,10 @@ Array2D<float> ImageStack::compose(const RawParameters & params, int featherRadi
 
     dst.displace(params.leftMargin, params.topMargin);
     // Scale to params.max and recover the black levels
-    float mult = (params.max - params.maxBlack) / max;
+    int max_possible = std::pow(2.0, bit_depth); // almost always 65536
+    int max_black = params.maxBlack * ((double)max_possible / (double)params.max);
+    float mult = (max_possible - max_black) / max; // scale dst to fit the bit depth space
+
     #pragma omp parallel for
     for (size_t y = 0; y < params.rawHeight; ++y) {
         for (size_t x = 0; x < params.rawWidth; ++x) {
diff --git a/src/ImageStack.hpp b/src/ImageStack.hpp
index 87610da..a16c63a 100644
--- a/src/ImageStack.hpp
+++ b/src/ImageStack.hpp
@@ -48,7 +48,7 @@ public:
     void crop();
     void computeResponseFunctions();
     void generateMask();
-    Array2D<float> compose(const RawParameters & md, int featherRadius) const;
+    Array2D<float> compose(const RawParameters & md, int featherRadius, int bit_depth) const;
 
     size_t size() const { return images.size(); }
 

@Floessie
Copy link
Contributor Author

@fanckush I'd like to try your patch, but it doesn't apply on master. Is it against another branch?

@fanckush
Copy link
Contributor

@Floessie it should work, it is from master. Here is output of git diff saved to a file:
patch.txt

@fanckush
Copy link
Contributor

if this works, I will work on making it less patchy for example:

if ((nv - (double)r.min) / ((double)r.max - (double)r.min) > 0.1) {

is not stable in the case of a dark stack such as #173. The result HDR image is actually simply black because the if is always false

@Beep6581 Beep6581 modified the milestones: v1.0, v0.7 Sep 15, 2019
Entropy512 added a commit to Entropy512/hdrmerge that referenced this issue Nov 24, 2022
There was a rejected hunk against ImageStack where I made different changes for jcelaya#216

DO NOT MERGE/CHERRY-PICK - commit authorship is wrong and needs to be fixed!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants