Хорошо, итак, после 1 месяца головной боли и тестирования всех версий AMI, узнал, что в угловой среде только AMI 0.0.17 дает цветную карту.
Хотя он не поддерживает MGZ / MGHфайлы, я под углом и интегрировал парсер MGH из последней AMI (AMI 0.32.0) в свой проект, чтобы иметь поддержку парсинга файлов MGZ / MGH.
Теперь он работает как шарм.
ami.component.ts
async loadAMIFile(files, hasSegmentationMap = false) {
const ext0 = files[0].name.split('.').pop();
const arrayBuffer0 = await this.readAsArrayBuffer(files[0]);
const resp0 = {
buffer: arrayBuffer0,
extension: ext0,
filename: files[0].name,
gzcompressed: null,
pathname: files[0].name,
query: 'filename=' + files[0].name,
url: files[0].name
};
if (hasSegmentationMap) {
const ext1 = files[1].name.split('.').pop();
const arrayBuffer1 = await this.readAsArrayBuffer(files[1]);
const resp1 = {
buffer: arrayBuffer1,
extension: ext1,
filename: files[1].name,
gzcompressed: null,
pathname: files[1].name,
query: 'filename=' + files[1].name,
url: files[1].name
};
this.amiProvider.toParse([resp0, resp1], hasSegmentationMap);
} else {
this.amiProvider.toParse([resp0], hasSegmentationMap);
}
}
ami.provider.ts
toParse(toBeParsedDictArr, hasSegmentationMap = false) {
this.loader = new this.LoadersVolume(this.threeD);
const promises = [];
toBeParsedDictArr.forEach(toBeParsedDict_ => {
const copied = {...toBeParsedDict_};
if (['mgz', 'mgh'].includes(copied.extension)) {
const data = this._parseMGH(copied);
promises.push(this.loader.parse(data));
}
promises.push(this.loader.parse(copied));
});
Promise.all(promises).then(data => {
this.handleSeries(data, hasSegmentationMap);
}).catch(err => console.log(err));
}
_parseMGH(data) {
// unzip if extension is '.mgz'
if (data.extension === 'mgz') {
data.gzcompressed = false; // true
data.extension = 'mgh';
data.filename = data.filename.split('.')[0] + '.' + data.extension;
const decompressedData = PAKO.inflate(data.buffer);
data.buffer = decompressedData.buffer;
} else {
data.gzcompressed = false;
}
const mghVolumeParser = new ParsersMgh(data, 0, this.THREE);
data.volumeParser = mghVolumeParser;
return data;
}
}
mghParser.helper.ts
import {Inject} from '@angular/core';
import {VolumeParser} from './volumeParser.helper';
/**
* @module parsers/mgh
*/
export class ParsersMgh extends VolumeParser {
// https://github.com/freesurfer/freesurfer/
// See include/mri.h
MRI_UCHAR = 0;
MRI_INT = 1;
MRI_LONG = 2;
MRI_FLOAT = 3;
MRI_SHORT = 4;
MRI_BITMAP = 5;
MRI_TENSOR = 6;
MRI_FLOAT_COMPLEX = 7;
MRI_DOUBLE_COMPLEX = 8;
MRI_RGB = 9;
// https://github.com/freesurfer/freesurfer/
// See include/tags.h
TAG_OLD_COLORTABLE = 1;
TAG_OLD_USEREALRAS = 2;
TAG_CMDLINE = 3;
TAG_USEREALRAS = 4;
TAG_COLORTABLE = 5;
TAG_GCAMORPH_GEOM = 10;
TAG_GCAMORPH_TYPE = 11;
TAG_GCAMORPH_LABELS = 12;
TAG_OLD_SURF_GEOM = 20;
TAG_SURF_GEOM = 21;
TAG_OLD_MGH_XFORM = 30;
TAG_MGH_XFORM = 31;
TAG_GROUP_AVG_SURFACE_AREA = 32;
TAG_AUTO_ALIGN = 33;
TAG_SCALAR_DOUBLE = 40;
TAG_PEDIR = 41;
TAG_MRI_FRAME = 42;
TAG_FIELDSTRENGTH = 43;
public _id;
public _url;
public _buffer;
public _bufferPos;
public _dataPos;
public _pixelData;
// Default MGH Header as described at:
// https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat
// Image "header" with default values
public _version;
public _width;
public _height;
public _depth;
public _nframes;
public _type; // 0-UCHAR, 4-SHORT, 1-INT, 3-FLOAT
public _dof;
public _goodRASFlag; // True: Use directional cosines, false assume CORONAL
public _spacingXYZ;
public _Xras;
public _Yras;
public _Zras;
public _Cras;
// Image "footer"
public _tr; // ms
public _flipAngle; // radians
public _te; // ms
public _ti; // ms
public _fov; // from doc: IGNORE THIS FIELD (data is inconsistent)
public _tags; // Will then contain variable length char strings
// Other misc
public _origin;
public _imageOrient;
// Read header
// ArrayBuffer in data.buffer may need endian swap
// public _buffer = data.buffer;
// public _version;
public _swapEndian;
// public _width;
// public _height;
// public _depth; // AMI calls this frames
// public _nframes;
// public _type;
// public _dof;
// public _goodRASFlag;
// public _spacingXYZ;
// public _Xras;
// public _Yras;
// public _Zras;
// public _Cras;
// @Inject('THREE') public THREE;
public dataSize;
public vSize;
constructor(data, id, @Inject('THREE') public THREE) {
super();
/**
* @member
* @type {arraybuffer}
*/
this._id = id;
this._url = data.url;
this._buffer = null;
this._bufferPos = 0;
this._dataPos = 0;
this._pixelData = null;
// Default MGH Header as described at:
// https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat
// Image "header" with default values
this._version = 1;
this._width = 0;
this._height = 0;
this._depth = 0;
this._nframes = 0;
this._type = this.MRI_UCHAR; // 0-UCHAR, 4-SHORT, 1-INT, 3-FLOAT
this._dof = 0;
this._goodRASFlag = 0; // True: Use directional cosines, false assume CORONAL
this._spacingXYZ = [1, 1, 1];
this._Xras = [-1, 0, 0];
this._Yras = [0, 0, -1];
this._Zras = [0, 1, 0];
this._Cras = [0, 0, 0];
// Image "footer"
this._tr = 0; // ms
this._flipAngle = 0; // radians
this._te = 0; // ms
this._ti = 0; // ms
this._fov = 0; // from doc: IGNORE THIS FIELD (data is inconsistent)
this._tags = []; // Will then contain variable length char strings
// Other misc
this._origin = [0, 0, 0];
this._imageOrient = [0, 0, 0, 0, 0, 0];
// Read header
// ArrayBuffer in data.buffer may need endian swap
this._buffer = data.buffer;
this._version = this._readInt();
this._swapEndian = false;
if (this._version === 1) {
// Life is good
} else if (this._version === 16777216) {
this._swapEndian = true;
this._version = this._swap32(this._version);
} else {
const error = new Error('MGH/MGZ parser: Unknown Endian. Version reports: ' + this._version);
throw error;
}
this._width = this._readInt();
this._height = this._readInt();
this._depth = this._readInt(); // AMI calls this frames
this._nframes = this._readInt();
this._type = this._readInt();
this._dof = this._readInt();
this._goodRASFlag = this._readShort();
this._spacingXYZ = this._readFloat(3);
this._Xras = this._readFloat(3);
this._Yras = this._readFloat(3);
this._Zras = this._readFloat(3);
this._Cras = this._readFloat(3);
this._bufferPos = 284;
const dataSize = this._width * this._height * this._depth * this._nframes;
const vSize = this._width * this._height * this._depth;
switch (this._type) {
case this.MRI_UCHAR:
this._pixelData = this._readUChar(dataSize);
break;
case this.MRI_INT:
this._pixelData = this._readInt(dataSize);
break;
case this.MRI_FLOAT:
this._pixelData = this._readFloat(dataSize);
break;
case this.MRI_SHORT:
this._pixelData = this._readShort(dataSize);
break;
default:
throw Error('MGH/MGZ parser: Unknown _type. _type reports: ' + this._type);
}
this._tr = this._readFloat(1);
this._flipAngle = this._readFloat(1);
this._te = this._readFloat(1);
this._ti = this._readFloat(1);
this._fov = this._readFloat(1);
const enc = new TextDecoder();
let t = this._tagReadStart();
while (t[0] !== undefined) {
const tagType = t[0];
const tagLen = t[1];
let tagValue;
switch (tagType) {
case this.TAG_OLD_MGH_XFORM:
case this.TAG_MGH_XFORM:
tagValue = this._readChar(tagLen);
break;
default:
tagValue = this._readChar(tagLen);
}
tagValue = enc.decode(tagValue);
this._tags.push({tagType: tagType, tagValue: tagValue});
// read for next loop
t = this._tagReadStart();
}
// detect if we are in a right handed coordinate system
const first = new this.THREE.Vector3().fromArray(this._Xras);
const second = new this.THREE.Vector3().fromArray(this._Yras);
const crossFirstSecond = new this.THREE.Vector3().crossVectors(first, second);
const third = new this.THREE.Vector3().fromArray(this._Zras);
if (crossFirstSecond.angleTo(third) > Math.PI / 2) {
this._rightHanded = false;
}
// - sign to move to LPS space
this._imageOrient = [
-this._Xras[0],
-this._Xras[1],
this._Xras[2],
-this._Yras[0],
-this._Yras[1],
this._Yras[2],
];
// Calculate origin
const fcx = this._width / 2.0;
const fcy = this._height / 2.0;
const fcz = this._depth / 2.0;
for (let ui = 0; ui < 3; ++ui) {
this._origin[ui] =
this._Cras[ui] -
(this._Xras[ui] * this._spacingXYZ[0] * fcx +
this._Yras[ui] * this._spacingXYZ[1] * fcy +
this._Zras[ui] * this._spacingXYZ[2] * fcz);
}
// - sign to move to LPS space
this._origin = [-this._origin[0], -this._origin[1], this._origin[2]];
}
seriesInstanceUID() {
// use filename + timestamp..?
return this._url;
}
numberOfFrames() {
// AMI calls Z component frames, not T (_nframes)
return this._depth;
}
sopInstanceUID(frameIndex = 0) {
return frameIndex;
}
rows(frameIndex = 0) {
return this._width;
}
columns(frameIndex = 0) {
return this._height;
}
pixelType(frameIndex = 0) {
// Return: 0 integer, 1 float
switch (this._type) {
case this.MRI_UCHAR:
case this.MRI_INT:
case this.MRI_SHORT:
return 0;
case this.MRI_FLOAT:
return 1;
default:
throw Error('MGH/MGZ parser: Unknown _type. _type reports: ' + this._type);
}
}
bitsAllocated(frameIndex = 0) {
switch (this._type) {
case this.MRI_UCHAR:
return 8;
case this.MRI_SHORT:
return 16;
case this.MRI_INT:
case this.MRI_FLOAT:
return 32;
default:
throw Error('MGH/MGZ parser: Unknown _type. _type reports: ' + this._type);
}
}
pixelSpacing(frameIndex = 0) {
return this._spacingXYZ;
}
imageOrientation(frameIndex = 0) {
return this._imageOrient;
}
imagePosition(frameIndex = 0) {
return this._origin;
}
extractPixelData(frameIndex = 0) {
const sliceSize = this._width * this._height;
return this._pixelData.slice(frameIndex * sliceSize, (frameIndex + 1) * sliceSize);
}
// signed int32
_readInt(len = 1) {
const tempBuff = new DataView(this._buffer.slice(this._bufferPos, this._bufferPos + len * 4));
this._bufferPos += len * 4;
let v;
if (len === 1) {
v = tempBuff.getInt32(0, this._swapEndian);
} else {
v = new Int32Array(len);
for (let i = 0; i < len; i++) {
v[i] = tempBuff.getInt32(i * 4, this._swapEndian);
}
}
return v;
}
// signed int16
_readShort(len = 1) {
const tempBuff = new DataView(this._buffer.slice(this._bufferPos, this._bufferPos + len * 2));
this._bufferPos += len * 2;
let v;
if (len === 1) {
v = tempBuff.getInt16(0, this._swapEndian);
} else {
v = new Int16Array(len);
for (let i = 0; i < len; i++) {
v[i] = tempBuff.getInt16(i * 2, this._swapEndian);
}
}
return v;
}
// signed int64
_readLong(len = 1) {
const tempBuff = new DataView(this._buffer.slice(this._bufferPos, this._bufferPos + len * 8));
this._bufferPos += len * 8;
const v = new Uint16Array(len);
for (let i = 0; i < len; i++) {
/* DataView doesn't have Int64.
* This work around based off Scalajs
* (https://github.com/scala-js/scala-js/blob/master/library/src/main/scala/scala/scalajs/js/typedarray/DataViewExt.scala)
* v[i]=tempBuff.getInt64(i*8,this._swapEndian);
*/
let shiftHigh = 0;
let shiftLow = 0;
if (this._swapEndian) {
shiftHigh = 4;
} else {
shiftLow = 4;
}
const high = tempBuff.getInt32(i * 8 + shiftHigh, this._swapEndian);
let low = tempBuff.getInt32(i * 8 + shiftLow, this._swapEndian);
if (high !== 0) {
console.log('Unable to read Int64 with high word: ' + high + 'low word: ' + low);
low = undefined;
}
v[i] = low;
}
if (len === 0) {
return undefined;
} else if (len === 1) {
return v[0];
} else {
return v;
}
}
// signed int8
_readChar(len = 1) {
const tempBuff = new DataView(this._buffer.slice(this._bufferPos, this._bufferPos + len));
this._bufferPos += len;
let v;
if (len === 1) {
v = tempBuff.getInt8(0); // , this._swapEndian
} else {
v = new Int8Array(len);
for (let i = 0; i < len; i++) {
v[i] = tempBuff.getInt8(i); // , this._swapEndian
}
}
return v;
}
// unsigned int8
_readUChar(len = 1) {
const tempBuff = new DataView(this._buffer.slice(this._bufferPos, this._bufferPos + len));
this._bufferPos += len;
let v;
if (len === 1) {
v = tempBuff.getUint8(0); // , this._swapEndian
} else {
v = new Uint8Array(len);
for (let i = 0; i < len; i++) {
v[i] = tempBuff.getUint8(i); // , this._swapEndian
}
}
return v;
}
// float32
_readFloat(len = 1) {
const tempBuff = new DataView(this._buffer.slice(this._bufferPos, this._bufferPos + len * 4));
this._bufferPos += len * 4;
let v;
if (len === 1) {
v = tempBuff.getFloat32(0, this._swapEndian);
} else {
v = new Float32Array(len);
for (let i = 0; i < len; i++) {
v[i] = tempBuff.getFloat32(i * 4, this._swapEndian);
}
}
return v;
}
_tagReadStart() {
if (this._bufferPos >= this._buffer.byteLength) {
return [undefined, undefined];
}
let tagType = this._readInt();
let tagLen;
switch (tagType) {
case this.TAG_OLD_MGH_XFORM:
tagLen = this._readInt();
tagLen -= 1;
break;
case this.TAG_OLD_SURF_GEOM:
case this.TAG_OLD_USEREALRAS:
case this.TAG_OLD_COLORTABLE:
tagLen = 0;
break;
default:
tagLen = this._readLong();
}
if (tagLen === undefined) {
tagType = undefined;
}
return [tagType, tagLen];
}
}
volumeParser.helper.ts
/** * Imports ***/
// import ParsersVolume from './parsers.volume';
// import * as THREE from 'three';
/**
* @module parsers/volume
*/
export class VolumeParser {
public _rightHanded;
constructor() {
this._rightHanded = true;
}
pixelRepresentation() {
return 0;
}
pixelPaddingValue(frameIndex = 0) {
return null;
}
modality() {
return 'unknown';
}
segmentationType() {
return 'unknown';
}
segmentationSegments() {
return [];
}
referencedSegmentNumber(frameIndex) {
return -1;
}
rightHanded() {
return this._rightHanded;
}
spacingBetweenSlices() {
return null;
}
numberOfChannels() {
return 1;
}
sliceThickness() {
return null;
}
dimensionIndexValues(frameIndex = 0) {
return null;
}
instanceNumber(frameIndex = 0) {
return frameIndex;
}
windowCenter(frameIndex = 0) {
return null;
}
windowWidth(frameIndex = 0) {
return null;
}
rescaleSlope(frameIndex = 0) {
return 1;
}
rescaleIntercept(frameIndex = 0) {
return 0;
}
ultrasoundRegions(frameIndex = 0) {
return [];
}
frameTime(frameIndex = 0) {
return null;
}
_decompressUncompressed() {
}
// /4980097/kak-ya-mogu-pomenyat-mestami-poryadok-baitov-peremennoi-v-javascript
_swap16(val) {
return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}
_swap32(val) {
return (
((val & 0xff) << 24) | ((val & 0xff00) << 8) | ((val >> 8) & 0xff00) | ((val >> 24) & 0xff)
);
}
invert() {
return false;
}
/**
* Get the transfer syntax UID.
* @return {*}
*/
transferSyntaxUID() {
return 'no value provided';
}
/**
* Get the study date.
* @return {*}
*/
studyDate() {
return 'no value provided';
}
/**
* Get the study desciption.
* @return {*}
*/
studyDescription() {
return 'no value provided';
}
/**
* Get the series date.
* @return {*}
*/
seriesDate() {
return 'no value provided';
}
/**
* Get the series desciption.
* @return {*}
*/
seriesDescription() {
return 'no value provided';
}
/**
* Get the patient ID.
* @return {*}
*/
patientID() {
return 'no value provided';
}
/**
* Get the patient name.
* @return {*}
*/
patientName() {
return 'no value provided';
}
/**
* Get the patient age.
* @return {*}
*/
patientAge() {
return 'no value provided';
}
/**
* Get the patient birthdate.
* @return {*}
*/
patientBirthdate() {
return 'no value provided';
}
/**
* Get the patient sex.
* @return {*}
*/
patientSex() {
return 'no value provided';
}
/**
* Get min/max values in array
*
* @param {*} pixelData
*
* @return {*}
*/
minMaxPixelData(pixelData = []) {
const minMax = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
const numPixels = pixelData.length;
for (let index = 0; index < numPixels; index++) {
const spv = pixelData[index];
minMax[0] = Math.min(minMax[0], spv);
minMax[1] = Math.max(minMax[1], spv);
}
return minMax;
}
}
АНебольшое изменение в parse () библиотеки AMI 0.0.17 для поддержки парсера MGH.В будущем, с поддержкой новейшего AMI с правильно интегрированной картой цветов, код будет работать без необходимости каких-либо изменений в библиотеке.
var volumeParser = null;
try {
if (['mgh', 'mgz'].includes(response.extension)) {
volumeParser = response.volumeParser;
} else {
var Parser = _this2._parser(data.extension);
if (!Parser) {
// emit 'parse-error' event
_this2.emit('parse-error', {
file: response.url,
time: new Date(),
error: data.filename + 'can not be parsed.'
});
reject(data.filename + ' can not be parsed.');
}
volumeParser = new Parser(data, 0);
}