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 :slight_smile:

8 Likes

Hey @imagejan,

awesome! You’re answering my question without me having asked :slight_smile:

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
	Ext.CLIJ_addImages(c1, c2, temp1);
	// divide by 2
	Ext.CLIJ_multiplyImageAndScalar(temp1, temp2, 0.5);
	// -
	Ext.CLIJ2_invert(temp2, temp1);
	// add 255
	Ext.CLIJ2_addImageAndScalar(temp1, temp2, 255);
	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};
 
    // read original pixel values
    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:
image
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 :wink:

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 :wink:

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 :slightly_smiling_face:

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 :astonished:) approach of rotating the hue by @jerome that I could observe what I envisioned as the desired result in this 3D view.

3 Likes