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
    $\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$ – Rahul Sep 18 '14 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 Sep 20 '14 at 16:25
  • $\begingroup$ @Rahul It would be nice if you post such an implementation. $\endgroup$ – Alexey Popkov Sep 21 '17 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$ – Rahul Sep 21 '17 at 18:10
16
+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["http://i.stack.imgur.com/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["http://i.stack.imgur.com/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["http://i.stack.imgur.com/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$
  • $\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$ – Simon Woods Sep 22 '17 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$ – Alexey Popkov Nov 3 '17 at 0:42
  • 2
    $\begingroup$ @SimonWoods It seems the bug is fixed in version 11.3. $\endgroup$ – Alexey Popkov Apr 26 '18 at 4:27
10
$\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$
  • $\begingroup$ Unfortunately it only looks like ImageCompose, but it doesn't produce a new image. May be with further Rasterize? $\endgroup$ – ybeltukov Sep 19 '14 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 Sep 19 '14 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$ – Simon Woods Sep 19 '14 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$ – Karsten 7. Sep 19 '14 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$ – Simon Woods Sep 20 '14 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$
  • $\begingroup$ Also Image@Show[i1, i2]? $\endgroup$ – mathe Jul 15 '16 at 3:33
  • $\begingroup$ @mathe Image doesn't allow to pass the Background -> None option. $\endgroup$ – Alexey Popkov Sep 22 '17 at 0:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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