Compare commits
No commits in common. "2bc4b32d3a3a05f3bea07101d1edfa68f4b062d8" and "866e2e5c6ba407d61d7268794265665108158a51" have entirely different histories.
2bc4b32d3a
...
866e2e5c6b
|
|
@ -1 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,366 +0,0 @@
|
||||||
<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