main
parent
ee6c5bf8a5
commit
366ec62cd7
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1775115523115" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5347" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M458.105263 970.105263h-215.578947V264.084211h215.578947V970.105263z m-161.68421-53.894737h107.789473V317.978947h-107.789473V916.210526z" fill="#ffffff" p-id="5348"></path><path d="M296.421053 970.105263h-215.578948V479.663158h215.578948V970.105263z m-161.684211-53.894737h107.789474V533.557895h-107.789474V916.210526zM943.157895 970.105263h-215.578948V479.663158h215.578948V970.105263z m-161.684211-53.894737h107.789474V533.557895h-107.789474V916.210526zM619.789474 970.105263h-215.578948V53.894737h215.578948v916.210526z m-161.684211-53.894737h107.789474V107.789474h-107.789474v808.421052z" fill="#ffffff" p-id="5349"></path><path d="M781.473684 970.105263h-215.578947V264.084211h215.578947V970.105263z m-161.68421-53.894737h107.789473V317.978947h-107.789473V916.210526z" fill="#ffffff" p-id="5350"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,366 @@
|
||||||
|
<template>
|
||||||
|
<div class="histogram">
|
||||||
|
<div ref="chartContainer" style="width: 490px; height: 290px;" v-loading="loading"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { LineChart } from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
LegendComponent,
|
||||||
|
DatasetComponent,
|
||||||
|
// 内置数据转换器组件 (filter, sort)
|
||||||
|
TransformComponent
|
||||||
|
} from 'echarts/components';
|
||||||
|
// 标签自动布局、全局过渡动画等特性
|
||||||
|
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||||
|
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
echarts.use([
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
DatasetComponent,
|
||||||
|
TransformComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
LegendComponent,
|
||||||
|
LineChart,
|
||||||
|
LabelLayout,
|
||||||
|
UniversalTransition,
|
||||||
|
CanvasRenderer
|
||||||
|
]);
|
||||||
|
import {
|
||||||
|
getRenderingEngine,
|
||||||
|
cache,
|
||||||
|
utilities,
|
||||||
|
Enums,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
export default {
|
||||||
|
name: "histogram",
|
||||||
|
props: {
|
||||||
|
viewportKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'viewport'
|
||||||
|
},
|
||||||
|
activeViewportIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
renderingEngineId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
|
||||||
|
},
|
||||||
|
initChart() {
|
||||||
|
this.chart = echarts.init(this.$refs.chartContainer);
|
||||||
|
var formatTime = echarts.time.format;
|
||||||
|
var _data = this.generateData1();
|
||||||
|
const option = {
|
||||||
|
// Choose axis ticks based on UTC time.
|
||||||
|
useUTC: true,
|
||||||
|
title: {
|
||||||
|
text: 'Intraday Chart with Breaks (Single Day)',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
interval: 1000 * 60 * 30,
|
||||||
|
axisLabel: {
|
||||||
|
showMinLabel: true,
|
||||||
|
showMaxLabel: true,
|
||||||
|
formatter: (value, index, extra) => {
|
||||||
|
if (!extra || !extra.break) {
|
||||||
|
// The third parameter is `useUTC: true`.
|
||||||
|
return formatTime(value, '{HH}:{mm}', true);
|
||||||
|
}
|
||||||
|
// Only render the label on break start, but not on break end.
|
||||||
|
if (extra.break.type === 'start') {
|
||||||
|
return (
|
||||||
|
formatTime(extra.break.start, '{HH}:{mm}', true) +
|
||||||
|
'/' +
|
||||||
|
formatTime(extra.break.end, '{HH}:{mm}', true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
breakLabelLayout: {
|
||||||
|
// Disable auto move of break labels if overlapping,
|
||||||
|
// and use `axisLabel.formatter` to control the label display.
|
||||||
|
moveOverlap: false
|
||||||
|
},
|
||||||
|
breaks: [
|
||||||
|
{
|
||||||
|
start: _data.breakStart,
|
||||||
|
end: _data.breakEnd,
|
||||||
|
gap: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
breakArea: {
|
||||||
|
expandOnClick: false,
|
||||||
|
zigzagAmplitude: 0,
|
||||||
|
zigzagZ: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 'dataMin'
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
xAxisIndex: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'slider',
|
||||||
|
xAxisIndex: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
symbolSize: 0,
|
||||||
|
data: _data.seriesData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
this.chart.setOption(option);
|
||||||
|
},
|
||||||
|
generateData1() {
|
||||||
|
var seriesData = [];
|
||||||
|
var time = new Date('2024-04-09T09:30:00Z');
|
||||||
|
var endTime = new Date('2024-04-09T15:00:00Z').getTime();
|
||||||
|
var breakStart = new Date('2024-04-09T11:30:00Z').getTime();
|
||||||
|
var breakEnd = new Date('2024-04-09T13:00:00Z').getTime();
|
||||||
|
for (var val = 1669; time.getTime() <= endTime;) {
|
||||||
|
if (time.getTime() <= breakStart || time.getTime() >= breakEnd) {
|
||||||
|
val =
|
||||||
|
val +
|
||||||
|
Math.floor((Math.random() - 0.5 * Math.sin(val / 1000)) * 20 * 100) /
|
||||||
|
100;
|
||||||
|
val = +val.toFixed(2);
|
||||||
|
seriesData.push([time.getTime(), val]);
|
||||||
|
}
|
||||||
|
time.setMinutes(time.getMinutes() + 1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
seriesData: seriesData,
|
||||||
|
breakStart: breakStart,
|
||||||
|
breakEnd: breakEnd
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.resize()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dispose() {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
this.chart = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getCurrentSliceValuesFromVoxelManager(renderingEngineId, viewportId) {
|
||||||
|
const renderingEngine = getRenderingEngine(renderingEngineId);
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
|
||||||
|
const volumeId = viewport.getVolumeId();
|
||||||
|
if (!volumeId) {
|
||||||
|
throw new Error('No volumeId found on viewport');
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = cache.getVolume(volumeId);
|
||||||
|
if (!volume) {
|
||||||
|
throw new Error(`Volume not found in cache: ${volumeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!volume.load) {
|
||||||
|
await volume.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
const voxelManager = volume.voxelManager;
|
||||||
|
if (!voxelManager) {
|
||||||
|
throw new Error('voxelManager not found on volume');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [dimX, dimY, dimZ] = volume.dimensions;
|
||||||
|
|
||||||
|
const camera = viewport.getCamera();
|
||||||
|
const focalPoint = camera.focalPoint;
|
||||||
|
const orientation = this.guessOrthogonalOrientation(camera.viewPlaneNormal);
|
||||||
|
|
||||||
|
const ijk = utilities.transformWorldToIndex(volume.imageData, focalPoint);
|
||||||
|
const i = this.clamp(Math.round(ijk[0]), 0, dimX - 1);
|
||||||
|
const j = this.clamp(Math.round(ijk[1]), 0, dimY - 1);
|
||||||
|
const k = this.clamp(Math.round(ijk[2]), 0, dimZ - 1);
|
||||||
|
|
||||||
|
const modality =
|
||||||
|
volume.metadata?.Modality ||
|
||||||
|
volume.metadata?.modality ||
|
||||||
|
'UNKNOWN';
|
||||||
|
|
||||||
|
let width, height, values;
|
||||||
|
|
||||||
|
if (orientation === Enums.OrientationAxis.AXIAL) {
|
||||||
|
width = dimX;
|
||||||
|
height = dimY;
|
||||||
|
values = new Float32Array(width * height);
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
for (let y = 0; y < dimY; y++) {
|
||||||
|
for (let x = 0; x < dimX; x++) {
|
||||||
|
const raw = voxelManager.getAtIJK(x, y, k);
|
||||||
|
values[idx++] = this.convertToPhysicalValue(raw, volume.metadata, modality);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { orientation: 'AXIAL', sliceIndex: k, width, height, values, modality };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === Enums.OrientationAxis.CORONAL) {
|
||||||
|
width = dimX;
|
||||||
|
height = dimZ;
|
||||||
|
values = new Float32Array(width * height);
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
for (let z = 0; z < dimZ; z++) {
|
||||||
|
for (let x = 0; x < dimX; x++) {
|
||||||
|
const raw = voxelManager.getAtIJK(x, j, z);
|
||||||
|
values[idx++] = this.convertToPhysicalValue(raw, volume.metadata, modality);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { orientation: 'CORONAL', sliceIndex: j, width, height, values, modality };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === Enums.OrientationAxis.SAGITTAL) {
|
||||||
|
width = dimY;
|
||||||
|
height = dimZ;
|
||||||
|
values = new Float32Array(width * height);
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
for (let z = 0; z < dimZ; z++) {
|
||||||
|
for (let y = 0; y < dimY; y++) {
|
||||||
|
const raw = voxelManager.getAtIJK(i, y, z);
|
||||||
|
values[idx++] = this.convertToPhysicalValue(raw, volume.metadata, modality);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { orientation: 'SAGITTAL', sliceIndex: i, width, height, values, modality };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unsupported orientation');
|
||||||
|
},
|
||||||
|
|
||||||
|
convertToPhysicalValue(storedValue, metadata, modality) {
|
||||||
|
if (modality === 'CT') {
|
||||||
|
return this.convertCTToHU(storedValue, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modality === 'PT' || modality === 'PET') {
|
||||||
|
return this.convertPETToSUV(storedValue, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return storedValue;
|
||||||
|
},
|
||||||
|
|
||||||
|
convertCTToHU(value, metadata) {
|
||||||
|
const slope = Number(metadata?.RescaleSlope ?? metadata?.rescaleSlope ?? 1);
|
||||||
|
const intercept = Number(
|
||||||
|
metadata?.RescaleIntercept ?? metadata?.rescaleIntercept ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return value * slope + intercept;
|
||||||
|
},
|
||||||
|
|
||||||
|
convertPETToSUV(value, metadata) {
|
||||||
|
// 先尝试判断当前值是否已经是 SUV
|
||||||
|
const units = metadata?.Units || metadata?.units || '';
|
||||||
|
const correctedImage = metadata?.CorrectedImage || metadata?.correctedImage;
|
||||||
|
// 如果数据已明确是 SUV,可以直接返回
|
||||||
|
if (
|
||||||
|
typeof units === 'string' &&
|
||||||
|
units.toUpperCase().includes('SUV')
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有些数据会先经过Rescale
|
||||||
|
const slope = Number(metadata?.RescaleSlope ?? metadata?.rescaleSlope ?? 1);
|
||||||
|
const intercept = Number(
|
||||||
|
metadata?.RescaleIntercept ?? metadata?.rescaleIntercept ?? 0
|
||||||
|
);
|
||||||
|
const activityConcentration = value * slope + intercept;
|
||||||
|
|
||||||
|
// 如果 metadata 中已有 suvFactor,优先使用
|
||||||
|
const suvFactor = metadata?.suvFactor ?? metadata?.SUVFactor;
|
||||||
|
if (suvFactor != null) {
|
||||||
|
return activityConcentration * Number(suvFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有足够信息计算 SUV,就退化返回 rescale 后值
|
||||||
|
// 这可能是 Bq/ml,而不是真正SUV
|
||||||
|
return activityConcentration;
|
||||||
|
},
|
||||||
|
|
||||||
|
getValueType(modality, metadata) {
|
||||||
|
if (modality === 'CT') {
|
||||||
|
return 'HU';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modality === 'PT' || modality === 'PET') {
|
||||||
|
const units = metadata?.Units || metadata?.units || '';
|
||||||
|
if (typeof units === 'string' && units.toUpperCase().includes('SUV')) {
|
||||||
|
return 'SUV';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata?.suvFactor != null || metadata?.SUVFactor != null) {
|
||||||
|
return 'SUV';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'PET';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'RAW';
|
||||||
|
},
|
||||||
|
|
||||||
|
clamp(v, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, v));
|
||||||
|
},
|
||||||
|
|
||||||
|
guessOrthogonalOrientation(viewPlaneNormal) {
|
||||||
|
const [nx, ny, nz] = viewPlaneNormal.map(Math.abs);
|
||||||
|
|
||||||
|
if (nz >= nx && nz >= ny) {
|
||||||
|
return Enums.OrientationAxis.AXIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ny >= nx && ny >= nz) {
|
||||||
|
return Enums.OrientationAxis.CORONAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Enums.OrientationAxis.SAGITTAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
Loading…
Reference in New Issue