28
$\begingroup$

Bug fixed in 11.3


Update

This issue caught me out again so I contacted support once more. They have confirmed that the development team considers the new behaviour to be correct and claim that "ImageCompose uses standard Duff-Porter definitions".

I think the development team are mistaken about the standard definitions, however it looks like the new behaviour is here to stay.


Prior to version 10, if ImageCompose was used to overlay a partially transparent image region over a completely transparent image region, the result would keep the colour of the overlay. Like this:

enter image description here

Notice how the upper part of the red circle in the result is the same colour as the original red circle.

In version 10 the behaviour changed to this:

enter image description here

Notice that the upper part of the red circle in the result is now darker than the original.

I reported this as a bug to Wolfram Research, pointing out that the new behaviour was at odds with the documentation (the first image above is actually a wayback machine snapshot of the version 10.0.0 documentation.)

Wolfram Research confirmed the bug and I was hopeful that version 10.0.1 would see it fixed. Unfortunately instead of reverting the functionality of ImageCompose they have merely updated the documentation to reflect the new behaviour (the second image above is a snapshot of the version 10.0.1 documentation).

I'm not sure if this is now considered a bug or a change in functionality, but in either case I would like some way to get the old behaviour back. Any ideas?

For testing you can use this code to recreate the example from the documentation:

i1 = SetAlphaChannel[
  Image[Blue]~ImageResize~100,
  Image@LowerTriangularize@ConstantArray[1, {100, 100}]]

i2 = SetAlphaChannel[
  Image[Red]~ImageResize~100,
  Image[0.5 DiskMatrix[40, 100]]]

ImageCompose[i1, i2]
$\endgroup$
4
  • 4
    $\begingroup$ Well, that's certainly undesirable! Alpha compositing is supposed to be associative (i0~ImageCompose~(i1~ImageCompose~i2) should equal (i0~ImageCompose~i1)~ImageCompose~i2) and this doesn't do that. One could implement correct alpha compositing manually using ImageApply, but let's see if someone has a better way. $\endgroup$
    – user484
    Commented Sep 18, 2014 at 20:57
  • 3
    $\begingroup$ I imagine that the documentation was updated just as a result of an automatic processing, and not to show a new/different functionality. Just to make sure that WR will eventually correct the function to our expectations, I think you should send a bug report update, stating that now, not only the function is giving the wrong result, but also the documentation is showing the bug ;-) $\endgroup$
    – P. Fonseca
    Commented Sep 20, 2014 at 16:25
  • $\begingroup$ @Rahul It would be nice if you post such an implementation. $\endgroup$ Commented Sep 21, 2017 at 17:17
  • 1
    $\begingroup$ @Alexey Sorry, I don't have the time to do it right now, but the definition of the "over" operator is on the Wikipedia page if you want to take a stab at it. $\endgroup$
    – user484
    Commented Sep 21, 2017 at 18:10

3 Answers 3

18
+100
$\begingroup$

The $over$ operator correctly implemented

According to the Wikipedia, when composing $A$ $over$ $B$, the output alpha channel value $\alpha_O$ and the output color channel value $C_O$ are calculated as follows:

$\begin{cases}\alpha_O = 1 - (1 - \alpha_A) (1 - \alpha_B) \\C_O = \frac{\alpha_A C_A + (1 - \alpha_A)\alpha_B C_B}{\alpha_O}, \text{if $\alpha_O \neq 0$} \\C_O = 0, \text{if $\alpha_O = 0$} \end{cases}$

where $\alpha_A$ and $\alpha_B$ are alpha channel values of $A$ and $B$, and $C_A$ and $C_B$ – color channel values of $A$ and $B$ correspondingly.

This can be directly implemented in Mathematica 11.1 or above as follows:

imageCompose[b_Image, a_Image] := 
 Module[{alphaA = AlphaChannel@a, alphaB = AlphaChannel@b, alphaO, 
         cA = RemoveAlphaChannel@a, cB = RemoveAlphaChannel@b},
  alphaO = 1 - (1 - alphaA) (1 - alphaB);
  SetAlphaChannel[(alphaA*cA + (1 - alphaA) alphaB*cB)/alphaO, alphaO]]

Let us check the associative property:

{{i0, i1, i2}} = ImagePartition[
  Import["https://i.sstatic.net/r13gh.png"], {Scaled[1/3], Scaled[1]}]

{i0~imageCompose~(i1~imageCompose~i2), (i0~imageCompose~i1)~imageCompose~i2}
Equal @@ %
ColorSeparate /@ %%

output

output

True

output

It holds! So what is the problem with ImageCompose of version 10 and later?

Current implementation of ImageCompose: the diagnosis

When writing the above implementation for the first time I unintentionally made a simple mistake: I forgot to divide the output value for the color channel by $\alpha_O$. Here is what happened:

imageComposeWrong[b_Image, a_Image] := 
 Module[{alphaA = AlphaChannel@a, alphaB = AlphaChannel@b, alphaO, 
         cA = RemoveAlphaChannel@a, cB = RemoveAlphaChannel@b},
  alphaO = 1 - (1 - alphaA) (1 - alphaB);
  SetAlphaChannel[alphaA*cA + (1 - alphaA) alphaB*cB, alphaO]]

{i0~imageComposeWrong~(i1~imageComposeWrong~i2), 
 (i0~imageComposeWrong~i1)~imageComposeWrong~i2}
Equal @@ %
ColorSeparate /@ %%

output

False

output

The output looks exactly the same as for the current ImageCompose:

{i0~ImageCompose~(i1~ImageCompose~i2), (i0~ImageCompose~i1)~ImageCompose~i2}
Equal @@ %
ColorSeparate /@ %%

output

False

output

Numerical comparison reveals tiny differences due to rounding off errors. But the final diagnosis is clear: the developer just forgot to divide the color channel by the alpha channel!

It is a great shame that during more than three years after the release of version 10.0.0 nobody noticed this in the company! Do they themselves use this functionality – or not?!

Please, do not be lazy to report this to the technical support, so that this shameful bug will be fixed as soon as possible! A high priority is given to bugs, which many users write about...

The remedy

From the above considerations the remedy is obvious: we must just divide the color of the ImageCompose output by the alpha channel:

icFix[img_Image] := img/AlphaChannel[img];

{i0~icFix@*ImageCompose~(i1~icFix@*ImageCompose~i2), (i0~icFix@*ImageCompose~i1)~icFix@*ImageCompose~i2}
Subtract @@ % // MinMax
ColorSeparate /@ %%

output

{-3.10689*10^-6, 3.08454*10^-6}

output

As one can see, there are still tiny differences due to rounding-off errors, but the associative property in fact is restored and the output is correct!


Original answer

Citing a comment by Rahul:

Well, that's certainly undesirable! Alpha compositing is supposed to be associative (i0~ImageCompose~(i1~ImageCompose~i2) should equal (i0~ImageCompose~i1)~ImageCompose~i2) and this doesn't do that. One could implement correct alpha compositing manually using ImageApply, but let's see if someone has a better way.

Indeed, in versions 8.0.4 and 9.0.1 ImageCompose is associative:

$Version

{{i0, i1, i2}} = ImagePartition[
  Import["https://i.sstatic.net/r13gh.png"], {Scaled[1/3], Scaled[1]}]

{i0~ImageCompose~(i1~ImageCompose~i2), (i0~ImageCompose~i1)~ImageCompose~i2}

Equal @@ %

ColorSeparate /@ %%
"9.0 for Microsoft Windows (64-bit) (January 25, 2013)"

output

output

True

output

... while starting from version 10.0 it is not:

$Version

{{i0, i1, i2}} = ImagePartition[
  Import["https://i.sstatic.net/r13gh.png"], {Scaled[1/3], Scaled[1]}]

{i0~ImageCompose~(i1~ImageCompose~i2), (i0~ImageCompose~i1)~ImageCompose~i2}

Equal @@ %

ColorSeparate /@ %%
"10.0 for Microsoft Windows (64-bit) (September 9, 2014)"

output

output

False

output

It is also worth to note that despite that Overlay[{i0, i1}] and Show[i0, i1] look the same as the old ImageCompose[i0, i1], they are not the same. But they do (approximately) equal to each other and do approximately hold the associative property:

$Version

overlayCompose[i0_, i1_] := Rasterize[Overlay[{i0, i1}], "Image", Background -> None];
showCompose[i0_, i1_] := Rasterize[Show[i0, i1], "Image", Background -> None];

{overlayCompose[i0, i1], showCompose[i0, i1], i0~ImageCompose~i1}

ColorSeparate /@ %

{i0~overlayCompose~(i1~overlayCompose~i2), (i0~overlayCompose~i1)~ overlayCompose~i2}

ColorSeparate /@ %

{i0~showCompose~(i1~showCompose~i2), (i0~showCompose~i1)~showCompose~ i2}

ColorSeparate /@ %
"9.0 for Microsoft Windows (64-bit) (January 25, 2013)"

outout

output

output

output

output

output

As one can see from the above, Overlay introduces artifact at the top, while Show does not.

$\endgroup$
3
  • 1
    $\begingroup$ Thanks for the thorough investigation. It would be great if your bug report gets the code fixed, but I'm not hopeful. The second time I reported this they acknowledged the non-associative behaviour but considered it correct. $\endgroup$ Commented Sep 22, 2017 at 18:03
  • 2
    $\begingroup$ Reported to the support as [CASE:3967986]. Published on the Wolfram Community: community.wolfram.com/groups/-/m/t/1212695 $\endgroup$ Commented Nov 3, 2017 at 0:42
  • 2
    $\begingroup$ @SimonWoods It seems the bug is fixed in version 11.3. $\endgroup$ Commented Apr 26, 2018 at 4:27
11
$\begingroup$

You can get the old ImageCompose behavior by using Overlay instead:

Overlay[{i1, i2}]

Overlay


Edit:

As pointed out by the comment by ybeltukov the Head of an Overlay is "Overlay" and therefore doesn't match the Head of ImageCompose, which is "Image". I didn't realize this, because exporting to a .png file did handle the transformation.
One can use e.g.

ImportString@ExportString[Overlay[{i1, i2}], "PNG"]

to get an object with Head "Image", and that therefore can be used the same way as an object created with ImageCompose inside the notebook.

$\endgroup$
5
  • $\begingroup$ Unfortunately it only looks like ImageCompose, but it doesn't produce a new image. May be with further Rasterize? $\endgroup$
    – ybeltukov
    Commented Sep 19, 2014 at 20:33
  • $\begingroup$ Head is Graphics, however it works as an image for all image-processing functions that I tried! For example, ImageResize[#, Scaled[1]] &@ produces "real" image with Image head. $\endgroup$
    – ybeltukov
    Commented Sep 19, 2014 at 21:48
  • 4
    $\begingroup$ Many thanks for the suggestion. The Export-Import conversion seems to remove the alpha channel though. However I can rasterize direct to Image and maintain the alpha channel using Rasterize[Overlay[{i1, i2}], "Image", Background -> None]. I have some tests to run but this looks good so far. $\endgroup$ Commented Sep 19, 2014 at 22:08
  • 2
    $\begingroup$ @SimonWoods Using Rasterize with this options seems to be the best way. However, I'd like to add that ImportString@ ExportString[Overlay[{i1, i2}], "PNG", Background -> None] also seems to preserve the alpha channel. $\endgroup$
    – Karsten7
    Commented Sep 19, 2014 at 22:23
  • 1
    $\begingroup$ This solution seems robust - I can't find a way to break it! And it's faster (and probably more accurate) than post-processing the result from broken ImageCompose. Thanks again. $\endgroup$ Commented Sep 20, 2014 at 14:44
5
$\begingroup$

As well Karsten's solution using Overlay, technical support pointed out that Show can be used in the same way:

Rasterize[Show[i1, i2], "Image", Background -> None]

(Show converts the images to Raster expressions and overlays them in Graphics).

In both cases the alpha compositing is done by the front end, which uses the conventional associative "over" operator.

$\endgroup$
2
  • $\begingroup$ Also Image@Show[i1, i2]? $\endgroup$
    – matrix42
    Commented Jul 15, 2016 at 3:33
  • $\begingroup$ @mathe Image doesn't allow to pass the Background -> None option. $\endgroup$ Commented Sep 22, 2017 at 0:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.