# Invert RGB image without changing colors

Triggered by a recent Twitter thread, I got reminded of a discussion of how to invert RGB images without actually changing their colors. Unfortunately, I don’t remember where this discussion happened (on the old ImageJ mailing list?) and wasn’t able to find it again.

I do remember though that the solution involved basically inverting each of the RGB channels and then creating a new images with each channel being the combined inverted version of the two others:

R = 1 - \frac{(G+B)}{2}

G = 1 - \frac{(R+B)}{2}

B = 1 - \frac{(R+G)}{2}

I created a Groovy script using #imagej-ops that inverts any RGB image this way:

Here’s the Fluorescent Cells sample image:

And here’s the plot by @haesleinhuepf from the mentioned tweet:

Feel free to use the script if you like it; and if you have improvements, please share them here!
I’d also be curious to see a #clij version of the same

8 Likes

Hey @imagejan,

So I’m happy to share how to do this in #clij :

Basically, there are two options.

# The recordable way

The recordable way using ImageJ macro leads you doing math step by step. The following method is called three times for the three cases (G+B, R+B and R+G). The full script is online.

function mixChannels(c1, c2) {
temp1 = "temp1";
temp2 = "temp2";

// c1 + c2
// divide by 2
Ext.CLIJ_multiplyImageAndScalar(temp1, temp2, 0.5);
// -
Ext.CLIJ2_invert(temp2, temp1);
return temp2;
}


# The hacky way

The alternate way of doing it is implementing an own OpenCL-kernel which is not so complicated in this case. The opencl kernel code is online as well:

__kernel void rgbReplaceBlackAndWhite(
DTYPE_IMAGE_IN_3D src,
DTYPE_IMAGE_OUT_3D dst
)
{
const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;

// set positions from where to read
int4 posR = {get_global_id(0), get_global_id(1), 0, 0};
int4 posG = {get_global_id(0), get_global_id(1), 1, 0};
int4 posB = {get_global_id(0), get_global_id(1), 2, 0};

float r = READ_IMAGE_3D(src, sampler, posR).x;
float g = READ_IMAGE_3D(src, sampler, posG).x;
float b = READ_IMAGE_3D(src, sampler, posB).x;

// do the math suggested by Jan Eglinger  https://forum.image.sc/t/invert-rgb-image-without-changing-colors/33571
float ir = 255 - (g + b) / 2;
float ig = 255 - (r + b) / 2;
float ib = 255 - (r + g) / 2;

// write the pixels back to the destination image
WRITE_IMAGE_3D(dst, posR, CONVERT_DTYPE_OUT(ir));
WRITE_IMAGE_3D(dst, posG, CONVERT_DTYPE_OUT(ig));
WRITE_IMAGE_3D(dst, posB, CONVERT_DTYPE_OUT(ib));
}


In order to call the kernel, one has to use clij.execute. It has three parameters: the kernel filename, the function name inside that file and a dictionary of parameters. The jython script can be found online:

parameters = {
"src":input,
"dst":output
};
clij.execute(filesPath + "rgbReplaceBlackAndWhite.cl", "rgbReplaceBlackAndWhite", parameters);


More details on custom OpenCL kernel development for #clij are available on the website as well.

# Are the results the same?

Almost. I suspect my code has a typo somewhere.

# Which is faster?

Not a big suprise: Calling one OpenCL kernel is faster than calling 18:

If one excludes push/pull/show commands from the time measurement, the custom OpenCL kernel is executed in 7-11 ms on an Intel HD 620 GPU.

That was fun @imagejan ! We should do this more often

Cheers,
Robert

1 Like

There used to be a macro (I think in Fiji?) called “Invert for Printing” that did exactly this.

2 Likes

Thanks for the heads up. I now dug it out from the mailing list archives:

Re: converting colors, May 09, 2014; 10:24am

and the reply by @jerome.mutterer/@jerome :

Maybe pasting the replies here makes things a little more discoverable, for the next time someone asks for this on Twitter

Just a detail, I think Jerome’s version is better, as it preserves some colours that the other does not.

2 Likes

For the record, here are the results of the three suggested approaches, applied to the Fluorescent Cells sample image:

2 Likes

The correct 180° hue rotation is modulus 256 instead of 255

run(“Macro…”, “code=v=(v+128)%256 slice”);

2 Likes

Try adding some Cyan Magenta and yellow spots… you’'ll see what I mean.

2 Likes

Alright, here’s the next comparison, on the same image, with channels converted to Cyan, Magenta, Yellow:

I agree the approach changing the hue looks nicest

2 Likes

Hi everyone!

Just to add to this topic, and for future reference: as part of the twitter thread that sparked this color inversion theme revival, @jerome (or @jerome.mutterer ?) and I ended up putting together an action tool macro to exactly accomplish this very same approach invertion, but using a faster alternative than the pixel-by-pixel macro call:

It also features a final gamma correction, but can be disabled (or customized) via right click:

Cheers!
Nico

3 Likes

And just to add to the comparison, here are some examples, alongside their 3D color distribution in RGB (using the excellent Plugins > Color Inspector 3D, by @barthel).

Original (thanks @haesleinhuepf):

Hue rotated, no gamma:

@imagejan’s inversion:

Hue rotated + .45 gamma:

When I first saw @imagejan’s invertion, I noticed the “compression” towards the RGB diagonal (pure B-W line), which deviated from what I imagined the expected result should be. It was not until someone suggested inverting the lightness (in HSL), and then the completely equivalent (add shock and amazement ) approach of rotating the hue by @jerome that I could observe what I envisioned as the desired result in this 3D view.

3 Likes