Skip to content

Commit

Permalink
Merge pull request #180 from epam/EPMUII-8311-Enable-16-bit-rendering…
Browse files Browse the repository at this point in the history
…-option

EPMUII 8311 enable 16 bit rendering option
  • Loading branch information
oleksandr-zhynzher authored Nov 16, 2023
2 parents 99e4fbf + 9feeaa8 commit e4cd136
Show file tree
Hide file tree
Showing 26 changed files with 476 additions and 250 deletions.
67 changes: 51 additions & 16 deletions src/engine/Graphics2d.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Graphics2d extends React.Component {
this.onMouseUp = this.onMouseUp.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseWheel = this.onMouseWheel.bind(this);
this.setDataWindow = this.setDataWindow.bind(this);

this.m_sliceRatio = 0.5;
this.m_mode2d = Modes2d.TRANSVERSE;
Expand Down Expand Up @@ -63,6 +64,10 @@ class Graphics2d extends React.Component {
// segm 2d
this.segm2d = new Segm2d(this);
this.m_isSegmented = false;
// data window
this.m_winRight = 1;
this.m_winLeft = 0;
this.m_newWin = false;

// tools2d
this.m_toolPick = new ToolPick(this);
Expand Down Expand Up @@ -105,6 +110,13 @@ class Graphics2d extends React.Component {
return this.m_mount.current.toDataURL();
}

setDataWindow(value) {
const [min, max] = value;
this.m_winLeft = min;
this.m_winRight = max;
this.forceUpdate();
}

prepareImageForRender(volIndexArg) {
//TODO: center the image by click
const objCanvas = this.m_mount.current; // Canvas HTML element reference
Expand All @@ -125,6 +137,11 @@ class Graphics2d extends React.Component {

const store = this.props;
const volSet = store.volumeSet;
const dicom = store.loaderDicom;
if (dicom != null && !this.props.is16bit) {
this.m_winRight = dicom.m_winRight;
this.m_winLeft = dicom.m_winLeft;
}
// const volIndex = this.m_volumeIndex;
const volIndex = volIndexArg !== undefined ? volIndexArg : store.volumeIndex;

Expand All @@ -142,7 +159,10 @@ class Graphics2d extends React.Component {
const yDim = vol.m_yDim;
const zDim = vol.m_zDim;
const xyDim = xDim * yDim;
const dataSrc = vol.m_dataArray; // 1 or 4 bytes array of pixels
let dataSrc = vol.m_dataArray; // 1 or 4 bytes array of pixels
if (dicom != null && this.props.is16bit) {
dataSrc = vol.m_dataArray16; // 2 bytes array of pixels
}
if (dataSrc.length !== xDim * yDim * zDim * vol.m_bytesPerVoxel) {
console.log(`Bad src data len = ${dataSrc.length}, but expect ${xDim}*${yDim}*${zDim}`);
}
Expand Down Expand Up @@ -226,9 +246,16 @@ class Graphics2d extends React.Component {
for (let x = 0; x < wScreen; x++) {
const xSrc = Math.floor(x * xStep);
const val = dataSrc[zOff + yOff + xSrc];
dataDst[j + 0] = val;
dataDst[j + 1] = val;
dataDst[j + 2] = val;
let newVal = val;
if (dicom != null && this.props.is16bit) {
const scale = 255 / ((this.m_winRight - this.m_winLeft) * (dicom.m_maxVal - dicom.m_minVal));
newVal = Math.floor((val - this.m_winLeft * (dicom.m_maxVal - dicom.m_minVal)) * scale);
}
if (newVal < 0) newVal = 0;
if (newVal > 255) newVal = 255;
dataDst[j + 0] = newVal;
dataDst[j + 1] = newVal;
dataDst[j + 2] = newVal;
dataDst[j + 3] = 255; // opacity
j += 4;
} // for (x)
Expand Down Expand Up @@ -311,12 +338,17 @@ class Graphics2d extends React.Component {
const ySrc = Math.floor(x * yStep);
const yOff = ySrc * xDim;
const val = dataSrc[zOff + yOff + xSlice];

dataDst[j + 0] = val;
dataDst[j + 1] = val;
dataDst[j + 2] = val;
let newVal = val;
if (dicom != null) {
const scale = 255 / ((this.m_winRight - this.m_winLeft) * (dicom.m_maxVal - dicom.m_minVal));
newVal = Math.floor((val - this.m_winLeft * (dicom.m_maxVal - dicom.m_minVal)) * scale);
}
if (newVal < 0) newVal = 0;
if (newVal > 255) newVal = 255;
dataDst[j + 0] = newVal;
dataDst[j + 1] = newVal;
dataDst[j + 2] = newVal;
dataDst[j + 3] = 255; // opacity

j += 4;
} // for (x)
} // for (y)
Expand Down Expand Up @@ -400,12 +432,17 @@ class Graphics2d extends React.Component {
for (let x = 0; x < wScreen; x++) {
const xSrc = Math.floor(x * xStep);
const val = dataSrc[zOff + yOff + xSrc];

dataDst[j + 0] = val;
dataDst[j + 1] = val;
dataDst[j + 2] = val;
let newVal = val;
if (dicom != null) {
const scale = 255 / ((this.m_winRight - this.m_winLeft) * (dicom.m_maxVal - dicom.m_minVal));
newVal = Math.floor((val - this.m_winLeft * (dicom.m_maxVal - dicom.m_minVal)) * scale);
}
if (newVal < 0) newVal = 0;
if (newVal > 255) newVal = 255;
dataDst[j + 0] = newVal;
dataDst[j + 1] = newVal;
dataDst[j + 2] = newVal;
dataDst[j + 3] = 255; // opacity

j += 4;
} // for (x)
} // for (y)
Expand Down Expand Up @@ -752,8 +789,6 @@ class Graphics2d extends React.Component {
* Main component render func callback
*/
render() {
// const store = this.props;
// const volSet = store.volumeSet;
this.m_sliceRatio = this.props.sliderValue;
this.m_mode2d = this.props.mode2d;
return (
Expand Down
1 change: 1 addition & 0 deletions src/engine/Volume.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Volume extends React.Component {
this.m_zDim = 0;
this.m_bytesPerVoxel = 0;
this.m_dataArray = null;
this.m_dataArray16 = null;
this.m_dataSize = 0;
this.m_boxSize = {
x: 0.0,
Expand Down
2 changes: 2 additions & 0 deletions src/engine/lib/services/StoreService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export class MRIStoreService {
{ type: StoreActionType.SET_LOADER_DICOM, loaderDicom },
{ type: StoreActionType.SET_VOLUME_INDEX, volumeIndex },
{ type: StoreActionType.SET_SPINNER, spinner: false },
{ type: StoreActionType.SET_IS_16_BIT, is16bit: true },
{ type: StoreActionType.SET_SHOW_MODAL_CONFIRMATION, showModalConfirmation: true },
];

this.dispatchActions(actions);
Expand Down
31 changes: 31 additions & 0 deletions src/engine/loaders/LoaderDicom.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ class LoaderDicom {
this.m_padValue = -LARGE_NUMBER;
this.m_windowCenter = LARGE_NUMBER; // TAG_WINDOW_CENTER
this.m_windowWidth = LARGE_NUMBER; // TAG_WINDOW_WIDTH
this.m_winMin = 0;
this.m_winMax = 1;
this.m_minVal = 0;
this.m_maxVal = 0;
this.m_rescaleIntercept = 0; // TAG_RESCALE_INTERCEPT, used as v` = v * rescaleSlope + rescaleIntercept
this.m_rescaleSlope = 1; // TAG_RESCALE_SLOPE
this.m_rescaleHounsfield = false;
Expand Down Expand Up @@ -263,6 +267,7 @@ class LoaderDicom {
console.assert(volSet instanceof VolumeSet, 'Should be volume set');
console.assert(typeof indexSelected === 'number', 'index should be number');
console.assert(typeof hashSelected === 'number', 'index should be number');
const is16bit = true;

let volDst = null;
if (indexSelected < volSet.getNumVolumes()) {
Expand Down Expand Up @@ -327,6 +332,7 @@ class LoaderDicom {
let i;
let dataSize = 0;
let dataArray = null;
let dataArray16 = null;

// convert big endian in slices
if (!this.m_littleEndian) {
Expand Down Expand Up @@ -395,6 +401,8 @@ class LoaderDicom {
} // for (i) all slice pixels
} // for sl
} // for ser
this.m_minVal = minVal;
this.m_maxVal = maxVal;

console.log(`Build Volume. min/max value=${minVal}/${maxVal}. Volume dim = ${this.m_xDim}*${this.m_yDim}*${this.m_zDim}`);
console.log(`Min slice number = ${serie.m_minSlice}`);
Expand Down Expand Up @@ -452,13 +460,19 @@ class LoaderDicom {

dataSize = this.m_xDim * this.m_yDim * this.m_zDim;
dataArray = new Uint8Array(dataSize);
if (is16bit) dataArray16 = new Uint16Array(dataSize);
if (dataArray === null) {
console.log('no memory');
return LoadResult.ERROR_NO_MEMORY;
}
for (i = 0; i < dataSize; i++) {
dataArray[i] = 0;
}
if (is16bit) {
for (i = 0; i < dataSize; i++) {
dataArray16[i] = 0;
}
}

// convert slices data into volume set data (16 bpp -> 8 bpp, etc)
const MAX_BYTE = 255;
Expand All @@ -480,6 +494,17 @@ class LoaderDicom {

if (dataSrc16 !== null) {
const offZ = z * xyDim;
const BITS_16 = 16;
const max16 = 1 << BITS_16;
if (is16bit) {
for (i = 0; i < xyDim; i++) {
let val_16 = dataSrc16[i] * this.m_rescaleSlope + this.m_rescaleIntercept;
//let val = (val_16 - minVal) * max16 / (maxVal - minVal);
let val = val_16 - minVal;
val = Math.floor(val);
dataArray16[offZ + i] = val;
} // for i
}

if (this.m_windowCenter !== LARGE_NUMBER && this.m_windowWidth !== LARGE_NUMBER) {
const winMin = this.m_windowCenter - this.m_windowWidth * 0.5;
Expand Down Expand Up @@ -508,6 +533,11 @@ class LoaderDicom {
dataArray[offZ + i] = val;
} // for i
} // no defined window center, width

if (this.m_windowCenter !== LARGE_NUMBER && this.m_windowWidth !== LARGE_NUMBER && !this.m_rescaleHounsfield) {
this.m_winRight = (-this.m_minVal + this.m_windowCenter + this.m_windowWidth / 2) / (this.m_maxVal - this.m_minVal);
this.m_winLeft = (-this.m_minVal + this.m_windowCenter - this.m_windowWidth / 2) / (this.m_maxVal - this.m_minVal);
}
} // if has slice data
} // for(s) all slices

Expand Down Expand Up @@ -739,6 +769,7 @@ class LoaderDicom {
volDst.m_yDim = this.m_yDim;
volDst.m_zDim = this.m_zDim;
volDst.m_dataArray = dataArray;
volDst.m_dataArray16 = dataArray16;
volDst.m_dataSize = dataSize;
volDst.m_bytesPerVoxel = ONE;
volDst.m_boxSize.x = this.m_boxSize.x;
Expand Down
23 changes: 23 additions & 0 deletions src/engine/utils/SettingsGraphics2d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2021 EPAM Systems, Inc. (https://www.epam.com/)
* SPDX-License-Identifier: Apache-2.0
*/
import { DEFAULT_WIN_MAX, DEFAULT_WIN_MIN } from '../../ui/Constants/WindowSet.constants';
/**
* Apply window range data (min,max)
*/
export const applyWindowRangeData = (store, windowMin = DEFAULT_WIN_MIN, windowMax = DEFAULT_WIN_MAX) => {
const loaderDicom = store.loaderDicom;
const volSet = store.volumeSet;
// set loader features according current modal properties (window min, max)
loaderDicom.m_windowCenter = (windowMin + windowMax) * 0.5;
loaderDicom.m_windowWidth = windowMax - windowMin;
// apply for single slice dicom read
// select 1st slice and hash
const series = loaderDicom.m_slicesVolume.getSeries();
const numSeries = series.length;
for (let i = 0; i < numSeries; i++) {
const hashCode = series[i].m_hash;
loaderDicom.createVolumeFromSlices(volSet, i, hashCode);
}
};
5 changes: 5 additions & 0 deletions src/store/ActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,10 @@ const StoreActionType = {
SET_PROGRESS_INFO: 36,
SET_SPINNER_TITLE: 37,
SET_SPINNER_PROGRESS: 38,
SET_IS_16_BIT: 39,
SET_SHOW_WINDOW_RANGE: 40,
SET_SHOW_MODAL_CONFIRMATION: 41,
SET_SHOW_MODAL_WINDOW_WC: 42,
SET_SHOW_MODAL_SELECT_FILES: 43,
};
export default StoreActionType;
15 changes: 15 additions & 0 deletions src/store/Store.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export const initialState = {
dicomSeries: [],
loaderDicom: null,
spinner: false, //true when mock data is loading
is16bit: false, //true when dicom files are loaded
showWindowRangeSlider: false,
showModalConfirmation: false,
showModalWindowCW: false,
showModalSelectFiles: false,
};
//
// App reducer
Expand Down Expand Up @@ -125,6 +130,16 @@ const medReducer = (state = initialState, action) => {
return Object.assign({}, state, { showModalAlert: action.showModalAlert });
case StoreActionType.SET_SPINNER:
return Object.assign({}, state, { spinner: action.spinner });
case StoreActionType.SET_IS_16_BIT:
return Object.assign({}, state, { is16bit: action.is16bit });
case StoreActionType.SET_SHOW_WINDOW_RANGE:
return Object.assign({}, state, { showWindowRangeSlider: action.showWindowRangeSlider });
case StoreActionType.SET_SHOW_MODAL_CONFIRMATION:
return Object.assign({}, state, { showModalConfirmation: action.showModalConfirmation });
case StoreActionType.SET_SHOW_MODAL_WINDOW_WC:
return Object.assign({}, state, { showModalWindowCW: action.showModalWindowCW });
case StoreActionType.SET_SHOW_MODAL_SELECT_FILES:
return Object.assign({}, state, { showModalSelectFiles: action.showModalSelectFiles });
case StoreActionType.SET_SPINNER_TITLE:
return Object.assign({}, state, { spinnerTitle: action.spinnerTitle });
case StoreActionType.SET_SPINNER_PROGRESS:
Expand Down
7 changes: 7 additions & 0 deletions src/ui/Constants/WindowSet.constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright 2023 EPAM Systems, Inc. (https://www.epam.com/)
* SPDX-License-Identifier: Apache-2.0
*/
export const LARGE_NUMBER = 0x3fffffff;
export const DEFAULT_WIN_MIN = 650 - 2000 / 2;
export const DEFAULT_WIN_MAX = 650 + 2000 / 2;
9 changes: 0 additions & 9 deletions src/ui/FileReaders/DragAndDropComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { useState } from 'react';
import css from './DragAndDrop.module.css';
import { SVG } from '../Button/SVG';
import UiModalWindowCenterWidth from '../Modals/UiModalWinCW';
import MriViwer from '../../engine/lib/MRIViewer';
import { useOnEvent } from '../hooks/useOnEvent';
import { MriEvents } from '../../engine/lib/enums';

const IMG_DROPZONE_SIZE = 49;

export const DragAndDropComponent = () => {
const [isActiveDnd, setIsActiveDnd] = useState(false);
const [showModalWindowCW, setShowModalWindowCW] = useState(false);

useOnEvent(MriEvents.FILE_READ_SUCCESS, () => {
setShowModalWindowCW(true);
});

const handleDrop = (e) => {
e.preventDefault();
Expand All @@ -34,7 +26,6 @@ export const DragAndDropComponent = () => {
onDragLeave={() => setIsActiveDnd(false)}
onDrop={handleDrop}
></div>
{showModalWindowCW && <UiModalWindowCenterWidth stateVis={showModalWindowCW} onHide={() => setShowModalWindowCW(false)} />}
</>
);
};
7 changes: 6 additions & 1 deletion src/ui/FileReaders/OpenFromDeviceButton.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { UIButton } from '../Button/Button';
import ModalSelectFile from '../Modals/ModalSelectFile';
import { useOnEvent } from '../hooks/useOnEvent';
import { MriEvents } from '../../engine/lib/enums';
import StoreActionType from '../../store/ActionTypes';

export const OpenFromDeviceButtonComponent = ({ cx }) => {
const [showOpenFromDeviceModal, setShowOpenFromDeviceModal] = useState(false);
const dispatch = useDispatch();
const { showModalSelectFiles } = useSelector((state) => state);

const onHide = () => {
setShowOpenFromDeviceModal(false);
Expand All @@ -15,12 +19,13 @@ export const OpenFromDeviceButtonComponent = ({ cx }) => {

const onButtonOpenLocalFileClick = () => {
setShowOpenFromDeviceModal(true);
dispatch({ type: StoreActionType.SET_SHOW_MODAL_SELECT_FILES, showModalSelectFiles: true });
};

return (
<>
<UIButton icon="folder" text="Open From Device" cx={cx} handler={onButtonOpenLocalFileClick} />
{showOpenFromDeviceModal && <ModalSelectFile stateVis={showOpenFromDeviceModal} onHide={onHide} />}
{showOpenFromDeviceModal && <ModalSelectFile stateVis={showModalSelectFiles} onHide={onHide} />}
</>
);
};
Loading

0 comments on commit e4cd136

Please sign in to comment.