saucecontrol / PhotoSauce

MagicScaler high-performance, high-quality image processing pipeline for .NET
http://photosauce.net/
MIT License
589 stars 49 forks source link

Gamma corrected scaling is wrong when the source image is a jpeg #61

Closed mlaily closed 3 years ago

mlaily commented 3 years ago

Hello,

I'm reading http://www.ericbrasseur.org/gamma.html and trying to use MagicScaler on the dalai-lama test picture, downscaling it to 50% its original size. http://www.ericbrasseur.org/gamma_dalai_lama_gray.jpg

I don't understand why, and it's disappointing, but MagicScaler outputs a gray blob instead of the expected result. I tried disabling hybrid scaling and sharpening, but the result stays the same.

What's interesting though, is that if I resave the source image to bmp or png in its original size using MagicScaler, then use this as the input of the 50% resize, I get a correct result! (a gamma correct resized image, not a gray blob)

In case it can help, here is the code I use (it's pretty straightforward, but it's F# in an .fsx script, sorry about that):

#r "nuget: PhotoSauce.MagicScaler, 0.11.2"

open System.IO
open System.Threading.Tasks
open PhotoSauce.MagicScaler

let workingDir = @"D:\Test"

let magicResize () =
    Task.Run (fun () ->
        use inputFs = new FileStream(Path.Combine(workingDir, "gamma_dalai_lama_gray.jpg"), FileMode.Open)
        use outputFs = new FileStream(Path.Combine(workingDir, "magic.bmp"), FileMode.Create)
        let settings = ProcessImageSettings()
        // I just comment this line to get the non-resized
        //  version that works when used as an input:
        settings.Height <- 111
        settings.SaveFormat <- FileFormat.Bmp
        settings.Sharpen <- false
        settings.HybridMode <- HybridScaleMode.Off
        settings.ColorProfileMode <- ColorProfileMode.ConvertToSrgb
        MagicImageProcessor.ProcessImage(inputFs, outputFs, settings)
    )

magicResize () |> Async.AwaitTask |> Async.RunSynchronously

Is that expected, or is it a bug? Any workaround?

saucecontrol commented 3 years ago

That's kind of expected. Let me see if I can explain...

A JPEG source, if possible, will be processed in its native planar Y'CbCr format by MagicScaler. In this case, only the luma plane is processed in linear light since the chroma planes are technically already linear and don't represent light anyway. For real-world images, this will give the same visual result as converting to linear RGB and then processing. For that specific contrived image, it doesn't work out, because of the alternating single pixel-width lines. When you convert to PNG or BMP, you force the RGB conversion, so you get the expected result.

If it is important to you to process all images in linear RGB, you can disable the planar processing mode entirely by setting MagicImageProcessor.EnablePlanarPipeline to false. That's a static global setting, so you can do it on app startup and be done.

saucecontrol commented 3 years ago

For reference, here's another contrived gamma test image, which has consistent chroma values but stripes through the luma plane. This one will have correct output whether processed in planar or RGB mode:

gammared

mlaily commented 3 years ago

Thanks for the explanation!

I still don't quite understand why the alternating lines are processed differently in Y'CbCr, and why this is nothing to worry about in "normal" scenarios, but I guess it would make sense if I knew more about how this color space worked, so I will trust you on that.

(Github messed up your image, but I guess it's the same as http://www.ericbrasseur.org/gamma-1.0-or-2.2.png)

Also thank you for telling me about EnablePlanarPipeline, and thank you in general for your work :)

saucecontrol commented 3 years ago

Oh yeah, that image didn't work out. 😆 Somehow it got encoded with subsampled chroma, which makes it look the same whether processed in linear light or not.

The reason it's safe to process planar data directly for the vast majority of real-world images is that chroma data typically doesn't have high-frequency changes as it does in that Dalai Lama test image. In fact, most JPEG images in the wild have their chroma planes subsampled, so it's literally impossible for them to have such brightness details encoded into the chroma data and for that data to be of high frequency. The failed test image I pasted above was an unfortunate example of that, but it illustrates the point nicely.

Thanks for the kind words and the detailed report. I'm glad you find the software useful :)

mlaily commented 3 years ago

I thought you might be interested to known I ended up using your library in https://github.com/mlaily/MovieBarCodeGenerator :)

It fixes gamma correction issues when scaling down images. (I was previously using GDI...) https://github.com/mlaily/MovieBarCodeGenerator/issues/3

(and I just realized I didn't properly gave you credit in the user facing interface. I will have to fix this...)

saucecontrol commented 3 years ago

Whoah! I kind of can't believe I've never seen movie bar codes before.

Just did some googling and saw more examples on tumblr. The Trois Couleurs trilogy in particular is absolutely awesome!

Thanks so much for sharing that. I'll be playing with your util for sure :)

(I don't know that credit is necessary for using things in library form, but it's certainly appreciated)