AmiraMesh 是 Amira 原生的檔案格式,學術版的 Amira 是由 ZIB 所發展的,商業版的則有 Visage Imaging 與 VSG。
AmiraMesh 的檔案內容可以分為檔頭(header)與資料(data)兩部分,檔頭部分都是以 ASCII 的編碼來儲存,其中包含許多的 meta 資訊,而資料的部分則有 ASCII 與二進位(binary)兩種,這裡我們示範讀取二進位資料的方式。
這是一個 AmiraMesh 格式的範例檔案:
# AmiraMesh BINARY-LITTLE-ENDIAN 2.1
define Lattice 4 6 8
Parameters {
BoundingBox -1 0 0 1 -0.5 0.5,
CoordType "uniform"
}
Lattice { float[2] Data } @1
# Data section follows
@1
[二進位資料]
# AmiraMesh BINARY-LITTLE-ENDIAN 2.1:AmiraMesh 檔案檔頭,標示這個檔案的資料格式為 little-endian 的二進位檔案。define Lattice 4 6 8:指定 x, y, z 方向的資料點數。BoundingBox -1 0 0 1 -0.5 0.5:定義 bounding box。Lattice { float[2] Data } @1:定義@1資料中每一點的資料型態。@1:在資料區塊中的每一筆資料都會有一個編號,編號@1的資料從這裡開始,以下是二進位的資料內容。
這裡的二進位資料是以下面這些規則來儲存的:
- Little-endian:目前一般 PC 的記憶體都是使用 little-endian 格式在儲存資料的,所以我們可以直接將資料讀進記憶體,不需要另外轉換,如果是 big-endian 的伺服器,就要加上轉換的動作。
- x-fastest:若要以迴圈的方式存取整個 grid,則 x 軸的迴圈在最內層,z 軸在最外層。
- Interleaved components:在每一個資料點當中,如果有超過一個以上的數值,則依序存取,例如
(u0, v0, w0), (u0, v0, w0), ...,也就是說不同變量會交錯儲存。
以下是 C++ 語言的實作。
#include <stdio.h>
#include <string.h>
#include <assert.h>
/** Find a string in the given buffer and return a pointer
to the contents directly behind the SearchString.
If not found, return the buffer. A subsequent sscanf()
will fail then, but at least we return a decent pointer.
*/
const char* FindAndJump(const char* buffer, const char* SearchString)
{
const char* FoundLoc = strstr(buffer, SearchString);
if (FoundLoc) return FoundLoc + strlen(SearchString);
return buffer;
}
/** A simple routine to read an AmiraMesh file
that defines a scalar/vector field on a uniform grid.
*/
int main()
{
const char* FileName = "testscalar.am";
//const char* FileName = "testvector2c.am";
//const char* FileName = "testvector3c.am";
FILE* fp = fopen(FileName, "rb");
if (!fp)
{
printf("Could not find %s\n", FileName);
return 1;
}
printf("Reading %s\n", FileName);
//We read the first 2k bytes into memory to parse the header.
//The fixed buffer size looks a bit like a hack, and it is one, but it gets the job done.
char buffer[2048];
fread(buffer, sizeof(char), 2047, fp);
buffer[2047] = '\0'; //The following string routines prefer null-terminated strings
if (!strstr(buffer, "# AmiraMesh BINARY-LITTLE-ENDIAN 2.1"))
{
printf("Not a proper AmiraMesh file.\n");
fclose(fp);
return 1;
}
//Find the Lattice definition, i.e., the dimensions of the uniform grid
int xDim(0), yDim(0), zDim(0);
sscanf(FindAndJump(buffer, "define Lattice"), "%d %d %d", &xDim, &yDim, &zDim);
printf("\tGrid Dimensions: %d %d %d\n", xDim, yDim, zDim);
//Find the BoundingBox
float xmin(1.0f), ymin(1.0f), zmin(1.0f);
float xmax(-1.0f), ymax(-1.0f), zmax(-1.0f);
sscanf(FindAndJump(buffer, "BoundingBox"), "%g %g %g %g %g %g", &xmin, &xmax, &ymin, &ymax, &zmin, &zmax);
printf("\tBoundingBox in x-Direction: [%g ... %g]\n", xmin, xmax);
printf("\tBoundingBox in y-Direction: [%g ... %g]\n", ymin, ymax);
printf("\tBoundingBox in z-Direction: [%g ... %g]\n", zmin, zmax);
//Is it a uniform grid? We need this only for the sanity check below.
const bool bIsUniform = (strstr(buffer, "CoordType \"uniform\"") != NULL);
printf("\tGridType: %s\n", bIsUniform ? "uniform" : "UNKNOWN");
//Type of the field: scalar, vector
int NumComponents(0);
if (strstr(buffer, "Lattice { float Data }"))
{
//Scalar field
NumComponents = 1;
}
else
{
//A field with more than one component, i.e., a vector field
sscanf(FindAndJump(buffer, "Lattice { float["), "%d", &NumComponents);
}
printf("\tNumber of Components: %d\n", NumComponents);
//Sanity check
if (xDim <= 0 || yDim <= 0 || zDim <= 0
|| xmin > xmax || ymin > ymax || zmin > zmax
|| !bIsUniform || NumComponents <= 0)
{
printf("Something went wrong\n");
fclose(fp);
return 1;
}
//Find the beginning of the data section
const long idxStartData = strstr(buffer, "# Data section follows") - buffer;
if (idxStartData > 0)
{
//Set the file pointer to the beginning of "# Data section follows"
fseek(fp, idxStartData, SEEK_SET);
//Consume this line, which is "# Data section follows"
fgets(buffer, 2047, fp);
//Consume the next line, which is "@1"
fgets(buffer, 2047, fp);
//Read the data
// - how much to read
const size_t NumToRead = xDim * yDim * zDim * NumComponents;
// - prepare memory; use malloc() if you're using pure C
float* pData = new float[NumToRead];
if (pData)
{
// - do it
const size_t ActRead = fread((void*)pData, sizeof(float), NumToRead, fp);
// - ok?
if (NumToRead != ActRead)
{
printf("Something went wrong while reading the binary data section.\nPremature end of file?\n");
delete[] pData;
fclose(fp);
return 1;
}
//Test: Print all data values
//Note: Data runs x-fastest, i.e., the loop over the x-axis is the innermost
printf("\nPrinting all values in the same order in which they are in memory:\n");
int Idx(0);
for(int k=0;k<zDim;k++)
{
for(int j=0;j<yDim;j++)
{
for(int i=0;i<xDim;i++)
{
//Note: Random access to the value (of the first component) of the grid point (i,j,k):
// pData[((k * yDim + j) * xDim + i) * NumComponents]
assert(pData[((k * yDim + j) * xDim + i) * NumComponents] == pData[Idx * NumComponents]);
for(int c=0;c<NumComponents;c++)
{
printf("%g ", pData[Idx * NumComponents + c]);
}
printf("\n");
Idx++;
}
}
}
delete[] pData;
}
}
fclose(fp);
return 0;
}
