496 lines
16 KiB
JavaScript
496 lines
16 KiB
JavaScript
/**
|
|
* @namespace DICOM related.
|
|
*/
|
|
ovm.dicom = ovm.dicom || {};
|
|
|
|
/**
|
|
* @class Big Endian reader
|
|
* @param file
|
|
*/
|
|
ovm.dicom.BigEndianReader = function(file)
|
|
{
|
|
this.readByteAt = function(i) {
|
|
return file.charCodeAt(i) & 0xff;
|
|
};
|
|
this.readNumber = function(nBytes, startByte) {
|
|
var result = 0;
|
|
for(var i=startByte; i<startByte + nBytes; ++i){
|
|
result = result * 256 + this.readByteAt(i);
|
|
}
|
|
return result;
|
|
};
|
|
this.readCouple = function(a,b) {
|
|
return (b + a*256).toString(16);
|
|
};
|
|
this.readUint16Array = function(nBytes, startByte) {
|
|
return new Uint16Array(file.buffer, startByte, nBytes/2);
|
|
};
|
|
this.readUint16Array = function(nBytes, startByte) {
|
|
var data = [];
|
|
for(var i=startByte; i<startByte+nBytes; i+=2)
|
|
{
|
|
data.push(this.readNumber(1,i+1) + this.readNumber(1,i)*256);
|
|
}
|
|
return data;
|
|
};
|
|
this.readString = function(nChars, startChar) {
|
|
var result = "";
|
|
for(var i=startChar; i<startChar + nChars; i++){
|
|
result += String.fromCharCode(this.readNumber(1,i));
|
|
}
|
|
return result;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @class Litte Endian reader
|
|
* @param file
|
|
*/
|
|
ovm.dicom.LittleEndianReader = function(file)
|
|
{
|
|
this.readByteAt = function(i) {
|
|
return file.charCodeAt(i) & 0xff;
|
|
};
|
|
this.readNumber = function(nBytes, startByte) {
|
|
var result = 0;
|
|
for(var i=startByte + nBytes; i>startByte; i--){
|
|
result = result * 256 + this.readByteAt(i-1);
|
|
}
|
|
return result;
|
|
};
|
|
this.readCouple = function(a,b) {
|
|
return (a + b*256).toString(16);
|
|
};
|
|
this.readUint16Array = function(nBytes, startByte) {
|
|
var data = [];
|
|
for(var i=startByte; i<startByte+nBytes; i+=2)
|
|
{
|
|
data.push(this.readNumber(2,i));
|
|
}
|
|
return data;
|
|
};
|
|
this.readString = function(nChars, startChar) {
|
|
var result = "";
|
|
for(var i=startChar; i<startChar + nChars; i++){
|
|
result += String.fromCharCode(this.readNumber(1,i));
|
|
}
|
|
return result;
|
|
};
|
|
// beta...
|
|
this.readRaw = function(nBytes, startByte) {
|
|
var data = [];
|
|
for(var i=startByte; i<startByte+nBytes; ++i)
|
|
{
|
|
data.push(file[i]);
|
|
}
|
|
return data;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @class DicomParser class.
|
|
*/
|
|
ovm.dicom.DicomParser = function(file)
|
|
{
|
|
// the list of DICOM elements
|
|
this.dicomElements = {};
|
|
// the number of DICOM Items
|
|
this.numberOfItems = 0;
|
|
// the DICOM dictionary used to find tag names
|
|
this.dict = new ovm.dicom.Dictionary();
|
|
// the file
|
|
this.file = file;
|
|
// the pixel buffer
|
|
this.pixelBuffer = [];
|
|
};
|
|
|
|
/**
|
|
* Get the DICOM data pixel buffer.
|
|
* @returns The pixel buffer (as an array).
|
|
*/
|
|
ovm.dicom.DicomParser.prototype.getPixelBuffer=function()
|
|
{
|
|
return this.pixelBuffer;
|
|
};
|
|
|
|
/**
|
|
* Append a DICOM element to the dicomElements member object.
|
|
* Allows for easy retrieval of DICOM tag values from the tag name.
|
|
* If tags have same name (for the 'unknown' and private tags cases), a number is appended
|
|
* making the name unique.
|
|
* @param element The element to add.
|
|
*/
|
|
ovm.dicom.DicomParser.prototype.appendDicomElement=function( element )
|
|
{
|
|
// find a good tag name
|
|
var name = element.name;
|
|
// count the number of items
|
|
if( name === "Item" ) {
|
|
++this.numberOfItems;
|
|
}
|
|
var count = 1;
|
|
while( this.dicomElements[name] ) {
|
|
name = element.name + (count++).toString();
|
|
}
|
|
// store it
|
|
this.dicomElements[name] = {
|
|
"group": element.group,
|
|
"element": element.element,
|
|
"vr": element.vr,
|
|
"vl": element.vl,
|
|
"value": element.value };
|
|
};
|
|
|
|
/**
|
|
* Read a DICOM tag.
|
|
* @param reader The raw data reader.
|
|
* @param offset The offset where to start to read.
|
|
* @returns An object containing the tags 'group', 'element' and 'name'.
|
|
*/
|
|
ovm.dicom.DicomParser.prototype.readTag=function(reader, offset)
|
|
{
|
|
// group
|
|
var g0 = reader.readNumber( 1, offset );
|
|
var g1 = reader.readNumber( 1, offset+1 );
|
|
var group_str = reader.readCouple(g0, g1);
|
|
var group = "0x0000".substr(0, 6 - group_str.length) + group_str.toUpperCase();
|
|
// element
|
|
var e2 = reader.readNumber( 1, offset+2 );
|
|
var e3 = reader.readNumber( 1, offset+3 );
|
|
var element_str = reader.readCouple(e2, e3);
|
|
var element = "0x0000".substr(0, 6 - element_str.length) + element_str.toUpperCase();
|
|
// name
|
|
var name = "ovm::unknown";
|
|
if( this.dict.newDictionary[group] ) {
|
|
if( this.dict.newDictionary[group][element] ) {
|
|
name = this.dict.newDictionary[group][element][2];
|
|
}
|
|
}
|
|
// return as hex
|
|
return {'group': group, 'element': element, 'name': name};
|
|
};
|
|
|
|
/**
|
|
* Read a DICOM data element.
|
|
* @param reader The raw data reader.
|
|
* @param offset The offset where to start to read.
|
|
* @param implicit Is the DICOM VR implicit?
|
|
* @returns An object containing the element 'tag', 'vl', 'vr', 'data' and 'offset'.
|
|
*/
|
|
ovm.dicom.DicomParser.prototype.readDataElement=function(reader, offset, implicit)
|
|
{
|
|
// tag: group, element
|
|
var tag = this.readTag(reader, offset);
|
|
var tagOffset = 4;
|
|
|
|
var vr; // Value Representation (VR)
|
|
var vl; // Value Length (VL)
|
|
var vrOffset = 0; // byte size of VR
|
|
var vlOffset = 0; // byte size of VL
|
|
|
|
// (private) Item group case
|
|
if( tag.group === "0xFFFE" ) {
|
|
vr = "N/A";
|
|
vrOffset = 0;
|
|
vl = reader.readNumber( 4, offset+tagOffset );
|
|
vlOffset = 4;
|
|
}
|
|
// non Item case
|
|
else {
|
|
// implicit VR?
|
|
if(implicit) {
|
|
vr = "UN";
|
|
if( this.dict.newDictionary[tag.group] ) {
|
|
if( this.dict.newDictionary[tag.group][tag.element] ) {
|
|
vr = this.dict.newDictionary[tag.group][tag.element][0];
|
|
}
|
|
}
|
|
vrOffset = 0;
|
|
vl = reader.readNumber( 4, offset+tagOffset+vrOffset );
|
|
vlOffset = 4;
|
|
}
|
|
else {
|
|
vr = reader.readString( 2, offset+tagOffset );
|
|
vrOffset = 2;
|
|
// long representations
|
|
if(vr === "OB" || vr === "OF" || vr === "SQ" || vr === "OW" || vr === "UN") {
|
|
vl = reader.readNumber( 4, offset+tagOffset+vrOffset+2 );
|
|
vlOffset = 6;
|
|
}
|
|
// short representation
|
|
else {
|
|
vl = reader.readNumber( 2, offset+tagOffset+vrOffset );
|
|
vlOffset = 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check the value of VL
|
|
if( vl === 0xffffffff ) {
|
|
vl = 0;
|
|
}
|
|
|
|
// data
|
|
var data;
|
|
if( vr === "US" || vr === "UL")
|
|
{
|
|
data = [reader.readNumber( vl, offset+tagOffset+vrOffset+vlOffset)];
|
|
}
|
|
else if( vr === "OX" || vr === "OW" )
|
|
{
|
|
data = reader.readUint16Array(vl, offset+tagOffset+vrOffset+vlOffset);
|
|
}
|
|
else if( vr === "OB" || vr === "N/A")
|
|
{
|
|
var begin = offset+tagOffset+vrOffset+vlOffset;
|
|
var end = begin + vl;
|
|
data = [];
|
|
for(var i=begin; i<end; ++i)
|
|
{
|
|
data.push(reader.readNumber(1,i));
|
|
}
|
|
//data = reader.readRaw(vl, begin);
|
|
}
|
|
else
|
|
{
|
|
data = reader.readString( vl, offset+tagOffset+vrOffset+vlOffset);
|
|
data = data.split("\\");
|
|
}
|
|
|
|
// total element offset
|
|
var elementOffset = tagOffset + vrOffset + vlOffset + vl;
|
|
|
|
// return
|
|
return {
|
|
'tag': tag, 'vr': vr, 'vl': vl,
|
|
'data': data,
|
|
'offset': elementOffset};
|
|
};
|
|
|
|
/**
|
|
* Parse the complete DICOM file (given as input to the class).
|
|
* Fills in the member object 'dicomElements'.
|
|
*/
|
|
ovm.dicom.DicomParser.prototype.parseAll = function()
|
|
{
|
|
var offset = 0;
|
|
var i;
|
|
var implicit = false;
|
|
var jpeg = false;
|
|
var jpeg2000 = false;
|
|
// dictionary
|
|
this.dict.init();
|
|
// default readers
|
|
var metaReader = new ovm.dicom.LittleEndianReader(this.file);
|
|
var dataReader = new ovm.dicom.LittleEndianReader(this.file);
|
|
|
|
// 128 -> 132: magic word
|
|
offset = 128;
|
|
var magicword = metaReader.readString(4, offset);
|
|
if(magicword !== "DICM")
|
|
{
|
|
throw new Error("No magic DICM word found");
|
|
}
|
|
offset += 4;
|
|
|
|
// 0x0002, 0x0000: MetaElementGroupLength
|
|
var dataElement = this.readDataElement(metaReader, offset);
|
|
var metaLength = parseInt(dataElement.data, 10);
|
|
offset += dataElement.offset;
|
|
|
|
// meta elements
|
|
var metaStart = offset;
|
|
var metaEnd = offset + metaLength;
|
|
for( i=metaStart; i<metaEnd; i++ )
|
|
{
|
|
// get the data element
|
|
dataElement = this.readDataElement(metaReader, i, false);
|
|
// check the transfer syntax
|
|
if( dataElement.tag.name === "TransferSyntaxUID" ) {
|
|
var syntax = dataElement.data[0];
|
|
// get rid of ending zero-width space (u200B)
|
|
if( syntax[syntax.length-1] === String.fromCharCode("u200B") ) {
|
|
syntax = syntax.substring(0, syntax.length-1);
|
|
}
|
|
|
|
// Implicit VR - Little Endian
|
|
if( syntax === "1.2.840.10008.1.2" ) {
|
|
implicit = true;
|
|
}
|
|
// Explicit VR - Little Endian (default): 1.2.840.10008.1.2.1
|
|
// Deflated Explicit VR - Little Endian
|
|
else if( syntax === "1.2.840.10008.1.2.1.99" ) {
|
|
throw new Error("Unsupported DICOM transfer syntax (Deflated Explicit VR): "+syntax);
|
|
}
|
|
// Explicit VR - Big Endian
|
|
else if( syntax === "1.2.840.10008.1.2.2" ) {
|
|
dataReader = new ovm.dicom.BigEndianReader(this.file);
|
|
}
|
|
// JPEG
|
|
else if( syntax.match(/1.2.840.10008.1.2.4.5/)
|
|
|| syntax.match(/1.2.840.10008.1.2.4.6/)
|
|
|| syntax.match(/1.2.840.10008.1.2.4.7/)
|
|
|| syntax.match(/1.2.840.10008.1.2.4.8/) ) {
|
|
jpeg = true;
|
|
throw new Error("Unsupported DICOM transfer syntax (JPEG): "+syntax);
|
|
}
|
|
// JPEG 2000
|
|
else if( syntax.match(/1.2.840.10008.1.2.4.9/) ) {
|
|
jpeg2000 = true;
|
|
throw new Error("Unsupported DICOM transfer syntax (JPEG 2000): "+syntax);
|
|
}
|
|
// MPEG2 Image Compression
|
|
else if( syntax === "1.2.840.10008.1.2.4.100" ) {
|
|
throw new Error("Unsupported DICOM transfer syntax (MPEG2): "+syntax);
|
|
}
|
|
// RLE (lossless)
|
|
else if( syntax === "1.2.840.10008.1.2.4.5" ) {
|
|
throw new Error("Unsupported DICOM transfer syntax (RLE): "+syntax);
|
|
}
|
|
}
|
|
// store the data element
|
|
this.appendDicomElement( {
|
|
'name': dataElement.tag.name,
|
|
'group': dataElement.tag.group,
|
|
'element': dataElement.tag.element,
|
|
'value': dataElement.data } );
|
|
// increment index
|
|
i += dataElement.offset-1;
|
|
}
|
|
|
|
var startedPixelItems = false;
|
|
|
|
// DICOM data elements
|
|
for( i=metaEnd; i<this.file.length; i++)
|
|
{
|
|
// get the data element
|
|
dataElement = this.readDataElement(dataReader, i, implicit);
|
|
// store pixel data from multiple items
|
|
if( startedPixelItems ) {
|
|
if( dataElement.tag.name === "Item" ) {
|
|
if( dataElement.data.length !== 0 ) {
|
|
this.pixelBuffer = this.pixelBuffer.concat( dataElement.data );
|
|
}
|
|
}
|
|
else if( dataElement.tag.name === "SequenceDelimitationItem" ) {
|
|
startedPixelItems = false;
|
|
}
|
|
else {
|
|
throw new Error("Unexpected tag in encapsulated pixel data: "+dataElement.tag.name);
|
|
}
|
|
}
|
|
// check the pixel data tag
|
|
if( dataElement.tag.name === "PixelData") {
|
|
if( dataElement.data.length !== 0 ) {
|
|
this.pixelBuffer = dataElement.data;
|
|
}
|
|
else {
|
|
startedPixelItems = true;
|
|
}
|
|
}
|
|
// store the data element
|
|
this.appendDicomElement( {
|
|
'name': dataElement.tag.name,
|
|
'group' : dataElement.tag.group,
|
|
'vr' : dataElement.vr,
|
|
'vl' : dataElement.vl,
|
|
'element': dataElement.tag.element,
|
|
'value': dataElement.data } );
|
|
// increment index
|
|
i += dataElement.offset-1;
|
|
}
|
|
|
|
// uncompress data
|
|
if( jpeg ) {
|
|
console.log("JPEG");
|
|
// using jpgjs from https://github.com/notmasteryet/jpgjs
|
|
// -> error with ffc3 and ffc1 jpeg jfif marker
|
|
/*var j = new JpegImage();
|
|
j.parse(this.pixelBuffer);
|
|
var d = 0;
|
|
j.copyToImageData(d);
|
|
this.pixelBuffer = d.data;*/
|
|
}
|
|
else if( jpeg2000 ) {
|
|
console.log("JPEG 2000");
|
|
// using openjpeg.js from https://github.com/kripken/j2k.js
|
|
// -> 2 layers results????
|
|
/*var data = new Uint16Array(this.pixelBuffer);
|
|
var result = openjpeg(data, "j2k");
|
|
this.pixelBuffer = result.data;*/
|
|
|
|
// using jpx.js from https://github.com/mozilla/pdf.js
|
|
// -> ...
|
|
/*var j = new JpxImage();
|
|
j.parse(this.pixelBuffer);
|
|
console.log("width: "+j.width);
|
|
console.log("height: "+j.height);
|
|
console.log("tiles: "+j.tiles.length);
|
|
console.log("count: "+j.componentsCount);
|
|
this.pixelBuffer = j.tiles[0].items;*/
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get an Image object from the read DICOM file.
|
|
* @returns A new Image.
|
|
*/
|
|
ovm.dicom.DicomParser.prototype.getImage = function()
|
|
{
|
|
// size
|
|
if( !this.dicomElements.Columns ) {
|
|
throw new Error("Missing DICOM image number of columns");
|
|
}
|
|
if( !this.dicomElements.Rows ) {
|
|
throw new Error("Missing DICOM image number of rows");
|
|
}
|
|
var size = new ovm.image.ImageSize(
|
|
this.dicomElements.Columns.value[0],
|
|
this.dicomElements.Rows.value[0]);
|
|
// spacing
|
|
var rowSpacing = 1;
|
|
var columnSpacing = 1;
|
|
if( this.dicomElements.PixelSpacing ) {
|
|
rowSpacing = parseFloat(this.dicomElements.PixelSpacing.value[0]);
|
|
columnSpacing = parseFloat(this.dicomElements.PixelSpacing.value[1]);
|
|
}
|
|
else if( this.dicomElements.ImagerPixelSpacing ) {
|
|
rowSpacing = parseFloat(this.dicomElements.ImagerPixelSpacing.value[0]);
|
|
columnSpacing = parseFloat(this.dicomElements.ImagerPixelSpacing.value[1]);
|
|
}
|
|
var spacing = new ovm.image.ImageSpacing(
|
|
columnSpacing, rowSpacing);
|
|
// image
|
|
var image = new ovm.image.Image( size, spacing, this.pixelBuffer );
|
|
// lookup
|
|
var rescaleSlope = 1;
|
|
if( this.dicomElements.RescaleSlope ) {
|
|
rescaleSlope = parseFloat(this.dicomElements.RescaleSlope.value[0]);
|
|
}
|
|
var rescaleIntercept = 0;
|
|
if( this.dicomElements.RescaleIntercept ) {
|
|
rescaleIntercept = parseFloat(this.dicomElements.RescaleIntercept.value[0]);
|
|
}
|
|
var windowPresets = [];
|
|
var name;
|
|
if( this.dicomElements.WindowCenter && this.dicomElements.WindowWidth ) {
|
|
for( var i = 0; i < this.dicomElements.WindowCenter.value.length; ++i) {
|
|
if( this.dicomElements.WindowCenterWidthExplanation ) {
|
|
name = this.dicomElements.WindowCenterWidthExplanation.value[i];
|
|
}
|
|
else {
|
|
name = "Default"+i;
|
|
}
|
|
windowPresets.push({
|
|
"center": parseInt( this.dicomElements.WindowCenter.value[i], 10 ),
|
|
"width": parseInt( this.dicomElements.WindowWidth.value[i], 10 ),
|
|
"name": name
|
|
});
|
|
}
|
|
}
|
|
var lookup = new ovm.image.LookupTable( windowPresets, rescaleSlope, rescaleIntercept);
|
|
image.setLookup( lookup );
|
|
// return
|
|
return image;
|
|
};
|