diff --git a/src/api/reading.js b/src/api/reading.js index a7fa5a6d..66473ab8 100644 --- a/src/api/reading.js +++ b/src/api/reading.js @@ -251,3 +251,11 @@ export function uploadOCTLipidAngleTemplate(param) { data: param }) } + +export function saveTableQuestionMark(param) { + return request({ + url: `/ReadingImageTask/saveTableQuestionMark`, + method: 'post', + data: param + }) +} diff --git a/src/store/modules/reading.js b/src/store/modules/reading.js index 4a411426..51b7fd20 100644 --- a/src/store/modules/reading.js +++ b/src/store/modules/reading.js @@ -70,6 +70,12 @@ function getQuestions(questions) { answerObj.angle = angle answerObj.saveTypeEnum = isNaN(parseFloat(angle)) ? 1 : 2 } + } else if (criterionType === 21) { + // MRI-PDFF + let isMeasurable = getQuestionAnswer(item.TableQuestions.Questions, 1105, answerObj) + answerObj.isMeasurable = isMeasurable + answerObj.mean = getQuestionAnswer(item.TableQuestions.Questions, 1104, answerObj) + answerObj.saveTypeEnum = isNaN(parseFloat(isMeasurable)) ? 1 : 2 } else { answerObj.lesionPart = getQuestionAnswer(item.TableQuestions.Questions, 8, answerObj) answerObj.loctation = getQuestionAnswer(item.TableQuestions.Questions, 6, answerObj) @@ -507,6 +513,7 @@ const actions = { }) }, getMeasuredData({ state }, visitTaskId) { + console.log('getMeasuredData') return new Promise(resolve => { var index = state.visitTaskList.findIndex(i => i.VisitTaskId === visitTaskId) if (state.visitTaskList[index].measureDataInit) { @@ -523,6 +530,13 @@ const actions = { el.OtherMeasureData = JSON.parse(el.OtherMeasureData) el.OtherMeasureData.data.remark = el.OrderMarkName } + // if (el.TableQuestionMarkList.length > 0) { + // let list = el.TableQuestionMarkList.map(i=>{ + // i.MeasureData = i.MeasureData ? JSON.parse(i.MeasureData) : '' + // return i + // }) + // el.TableQuestionMarkList = list + // } arr.push(el) }) state.visitTaskList[index].MeasureData = arr @@ -606,6 +620,13 @@ const actions = { el.MeasureData = JSON.parse(el.MeasureData) el.MeasureData.data.remark = el.OrderMarkName } + // if (el.TableQuestionMarkList.length > 0) { + // let list = el.TableQuestionMarkList.map(i=>{ + // i.MeasureData = i.MeasureData ? JSON.parse(i.MeasureData) : '' + // return i + // }) + // el.TableQuestionMarkList = list + // } arr.push(el) }) state.visitTaskList[index].MeasureData = arr @@ -623,27 +644,40 @@ const actions = { resolve(noneDicomMeasureData) }) }, - addMeasuredData({ state }, obj) { + addMeasuredData({ state }, obj) { return new Promise(resolve => { + const criterionType = parseInt(localStorage.getItem('CriterionType')) var index = state.visitTaskList.findIndex(i => i.VisitTaskId === obj.visitTaskId) var measureData = state.visitTaskList[index].MeasureData // var idx = measureData.findIndex(item => item.MeasureData.uuid === obj.data.MeasureData.data.uuid) - var idx = measureData.findIndex(item => item.QuestionId === obj.data.QuestionId && item.RowIndex === obj.data.RowIndex) - if (idx > -1) { - for (const k in state.visitTaskList[index].MeasureData[idx]) { - if (k !== 'Id' && obj.data[k]) { - state.visitTaskList[index].MeasureData[idx][k] = obj.data[k] + + if (criterionType === 21) { + let i = measureData.findIndex(i=>i.TableQuestionId === obj.data.TableQuestionId) + if (i > -1) { + for (const k in state.visitTaskList[index].MeasureData[i]) { + if (k !== 'Id' && obj.data[k]) { + state.visitTaskList[index].MeasureData[i][k] = obj.data[k] + } } + } else { + state.visitTaskList[index].MeasureData.push(obj.data) } - - // state.visitTaskList[index].MeasureData[idx].MeasureData = obj.data.MeasureData - console.log('更新标记成功', idx) } else { - state.visitTaskList[index].MeasureData.push(obj.data) - console.log('新增标记成功') + var idx = measureData.findIndex(item => item.QuestionId === obj.data.QuestionId && item.RowIndex === obj.data.RowIndex) + if (idx > -1) { + for (const k in state.visitTaskList[index].MeasureData[idx]) { + if (k !== 'Id' && obj.data[k]) { + state.visitTaskList[index].MeasureData[idx][k] = obj.data[k] + } + } + console.log('更新标记成功', idx) + } else { + state.visitTaskList[index].MeasureData.push(obj.data) + console.log('新增标记成功') + } } + // sessionStorage.setItem('visitTaskList', state.visitTaskList.length > 0 ? JSON.stringify(state.visitTaskList) : '') - console.log(state.visitTaskList) resolve() }) }, @@ -738,32 +772,45 @@ const actions = { return new Promise(resolve => { var index = state.visitTaskList.findIndex(i => i.VisitTaskId === obj.visitTaskId) var measureData = state.visitTaskList[index].MeasureData - - // var uuid = obj.measureData.data.uuid - // var idx = measureData.findIndex(item => item.MeasureData && item.MeasureData.data && item.MeasureData.data.uuid === uuid) - var idx = measureData.findIndex(item => item.QuestionId === obj.questionId && item.RowIndex === obj.rowIndex) - if (idx > -1) { - if (measureData[idx].FristAddTaskId) { - measureData[idx].MeasureData = '' - console.log('清除标记成功', idx) - } else { - measureData.splice(idx, 1) - console.log('移除标记成功', idx) - } - state.visitTaskList[index].MeasureData = measureData - } else if (obj.orderMarkName) { + const criterionType = parseInt(localStorage.getItem('CriterionType')) + if (criterionType === 21) { const i = measureData.findIndex(item => item.QuestionId === obj.questionId && item.OrderMarkName === obj.orderMarkName) - if (i > -1) { - if (measureData[i].FristAddTaskId) { - measureData[i].MeasureData = '' - console.log('清除标记成功', i) - } else { - measureData.splice(i, 1) - console.log('移除标记成功', i) + if (i > -1) { + if (measureData[i].FristAddTaskId) { + measureData[i].MeasureData = '' + console.log('清除标记成功', i) + } else { + measureData.splice(i, 1) + console.log('移除标记成功', i) + } } - } state.visitTaskList[index].MeasureData = measureData + } else { + var idx = measureData.findIndex(item => item.QuestionId === obj.questionId && item.RowIndex === obj.rowIndex) + if (idx > -1) { + if (measureData[idx].FristAddTaskId) { + measureData[idx].MeasureData = '' + console.log('清除标记成功', idx) + } else { + measureData.splice(idx, 1) + console.log('移除标记成功', idx) + } + state.visitTaskList[index].MeasureData = measureData + } else if (obj.orderMarkName) { + const i = measureData.findIndex(item => item.QuestionId === obj.questionId && item.OrderMarkName === obj.orderMarkName) + if (i > -1) { + if (measureData[i].FristAddTaskId) { + measureData[i].MeasureData = '' + console.log('清除标记成功', i) + } else { + measureData.splice(i, 1) + console.log('移除标记成功', i) + } + } + state.visitTaskList[index].MeasureData = measureData + } } + // if (idx > -1) { // measureData.splice(idx, 1) @@ -1034,7 +1081,7 @@ const actions = { } }, setImageloadedInfo({ state }, obj) { - console.log('setImageloadedInfo', obj) + // console.log('setImageloadedInfo', obj) // if(obj.instance === '20dd8fc9-51b0-ec63-942b-cb3006c72650') // var index = state.visitTaskList.findIndex(i => i.VisitTaskId === obj.visitTaskId) // // const prefetchInstanceCount = state.visitTaskList[index].StudyList[obj.studyIndex].SeriesList[obj.seriesIndex].prefetchInstanceCount diff --git a/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue b/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue index 9c157e11..0bc5056d 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue +++ b/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue @@ -148,12 +148,14 @@ import LengthTool from '@/views/trials/trials-panel/reading/dicoms/tools/Length/ import BidirectionalTool from '@/views/trials/trials-panel/reading/dicoms/tools/Bidirectional/BidirectionalTool' import ArrowAnnotateTool from '@/views/trials/trials-panel/reading/dicoms/tools/ArrowAnnotate/ArrowAnnotateTool' import RectangleRoiTool from '@/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/RectangleRoiTool' +import ProbeTool from '@/views/trials/trials-panel/reading/dicoms/tools/Probe/ProbeTool' // import OrientationMarkersTool from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/OrientationMarkersTool' import ScaleOverlayTool from '@/views/trials/trials-panel/reading/dicoms/tools/ScaleOverlay/ScaleOverlayTool' import getOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/getOrientationString' import invertOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/invertOrientationString' // import calculateLongestAndShortestDiameters from '@/views/trials/trials-panel/reading/dicoms/tools/Bidirectional/calculateLongestAndShortestDiameters' import calculateSUV from '@/views/trials/trials-panel/reading/dicoms/tools/calculateSUV' +// import { ProbeTool } from '@cornerstonejs/tools' cornerstoneTools.external.cornerstone = cornerstone cornerstoneTools.external.Hammer = Hammer cornerstoneTools.external.cornerstoneMath = cornerstoneMath @@ -799,8 +801,8 @@ export default { measureData.ww = Math.round(viewport.voi.windowWidth) measureData.wc = Math.round(viewport.voi.windowCenter) var questionInfo = this.measureData[idx] - const canvas = this.canvas.querySelector('canvas') - measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) + // const canvas = this.canvas.querySelector('canvas') + // measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) measureData.data.active = false this.$emit('modifyMeasureData', { measureData, questionInfo }) // e.stopImmediatePropagation() @@ -1000,26 +1002,14 @@ export default { if (i > -1) { var idx = this.measureData.findIndex(item => item.MeasureData && item.MeasureData.data && item.MeasureData.data.uuid === toolState.data[i].uuid) if (idx > -1) { - // console.log('mouseClick') DicomEvent.$emit('setCollapseActive', this.measureData[idx]) if (this.readingTaskState < 2) { const measureData = {} var markName = this.measureData[idx].OrderMarkName if (this.disabledMarks.indexOf(markName) === -1 || !this.disabledMarks) { - // if (toolType === 'Bidirectional') { - // const { - // longestDiameter, - // shortestDiameter - // } = calculateLongestAndShortestDiameters(toolState.data[i], image, this.digitPlaces) - // toolState.data[i].longestDiameter = longestDiameter - // toolState.data[i].shortestDiameter = shortestDiameter - // } - // if (toolType === 'Length') { - // toolState.data[i].length = this.calculateLenth(toolState.data[i]) - // } var questionInfo = this.measureData[idx] - const canvas = this.canvas.querySelector('canvas') - measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) + // const canvas = this.canvas.querySelector('canvas') + // measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) measureData.studyId = this.stack.studyId measureData.seriesId = this.stack.seriesId measureData.instanceId = instanceId @@ -1031,6 +1021,10 @@ export default { measureData.ww = Math.round(viewport.voi.windowWidth) measureData.wc = Math.round(viewport.voi.windowCenter) measureData.data.active = false + var criterionType = parseInt(localStorage.getItem('CriterionType')) + if (criterionType === 21) { + measureData.tableQuestionId = this.measureData[idx].TableQuestionId + } this.$emit('modifyMeasureData', { measureData, questionInfo }) } } @@ -1161,6 +1155,8 @@ export default { cornerstoneTools.addToolForElement(element, ArrowAnnotateTool, { configuration: { allowEmptyLabel: true, handleRadius: false, drawHandlesOnHover: true, hideHandlesIfMoving: true }}) } else if (toolName === 'RectangleRoi') { cornerstoneTools.addToolForElement(element, RectangleRoiTool, { configuration: { allowEmptyLabel: true, handleRadius: false, drawHandlesOnHover: true, hideHandlesIfMoving: true }}) + } else if (toolName === 'Probe' && parseInt(localStorage.getItem('CriterionType')) === 21) { + cornerstoneTools.addToolForElement(element, ProbeTool, { configuration: { fixedRadius: 5, handleRadius: true, drawHandlesOnHover: true, hideHandlesIfMoving: true, digits: this.digitPlaces }}) } else { cornerstoneTools.addToolForElement(element, apiTool) } @@ -1351,8 +1347,8 @@ export default { measureData.ww = Math.round(viewport.voi.windowWidth) measureData.wc = Math.round(viewport.voi.windowCenter) - const canvas = this.canvas.querySelector('canvas') - measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) + // const canvas = this.canvas.querySelector('canvas') + // measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) this.$emit('setMeasureData', measureData) cornerstoneTools.setToolPassiveForElement(this.canvas, e.detail.toolName) } else if (e.detail.toolName === 'Bidirectional') { @@ -1367,8 +1363,22 @@ export default { measureData.location = this.dicomInfo.location measureData.ww = Math.round(viewport.voi.windowWidth) measureData.wc = Math.round(viewport.voi.windowCenter) - const canvas = this.canvas.querySelector('canvas') - measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) + // const canvas = this.canvas.querySelector('canvas') + // measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) + this.$emit('setMeasureData', measureData) + cornerstoneTools.setToolPassiveForElement(this.canvas, e.detail.toolName) + } else if (e.detail.toolName === 'Probe') { + const measureData = {} + measureData.studyId = this.stack.studyId + measureData.seriesId = this.stack.seriesId + measureData.instanceId = instanceId + measureData.frame = this.stack.frame ? this.stack.frame : 0 + measureData.data = e.detail.measurementData + measureData.type = e.detail.toolName + measureData.thick = this.dicomInfo.thick + measureData.location = this.dicomInfo.location + measureData.ww = Math.round(viewport.voi.windowWidth) + measureData.wc = Math.round(viewport.voi.windowCenter) this.$emit('setMeasureData', measureData) cornerstoneTools.setToolPassiveForElement(this.canvas, e.detail.toolName) } else if (!e.detail.toolName) { @@ -1448,7 +1458,7 @@ export default { }, onMeasurementmodified(e) { // 移动 - // console.log('modified') + console.log('modified') if (this.readingTaskState >= 2) return const { measurementData, toolType } = e.detail var element = cornerstone.getEnabledElement(this.canvas) @@ -1466,8 +1476,8 @@ export default { var markName = this.measureData[idx].OrderMarkName if (this.disabledMarks.indexOf(markName) === -1 || !this.disabledMarks) { var questionInfo = this.measureData[idx] - const canvas = this.canvas.querySelector('canvas') - measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) + // const canvas = this.canvas.querySelector('canvas') + // measureData.pictureBaseStr = canvas.toDataURL('image/png', 1) measureData.studyId = this.stack.studyId measureData.seriesId = this.stack.seriesId measureData.instanceId = instanceId @@ -1479,6 +1489,10 @@ export default { measureData.ww = Math.round(viewport.voi.windowWidth) measureData.wc = Math.round(viewport.voi.windowCenter) measureData.data.active = false + var criterionType = parseInt(localStorage.getItem('CriterionType')) + if (criterionType === 21) { + measureData.tableQuestionId = this.measureData[idx].TableQuestionId + } this.$emit('modifyMeasureData', { measureData, questionInfo }) } } diff --git a/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue b/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue index 64973ddc..877d4408 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue +++ b/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue @@ -493,6 +493,14 @@ :is-show="isShow" :is-reading-show-subject-info="isReadingShowSubjectInfo" /> +

Developing...

@@ -786,6 +794,7 @@ import PCWGQuestionList from './PCWG/QuestionList' import LuganoQuestionList from './Lugano/QuestionList' import IVUSList from './IVUS/QuestionList' import OCTList from './OCT/QuestionList' +import MRIPDFF from './MRIPDFF/QuestionList' import CustomWwwcForm from './CustomWwwcForm' import Manuals from './Manuals' import Hotkeys from './Hotkeys' @@ -818,6 +827,7 @@ export default { LuganoQuestionList, IVUSList, OCTList, + MRIPDFF, 'download-dicom-and-nonedicom': downloadDicomAndNonedicom, 'upload-dicom-and-nonedicom': uploadDicomAndNonedicom, SignForm @@ -978,7 +988,8 @@ export default { taskId: '', signVisible: false, signCode: null, - currentUser: zzSessionStorage.getItem('userName') + currentUser: zzSessionStorage.getItem('userName'), + tmpData: null } }, @@ -1067,6 +1078,10 @@ export default { this.measuredTools = [] } else if (this.CriterionType === 20) { this.measuredTools = [] + } else if (this.CriterionType === 21) { + this.measuredTools = [{ + toolName: 'Probe', text: this.$t('trials:reading:button:circle'), icon: 'oval', isDisabled: false, disabledReason: '' + }] } this.rotateList[0] = '1' this.colorList[0] = '' @@ -1178,6 +1193,11 @@ export default { this.petctWindow.close() } }) + DicomEvent.$on('addAnnotation', async obj => { + this.tmpData = Object.assign({}, obj.question) + await this.imageLocation(obj.locateInfo) + this.setToolActive('Probe', true, null, 'tableQuestion') + }) window.addEventListener('beforeunload', () => { if (this.petctWindow) { this.petctWindow.close() @@ -1600,7 +1620,7 @@ export default { var activeCanvasTaskId = obj.visitTaskId var index = this.visitTaskList.findIndex(i => i.VisitTaskId === activeCanvasTaskId) - if (index === -1) { + if (index === -1) { resolve() return } @@ -2046,6 +2066,9 @@ export default { }, // 设置测量工具启用(不会对输入作出反应) setToolActive(toolName, isMeasuredTool, e, type) { + if (!type) { + this.tmpData = null + } if (isMeasuredTool) { // var i = this.measuredTools.findIndex(item => item.toolName === toolName) // if (i === -1 && this.measuredTools[i].isDisabled) return @@ -2144,13 +2167,27 @@ export default { }, // 添加标记 setMeasureData(data) { - this.$refs['measurementList'].setMeasuredData(data) + if (this.CriterionType === 21) { + if (this.tmpData) { + data.tableQuestionId = this.tmpData.Id + data.tableQuestionMark = this.tmpData.QuestionMark + this.$refs['measurementList'].setMeasuredData(data) + } + } else { + this.$refs['measurementList'].setMeasuredData(data) + } this.activeTool = '' + }, // 修改标记 modifyMeasureData(data) { - this.$refs['measurementList'].modifyMeasuredData(data) + if (this.CriterionType === 21 && data.measureData.tableQuestionId) { + this.$refs['measurementList'].modifyMeasuredData(data) + } else { + this.$refs['measurementList'].modifyMeasuredData(data) + } this.activeTool = '' + }, async saveImage() { // this.$refs[`dicomCanvas${this.currentDicomCanvasIndex}`][0].saveImage() diff --git a/src/views/trials/trials-panel/reading/dicoms/components/MRIPDFF/QuestionForm.vue b/src/views/trials/trials-panel/reading/dicoms/components/MRIPDFF/QuestionForm.vue new file mode 100644 index 00000000..92df7c74 --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms/components/MRIPDFF/QuestionForm.vue @@ -0,0 +1,771 @@ + + + diff --git a/src/views/trials/trials-panel/reading/dicoms/components/MRIPDFF/QuestionList.vue b/src/views/trials/trials-panel/reading/dicoms/components/MRIPDFF/QuestionList.vue new file mode 100644 index 00000000..b306cb62 --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms/components/MRIPDFF/QuestionList.vue @@ -0,0 +1,616 @@ + + + diff --git a/src/views/trials/trials-panel/reading/dicoms/components/ReportPage.vue b/src/views/trials/trials-panel/reading/dicoms/components/ReportPage.vue index 2e7c5019..4f9b4432 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/ReportPage.vue +++ b/src/views/trials/trials-panel/reading/dicoms/components/ReportPage.vue @@ -373,13 +373,13 @@ export default { }) }, async beforeLeave() { - if (this.questionFormChangeState && this.CriterionType !== 2) { - var msg = this.$t('trials:readingReport:message:msg5') - var isgo = await this.myConfirm(msg) - if (!isgo) { - return Promise.resolve(true) - } - } + // if (this.questionFormChangeState && this.CriterionType !== 2) { + // var msg = this.$t('trials:readingReport:message:msg5') + // var isgo = await this.myConfirm(msg) + // if (!isgo) { + // return Promise.resolve(true) + // } + // } var list = null DicomEvent.$emit('getAllUnSaveLesions', val => { list = val diff --git a/src/views/trials/trials-panel/reading/dicoms/tools/Probe/ProbeTool.js b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/ProbeTool.js new file mode 100644 index 00000000..92caea2c --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/ProbeTool.js @@ -0,0 +1,404 @@ +import * as cornerstoneTools from 'cornerstone-tools' +const external = cornerstoneTools.external +// State +const getToolState = cornerstoneTools.getToolState +const textStyle = cornerstoneTools.textStyle +const toolColors = cornerstoneTools.toolColors +const toolStyle = cornerstoneTools.toolStyle +// Drawing +const getNewContext = cornerstoneTools.import('drawing/getNewContext') +const draw = cornerstoneTools.import('drawing/draw') +const drawHandles = cornerstoneTools.import('drawing/drawHandles') +const drawTextBox = cornerstoneTools.import('drawing/drawTextBox') +// Utilities +const getRGBPixels = cornerstoneTools.import('util/getRGBPixels') +const calculateSUV = cornerstoneTools.import('util/calculateSUV') +// import { probeCursor } from '../cursors/index.js'; +// import { getLogger } from '../../util/logger.js'; +const throttle = cornerstoneTools.import('util/throttle') +const getModule = cornerstoneTools.getModule +const getPixelSpacing = cornerstoneTools.import('util/getPixelSpacing') +// import numbersWithCommas from './../../util/numbersWithCommas.js'; +const numbersWithCommas = cornerstoneTools.import('util/numbersWithCommas') +// const logger = getLogger('tools:annotation:ProbeTool'); +import calculateEllipseStatistics from './calculateEllipseStatistics' +import getCircleCoords from './getCircleCoords' +/** + * @public + * @class ProbeTool + * @memberof Tools.Annotation + * @classdesc Tool which provides a probe of the image data at the + * desired position. + * @extends Tools.Base.BaseAnnotationTool + */ +export default class ProbeTool extends cornerstoneTools.ProbeTool { + constructor(props = {}) { + const defaultProps = { + name: 'Probe', + supportedInteractionTypes: ['Mouse', 'Touch'], + // svgCursor: probeCursor, + configuration: { + drawHandles: true, + renderDashed: false, + handleRadius: 0, + fixedRadius: 0, + hideHandlesIfMoving: false, + digits: 1, + showRadius: false, + }, + }; + + super(props, defaultProps); + + this.throttledUpdateCachedStats = throttle(this.updateCachedStats, 110); + } + + createNewMeasurement(eventData) { + const goodEventData = + eventData && eventData.currentPoints && eventData.currentPoints.image; + + if (!goodEventData) { + logger.error( + `required eventData not supplied to tool ${this.name}'s createNewMeasurement` + ); + + return; + } + + return { + visible: true, + active: true, + color: undefined, + invalidated: true, + handles: { + // start: { + // x: eventData.currentPoints.image.x, + // y: eventData.currentPoints.image.y, + // highlight: true, + // active: false, + // }, + end: { + x: eventData.currentPoints.image.x, + y: eventData.currentPoints.image.y, + highlight: true, + active: true, + radius: 0 + }, + }, + }; + } + + /** + * + * + * @param {*} element + * @param {*} data + * @param {*} coords + * @returns {Boolean} + */ + pointNearTool(element, data, coords) { + const hasEndHandle = data && data.handles && data.handles.end; + const validParameters = hasEndHandle; + + if (!validParameters) { + logger.warn( + `invalid parameters supplied to tool ${this.name}'s pointNearTool` + ); + } + + if (!validParameters || data.visible === false) { + return false; + } + + const probeCoords = external.cornerstone.pixelToCanvas( + element, + data.handles.end + ); + + return external.cornerstoneMath.point.distance(probeCoords, coords) < 5; + } + // pointNearTool(element, data, coords) { + // const hasEndHandle = data && data.handles && data.handles.end; + // const validParameters = hasEndHandle; + + // if (!validParameters) { + // logger.warn( + // `invalid parameters supplied to tool ${this.name}'s pointNearTool` + // ); + // } + + // if (!validParameters || data.visible === false) { + // return false; + // } + // // data.handles.end.x = data.handles.end.x + 10 + // let endCoords = { + // x: data.handles.end.x + 50, + // y: data.handles.end.y, + // highlight: data.handles.end.highlight, + // active: data.handles.end.active, + // } + // const probeCoords = external.cornerstone.pixelToCanvas( + // element, + // endCoords + // ); + + // return external.cornerstoneMath.point.distance(probeCoords, coords) < 5; + // } + + // updateCachedStats(image, element, data) { + // const x = Math.round(data.handles.end.x); + // const y = Math.round(data.handles.end.y); + + // const stats = {}; + + // if (x >= 0 && y >= 0 && x < image.columns && y < image.rows) { + // stats.x = x; + // stats.y = y; + + // if (image.color) { + // stats.storedPixels = getRGBPixels(element, x, y, 1, 1); + // } else { + // stats.storedPixels = external.cornerstone.getStoredPixels( + // element, + // x, + // y, + // 1, + // 1 + // ); + // stats.sp = stats.storedPixels[0]; + // stats.mo = stats.sp * image.slope + image.intercept; + // stats.suv = calculateSUV(image, stats.sp); + // } + // } + + // data.cachedStats = stats; + // data.invalidated = false; + // } + updateCachedStats(image, element, data) { + const seriesModule = + external.cornerstone.metaData.get('generalSeriesModule', image.imageId) || + {}; + const modality = seriesModule.modality; + const pixelSpacing = getPixelSpacing(image); + const { fixedRadius, digits } = this.configuration; + const stats = _calculateStats( + image, + element, + data.handles, + modality, + pixelSpacing, + fixedRadius, + digits + ); + + data.cachedStats = stats; + data.invalidated = false; + } + + renderToolData(evt) { + const eventData = evt.detail; + const { fixedRadius, renderDashed } = this.configuration; + const toolData = getToolState(evt.currentTarget, this.name); + + if (!toolData) { + return; + } + + // We have tool data for this element - iterate over each one and draw it + const context = getNewContext(eventData.canvasContext.canvas); + const { image, element } = eventData; + const fontHeight = textStyle.getFontSize(); + const lineDash = getModule('globalConfiguration').configuration.lineDash; + const pixelSpacing = getPixelSpacing(image); + // Meta + const seriesModule = + external.cornerstone.metaData.get('generalSeriesModule', image.imageId) || + {}; + + // Pixel Spacing + const modality = seriesModule.modality; + const hasPixelSpacing = pixelSpacing && pixelSpacing.rowPixelSpacing && pixelSpacing.colPixelSpacing; + for (let i = 0; i < toolData.data.length; i++) { + const data = toolData.data[i]; + if (data.visible === false) { + continue; + } + + draw(context, context => { + const color = toolColors.getColorIfActive(data); + + if (this.configuration.drawHandles) { + // Draw the handles + let radius = getCanvasRadius(data.handles, fixedRadius, element, pixelSpacing) + data.handles.end.radius = radius + const handleOptions = { handleRadius: radius, color }; + + if (renderDashed) { + handleOptions.lineDash = lineDash; + } + drawHandles(context, eventData, data.handles, handleOptions); + } + + // Update textbox stats + if (data.invalidated === true) { + if (data.cachedStats) { + this.throttledUpdateCachedStats(image, element, data); + } else { + this.updateCachedStats(image, element, data); + } + } + + // let text, str; + let textLines = [] + const { x, y, area, mean, radius } = data.cachedStats; + + if (x >= 0 && y >= 0 && x < image.columns && y < image.rows) { + // text = `${x}, ${y}`; + if (data.remark) { + textLines.push(`${data.remark}`) + } + if (!image.color) { + // Draw text + let unit = _getUnit(modality, this.configuration.showHounsfieldUnits); + + // textLines.push(_formatArea(area, hasPixelSpacing)); + if (mean) { + // str += `mean: ${parseFloat(mean.toFixed(3))}`; + textLines.push(`Mean: ${mean} ${unit}`) + } + // if (radius) { + // str += `radius: ${parseFloat(radius.toFixed(3))}`; + // textLines.push(`radius: ${parseFloat(radius.toFixed(3))}`) + // } + } + let r = getPixelRadius(fixedRadius, pixelSpacing) + // Coords for text + const coords = { + // Translate the x/y away from the cursor + x: data.handles.end.x + r, + y: data.handles.end.y - r, + }; + const textCoords = external.cornerstone.pixelToCanvas( + eventData.element, + coords + ); + drawTextBox( + context, + textLines, + textCoords.x, + textCoords.y , + color + ); + // drawTextBox(context, '', textCoords.x, textCoords.y, color); + } + }); + } + } +} +function _getUnit(modality, showHounsfieldUnits) { + return modality === 'CT' && showHounsfieldUnits !== false ? 'HU' : ''; +} +function _formatArea(area, hasPixelSpacing) { + const suffix = hasPixelSpacing + ? ` mm${String.fromCharCode(178)}` + : ` px${String.fromCharCode(178)}`; + + return `Area: ${numbersWithCommas(area.toFixed(2))}${suffix}`; +} +function getCanvasRadius(handles, fixedRadius, element, pixelSpacing) { + let startCoords = { + x: handles.end.x, + y: handles.end.y + } + const handleCanvasStartCoords = external.cornerstone.pixelToCanvas( + element, + startCoords + ); + let r = getPixelRadius(fixedRadius, pixelSpacing) + let endCoords = { + x: handles.end.x + r, + y: handles.end.y + } + const handleCanvasEndCoords = external.cornerstone.pixelToCanvas( + element, + endCoords + ); + return Math.abs(handleCanvasEndCoords.x - handleCanvasStartCoords.x) +} +function getPixelRadius(r, pixelSpacing) { + return r / ((pixelSpacing && pixelSpacing.colPixelSpacing) || 1) +} +function _calculateStats(image, element, handles, modality, pixelSpacing, fixedRadius, digits) { + // Retrieve the bounds of the ellipse in image coordinates + let startCoords = { + x: handles.end.x, + y: handles.end.y + } + let r = getPixelRadius(fixedRadius, pixelSpacing) + let endCoords = { + x: handles.end.x + r, + y: handles.end.y + } + const circleCoordinates = getCircleCoords(startCoords, endCoords); + + // Retrieve the array of pixels that the ellipse bounds cover + const pixels = external.cornerstone.getPixels( + element, + circleCoordinates.left, + circleCoordinates.top, + circleCoordinates.width, + circleCoordinates.height + ); + + // Calculate the mean & standard deviation from the pixels and the ellipse details. + const ellipseMeanStdDev = calculateEllipseStatistics( + pixels, + circleCoordinates + ); + + let meanStdDevSUV; + + if (modality === 'PT') { + meanStdDevSUV = { + mean: calculateSUV(image, ellipseMeanStdDev.mean, true) || 0, + stdDev: calculateSUV(image, ellipseMeanStdDev.stdDev, true) || 0, + }; + } + + const radius = + (circleCoordinates.width * + ((pixelSpacing && pixelSpacing.colPixelSpacing) || 1)) / + 2; + const perimeter = 2 * Math.PI * radius; + const area = + Math.PI * + ((circleCoordinates.width * + ((pixelSpacing && pixelSpacing.colPixelSpacing) || 1)) / + 2) * + ((circleCoordinates.height * + ((pixelSpacing && pixelSpacing.rowPixelSpacing) || 1)) / + 2); + const x = Math.round(handles.end.x); + const y = Math.round(handles.end.y); + + const stats = {}; + + if (x >= 0 && y >= 0 && x < image.columns && y < image.rows) { + stats.x = x; + stats.y = y; + } + return { + area: area || 0, + radius: radius || 0, + perimeter: perimeter || 0, + mean: ellipseMeanStdDev.mean.toFixed(digits) || 0, + stdDev: ellipseMeanStdDev.stdDev.toFixed(digits) || 0, + min: ellipseMeanStdDev.min.toFixed(digits) || 0, + max: ellipseMeanStdDev.max.toFixed(digits) || 0, + meanStdDevSUV, + x: stats.x, + y: stats.y + }; +} + + diff --git a/src/views/trials/trials-panel/reading/dicoms/tools/Probe/calculateEllipseStatistics.js b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/calculateEllipseStatistics.js new file mode 100644 index 00000000..867437f5 --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/calculateEllipseStatistics.js @@ -0,0 +1,67 @@ +import pointInEllipse from './pointInEllipse.js'; + +/** + * Calculates the statistics of an elliptical region of interest. + * + * @private + * @function calculateEllipseStatistics + * + * @param {number[]} sp - Array of the image data's pixel values. + * @param {Object} ellipse - { top, left, height, width } - An object describing the ellipse. + * @returns {Object} { count, mean, variance, stdDev, min, max } + */ +export default function(sp, ellipse) { + let sum = 0; + let sumSquared = 0; + let count = 0; + let index = 0; + let min = null; + let max = null; + + for (let y = ellipse.top; y < ellipse.top + ellipse.height; y++) { + for (let x = ellipse.left; x < ellipse.left + ellipse.width; x++) { + const point = { + x, + y, + }; + + if (pointInEllipse(ellipse, point)) { + if (min === null) { + min = sp[index]; + max = sp[index]; + } + + sum += sp[index]; + sumSquared += sp[index] * sp[index]; + min = Math.min(min, sp[index]); + max = Math.max(max, sp[index]); + count++; + } + + index++; + } + } + + if (count === 0) { + return { + count, + mean: 0.0, + variance: 0.0, + stdDev: 0.0, + min: 0.0, + max: 0.0, + }; + } + + const mean = sum / count; + const variance = sumSquared / count - mean * mean; + + return { + count, + mean, + variance, + stdDev: Math.sqrt(variance), + min, + max, + }; +} diff --git a/src/views/trials/trials-panel/reading/dicoms/tools/Probe/getCircleCoords.js b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/getCircleCoords.js new file mode 100644 index 00000000..ddc90942 --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/getCircleCoords.js @@ -0,0 +1,21 @@ +import * as cornerstoneTools from 'cornerstone-tools' +const external = cornerstoneTools.external + +/** + * Retrieve the bounds of the circle in image coordinates + * + * @param {*} startHandle + * @param {*} endHandle + * @returns {{ left: number, top: number, width: number, height: number }} + */ +export default function getCircleCoords(startHandle, endHandle) { + const { distance } = external.cornerstoneMath.point + const radius = distance(startHandle, endHandle) + + return { + left: Math.floor(Math.min(startHandle.x - radius, endHandle.x)), + top: Math.floor(Math.min(startHandle.y - radius, endHandle.y)), + width: radius * 2, + height: radius * 2 + } +} diff --git a/src/views/trials/trials-panel/reading/dicoms/tools/Probe/pointInEllipse.js b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/pointInEllipse.js new file mode 100644 index 00000000..e7a09e2e --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms/tools/Probe/pointInEllipse.js @@ -0,0 +1,39 @@ +/** + * Returns true if a point is within an ellipse + * @export @public @method + * @name pointInEllipse + * + * @param {Object} ellipse Object defining the ellipse. + * @param {Object} location The location of the point. + * @returns {boolean} True if the point is within the ellipse. + */ +export default function(ellipse, location) { + const xRadius = ellipse.width / 2; + const yRadius = ellipse.height / 2; + + if (xRadius <= 0.0 || yRadius <= 0.0) { + return false; + } + + const center = { + x: ellipse.left + xRadius, + y: ellipse.top + yRadius, + }; + + /* This is a more general form of the circle equation + * + * X^2/a^2 + Y^2/b^2 <= 1 + */ + + const normalized = { + x: location.x - center.x, + y: location.y - center.y, + }; + + const inEllipse = + (normalized.x * normalized.x) / (xRadius * xRadius) + + (normalized.y * normalized.y) / (yRadius * yRadius) <= + 1.0; + + return inEllipse; +}