Skip to content

Commit

Permalink
Add support to PACScenter ImageJS format
Browse files Browse the repository at this point in the history
  • Loading branch information
eriksson authored and Enet4 committed Oct 26, 2021
1 parent d6b1f96 commit cb9ed32
Show file tree
Hide file tree
Showing 9 changed files with 1,178 additions and 255 deletions.
185 changes: 185 additions & 0 deletions core/dicomUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// provides
goog.provide('X.dicomUtils');

// requires
goog.require('X.base');

X.dicomUtils = {};

X.dicomUtils.BitsAllocated = Object.freeze({ "B8": 8, "B16": 16, "B24": 24, "B32": 32 });
X.dicomUtils.VOIFunction = Object.freeze({ "LINEAR": 0, "SIGMOID": 1 });
X.dicomUtils.PresentationLutShape = Object.freeze({ "IDENTITY": 0, "INVERSE": 1, "UNKNOWN": 2 });
X.dicomUtils.PhotometricInterpretation = Object.freeze({
"MONOCHROME1": 0,
"MONOCHROME2": 1,
"RGB": 2,
"PALETTECOLOR": 3,
"UNKNOWN": 8
});
X.dicomUtils.PixelRepresentation = Object.freeze({ "UNSIGNED": 0, "SIGNED": 1 });

X.dicomUtils.ImageType = Object.freeze({ "JPEG": "jpeg", "DCM": "dcm", "MOBILE": "mobile", "CUSTOM": "custom" });

X.dicomUtils.applyPixelPaddingToHULookupTable = function (img, HULookupTable) {
if (img.photometricInterpretation == X.dicomUtils.PhotometricInterpretation.MONOCHROME1 ||
img.photometricInterpretation == X.dicomUtils.PhotometricInterpretation.MONOCHROME2) {

if (img.pixelPaddingValue == null) {
return;
}

var paddingValueMin = (img.pixelPaddingRange == null) ? img.pixelPaddingValue : Math.min(img.pixelPaddingValue, img.pixelPaddingRange);
var paddingValueMax = (img.pixelPaddingRange == null) ? img.pixelPaddingValue : Math.max(img.pixelPaddingValue, img.pixelPaddingRange);

var numPaddingValues = paddingValueMax - paddingValueMin + 1;
var paddingValuesStartIndex = paddingValueMin;

if (paddingValuesStartIndex >= HULookupTable.length) {
return;
}
var isInverse = img.presentationLutShape == PresentationLutShape.INVERSE ? true : img.photometricInterpretation == X.dicomUtils.PhotometricInterpretation.MONOCHROME1 ? true : false;
var fill;
if (isInverse) {
fill = HULookupTable[HULookupTable.length - 1];
} else {
fill = HULookupTable[0];
}

for (var i = paddingValuesStartIndex; i < HULookupTable.length && i < paddingValuesStartIndex + numPaddingValues; i++) {
HULookupTable[i] = fill;
}
}
};

X.dicomUtils.calculateHULookupTable = function (img) {

var rs = img.rescale.getSlope();
var ri = img.rescale.getIntercept();

var bits = img.pixelPaddingValue != null ? img.bits : (rs == 1.0 && ri == 0.0 && img.pixelPaddingValue == null) ? img.bits : img.bitsStored;

var size = Math.pow(2, bits);
var HULookupTable = new Array(size);
for (var i = 0; i < size; i++) {
HULookupTable[i] = i * rs + ri;
}

X.dicomUtils.applyPixelPaddingToHULookupTable(img, HULookupTable);
return HULookupTable;
};

X.dicomUtils.calculateLookupTable = function (img) {

var HULookupTable = X.dicomUtils.calculateHULookupTable(img);

var rs = img.rescale.getSlope();
var ri = img.rescale.getIntercept();

var size = Math.pow(2, img.pixelPaddingValue != null ? img.bits : (rs == 1.0 && ri == 0.0 && img.pixelPaddingValue == null) ? img.bits : img.bitsStored);

var wc = img.windowLevel.getCenter();
var ww = img.windowLevel.getWidth();

// Create LUT it it does not exist
var lookupTable = new Uint8Array(size);

if (img.voiFunction == X.dicomUtils.VOIFunction.LINEAR) {

var unsigned = img.pixelRepresentation == 0; //PixelRepresentation.UNSIGNED
// Get offset and window limits
var offset = (unsigned == true) ? 0 : size / 2,
xMin = offset + wc - 0.5 - (ww - 1) / 2,
xMax = offset + wc - 0.5 + (ww - 1) / 2,
yMax = 255,
yMin = 0;

var minPixelValue = 0;
var maxPixelValue = size;

for (var inputValue = minPixelValue; inputValue < maxPixelValue; inputValue++) {
if (HULookupTable[inputValue] <= xMin) {
lookupTable[inputValue] = yMin;
}
else if (HULookupTable[inputValue] > xMax) {
lookupTable[inputValue] = yMax;
}
else {
var y = (((HULookupTable[inputValue] - offset) - (wc - 0.5)) / (ww - 1) + 0.5) * (yMax - yMin) + yMin;
lookupTable[inputValue] = parseInt(y, 10);
}
}

} else if (img.voiFunction == X.dicomUtils.VOIFunction.SIGMOID) {

var outRangeSize = (1 << BitsAllocated.B8) - 1;
var maxOutValue = img.pixelRepresentation == PixelRepresentation.SIGNED ? (1 << (BitsAllocated.B8 - 1)) - 1 : outRangeSize;
var minOutValue = img.pixelRepresentation == PixelRepresentation.SIGNED ? -(maxOutValue + 1) : 0;
var minInValue = 0;

var nFactor = -20.0; // factor defined by default in Dicom standard ( -20*2/10 = -4 )
var outRange = maxOutValue - minOutValue;
var normalize = false;
var minValue = 0, maxValue = 0, outRescaleRatio = 1;
if (normalize) {
var lowLevel = wc - ww / 2.0;
var highLevel = wc + ww / 2.0;
minValue = minOutValue + outRange / (1.0 + Math.exp((2.0 * nFactor / 10.0) * (lowLevel - wc) / ww));
maxValue = minOutValue + outRange / (1.0 + Math.exp((2.0 * nFactor / 10.0) * (highLevel - wc) / ww));
outRescaleRatio = (maxOutValue - minOutValue) / Math.abs(maxValue - minValue);
}

for (var i = 0; i < size; i++) {
var value = outRange / (1.0 + Math.exp((2.0 * nFactor / 10.0) * (HULookupTable[i] + minInValue - wc) / ww));

if (normalize) {
value = (value - minValue) * outRescaleRatio;
}

value = parseInt(Math.round(value + minOutValue));
value = parseInt((value > maxOutValue) ? maxOutValue : ((value < minOutValue) ? minOutValue : value));
//value = parseInt(inverse ? (maxOutValue + minOutValue - value) : value);

lookupTable[i] = value;
}

} else {
throw ("Image VOI LUT not supported");
}

return lookupTable;
};

X.dicomUtils.initColorTableFromLUT = function (img, colorTable, lut) {

var rs = img.rescale.getSlope();
var ri = img.rescale.getIntercept();

var offset;
if (img.pixelRepresentation == X.dicomUtils.PixelRepresentation.UNSIGNED) {
offset = 0;
} else {
offset = Math.pow(2, img.pixelPaddingValue != null ? img.bits : (rs == 1.0 && ri == 0.0 && img.pixelPaddingValue == null) ? img.bits : img.bitsStored) / 2;
}

// console.time('CALCULATE PIXELS');
switch (img.photometricInterpretation) {
case X.dicomUtils.PhotometricInterpretation.MONOCHROME1:
for (var i = 0; i < lut.length; i++) {
r = g = b = ((255 - lut[i]) / 255.0);
colorTable.add(i, 'lut', r, g, b, 1);
}
break;

case X.dicomUtils.PhotometricInterpretation.MONOCHROME2:
for (var i = 0; i < lut.length; i++) {
r = g = b = (lut[i] / 255.0);
colorTable.add(i - offset, 'lut', r, g, b, 1);
}
break;
default:
throw new Error('X.dicomUtils.initColorTableFromLUT: photometricNotSupported');
}
};

// export symbols (required for advanced compilation)
goog.exportSymbol('X.dicomUtils', X.dicomUtils);
34 changes: 34 additions & 0 deletions core/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ X.event.events = {
// the object modified event
MODIFIED: X.event.uniqueId('modified'),

// the object refresh event
REFRESHED: X.event.uniqueId('update'),

// the object remove event
REMOVE: X.event.uniqueId('remove'),

Expand Down Expand Up @@ -399,6 +402,37 @@ X.event.ModifiedEvent = function() {
// inherit from goog.events.Event
goog.inherits(X.event.ModifiedEvent, X.event);

/**
* The refresh event to flag an object as 'dirty'.
*
* @constructor
* @extends X.event
*/
X.event.RefreshedEvent = function() {

// call the default event constructor
goog.base(this, X.event.events.REFRESHED);

/**
* The object which was refreshed.
*
* @type {?X.object}
* @protected
*/
this._object = null;

/**
* A container for an X.base derived instance.
*
* @type{?X.base}
* @protected
*/
this._container = null;

};
// inherit from goog.events.Event
goog.inherits(X.event.RefreshedEvent, X.event);

/**
* The remove event to flag an object as 'dirty'.
*
Expand Down
2 changes: 2 additions & 0 deletions io/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ goog.require('X.parserRAW');
goog.require('X.parserSTL');
goog.require('X.parserTRK');
goog.require('X.parserVTK');
goog.require('X.parserIMAGEJS');
goog.require('goog.structs.Map');


Expand Down Expand Up @@ -387,6 +388,7 @@ X.loader.extensions = {
'RZ': [X.parserRAW, true],
'TXT': [X.parserLUT, null],
'LUT': [X.parserLUT, null],
'IMAGEJS': [X.parserIMAGEJS, null],
'PNG': [X.parserIMAGE, 'png'], // here we use the arraybuffer
// response type
'JPG': [X.parserIMAGE, 'jpeg'],
Expand Down
24 changes: 20 additions & 4 deletions io/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,12 @@ X.parser.xyrasTransform = function(_sliceNormal, _XYNormal){
(a*a+d*d-c*c-b*b),
0
);

var sin0 = _sliceNormal[1];
var cos0 = _sliceNormal[0];
var rotM = goog.vec.Mat4.createFloat32FromValues(cos0, -sin0, 0, 0, sin0, cos0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
goog.vec.Mat4.multMat(rotM, _RASToXY, _RASToXY);

}


Expand Down Expand Up @@ -953,8 +959,11 @@ X.parser.reslice2 = function(_sliceOrigin, _sliceXYSpacing, _sliceNormal, _color
sliceXY._texture = pixelTexture;
// setup slice spacial information
sliceXY._xyBBox = _xyBBox;
sliceXY._xyIBox = _solutionsXY;
sliceXY._XYToRAS = _XYToRAS;
sliceXY._XYToIJK = _XYToIJK;
sliceXY._RASToXY = _RASToXY;
sliceXY._sliceOrigin = _sliceOrigin;
sliceXY._hmin = _hmin;
sliceXY._hmax = _hmax;
sliceXY._wmin = _wmin;
Expand Down Expand Up @@ -1033,7 +1042,9 @@ X.parser.prototype.updateSliceInfo = function(_index, _sliceOrigin, _sliceNormal

var _XYNormal = goog.vec.Vec3.createFloat32FromValues(0, 0, 1);

var _XYRASTransform = X.parser.xyrasTransform(_sliceNormal, _XYNormal);
var _norm = goog.vec.Vec3.createFloat32FromArray(_sliceNormal.map(function(x){return Math.abs(x)}));

var _XYRASTransform = X.parser.xyrasTransform(_norm, _XYNormal);
var _RASToXY = _XYRASTransform[0];
var _XYToRAS = _XYRASTransform[1];
var _rasSpacing = goog.vec.Vec4.createFloat32FromValues(object._RASSpacing[0], object._RASSpacing[1], object._RASSpacing[2], 0);
Expand Down Expand Up @@ -1061,6 +1072,8 @@ X.parser.prototype.updateSliceInfo = function(_index, _sliceOrigin, _sliceNormal
object._childrenInfo[_index]._sliceXYSpacing = [Math.abs(_xySpacing[0]), Math.abs(_xySpacing[1])];
object._childrenInfo[_index]._sliceSpacing = _xySpacing[2];
object._childrenInfo[_index]._sliceDirection = _sliceDirection;
object._childrenInfo[_index]._RASToXY = _RASToXY;
object._childrenInfo[_index]._XYToRAS = _XYToRAS;

// ------------------------------------------
// GET NUMBER OF SLICES
Expand Down Expand Up @@ -1224,7 +1237,8 @@ X.parser.prototype.reslice = function(object) {
object._children[0]._children[Math.floor(object._childrenInfo[0]._nb/2)] = _slice;

object._indexX = Math.floor(object._childrenInfo[0]._nb/2);
object._indexXold = Math.floor(object._childrenInfo[0]._nb/2);
object._children[0]._currentIndex = object._indexX;
object._indexXold = object._indexX;

// ------------------------------------------
// GO CORONAL
Expand Down Expand Up @@ -1271,7 +1285,8 @@ X.parser.prototype.reslice = function(object) {
object._children[1]._children[Math.floor(object._childrenInfo[1]._nb/2)] = _slice;

object._indexY = Math.floor(object._childrenInfo[1]._nb/2);
object._indexYold = Math.floor(object._childrenInfo[1]._nb/2);
object._children[1]._currentIndex = object._indexY;
object._indexYold = object._indexY;

// ------------------------------------------
// GO AXIAL
Expand Down Expand Up @@ -1317,7 +1332,8 @@ X.parser.prototype.reslice = function(object) {
object._children[2]._children[Math.floor(object._childrenInfo[2]._nb/2)] = _slice;

object._indexZ = Math.floor(object._childrenInfo[2]._nb/2);
object._indexZold = Math.floor(object._childrenInfo[2]._nb/2);
object._children[2]._currentIndex = object._indexZ;
object._indexZold = object._indexZ;

X.TIMERSTOP(this._classname + '.reslice');

Expand Down
Loading

0 comments on commit cb9ed32

Please sign in to comment.