Quantify from RGB color coding

Hi All,
I have images like this one and need to attribute a value to the pixels within the ROI on the scan (to compute a value for the whole ROI), based on the scale to the right (max value is “300k” in this example) of the image.
I looked in previous posts and think I could get RGB values from each pixels but I am not sure about how to relate it to the calibration bar in a script…

example.tif (331.5 KB)

Hello Olivier -

I’m assuming you don’t already have the formula for the calibration
bar from some external source.

You should, however, be able to read it directly off the image.
You could look at individual pixels in the calibration bar (or
average over small ROIs), and create your own calibration
formula or look-up table.

I think you will have to mark locations on the calibration bar that
correspond to specific numerical values by hand. The scale of the
calibration bar looks linear, so this shouldn’t be to hard. Even if the
exact location (or size) of the calibration bar differs from image to
image, I would guess that the false-color calibration is the same
for all images. If so, you could build your calibration formula / table
by analyzing the calibration bar on just a single image, and then
use it for all the others.

RGB color space is three-dimensional, but your color-coded
pressure is one-dimensional, so, looking at your example image,
it might be easier and more accurate to do the calibration and
analysis in one-dimensional hue space.

You could run Image > Type > HSB Stack, and then just work
with the hue channel.

Thanks, mm

thanks @mountain_man. I manage to get the RGB values of each pixel in the ROI and in the calibration bar with a modified version of the color picker macro:

v = newArray(2970);
   		Roi.getContainedPoints(xpoints, ypoints);
   		for (i = 0; i < xpoints.length-1; i++) {
			v[i]= getPixel(xpoints[i], ypoints[i]);
        	row = nResults;
        	setResult("X", row, xpoints[i]);
        	setResult("Y", row, ypoints[i]);
        	if (nSlices>1) setResult("Z", row, z);
        	if (bitDepth==24) {
            	red = (v[i]>>16)&0xff;  // extract red byte (bits 23-17)
            	green = (v[i]>>8)&0xff; // extract green byte (bits 15-8)
            	blue = v[i]&0xff;       // extract blue byte (bits 7-0)
            	setResult("Red", row, red);
            	setResult("Green", row, green);
            	setResult("Blue", row, blue);
        	} else
            	setResult("Value", row, v[i]);
        		updateResults;
   		}

I tried the HSB command but the scaling bar does not look homogenous, I am not sure why.
I have only handled grey scale images before and I don’t know how to combine the RGB values to scale them to single values of pressure.

Hello Olivier -

Indeed, the calibration bar in the example.tif image you posted
does not look homogeneous. To my eye, example.tif appears to
display compression artifacts. (E.g. original image -> jpg -> tif.)

If the image you posted is not the original, please post the original
in a non-lossy (uncompressed) format. If it is the original, you’ll
have to live with it.

Am I right that you don’t have a separate source for the calibration,
and therefore have to extract it from the calibration bar that is part
of the image?

Post the cleanest image you have, and forum participants will likely
have suggestions for how to smooth things out / approximate things.

Thanks, mm

Hi @Olivier

The question you asked is a really interesting one!

As you have already noted, the suggestion by @mountain_man about the using the Hue value presents (at least in your case) a couple of problems: first, the upper limit of your range is near the “wrap arround” range for the hue value, and the compression just pushes it over the edge a the red end of the bar (plus adding a lot of weird border artifacts) :

Second, even when correcting for this by running:

run("32-bit");
run("Macro...", "code=[if(v>200) v=v-255+15; else v=v+15;]");

the hue saturates at both ends, so you would be unable to distinguish values for those ranges:

I’ve been pondering for a while about how to tackle this, and I think a simple (perhaps naïve, and even inefficient) approach is to actually calculate the RGB distance to the scale bar table, and keep the lowest value. In any case, this has an added bonus of being agnostic to the actual color table used, given that the LUT used varies smoothly and does not repeat colors.

I wrote this macro set to try it, and it seems to work:

var rgbprofile;
var scale_max;
var scale_min;

macro "Get scale [F1]"{
	waitForUser("Select the scale bar");
	scale_min = getNumber("Minimum value of scale", 0);
	scale_max = getNumber("Maximum value of scale", 300);
	setBatchMode(true);
	run("Duplicate...", "title=scalebar");
	run("RGB Stack");
	selectWindow("scalebar");
	run("Select All");
	
	rgbprofile=newArray();
	for(i=0; i<3; i++){
		setSlice(i+1);
		setKeyDown("alt");
		profile=getProfile();
		rgbprofile=Array.concat(rgbprofile, profile);
		}
	close();
	setBatchMode(false);
	showMessage("Scale bar recorded");
	}

macro "Process ROI [F2]"{
	if(selectionType()!=0) exit("Rectangular selection expected");
	setBatchMode(true);
	run("Duplicate...", "title=processed");
	run("Select All");
	Roi.getContainedPoints(xp, yp);
	nPoints=xp.length;
	values=newArray(nPoints);
	for (i = 0; i < nPoints; i++){
		showProgress(i, nPoints);
		v=getPixel(xp[i], yp[i]);
		r=(v>>16)&0xff;
		g=(v>>8)&0xff;
		b=v&0xff; 
		values[i]=findClosest(r, g, b, rgbprofile);
		}
	run("32-bit");
	for (i=0; i<nPoints; i++){
		setPixel(xp[i], yp[i], values[i]);
		}
		
	//scale image to proper range
	range = scale_max - scale_min;
	run("Multiply...", "value=&range");
	run("Add...", "value=&scale_min");
	resetMinAndMax();
	run("Select None");
	setBatchMode(false);
	}

function findClosest(r, g, b, rgb){
	lprofile=rgb.length/3;
	lower=-1;
	mindist2=1e6;
	for(i=0; i<lprofile; i++){
		dist2=pow((r-rgb[i]),2)+pow((g-rgb[i+lprofile]),2)+pow((b-rgb[i+2*lprofile]),2);
		if(dist2<mindist2){
			lower=i;
			mindist2=dist2;
			}
		}
	// as scale bar is read top-to-bottom, values have to be inverted
	return 1-lower/lprofile;
	}

You can try it by saving the code as an .ijm and installing it via Plugins > Macros > Install....
First press F1, make a selection arround the scale bar (be precise with top and bottom, but exclude the first and last rows of lateral pixels to avoid edge artifacts). Then, select the region to process, and press F2. A new 32-bit image will appear, with converted values.
Beware that this script provides quantized values (as many values as the length of the bar). You could indeed modify it to actually interpolate, but I’ll leave to you.

Cheers!
Nico

Thanks to you both for the support!

@mountain_man, you are right, the original images are time series dicoms that I open with Bio-Formats. I then get a composite hyperstack, with 3 channels per frames, although each of them displays all colours as on the image I posted. To get that image I did Color > Channels tool > Convert to RGB. This may not be the right way and caused the artefacts in the calibration bar. I could not find another way.
Because the original dicom contains sensitive information I cannot post it in its original form, I need to crop it. Any suggestion?

@NicoDF, thank you for taking the time to write this code, this will help a lot. I will try it as soon as possible and post back. regarding your last comment, the bar is always the same size, the user only changes the maximal value.

Hello Olivier -

It would be helpful if you could post a (nearly) unaltered single
frame (time slice) of your original dicom time series. (I’m assuming
that the calibration bar doesn’t jump around or otherwise change
from time slice to time slice (of the same dicom time series).

I’m a little confused. You say that you have a hyperstack with
three (color?) channels per frame, but “each of them” (Does
this mean each of the three channels?) displays all colors.
Could you elaborate a little? Also, posting the original image
would help clear up my confusion.

I don’t really have an opinion about the best way to do the channels
processing. (The original image would give forum participants
something to experiment with.

I assume that a single frame (time slice) would be enough, but
other than that, as few changes from the original as possible would
be the most helpful. Rather than crop the image, could you mask
out the sensitive information (set to black or zero or something)?

It would be important not to mask out any of the calibration bar,
and, in fact, leave a little unmasked buffer region around it.

Also, it would be helpful if you could leave unmasked – to the extent
possible – the equivalent of the false-color inset that appears in your
example.tif. I assume that this is the part of the image that you want
to apply the calibration to.

You say that “the user only changes the maximal value.” Does
this mean the “300 kPa” in example.tif might be 250 or 350 kPa
in other dicom hyperstacks? Does the size and appearance (i.e.,
range of colors) in the calibration bar remain the same in all of
the dicom hyperstacks, but maps to different pressure ranges,
e.g., 0 – 250 kPa vs, 0 – 300 kPa?

As a side note (unless it’s an artifact of your processing) I agree
with Nico that hue won’t work properly. It looks as if the ends of
the calibration bar have approximately constant hue, but fall off
in brightness. His suggestion of mapping data pixels to the closest
point in RGB color space in the calibration bar / calibration table
is likely to be the way to go.

Thanks, mm

1 Like

@NicoDF, I don’t think the macro worked. The first part worked I think but in the end I get the following image, similar on the three channels and with values that don’t seem to make sense:

image

As @mountain_man mentioned, I understand about the problem without having the original image. I post it here, along with a screen capture of how it is opened on my laptop:

Hi @Olivier,

It looks like you tried the macros on the original multichannel image. Remember that all I had as an example was your first 24-bit RGB, so the macro was written with that in mind. Can you try first transforming into RGB to see if it works?

I downloaded the new file you provided, but I cannot open it. What’s the format? Do you use any special plugin to open it?

Best,
Nico

Hello Nico -

Just FYI: I downloaded the “C0000013 copy” file that Olivier
posted a link to and renamed it to “C0000013.dcm”. (I don’t
know whether the renaming matters.)

I believe it’s a dicom file. I was able to open it using
File › Import › Bio-Formats. (I don’t know whether
this is a “special” plugin per se. It ships with Fiji / ImageJ.)

For convenience, here is one slice (the first) of Olivier’s
hyperstack file, uploaded as a single-slice, three-channel
TIFF file:

C0000013_one_slice.tif (1.1 MB)

And here is the same image with the three channels merged
into an RGB PNG image, just so it’s easy to see what it looks
like:

Thanks, mm

1 Like

Thanks for the renaming advice, @mountain_man! It did the trick! :smile:

The image seems to be in a compressed DICOM format, and the three channels are indeed just regular 8-bit RGB channels, so converting to 24bit RGB does not produce any loss of information.
The file metadada clearly indicates a lossy compression has been applied beforehand:

0028,2110 Lossy Image Compression #1 = 01
0028,2112 Lossy Image Compression Ratio #1 = 24.3772
0028,2114 Lossy Image Compression Method #1 = ISO_10918_1

So, @Olivier, I can confirm you that converting to RGB color should fix the problem you observed with the macro.
Regarding your comment:

What I meant is the following: as the macro tries to match the color values to a table that is constructed from the color bar, there is a finite (quantized) set of possible values it will return. If the image happens to have a large region near a particular value (e.g. from 10 to 15), but the scale bar covers a much larger range (0-300), the values in that region will come from a small set of posibilities, and the resolution of those values will thus be limited. This would be an issue if the original dynamic range of the measurement happened to be much larger (say 12 bit), but the image is then saved as a “baked” 8-bit color. In any case, if you don’t have acccess to the original measurement, there’s not much to be done.

Cheers!
Nico

Yes, I conform that the image was in the dicom format. I use Bio-Formats as it is the only way to open compressed dicoms “out of the box” in Fiji.

@NicoDF sorry, I did not think straight when trying your script on the original file, now everything works very well! I thank you both again for your patience and taking the time to help. I am not a “specialist” of image analysis as I you gathered, but solving problems like this gives the necessary head start to learn develop analysis alone in the future. So, thanks for your efforts!

1 Like