4

In my image editing app I have a function for converting a 32 bit float image from sRGB to linear color space. The formula is:

if value <= 0.04045: (value / 12.92)
if value > 0.04045: ((value + 0.055) / 1.055)^2.4)

My image is a three-dimensional numpy.ndarray named img32.

My implementation so far:

boolarray = img32 <= 0.04045
lower = (img32 / 12.92) * boolarray.astype(np.int)
upper = np.power(((img32 + 0.055) / 1.055), 2.4) * np.invert(boolarray).astype(np.int)
img32 = lower + upper

So, I am creating a new array boolarray with truth values for <= 0.04045 and multiply by that.

What would be a better solution?

I tried something like:

img32[img32 < 0.04045] = img32 / 12.92

which works on the first step but fails on the second:

img32[img32 >= 0.04045] = np.power(((img32 + 0.055) / 1.055), 2.4)

probably because it doesn't work when wrapped in the np.power function

Any help is appreciated.

tde
  • 150
  • 13
  • `better solution` - Better how? Memory-wise or performance-wise? – Divakar Sep 30 '16 at 13:03
  • Something cleaner because this seems to me like a bit of a hack. I am pretty much looking for the standard way to do this. Haven't tested the speed yet (it's reasonably fast) but I guess it's definitely a waste of memory? Sorry for being unclear. – tde Sep 30 '16 at 13:07

3 Answers3

3

A clean way would be with np.where that lets us choose between two values based on a mask. In our case, the mask could be img32 >= 0.04045 and we will choose ((img32 + 0.055) / 1.055)**2.4 when True, else go with img32/12.92.

So, we would have an implementation like so -

np.where( img32 >= 0.04045,((img32 + 0.055) / 1.055)**2.4, img32/12.92 )

If you care about memory a lot and would like to write back the results into the input array, you could it in three steps by creating and selectively set elems corresponding to those two conditions, like so -

mask = img32 >= 0.04045
img32[mask] = ((img32[mask] + 0.055) / 1.055)**2.4
img32[~mask] = img32[~mask] / 12.92

Sample case -

In [143]: img32 = np.random.rand(4,5).astype(np.float32)

In [144]: img32.nbytes
Out[144]: 80

In [145]: mask.nbytes
Out[145]: 20

So, we are avoiding the creation of an output array that would have costed us 80 bytes and instead using 20 bytes on the mask. Thus, making a saving of 75% of input array size there on memory. Please note that this might lead to a slight reduction in performance though.

Divakar
  • 218,885
  • 19
  • 262
  • 358
  • 1
    The opposite would be (linear to sRGB): `img = np.where( img < 0.0031308, img * 12.92, 1.055 * (pow(img, (1.0 / 2.4))) - 0.055)` – João Cartucho Feb 26 '20 at 10:48
1

You could also use numpy.piecewise:

In [11]: img32 = np.random.rand(800, 600).astype(np.float32)

In [12]: img_linear = np.piecewise(img32, 
           [img32  <= 0.04045, img32 > 0.04045], 
           [lambda v: v/12.92, lambda v: ((v + 0.055)/1.055)**2.4] )

In [13]: img_linear.shape
Out[13]: (800, 600)

In [14]: img_linear.dtype
Out[14]: dtype('float32')
user40314
  • 269
  • 2
  • 8
  • 1
    Thanks! Am using np.where for now but I will do a comparison of execution speed later to see what works the fastest and is most efficient. – tde Sep 30 '16 at 14:11
0
b = (img32 < 0.04045)
img32[b] /= 12.92

not_b = numpy.logical_not(b)
img32[not_b] += 0.05
img32[not_b] /= 1.055
img32[not_b] **= 2.4
Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249