Как перевести Tiff.ReadEncodedTile в матрицу рельефа местности с карты высот в C#?
Я новичок в работе с изображениями TIFF и пытаюсь получить значения рельефа местности из карты TIFF с помощью LibTiff. Карты, которые мне нужно декодировать, организованы по тайлам. Ниже приведен фрагмент кода, который я сейчас использую для получения этих значений, основываясь на документации библиотеки и исследованиях в Интернете:
private void getBytes()
{
int numBytes = bitsPerSample / 8; //Number of bytes depending the tiff map
int stride = numBytes * height;
byte[] bufferTiff = new byte[stride * height]; // this is the buffer with the tiles data
int offset = 0;
for (int i = 0; i < tif.NumberOfTiles() - 1; i++)
{
int rawTileSize = (int)tif.RawTileSize(i);
offset += tif.ReadEncodedTile(i, bufferTiff, offset, rawTileSize);
}
values = new double[height, width]; // this is the matrix to save the heigth values in meters
int ptr = 0; // pointer for saving the each data bytes
int m = 0;
int n = 0;
byte[] byteValues = new byte[numBytes]; // bytes of each height data
for (int i = 0; i < bufferTiff.Length; i++)
{
byteValues[ptr] = bufferTiff[i];
ptr++;
if (ptr % numBytes == 0)
{
ptr = 0;
if (n == height) // tiff map Y pixels
{
n = 0;
m++;
if (m == width) // tiff map X pixels
{
m = 0;
}
}
values[m, n] = BitConverter.ToDouble(byteValues, 0); // Converts each byte data to the height value in meters. If the map is 32 bps the method I use is BitConverter.ToFloat
if (n == height - 1 && m == width - 1)
break;
n++;
}
}
SaveArrayAsCSV(values, "values.txt");
}
//Only to show results in a cvs file:
public void SaveArrayAsCSV(double[,] arrayToSave, string fileName) // source: http://stackru.com/questions/8666518/how-can-i-write-a-general-array-to-csv-file
{
using (StreamWriter file = new StreamWriter(fileName))
{
WriteItemsToFile(arrayToSave, file);
}
}
//Only to show results in a cvs file:
private void WriteItemsToFile(Array items, TextWriter file) // source: http://stackru.com/questions/8666518/how-can-i-write-a-general-array-to-csv-file
{
int cont = 0;
foreach (object item in items)
{
if (item is Array)
{
WriteItemsToFile(item as Array, file);
file.Write(Environment.NewLine);
}
else {
file.Write(item + " | ");
cont++;
if(cont == width)
{
file.Write("\n");
cont = 0;
}
}
}
}
Я тестировал две разные карты (32 и 64 бита на выборку), и результаты схожи: в начале данные кажутся согласованными, но есть момент, когда все другие значения искажаются (даже ноль при конец данных результатов). Я делаю вывод, что есть некоторые байты, которые нужно игнорировать, но я не знаю, как их идентифицировать, чтобы лишить моего кода смысла. Метод Tiff.ReadScanline не работает для меня, потому что карты, которые мне нужно декодировать, организованы по тайлам, и этот метод не предназначен для работы с изображениями такого типа (согласно документации BitMiracle.LibTiff). Метод Tiff.ReadRGBATile также недопустим, потому что изображения tiff не являются RGB. Я могу прочитать эти значения с помощью Matlab, но мой проект должен быть построен на C#, поэтому я могу сравнить ожидаемые результаты с моими. Для справки (я думаю, что это может быть полезно), это некоторые данные, извлеченные из одного из файлов tiff с помощью методов чтения тегов LibTiff:
- Ширина изображения: 2001
- ImageLength: 2001
- BitsPerSample: 32
- Сжатие: PackBits (также известный как Macintosh RLE)
- Фотометрический: MinIsBlack
- SamplesPerPixel: 1
- ПланарКонфиг: Контиг
- TileWidth: 208
- Длина плитки: 208
- SampleFormat: 3
Спасибо заранее за вашу помощь, ребята!
2 ответа
Хорошо, наконец-то я нашел решение: моей ошибкой был параметр "count" в функции Tiff.ReadEncodedTile(плитка, буфер, смещение, счет). Функция Tiff.RawTileSize(int) возвращает размер сжатых байтов плитки (различный для каждой плитки в зависимости от алгоритма сжатия), но Tiff.ReadEncodedTile возвращает распакованные байты (больше и постояннее для всех плиток). Вот почему не вся информация была сохранена должным образом, а только часть данных. Ниже правильный код с матрицей рельефа местности (нужна оптимизация, но она работает, я думаю, что это может быть полезно)
private void getBytes()
{
int numBytes = bitsPerSample / 8;
int numTiles = tif.NumberOfTiles();
int stride = numBytes * height;
int bufferSize = tileWidth * tileHeight * numBytes * numTiles;
int bytesSavedPerTile = tileWidth * tileHeight * numBytes; //this is the real size of the decompressed bytes
byte[] bufferTiff = new byte[bufferSize];
FieldValue[] value = tif.GetField(TiffTag.TILEWIDTH);
int tilewidth = value[0].ToInt();
value = tif.GetField(TiffTag.TILELENGTH);
int tileHeigth = value[0].ToInt();
int matrixSide = (int)Math.Sqrt(numTiles); // this works for a square image (for example a tiles organized tiff image)
int bytesWidth = matrixSide * tilewidth;
int bytesHeigth = matrixSide * tileHeigth;
int offset = 0;
for (int j = 0; j < numTiles; j++)
{
offset += tif.ReadEncodedTile(j, bufferTiff, offset, bytesSavedPerTile); //Here was the mistake. Now it works!
}
double[,] aux = new double[bytesHeigth, bytesWidth]; //Double for a 64 bps tiff image. This matrix will save the alldata, including the transparency (the "blank zone" I was talking before)
terrainElevation = new double[height, width]; // Double for a 64 bps tiff image. This matrix will save only the elevation values, without transparency
int ptr = 0;
int m = 0;
int n = -1;
int contNumTile = 1;
int contBytesPerTile = 0;
int i = 0;
int tileHeigthReference = tileHeigth;
int tileWidthReference = tileWidth;
int row = 1;
int col = 1;
byte[] bytesHeigthMeters = new byte[numBytes]; // Buffer to save each one elevation value to parse
while (i < bufferTiff.Length && contNumTile < numTiles + 1)
{
for (contBytesPerTile = 0; contBytesPerTile < bytesSavedPerTile; contBytesPerTile++)
{
bytesHeigthMeters[ptr] = bufferTiff[i];
ptr++;
if (ptr % numBytes == 0 && ptr != 0)
{
ptr = 0;
n++;
if (n == tileHeigthReference)
{
n = tileHeigthReference - tileHeigth;
m++;
if (m == tileWidthReference)
{
m = tileWidthReference - tileWidth;
}
}
double heigthMeters = BitConverter.ToDouble(bytesHeigthMeters, 0);
if (n < bytesWidth)
{
aux[m, n] = heigthMeters;
}
else
{
n = -1;
}
}
i++;
}
if (i % tilewidth == 0)
{
col++;
if (col == matrixSide + 1)
{
col = 1;
}
}
if (contNumTile % matrixSide == 0)
{
row++;
n = -1;
if (row == matrixSide + 1)
{
row = 1;
}
}
contNumTile++;
tileHeigthReference = tileHeight * (col);
tileWidthReference = tileWidth * (row);
m = tileWidth * (row - 1);
}
for (int x = 0; x < height; x++)
{
for (int y = 0; y < width; y++)
{
terrainElevation[x, y] = aux[x, y]; // Final result. Each position of matrix has saved each pixel terrain elevation of the map
}
}
}
С уважением!
Вот улучшенный код, работающий с неквадратными плитками:
int imageWidth = tiff.GetField(TiffTag.IMAGEWIDTH)[0].ToInt();
int imageHeight = tiff.GetField(TiffTag.IMAGELENGTH)[0].ToInt();
int bytesPerSample = (int)tiff.GetField(TiffTag.BITSPERSAMPLE)[0].ToInt() / 8;
SampleFormat format = (SampleFormat)tiff.GetField(TiffTag.SAMPLEFORMAT)[0].ToInt();
//Array to return
float[,] decoded = new float[imageHeight, imageWidth];
//Get decode function (I only want a float array)
Func<byte[], int, float> decode = GetConversionFunction(format, bytesPerSample);
if (decode == null)
{
throw new ArgumentException("Unsupported TIFF format:"+format);
}
if(tiff.IsTiled())
{
//tile dimensions in pixels - the image dimensions MAY NOT be a multiple of these dimensions
int tileWidth = tiff.GetField(TiffTag.TILEWIDTH)[0].ToInt();
int tileHeight = tiff.GetField(TiffTag.TILELENGTH)[0].ToInt();
//tile matrix size
int numTiles = tiff.NumberOfTiles();
int tileMatrixWidth = (int)Math.Ceiling(imageWidth / (float)tileWidth);
int tileMatrixHeight = (int)Math.Ceiling(imageHeight / (float)tileHeight);
//tile dimensions in bytes
int tileBytesWidth = tileWidth * bytesPerSample;
int tileBytesHeight = tileHeight * bytesPerSample;
//tile buffer
int tileBufferSize = tiff.TileSize();
byte[] tileBuffer = new byte[tileBufferSize];
int imageHeightMinus1 = imageHeight - 1;
for (int tileIndex = 0 ; tileIndex < numTiles; tileIndex++)
{
int tileX = tileIndex / tileMatrixWidth;
int tileY = tileIndex % tileMatrixHeight;
tiff.ReadTile(tileBuffer, 0, tileX*tileWidth, tileY*tileHeight, 0, 0);
int xImageOffset = tileX * tileWidth;
int yImageOffset = tileY * tileHeight;
for (int col = 0; col < tileWidth && xImageOffset+col < imageWidth; col++ )
{
for(int row = 0; row < tileHeight && yImageOffset+row < imageHeight; row++)
{
decoded[imageHeightMinus1-(yImageOffset+row), xImageOffset+col] = decode(tileBuffer, row * tileBytesWidth + col * bytesPerSample);
}
}
}
}