Compare commits

...

4 Commits

Author SHA1 Message Date
wangxiaoshuang 946203f124 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing Details
# Conflicts:
#	src/components/Dicom/DicomViewer.vue
#	src/views/dicom-show/dicom-study.vue
2026-04-23 15:53:04 +08:00
wangxiaoshuang f0eb814492 质控数据匿名 2026-04-23 15:50:15 +08:00
wangxiaoshuang 47b05526a2 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web 2026-04-22 15:06:47 +08:00
wangxiaoshuang f7afca6e73 注释(未完成) 2026-04-22 15:06:39 +08:00
5 changed files with 673 additions and 345 deletions

View File

@ -408,3 +408,19 @@ export function changeSegmentationSavedStatus(data) {
data
})
}
// 图像数据匿名
export function studyMaskImage(data) {
return request({
url: `/Study/studyMaskImage`,
method: 'post',
data
})
}
// 撤销匿名
export function studyUndoMaskImage(data) {
return request({
url: `/Study/studyUndoMaskImage`,
method: 'post',
data
})
}

View File

@ -15,7 +15,7 @@
<div v-show="dicomInfo.thick">Slice Thickness {{ dicomInfo.thick }}mm</div>
<div>WW/WC {{ dicomInfo.wwwc }}</div>
<div>Zoom {{ dicomInfo.zoom }}</div>
<div v-show="dicomInfo.location">Location {{ dicomInfo.location }}mm</div> -->
<div v-show="dicomInfo.location">Location {{ dicomInfo.location }}mm</div>-->
<!-- <div v-show="toolState.clipPlaying">FPS {{ dicomInfo.fps }}</div> -->
<div v-show="mousePosition.mo">
Pos: {{ mousePosition.x ? mousePosition.x.toFixed(0) : '' }}, {{ mousePosition.y ? mousePosition.y.toFixed(0) :
@ -23,17 +23,11 @@
</div>
<div
v-if="(dicomInfo.modality === 'CT' || dicomInfo.modality === 'DR' || dicomInfo.modality === 'CR') && mousePosition.mo">
HU: {{ mousePosition.mo }}
</div>
<div v-else-if="(dicomInfo.modality === 'PT' && mousePosition.suv)">
SUVbw(g/ml): {{ mousePosition.suv.toFixed(3) }}
</div>
<div v-else-if="mousePosition.mo">
Density: {{ mousePosition.mo }}
</div>
<div>
W*H: {{ dicomInfo.size }}
</div>
HU: {{ mousePosition.mo }}</div>
<div v-else-if="(dicomInfo.modality === 'PT' && mousePosition.suv)">SUVbw(g/ml): {{ mousePosition.suv.toFixed(3)
}}</div>
<div v-else-if="mousePosition.mo">Density: {{ mousePosition.mo }}</div>
<div>W*H: {{ dicomInfo.size }}</div>
<div>Zoom: {{ dicomInfo.zoom }}</div>
</div>
@ -56,24 +50,16 @@
style="z-index:10;background: #9e9e9e;height: 20px;width: 100%;position: absolute;top: 0;cursor: move"
@mousedown="sliderMousedown($event)" />
</div>
<div style="position: absolute;left: 50%;top: 15px;color: #f44336;">
{{ markers.top }}
</div>
<div style="position: absolute;top: 50%;right: 15px;color: #f44336;">
{{ markers.right }}
</div>
<div style="position: absolute;left: 50%;top: 15px;color: #f44336;">{{ markers.top }}</div>
<div style="position: absolute;top: 50%;right: 15px;color: #f44336;">{{ markers.right }}</div>
<div style="position: absolute;left: 50%;bottom: 15px;color: #f44336;">
{{ markers.bottom }}
</div>
<div style="position: absolute;top: 50%;left: 15px;color: #f44336;">
{{ markers.left }}
</div>
<div style="position: absolute;left: 50%;bottom: 15px;color: #f44336;">{{ markers.bottom }}</div>
<div style="position: absolute;top: 50%;left: 15px;color: #f44336;">{{ markers.left }}</div>
<div class="info-instance">
<!-- <div v-show="dicomInfo.pixel">
Pixel: {{ dicomInfo.pixel }}mm
</div> -->
</div>-->
<div v-show="dicomInfo.location">Location: {{ dicomInfo.location }}</div>
<div v-show="dicomInfo.thick">Slice Thickness: {{ dicomInfo.thick }}mm</div>
<div v-show="dicomInfo.wwwc">WW/WL: {{ dicomInfo.wwwc }}</div>
@ -101,6 +87,7 @@ import invertOrientationString from '@/views/trials/trials-panel/reading/dicoms/
import calculateSUV from '@/views/trials/trials-panel/reading/dicoms/tools/calculateSUV'
// import requestPoolManager from '@/utils/request-pool'
import ScaleOverlayTool from '@/views/trials/trials-panel/reading/dicoms/tools/ScaleOverlay/ScaleOverlayTool'
import Note_RectangleRoiTool from '@/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool'
cornerstoneTools.external.cornerstone = cornerstone
cornerstoneTools.external.Hammer = Hammer
cornerstoneTools.external.cornerstoneMath = cornerstoneMath
@ -117,7 +104,7 @@ export default {
computed: {
NSTip() {
return `${this.$store.state.trials.downloadSize}, NS: ${this.$store.state.trials.downloadTip}`
}
},
},
data() {
return {
@ -130,7 +117,7 @@ export default {
seriesNumber: '',
imageIds: [],
currentImageIdIndex: 0,
firstImageLoading: false
firstImageLoading: false,
// preventCache: true
},
dicomInfo: {
@ -150,14 +137,14 @@ export default {
wwwc: '',
zoom: 0,
location: '',
fps: 5
fps: 5,
},
toolState: {
initialized: false,
activeTool: 'none',
dicomInfoVisible: false,
clipPlaying: false,
viewportInvert: false
viewportInvert: false,
},
loadImagePromise: null,
AnnotationSync: null,
@ -168,18 +155,20 @@ export default {
sliderInfo: {
oldB: null,
oldM: null,
isMove: false
isMove: false,
},
mousePosition: { x: '', y: '', mo: '' },
markers: { top: '', right: '', bottom: '', left: '' },
orientationMarkers: [],
originalMarkers: [],
dcmTag: { visible: false, title: this.$t('trials:dicom-tag:title') }
dcmTag: { visible: false, title: this.$t('trials:dicom-tag:title') },
}
},
mounted() {
this.type = this.$router.currentRoute.query.type ? this.$router.currentRoute.query.type : ''
this.type = this.$router.currentRoute.query.type
? this.$router.currentRoute.query.type
: ''
this.canvas = this.$refs.canvas
this.canvas.addEventListener('cornerstonenewimage', this.onNewImage)
this.canvas.addEventListener(
@ -200,7 +189,10 @@ export default {
document.addEventListener('mousemove', (e) => {
this.sliderMousemove(e)
})
this.canvas.addEventListener('cornerstonetoolsstackscroll', this.stackScrollCallback)
this.canvas.addEventListener(
'cornerstonetoolsstackscroll',
this.stackScrollCallback
)
},
methods: {
@ -210,7 +202,11 @@ export default {
this.stack.seriesId = dicomSeries.seriesId
this.stack.seriesNumber = dicomSeries.seriesNumber
this.stack.imageIds = dicomSeries.imageIds
this.stack.currentImageIdIndex = dicomSeries.imageIdIndex && dicomSeries.imageIdIndex < dicomSeries.imageIds.length ? dicomSeries.imageIdIndex : 0
this.stack.currentImageIdIndex =
dicomSeries.imageIdIndex &&
dicomSeries.imageIdIndex < dicomSeries.imageIds.length
? dicomSeries.imageIdIndex
: 0
this.stack.firstImageLoading = true
this.stack.description = dicomSeries.description
this.toolState.viewportInvert = false
@ -226,13 +222,17 @@ export default {
this.toolState.clipPlaying = false
this.loading = true
cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex])
.then(image => {
cornerstone
.loadAndCacheImage(
this.stack.imageIds[this.stack.currentImageIdIndex]
)
.then((image) => {
this.loading = false
if (this.stack.imageIds.indexOf(image.imageId) !== -1) {
this.onFirstImageLoaded(image)
}
}).catch((error) => {
})
.catch((error) => {
this.loading = false
if (error.error && error.error.message) {
this.$alert(error.error.message)
@ -255,15 +255,17 @@ export default {
const apiTool = cornerstoneTools[`${toolName}Tool`]
if (apiTool) {
const toolAlreadyAddedToElement = cornerstoneTools.getToolForElement(
element,
apiTool
)
const toolAlreadyAddedToElement =
cornerstoneTools.getToolForElement(element, apiTool)
if (!toolAlreadyAddedToElement) {
if (toolName === 'RectangleRoi') {
cornerstoneTools.addToolForElement(element, apiTool, { configuration: { showMinMax: true, showStatsText: true } })
cornerstoneTools.addToolForElement(element, apiTool, {
configuration: { showMinMax: true, showStatsText: true },
})
} else if (toolName === 'EllipticalRoi') {
cornerstoneTools.addToolForElement(element, apiTool, { configuration: { showMinMax: true } })
cornerstoneTools.addToolForElement(element, apiTool, {
configuration: { showMinMax: true },
})
} else {
cornerstoneTools.addToolForElement(element, apiTool)
}
@ -287,8 +289,28 @@ export default {
false
)
})
if (!cornerstoneTools.getToolForElement(element, cornerstoneTools.WwwcRegionTool)) {
cornerstoneTools.addToolForElement(element, cornerstoneTools.WwwcRegionTool)
if (
!cornerstoneTools.getToolForElement(element, Note_RectangleRoiTool)
) {
cornerstoneTools.addToolForElement(element, Note_RectangleRoiTool, {
configuration: {
color: '#f00',
lineWidth: 0.5,
drawHandles: false,
fillColor: 'rgba(0, 0, 0, 1)',
},
})
}
if (
!cornerstoneTools.getToolForElement(
element,
cornerstoneTools.WwwcRegionTool
)
) {
cornerstoneTools.addToolForElement(
element,
cornerstoneTools.WwwcRegionTool
)
}
if (
!cornerstoneTools.getToolForElement(
@ -347,7 +369,9 @@ export default {
// var instanceId = image.imageId.split('/')[image.imageId.split('/').length - 1]
// instanceId = instanceId.split('.')[0]
// this.stack.instanceId = instanceId
this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
this.height =
(this.stack.currentImageIdIndex * 100) /
(this.stack.imageIds.length - 1)
this.resetWwwc()
},
onNewImage(e) {
@ -379,21 +403,30 @@ export default {
if (this.dicomInfo.thick) {
this.dicomInfo.thick = this.dicomInfo.thick.toFixed(2)
}
const newImageIdIndex = this.stack.imageIds.findIndex(i => i === e.detail.image.imageId)
const newImageIdIndex = this.stack.imageIds.findIndex(
(i) => i === e.detail.image.imageId
)
if (newImageIdIndex === -1) return
this.stack.currentImageIdIndex = newImageIdIndex
this.stack.imageIdIndex = newImageIdIndex
this.series.imageIdIndex = newImageIdIndex
this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
this.height =
(this.stack.currentImageIdIndex * 100) /
(this.stack.imageIds.length - 1)
this.resetWwwc()
},
stackScrollCallback(e) {
const { detail } = e
if (this.isScrollSync) {
this.$emit('scrollSync', { canvasIndex: this.canvasIndex, direction: detail.direction })
this.$emit('scrollSync', {
canvasIndex: this.canvasIndex,
direction: detail.direction,
})
}
this.stack.currentImageIdIndex = e.detail.newImageIdIndex
this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
this.height =
(this.stack.currentImageIdIndex * 100) /
(this.stack.imageIds.length - 1)
// var priority = new Date(new Date().setHours(23, 59, 59, 999)).getTime()
// requestPoolManager.loadAndCacheImagePlus(this.stack.imageIds[this.stack.currentImageIdIndex], this.stack.seriesId, priority)
@ -408,7 +441,9 @@ export default {
if (date) {
date = `${date.substr(0, 4)}-${date.substr(4, 2)}-${date.substr(6, 2)}`
}
if (time) { time = `${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}` }
if (time) {
time = `${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}`
}
return time ? `${date} ${time}` : `${date} 00:00:00`
},
@ -453,7 +488,7 @@ export default {
top: oppositeColumn,
bottom: column,
left: oppositeRow,
right: row
right: row,
}
if (!markers) {
return
@ -500,13 +535,7 @@ export default {
if (image.color) {
stats.storedPixels = this.getRGBPixels(element, x, y, 1, 1)
} else {
stats.storedPixels = cornerstone.getStoredPixels(
element,
x,
y,
1,
1
)
stats.storedPixels = 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)
@ -533,7 +562,8 @@ export default {
if (enabledElement.image.color) {
for (row = 0; row < height; row++) {
for (column = 0; column < width; column++) {
spIndex = ((row + y) * enabledElement.image.columns + (column + x)) * 4
spIndex =
((row + y) * enabledElement.image.columns + (column + x)) * 4
const red = pixelData[spIndex]
const green = pixelData[spIndex + 1]
const blue = pixelData[spIndex + 2]
@ -551,7 +581,8 @@ export default {
},
sliderMousedown(e) {
var boxHeight = this.$refs['sliderBox'].clientHeight
this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100
this.sliderInfo.oldB =
(parseInt(e.srcElement.style.top) * boxHeight) / 100
this.sliderInfo.oldM = e.clientY
this.sliderInfo.isMove = true
e.stopImmediatePropagation()
@ -564,9 +595,14 @@ export default {
var boxHeight = this.$refs['sliderBox'].clientHeight
if (PX < 0) return
if (PX > boxHeight) return
var height = PX * 100 / boxHeight
var index = Math.trunc(this.stack.imageIds.length * this.height / 100)
index = index > this.stack.imageIds.length ? this.stack.imageIds.length : index < 0 ? 0 : index
var height = (PX * 100) / boxHeight
var index = Math.trunc((this.stack.imageIds.length * this.height) / 100)
index =
index > this.stack.imageIds.length
? this.stack.imageIds.length
: index < 0
? 0
: index
// if (!cornerstone.imageCache.getImageLoadObject(this.stack.imageIds[index])) return
this.height = height
if (this.stack.currentImageIdIndex !== index) {
@ -578,9 +614,9 @@ export default {
},
goViewer(e) {
// console.log(this.$refs['sliderBox'].clientHeight)
var height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
var height = (e.offsetY * 100) / this.$refs['sliderBox'].clientHeight
this.height = height
var index = Math.trunc(this.stack.imageIds.length * this.height / 100)
var index = Math.trunc((this.stack.imageIds.length * this.height) / 100)
scroll(this.canvas, index)
},
onClipStopped() {
@ -673,10 +709,7 @@ export default {
}
this.toolState.clipPlaying = true
cornerstoneTools.playClip(this.canvas, this.dicomInfo.fps)
cornerstoneTools.getToolState(
this.canvas,
'playClip'
).data[0].loop = true
cornerstoneTools.getToolState(this.canvas, 'playClip').data[0].loop = true
},
setFps(fps) {
this.dicomInfo.fps = fps
@ -712,8 +745,8 @@ export default {
invert: false,
preventZoomOutsideImage: false,
minScale: 0.1,
maxScale: 20.0
}
maxScale: 20.0,
},
})
cornerstoneTools.setToolActive('Zoom', { mouseButtonMask: 1 })
this.toolState.activeTool = 'zoom'
@ -797,9 +830,110 @@ export default {
setToolPassive(toolName) {
cornerstoneTools.setToolPassiveForElement(this.canvas, toolName)
},
async reloadImage(newImageId = null) {
// 1. imageIdimageId
let element = this.canvas
this.stack.imageIds.splice(this.stack.currentImageIdIndex, 1, newImageId)
const currentImageId =
newImageId || cornerstone.getImage(element)?.imageId
if (!currentImageId) {
console.error('没有找到可用的imageId')
return
}
// 2.
this.clearToolStateForImage(element, currentImageId)
// 3.
this.removeImageFromCache(currentImageId)
// 4.
await this.loadAndRenderImage(element, currentImageId)
},
clearToolStateForImage(element, imageId) {
//
const globalToolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager
if (globalToolStateManager) {
// 1
globalToolStateManager.clearImageIdToolState(imageId)
console.log(`已清除影像 ${imageId} 的所有标注数据`)
}
// 2使
const elementToolStateManager =
cornerstoneTools.getElementToolStateManager(element)
if (elementToolStateManager && elementToolStateManager.get(element)) {
//
elementToolStateManager.clear(element)
}
},
removeImageFromCache(imageId) {
const imageCache = cornerstone.imageCache
if (imageCache && imageCache[imageId]) {
//
delete imageCache[imageId]
console.log(`已从缓存中移除影像: ${imageId}`)
}
// 使API
if (typeof cornerstone.imageCache.removeImage === 'function') {
cornerstone.imageCache.removeImageLoadObject(imageId)
}
},
async loadAndRenderImage(element, imageId) {
try {
// 1.
const canvas = element.querySelector('canvas')
if (canvas) {
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
// 2.
const image = await cornerstone.loadAndCacheImage(imageId)
// 3.
let viewport = cornerstone.getViewport(element)
if (!viewport) {
viewport = cornerstone.getDefaultViewport(element, image)
}
// 4.
cornerstone.displayImage(element, image, viewport)
// 5.
cornerstone.updateImage(element)
console.log(`影像 ${imageId} 重新加载并渲染完成`)
return image
} catch (error) {
console.error('加载图像失败:', error)
throw error
}
},
getNote_RectangleRoi() {
return new Promise(resolve => {
let toolInfo = cornerstoneTools.getToolState(this.canvas, 'Note_RectangleRoi')
let image = cornerstone.getImage(this.canvas)
resolve({ toolInfo, image })
})
// console.log(
// cornerstoneTools.getToolState(this.canvas, 'Note_RectangleRoi')
// )
// console.log(cornerstone.getImage(this.canvas))
// let image = cornerstone.getImage(this.canvas)
// // cornerstone.imageCache.removeImageLoadObject(image.imageId)
// this.reloadImage(this.canvas, image.imageId)
},
setToolActive(toolName) {
cornerstoneTools.setToolActiveForElement(this.canvas, toolName, {
mouseButtonMask: 1
mouseButtonMask: 1,
})
},
setAllToolsPassive() {
@ -833,7 +967,7 @@ export default {
)
}
cornerstoneTools.setToolEnabledForElement(this.canvas, 'ReferenceLines', {
synchronizationContext: synchronizer
synchronizationContext: synchronizer,
})
// cornerstoneTools.addTool(cornerstoneTools.CrosshairsTool)
@ -864,7 +998,7 @@ export default {
}
cornerstoneTools.setToolActiveForElement(this.canvas, toolName, {
mouseButtonMask: 1,
synchronizationContext: synchronizer
synchronizationContext: synchronizer,
})
},
disabledViewPortToolSync(synchronizer, toolName) {
@ -1095,7 +1229,7 @@ export default {
'CobbAngle',
'Angle',
'Bidirectional',
'FreehandRoi'
'FreehandRoi',
]
for (let i = 0; i < toolROITypes.length; i++) {
const toolROIType = toolROITypes[i]
@ -1115,12 +1249,12 @@ export default {
removeLabel(item) {
const promise = scroll(this.canvas, item.data.imageIdIndex)
const scope = this
Promise.all([promise]).then(res => {
Promise.all([promise]).then((res) => {
cornerstoneTools.removeToolState(scope.canvas, item.type, item.data)
cornerstone.updateImage(scope.canvas)
})
}
}
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@ -123,7 +123,8 @@
</div>
</div>
<div class="viewerContent">
<dicom-viewer id="dicomViewer" ref="dicomViewer" style="height:100%" :modality="modality"/>
<dicom-viewer id="dicomViewer" ref="dicomViewer" style="height:100%" :loading.sync="loading"
:modality="modality" />
</div>
<!-- <div class="viewerRightSidePanel">
<dicom-tools />

View File

@ -0,0 +1,97 @@
// Note_RectangleRoiTool.js
import * as cornerstoneTools from 'cornerstone-tools';
export default class Note_RectangleRoiTool extends cornerstoneTools.RectangleRoiTool {
constructor(props = {}) {
const defaultProps = {
name: 'Note_RectangleRoi',
configuration: {
fillColor: 'rgba(255, 255, 0, 0.3)', // 默认填充颜色:半透明黄色
strokeColor: 'yellow', // 边框颜色
lineWidth: 2, // 边框宽度
drawHandles: true, // 是否绘制控制点
handleColor: 'white', // 控制点颜色
}
};
super({ ...defaultProps, ...props });
}
renderToolData(evt) {
const eventData = evt.detail;
const { element } = eventData;
// 获取工具状态
const toolData = cornerstoneTools.getToolState(element, this.name);
if (!toolData || !toolData.data || !toolData.data.length) {
return;
}
const canvas = eventData.canvasContext.canvas;
const context = canvas.getContext('2d');
context.save();
// 获取配置
const fillColor = this.configuration.fillColor || 'rgba(255, 255, 0, 0.3)';
const strokeColor = this.configuration.strokeColor || 'yellow';
const lineWidth = this.configuration.lineWidth || 2;
const drawHandles = this.configuration.drawHandles !== false;
// 遍历所有矩形标注
toolData.data.forEach((measurement) => {
if (!measurement.handles?.start || !measurement.handles?.end) {
return;
}
const start = measurement.handles.start;
const end = measurement.handles.end;
// 计算矩形坐标和尺寸
const x = Math.min(start.x, end.x);
const y = Math.min(start.y, end.y);
const width = Math.abs(end.x - start.x);
const height = Math.abs(end.y - start.y);
// 1. 绘制填充
if (fillColor) {
context.fillStyle = fillColor;
context.fillRect(x, y, width, height);
}
// 2. 绘制边框
context.strokeStyle = strokeColor;
context.lineWidth = lineWidth;
context.strokeRect(x, y, width, height);
// 3. 绘制控制点(可选)
if (drawHandles) {
this.drawHandles(context, measurement, eventData);
}
});
context.restore();
}
/**
* 绘制控制点
*/
drawHandles(context, measurement, eventData) {
const handles = measurement.handles;
const handleColor = this.configuration.handleColor || 'white';
// 绘制所有控制点
Object.keys(handles).forEach(key => {
const handle = handles[key];
if (handle && typeof handle.x === 'number' && typeof handle.y === 'number') {
context.beginPath();
context.arc(handle.x, handle.y, 5, 0, 2 * Math.PI);
context.fillStyle = handleColor;
context.fill();
context.strokeStyle = 'black';
context.lineWidth = 1;
context.stroke();
}
});
}
}