0

As context, I'm working with building a topographic program which needs relatively extreme detail. I do not expect the files to be small, and they do not formally need to be viewed on a monitor, they just need to have very high resolution.

I know that most image formats are limited to 8 bpp, on account of the standard limits on both monitors (at a reasonable price) and on human perception. However, 2⁸ is just 256 possible values, which induces plateauing artifacts in a reconstructed displacement. 2¹⁶ may be close enough at 65,536 possible values, which I have achieved.

I'm using FreeImage and DLang to construct the data, currently on a Linux Mint machine.

However, when I went on to 2³², software support seemed to fade on me. I tried a TIFF of this form and nothing seemed to be able to interpret it, either showing a completely (or mostly) transparent image (remembering that I didn't expect any monitor to really support 2³² shades of a channel) or complaining about being unable to decode the RGB data. I imagine that it's because it was assumed to be an RGB or RGBA image.

FreeImage is reasonably well documented for most purposes, but I'm now wondering, what is the highest-precision single-channel format I can export, and how would I do it? Can anyone provide an example? Am I really limited, in any typical and not-home-rolled image format, to 16-bit? I know that's high enough for, say, medical imaging, but I'm sure I'm not the first person to try to aim higher and we science-types can be pretty ambitious about our precision-level…

Did I make a glaring mistake in my code? Is there something else I should try instead for this kind of precision?

Here's my code.

The 16-bit TIFF that worked

void writeGrayscaleMonochromeBitmap(const double width, const double height) {
    FIBITMAP *bitmap = FreeImage_AllocateT(FIT_UINT16, cast(int)width, cast(int)height);
    for(int y = 0; y < height; y++) {
        ubyte *scanline = FreeImage_GetScanLine(bitmap, y);
        for(int x = 0; x < width; x++) {
            ushort v = cast(ushort)((x * 0xFFFF)/width);
            ubyte[2] bytes = nativeToLittleEndian(cast(ushort)(x/width * 0xFFFF));
            scanline[x * ushort.sizeof + 0] = bytes[0];
            scanline[x * ushort.sizeof + 1] = bytes[1];
        }
    }
    FreeImage_Save(FIF_TIFF, bitmap, "test.tif", TIFF_DEFAULT);
    FreeImage_Unload(bitmap);
}

The 32-bit TIFF that didn't really work

void writeGrayscaleMonochromeBitmap32(const double width, const double height) {
    FIBITMAP *bitmap = FreeImage_AllocateT(FIT_UINT32, cast(int)width, cast(int)height);
    writeln(width, ", ", height);
    writeln("Width: ", FreeImage_GetWidth(bitmap));
    for(int y = 0; y < height; y++) {
        ubyte *scanline = FreeImage_GetScanLine(bitmap, y);
        writeln(y, ": ", scanline);
        for(int x = 0; x < width; x++) {
            //writeln(x, " < ", width);
            uint v = cast(uint)((x/width) * 0xFFFFFFFF);
            writeln("V: ", v);
            ubyte[4] bytes = nativeToLittleEndian(v);
            scanline[x * uint.sizeof + 0] = bytes[0];
            scanline[x * uint.sizeof + 1] = bytes[1];
            scanline[x * uint.sizeof + 2] = bytes[2];
            scanline[x * uint.sizeof + 3] = bytes[3];
        }
    }
    FreeImage_Save(FIF_TIFF, bitmap, "test32.tif", TIFF_NONE);
    FreeImage_Unload(bitmap);
}

Thanks for any pointers.

Michael Macha
  • 1,729
  • 1
  • 16
  • 25
  • does it specifically have to be greyscale? you could use a 24 or 32 bit number as a color triplet.. it isn't *strictly* a big single channel but you could process it as such and maybe have a color table for users to translate (e.g. "more red means big changes, more blue means small changes") – Adam D. Ruppe Oct 22 '19 at 15:19
  • 2
    try replacing `ushort.sizeof` with `uint.sizeof`. – beerboy Oct 22 '19 at 20:09
  • @beerboy Oh my God. How did I miss that!? – Michael Macha Oct 23 '19 at 06:16
  • OK, I fixed the glaring error in the code; unfortunately, nothing's changed in behavior. (Thanks for pointing that out, @beerboy.) Everything continues to fail to read the RGB data. As far as just using color goes, @adamdruppe, for efficiency's sake I would like to avoid unnecessary pointer math. (I've already read your book, by the way; it's very good.) If it's at all possible to reliably encode this as gray scale, then that's my goal; as 32-bits/4-billionish grades might not always be adequate. – Michael Macha Oct 23 '19 at 06:29
  • Last comment for the evening; I just read that TIFF is limited to 16 bits. I'm not yet sure if that's a mistake; either it is, or my previous data is. It does seem strange that `file` succeeds in reporting the tiff as 32-bit. In any case, I'm digging into extreme-bit-depth images and will likely post my solution when I find it. – Michael Macha Oct 23 '19 at 06:58
  • `file` prolly just reads the header without referring back to the spec, while the actual image programs do. Like in png, it is technically possible to put 32 in the depth header field... but the spec says the only valid values are 1,2,4,8, and 16 (and even then certain combinations only apply to certain image types, like you can't do a 16 bit indexed image) so most programs will error out saying the file is invalid if you tried. But an rgb thing shouldn't be much different on efficiency than other loading, it is all just bytes at that level. Another idea is to use two side-by-side grey pixels – Adam D. Ruppe Oct 23 '19 at 12:28
  • Good point, I think I'm getting to the bottom of this though. I just got a table together of respective formats to supported pixel types, and I haven't really found anything on wide support for gray-scale beyond 32-bit. You're right, in the end they're just bytes. I did bump into a reference to 128RGBAF, meaning each channel getting its own 32-bit float, and I think I could work with that if I really needed to. Humans do better with color data anyway. I'm not going to feel /done/ until I've got that 32-bit channel going though. – Michael Macha Oct 23 '19 at 17:21
  • So, I've gotten a rough answer—if we're talking about single channels, the highest is 32-bit. (Sensible.) I'm aware of it being able to export as high as 128 bits across four 32-bit channels, which could be combined via @adamdruppe's method. I intend to answer this question with a code sample soon; but unfortunately, while TIF_UINT32 exports without complaint, and the header verifies 32-bit channels, nothing seems to be able to display it. I may have to unit test the data integrity first. – Michael Macha Oct 24 '19 at 02:01

1 Answers1

1

For a single channel, the highest available from FreeImage is 32-bit, as FIT_UINT32. However, the file format must be capable of this, and as of the moment, only TIFF appears to be up to the task (See page 104 of the Stanford Documentation). Additionally, most monitors are incapable of representing more than 8-bits-per-sample, 12 in extreme cases, so it is very difficult to read data back out and have it render properly.

A unit test involving comparing bytes before marshaling to the bitmap, and sampled from the same bitmap afterward, show that the data is in fact being encoded.

To imprint data to a 16-bit gray scale (currently supported by J2K, JP2, PGM, PGMRAW, PNG and TIF), you would do something like this:

void toFreeImageUINT16PNG(string fileName, const double width, const double height, double[] data) {
    FIBITMAP *bitmap = FreeImage_AllocateT(FIT_UINT16, cast(int)width, cast(int)height);
    for(int y = 0; y < height; y++) {
            ubyte *scanline = FreeImage_GetScanLine(bitmap, y);
            for(int x = 0; x < width; x++) {
                    //This magic has to happen with the y-coordinate in order to keep FreeImage from following its default behavior, and generating
                    //the image upside down.
                    ushort v = cast(ushort)(data[cast(ulong)(((height - 1) - y) * width + x)] * 0xFFFF); //((x * 0xFFFF)/width);
                    ubyte[2] bytes = nativeToLittleEndian(v);
                    scanline[x * ushort.sizeof + 0] = bytes[0];
                    scanline[x * ushort.sizeof + 1] = bytes[1];
            }
    }
    FreeImage_Save(FIF_PNG, bitmap, fileName.toStringz);
    FreeImage_Unload(bitmap);
}

Of course you would want to make adjustments for your target file type. To export as 48-bit RGB16, you would do this.

void toFreeImageColorPNG(string fileName, const double width, const double height, double[] data) {
    FIBITMAP *bitmap = FreeImage_AllocateT(FIT_RGB16, cast(int)width, cast(int)height);
    uint pitch = FreeImage_GetPitch(bitmap);
    uint bpp = FreeImage_GetBPP(bitmap);
    for(int y = 0; y < height; y++) {
            ubyte *scanline = FreeImage_GetScanLine(bitmap, y);
            for(int x = 0; x < width; x++) {
                    ulong offset = cast(ulong)((((height - 1) - y) * width + x) * 3);
                    ushort r = cast(ushort)(data[(offset + 0)] * 0xFFFF);
                    ushort g = cast(ushort)(data[(offset + 1)] * 0xFFFF);
                    ushort b = cast(ushort)(data[(offset + 2)] * 0xFFFF);
                    ubyte[6] bytes = nativeToLittleEndian(r) ~ nativeToLittleEndian(g) ~ nativeToLittleEndian(b);
                    scanline[(x * 3 * ushort.sizeof) + 0] = bytes[0];
                    scanline[(x * 3 * ushort.sizeof) + 1] = bytes[1];
                    scanline[(x * 3 * ushort.sizeof) + 2] = bytes[2];
                    scanline[(x * 3 * ushort.sizeof) + 3] = bytes[3];
                    scanline[(x * 3 * ushort.sizeof) + 4] = bytes[4];
                    scanline[(x * 3 * ushort.sizeof) + 5] = bytes[5];
            }
    }
    FreeImage_Save(FIF_PNG, bitmap, fileName.toStringz);
    FreeImage_Unload(bitmap);
}

Lastly, to encode a UINT32 greyscale image (limited purely to TIFF at the moment), you would do this.

void toFreeImageTIF32(string fileName, const double width, const double height, double[] data) {
    FIBITMAP *bitmap = FreeImage_AllocateT(FIT_UINT32, cast(int)width, cast(int)height);

    //DEBUG
    int xtest = cast(int)(width/2);
    int ytest = cast(int)(height/2);
    uint comp1a = cast(uint)(data[cast(ulong)(((height - 1) - ytest) * width + xtest)] * 0xFFFFFFFF);
    writeln("initial: ", nativeToLittleEndian(comp1a));

    for(int y = 0; y < height; y++) {
            ubyte *scanline = FreeImage_GetScanLine(bitmap, y);
            for(int x = 0; x < width; x++) {
                    //This magic has to happen with the y-coordinate in order to keep FreeImage from following its default behavior, and generating
                    //the image upside down.
                    ulong i = cast(ulong)(((height - 1) - y) * width + x);
                    uint v = cast(uint)(data[i] * 0xFFFFFFFF);
                    ubyte[4] bytes = nativeToLittleEndian(v);
                    scanline[x * uint.sizeof + 0] = bytes[0];
                    scanline[x * uint.sizeof + 1] = bytes[1];
                    scanline[x * uint.sizeof + 2] = bytes[2];
                    scanline[x * uint.sizeof + 3] = bytes[3];
            }
    }

    //DEBUG
    ulong index = cast(ulong)(xtest * uint.sizeof);
    writeln("Final: ", FreeImage_GetScanLine(bitmap, ytest)
            [index .. index + uint.sizeof]);

    FreeImage_Save(FIF_TIFF, bitmap, fileName.toStringz);
    FreeImage_Unload(bitmap);
}

I've yet to find a program, built by anyone else, which will readily render a 32-bit gray-scale image on a monitor's available palette. However, I left my checking code in which will consistently write out the same array both at the top DEBUG and the bottom one, and that's consistent enough for me.

Hopefully this will help someone else out in the future.

Michael Macha
  • 1,729
  • 1
  • 16
  • 25