Problem reading RGB tiff

Is it that PLANARCONFIG_SEPARATE stores the planes one after the other: rrrrrrrrr…gggggg…bbbbbb…
while PLANARCONFIG_CONTIG stores them as: rgbrgbrgbrgb…?

Yep, looks like it. I was hoping that tiff2rgb would change plane formatting, and it does, but only with 8 bit pixels :frowning:

That dump by printing the reader:

std::cout << ReaderPointer ;

So much to learn, so little time…

Sometimes the different colours are stored in separate directories, and then you can read as a 3D image and take colours from separate planes in ITK. But that doesn’t seem to work either in this case.

At the end of this: http://paulbourke.net/dataformats/tiff/
he writes:
"TIFF RGB images should always have a contiguous (RGBRGBRGB …) not planar (RRR…RGGG…GBBB…B) format. While the header allows you to specify planar, it is not well supported. "
Maybe libtiff (which was used to extract the 2D tiffs from the 3D LSM file) has the ability to do the planar->contiguous conversion.
This is the tiffsplit code:
if (in != NULL) {
do {
size_t path_len;
char *path;

		newfilename();

		path_len = strlen(fname) + sizeof(TIFF_SUFFIX);
		path = (char *) _TIFFmalloc(path_len);
		strncpy(path, fname, path_len);
		path[path_len - 1] = '\0';
		strncat(path, TIFF_SUFFIX, path_len - strlen(path) - 1);
		out = TIFFOpen(path, TIFFIsBigEndian(in)?"wb":"wl");
		_TIFFfree(path);

		if (out == NULL)
			return (-2);
		if (!tiffcp(in, out))
			return (-1);
		TIFFClose(out);
	} while (TIFFReadDirectory(in));
	(void) TIFFClose(in);
}

It reads the directory for each slice.

A partial, but very ugly solution. Imagemagick does an OK job of reading planar and writing something more standard:

convert xaas.tif -depth 16 a.tif

tiffinfo a.tif
TIFF Directory at offset 0x7c0b608 (130070024)
  Image Width: 4656 Image Length: 4656
  Bits/Sample: 16
  Compression Scheme: None
  Photometric Interpretation: RGB color
  FillOrder: msb-to-lsb
  Orientation: row 0 top, col 0 lhs
  Samples/Pixel: 3
  Rows/Strip: 1
  Planar Configuration: single image plane
  Page Number: 0-1
  White Point: 0.3127-0.329
  PrimaryChromaticities: 0.640000,0.330000,0.300000,0.600000,0.150000,0.060000

You may be able to turn off some of the fancier bits, as ITK seems to complain about them.

However, this appears to do something sensible now:

#include "itkImage.h"
#include <itkImageFileReader.h>
#include <itkImageFileWriter.h>
#include "itkRGBToLuminanceImageFilter.h"

typedef itk::Image<itk::RGBPixel<unsigned short>, 2> RGBImageType;
typedef itk::Image<unsigned short, 2>  ScalarImageType;

int main(int argc, char * argv[])
{
  typedef itk::ImageFileReader<RGBImageType> ReaderType;
  typedef itk::ImageFileWriter<ScalarImageType> WriterType;

  ReaderType::Pointer reader = ReaderType::New();

  reader->SetFileName("a.tif");

  typedef itk::RGBToLuminanceImageFilter<RGBImageType, ScalarImageType> LuminanceType;
  LuminanceType::Pointer L = LuminanceType::New();
  L->SetInput(reader->GetOutput());
  
  
  WriterType::Pointer writer = WriterType::New();
  writer->SetInput(L->GetOutput());

  writer->SetFileName("gs.nii.gz");
  writer->Update(); 
}

1 Like

That’s promising (I am not familiar with .nii files). My ReadVectorImage program correctly reports the number of components in a.tif as 3, but gives the max RGB values as: 3855 0 0
That looks about right for R, but G and B should be something similar.

My ReadRGB shows max RGB as: 3855 3855 3855
which is suspicious.

My viewer (an old thing called imview) was certainly seeing the colour.

This one is reporting the same range for each channel - possibly due to that chromacities tag, which could be some sort of per channel scaling thing.

#include "itkImage.h"
#include <itkImageFileReader.h>
#include <itkImageFileWriter.h>
#include "itkRGBToLuminanceImageFilter.h"


#include "itkMinimumMaximumImageCalculator.h"
#include "itkVectorIndexSelectionCastImageFilter.h"


typedef itk::Image<itk::RGBPixel<unsigned short>, 2> RGBImageType;
typedef itk::Image<unsigned short, 2>  ScalarImageType;

int main(int argc, char * argv[])
{
  typedef itk::ImageFileReader<RGBImageType> ReaderType;
  typedef itk::ImageFileWriter<ScalarImageType> WriterType;

  ReaderType::Pointer reader = ReaderType::New();

  reader->SetFileName("a.tif");
  reader->Update();
  
  typedef itk::VectorIndexSelectionCastImageFilter<RGBImageType, ScalarImageType> IndexSelectionType;
  IndexSelectionType::Pointer indexSelectionFilter = IndexSelectionType::New();
  indexSelectionFilter->SetInput(reader->GetOutput());
  indexSelectionFilter->SetIndex(0);
  indexSelectionFilter->Update();
  
  typedef itk::MinimumMaximumImageCalculator <ScalarImageType> ImageCalculatorFilterType;
  ImageCalculatorFilterType::Pointer imageCalculatorFilter = ImageCalculatorFilterType::New();
  imageCalculatorFilter->SetImage(indexSelectionFilter->GetOutput());
  imageCalculatorFilter->Compute();
  
  std::cout << "Red " << (int)imageCalculatorFilter->GetMinimum() << " " <<
    (int)imageCalculatorFilter->GetMaximum() << std::endl;

  indexSelectionFilter->SetIndex(1);
  indexSelectionFilter->Update();
  imageCalculatorFilter->Compute();

  std::cout << "Green " << (int)imageCalculatorFilter->GetMinimum() << " " <<
    (int)imageCalculatorFilter->GetMaximum() << std::endl;

  indexSelectionFilter->SetIndex(2);
  indexSelectionFilter->Update();

  imageCalculatorFilter->Compute();

  std::cout << "Blue " << (int)imageCalculatorFilter->GetMinimum() << " " <<
    (int)imageCalculatorFilter->GetMaximum() << std::endl;

  return EXIT_SUCCESS;
    
}

1 Like

Interestingly, after splitting the channels in ImageJ, I see that each channel has the same maximum value of 3855. However ImageJ shows a maximum of 4095 (2^12 - 1) for each channel of xaas.tif. That is consistent with the fact that the microscope is using 12 bits to encode the data. Presumably each colour is scaled to use the whole available range. But why does 4096 -> 3856? I don’t see where the 3855 comes from. From a practical point of view, this small loss of precision is insignificant.
It’s also interesting and beyond my understanding why my ReadRGB program seems to be working, but ReadVectorImage is not. You have a third way to access the channels - any advantages/disadvantages of this method over ReadRGB?

By the way, the separate images produced by extracting with ImageMagick:
convert xaas.tif -channel RGB -separate out_%d.tif
also each have maximum values of 3855. So this is something that is done by convert, not by ITK.

Perhaps imageJ is showing the max possible? That would make sense if there was extra meta data in the LSM storing information about the sensor precision.

In ReadVectorImage you have only called SetExtractComponentIndex once, so I don’t think the GetRed, GetGreen will be doing the right thing.

Advantages/disadvantages depend on what happens next. VectorIndexSelection and VectorImageToImageAdaptor are similar, I think, and are most useful if you’re going to pass each channel through a series of filters.

Another possibility - was imageJ reporting the range over the entire stack? I’m getting 4112 as the max, which is strange.

No, I don’t attempt to look at the stack in ImageJ (the 3D file is 50 GB, much too big for my machine). 3855 I can believe because I trust ImageJ - where does 4112 come from?

If I do the single channel thing, and view out_0.tif in imview, I can find a pixel with intensity = 4112! So at that point it hasn’t touched ITK. What does imageJ say about out_0.tif?

I get a max of 3855.
I fixed ReadVectorImage - thanks for pointing out my mistake - and now it gives 3855 3855 3855 for a.tif, so I’m happy that my ITK programs are consistent.

It was about 3 years ago now when a major refactoring of the TIFFIO was done.

@richard.beare correctly determined that this file, and with it’s particular TIFF tags, is utilizing the fallback method TIFFReadRGBAImageOriented. There are an incredible number of combinations of features a TIFF image could have and this method “handles” them all. For this particular case it should like the file is just using 12-bits of the 16-bit range. So when this method maps the full 16-bit range to 8-bit = 3855/2^16 = .05, the result is an image of all zeros.

The other code path utilized TIFFReadScanline. The following page has a suggested approach for using that method for PLANARCONFIG_SEPARATE image:
http://www.libtiff.org/libtiff.html

My conclusion is that I don’t think that there is really a bug in the code, but the bad sideffect of the file not using the full dynamic range of the pixel type. The best ( not easiest ) would be to update TIFFImageIO::ReadGenericImage to support PLANARCONFIG_SEPARATE.

Wouldn’t mapping 3855 from 16-bit to 8-bit give 15?
In any case, dropping down to 8-bit is obviously not acceptable in this context. Richard pointed out a good solution to my problem using ImageMagick’s convert tool, so I’m fixed up.

1 Like

This is not correct:

It should be 3855/2^8 = 15.058 ( reducing from 16-bits to 8 ). So yes, that nearly exactly matches the process of scaling 16-bits to 8-bits, and the source not spanning the full 16-bits.

3855(base 10) = 0000111100001111(base 2)
Then the lower 8 bits are dropped you only have 4 bits or 0-15.