AmiraMesh 是 Amira 原生的檔案格式,學術版的 Amira 是由 ZIB 所發展的,商業版的則有 Visage Imaging 與 VSG。
AmiraMesh 的檔案內容可以分為檔頭(header)與資料(data)兩部分,檔頭部分都是以 ASCII 的編碼來儲存,其中包含許多的 meta 資訊,而資料的部分則有 ASCII 與二進位(binary)兩種,這裡我們示範讀取二進位資料的方式。
這是一個 AmiraMesh 格式的範例檔案:
# AmiraMesh BINARY-LITTLE-ENDIAN 2.1 1 define Lattice 4 6 8 2 Parameters { BoundingBox -1 0 0 1 -0.5 0.5, 3 CoordType "uniform" 4 } Lattice { float[2] Data } @1 5 # Data section follows 6 @1 7 [二進位資料]
1 AmiraMesh 檔案檔頭,標示這個檔案的資料格式為 little-endian 的二進位檔案。
2 指定 x, y, z 方向的資料點數。
3 定義 uniform grid 的 bounding box。
4 定義 grid 為 uniform。
5 定義 @1
資料中每一點的資料型態。
6 在資料區塊之前,會有一行提示訊息,實際的資料從這裡開始。
7 在資料區塊中的每一筆資料都會有一個編號,編號 @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; }
參考資料:weinkauf