1

I currently have a Unity3D tool that allows users to import different heightmap files into the Unity3d Game Engine, in order to produce Unity Terrain objects. The tool currently has limited support for GeoTIFF files, as I am manually reading them and that only works for some projection types. I would like to expand support to (hopefully) all GeoTIFF files by using the GDAL Nuget package from Tamas Szekeres.

I basically need to take a GeoTIFF file, and produce a heightmap from it, which is a float[,] array of normalized values between 0 and 1. Unity uses this information to create the Terrain object.

What is the best way to do this? I first attempted to use the DataSet.ReadRaster method, but that is crashing Unity with an error saying I am trying to read an invalid memory address. Even if I could get that method working, I am not sure if it is the correct route to take.

Here is what that looked like:

    double[] rasterData = new double[outputWidth * outputLength];
    ds.ReadRaster(0, 0, 65, 65, rasterData, 65, 65, ds.RasterCount, null, 0, 0, 0);

Most suggestions I see say to use the individual Raster Bands, which I did test and it does not cause an error (I did not actually try to use this data), however how do I handle different GeoTIFF files (using different projections and data formats) with this method? For example, I have many files with a single Raster Band and which store individual height data as floats, which seems easy enough to read, but then I have other files where the height data is in RGB format.

Is there a single method I can call to translate the data, no matter what format it is, to a normalized value or a value that I can manually normalize? Or do I need to handle each projection type independently?

I am using this in a 64 bit Unity Editor.

I have been using LibTiff.Net to manually read the GeoTIFF files. I thought using GDAL I would be able to gain access to some sort of auto translate function to convert almost any GeoTIFF file to either an array of raw height values or even an array of normalized values. But the more I look into it it seems I will have to read the DataType of each file and manually convert each data point as I am already doing, so I am not sure if there is any advantage in switching to GDAL.

Is that correct?

  • Height as RGB cannot be worked with, there is no way to tell what renderer was used to convert the heights into RGB values. As this is a code question do you have any code to work from? Are you operating in 32 or 64 bits? How big are your TIFF files (rows x columns x bytes per pixel)? Are you using managed or unsafe memory? It is possible that your array is too large to store in memory which would return a nullptr on allocation which is an invalid memory address. – Michael Stimson Sep 12 at 23:47
  • The test file I am using is indeed very large (46080 x 23040 pixels), however I am reading a smaller 65 x 65 subset of that. Does DataSet.ReadRaster read the whole file no matter if you specify a subset? The Unity Editor I am using is a 64 bit process. – Kyle G Sep 13 at 1:40
  • It looks like you're trying to read band 0, it should be band 1. I've always used raster band objects like :gThisBand = ds.GetRasterBand(1); gThisBand.ReadRaster(col, row, 1, 1, buff, 1, 1, 0, 0); in C#; this example reads a single pixel as an array at col,row. – Michael Stimson Sep 13 at 4:01
  • When reading the band I do indeed read band 1, not 0. The example I posted in the OP is reading the DataSet. As I said, however, I need to be able to read GeoTIFF's of multiple different types. How far will reading the band go towards this? So long as a file has one raster band, will just reading band 1 work no matter the format? I assume not, since you have already stated a file in RGB format won't be able to be read in this way. – Kyle G Sep 13 at 4:14
  • It's not that you can't read an RGB it's just you can't convert RGB into elevation. – Michael Stimson Sep 13 at 4:36
0

You can use the TiffBitmapDecoder in .NET to open the the GeoTiff and extract metadata in a fairly painless way. I am primarily working with single band elevation GeoTIFF files and the following code represents that, though trimmed down a little. After reading the GeoTIFF in you should have little issue normalizing the values. Using the PixelSize and PixelFormat you can determine what IPixelFormat implementation you will need.

BitmapMetadata _metadata;
Stream _stream;
IPixelFormat _pixels;

public GeoTiff(string tiff_path)
{
    Path = tiff_path;
    Name = System.IO.Path.GetFileNameWithoutExtension(tiff_path);

    _stream = new System.IO.FileStream(tiff_path, FileMode.Open, FileAccess.Read, FileShare.Read);

    TiffBitmapDecoder decoder = new TiffBitmapDecoder(_stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    //load the first frame of the data
    BitmapSource source = decoder.Frames[0];

    //save off the metadata to use when saving to new file later, 
    //keep the stream open until done with the metadata object
    //otherwise saving with it will fail.
    _metadata = (BitmapMetadata)source.Metadata.Clone();
    var obj = _metadata.GetQuery("/ifd/{uint=339}");

    DpiX = source.DpiX;
    DpiY = source.DpiY;
    Palette = source.Palette;

    //gets the Height and Width of the image in pixels
    Width = source.PixelWidth;
    Height = source.PixelHeight;

    //Getes the dimensions of the pixel in geographic space
    double[] pixels_dimensions = (double[])_metadata.GetQuery("/ifd/{uint=33550}");
    PixelWidth = pixels_dimensions[0];
    PixelHeight = pixels_dimensions[1];

    //Finds the origin coordinates of the tiff
    double[] coordinate = (double[])_metadata.GetQuery("/ifd/{uint=33922}");
    Longitude = coordinate[3] - PixelWidth / 2;
    Latitude = coordinate[4] + PixelHeight / 2;

    //get size of pixel in bytes
    PixelSize = Convert.ToInt32(_metadata.GetQuery("/ifd/{uint=258}")) / 8;

    //gets the format of the pixel
    PixelFormat = Convert.ToInt32(_metadata.GetQuery("/ifd/{uint=339}"));

    //Pixel is 4 bytes long and in a signed integer format
    if (PixelSize == 4 && PixelFormat == 2)
        _pixels = new PixelFormatInt(source.PixelWidth * source.PixelHeight);
    else
        throw new Exception("Unsupported Pixel Format");
    source.CopyPixels(_pixels.GetArray(), 2 * source.PixelWidth, 0);

    //find the value that should be considered a null value
    var no_data_obj = _metadata.GetQuery("/ifd/{uint=42113}");
    if (no_data_obj != null)
        NoDataValue = int.Parse(no_data_obj.ToString());
}

/// <summary>Converts a coordindate from map space into pixel space</summary>
public void MapToPixel(double x, double y, out int column, out int row)
{
    column = (int)Math.Round((x - Longitude - PixelWidth / 2) / PixelWidth);
    row = (int)Math.Round((-y + Latitude - PixelHeight / 2)/PixelHeight);
}

/// <summary>Converts a pixel coordinate from pixel space to map space (pixel center is used)</summary>
public void PixelToMap(int column, int row, out double x, out double y)
{
    x = Longitude + column * PixelWidth + PixelWidth / 2;
    y = Latitude - row * PixelHeight - PixelHeight / 2;
}

And then PixelFormatInt's implementation

class PixelFormatInt : IPixelFormat
{
    int[] _array;

    public PixelFormatInt(int size)
    {
        _array = new int[size];
    }

    public Array GetArray()
    {
        return _array;
    }

    public double GetPixel(int index)
    {
        return _array[index];
    }

    public void SetPixel(int index, double value)
    {
        _array[index] = (int)Math.Round(value);
    }
}
  • Thank you, however I have stated that I have a working implementation of LibTiff.Net which I use to read the Meta Data and get the height information from some files. However, the issue is some users send me files to test that don't work using this method. I was hoping GDAL would alleviate these issues, but it's possible I am misunderstanding GDAL and it is not capable of doing what I'm looking for. – Kyle G Sep 19 at 6:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.