# ImageApply Performance Difference

How come the timing difference between these calls is so big? (0.2 and 39 seconds on my computer)

ImageApply[q3, img]

ImageApply[q3[#] &, img]


Where q3 quantizes pixel channel value to three levels:

q3[x_] := Piecewise[{
{0., x <= 1/3.}, {.5, x <= 2/3.}, {1., x <= 1.}}, 0]

SetAttributes[q3, Listable]


and img is an example image:

img = ExampleData[{"TestImage", "Apples"}]


It seems, that a pure function calling a Listable one breaks internal optimization in Mathematica's ImageApply.

Compare:

t = Abs[Sqrt[#]] &;          (* pure function *)
q[x_] := Abs[Sqrt[x]];       (* "standard" function, implicitly listable *)
SetAttributes[h, Listable];
h[x_] := Abs[Sqrt[x]];       (* explicitly listable function *)

First /@ {
(* pure function *)
AbsoluteTiming@ImageApply[t, img],
(* "standard" function, implicitly listable *)
AbsoluteTiming@ImageApply[q, img],
(* explicitly listable function *)
AbsoluteTiming@ImageApply[h, img],
(* encapsulation of listable function in pure function *)
AbsoluteTiming@ImageApply[h[#] &, img]}

(* {0.25466, 2.13473, 0.0406089, 4.35027} *)


While the explicitly listable function is way faster than any other (only implicitly listable) approach, wrapping it in a (redundant) pure function breaks performance.

Generally speaking, this pure-function-wrapping is just redundant and - obviously - seems ill-advised.

The function q3[#] & is not Listable and because the Interleaving option value is True by default, there is not much optimization that can be figured out automatically.

Setting the Listable attribute whenever possible will help. Working with "Bit", "Byte", or "Bit16" data types will be faster, too.

In this very case, ImageApply[q3, img] and ImageApply[q3[#] &, img, Interleaving -> False] have similar timings.

I'm not too sure why one is so much slower than the other, but your second (slower) method can be improved by compilation (inspired by this answer).

q3Compile = Compile[{{x, _Real}},
Piecewise[{{0., x <= 1/3.}, {.5, x <= 2/3.}, {1., x <= 1.}}, 0],
RuntimeAttributes -> {Listable}
];

img = ExampleData[{"TestImage", "Apples"}]
Image[q3Compile[#] & /@ ImageData@img] // AbsoluteTiming
(* 1.4 seconds *)


This compilation approach might be useful for certain applications and functions, given what @Jinxed says in the other answer, but obviously not here given it's still slower than your first method!

It appears that among other optimizations ImageApply uses a kind of memoization but that it is inactive in the second example. With a small change to the definition we can see how many times the function is actually applied:

qX[x_] := (Sow @ x;
Piecewise[{{0., x <= 1/3.}, {.5, x <= 2/3.}, {1., x <= 1.}}, 0])

SetAttributes[qX, Listable]


The sample image:

img = ExampleData[{"TestImage", "Apples"}];


The analogue to the first application:

255 Sort @@ Last @ Reap @ ImageApply[qX, img] // Round

{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, \
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, \
46, 47, 48, 49, 50, 51, 52, 53, 53, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, \
66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, \
88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, \
108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, \
126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, \
144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, \
162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 172, 172, 173, 174, 175, 176, 177, \
178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, \
196, 197, 197, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, \
212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, \
230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, \
248, 249, 250, 251, 252, 253, 254, 255}


There are a few duplicates after requantizing, probably due to round-off errors, but the function is only applied 262 times.

Analogue to the second application:

Length @@ Last @ Reap[ImageApply[qX[#] &, img]]

14580006


Here the function is applied to every single value in the image, plus a few more times probably in the course of analysis:

3 Times @@ ImageDimensions[img]

14580000


As another way to demonstrate that this is the reason for the performance difference we can apply the original q3 to an image for which memoization will be ineffective, i.e. one with many unique Real values.

gradient = Array[{#, #2, #/3} &, {2700, 1800}, {{0, 1}, {1, 0}}] ~Image~ "Real32";

ImageApply[q3, img3] // AbsoluteTiming