当前的DICOM图像查看器基于Cornerstone.js构建,支持单帧图像显示和各种图像处理工具。现需要添加多帧播放功能,允许用户播放DICOM系列中的多张切片图像(如CT、MRI的多个层面),提供类似医学影像工作站的专业体验。
graph TB
A[ViewerContainer] --> B[StackViewer]
B --> C[PlaybackController]
B --> D[FloatingPlaybackControls]
B --> I[DicomMetadataAnalyzer]
C --> E[PlaybackState Redux]
C --> F[FrameManager]
F --> G[Cornerstone StackViewport]
D --> H[UI控制组件]
I --> J[分析结果缓存]
subgraph "DICOM元数据分析层"
I --> I1[多帧检测]
I --> I2[元数据解析]
I --> I3[能力评估]
I --> I4[错误处理]
I --> I5[结果缓存管理]
end
subgraph "播放状态管理"
E --> E1[isPlaying]
E --> E2[currentFrame]
E --> E3[totalFrames]
E --> E4[playbackSpeed]
E --> E5[isLoop]
end
subgraph "UI控制组件"
H --> H1[播放/暂停按钮]
H --> H2[上一帧/下一帧]
H --> H3[帧数显示]
H --> H4[播放速度控制]
H --> H5[循环模式开关]
end
classDef analyzer fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef analyzerFunction fill:#f3e5f5,stroke:#4a148c,stroke-width:1px
class I analyzer
class I1,I2,I3,I4,I5 analyzerFunction
// src/states/view/playbackSlice.ts
interface PlaybackState {
[viewportId: string]: {
// 播放控制状态
isPlaying: boolean; // 是否正在播放
currentFrame: number; // 当前帧索引
totalFrames: number; // 总帧数
playbackSpeed: number; // 播放速度(帧/秒)
isLoop: boolean; // 是否循环播放
playIntervalId?: number; // 播放定时器ID
// 多帧检测状态
isMultiFrame: boolean; // 是否为多帧DICOM
frameDetectionStatus: 'idle' | 'detecting' | 'completed' | 'failed';
detectionError?: string; // 检测错误信息
}
}
// 多帧检测相关的Actions
interface PlaybackActions {
// 原有播放控制Actions
initPlayback: (payload: { viewportId: string; totalFrames: number }) => void;
togglePlayback: (viewportId: string) => void;
setCurrentFrame: (payload: { viewportId: string; frame: number }) => void;
// 新增:多帧检测Actions
startFrameDetection: (viewportId: string) => void;
setFrameDetectionResult: (payload: {
viewportId: string;
isMultiFrame: boolean;
totalFrames: number
}) => void;
setFrameDetectionError: (payload: { viewportId: string; error: string }) => void;
}
建议命名: DicomMetadataAnalyzer - DICOM元数据分析器
DicomMetadataAnalyzer是系统中负责DICOM元数据深度分析的独立模块,其职责明确且边界清晰:
多帧检测与分析
DICOM元数据深度解析
图像能力评估
缓存与性能管理
错误处理与容错
不负责图像渲染
不负责播放控制
不负责UI状态管理
不负责网络通信
graph TB
subgraph "DicomMetadataAnalyzer 职责边界"
DMA[DicomMetadataAnalyzer]
DMA --> A1[元数据解析]
DMA --> A2[多帧检测]
DMA --> A3[能力评估]
DMA --> A4[结果缓存]
DMA --> A5[错误处理]
end
subgraph "协作组件"
SV[StackViewer] --> DMA
PC[PlaybackController] --> DMA
RS[Redux Store]
CS[Cornerstone Viewport]
end
DMA --> |分析结果| SV
DMA --> |帧数信息| PC
SV --> |初始化播放| RS
PC --> |控制播放| CS
style DMA fill:#e1f5fe
style A1 fill:#f3e5f5
style A2 fill:#f3e5f5
style A3 fill:#f3e5f5
style A4 fill:#f3e5f5
style A5 fill:#f3e5f5
// src/utils/dicom/DicomMetadataAnalyzer.ts
export interface DicomAnalysisResult {
// 多帧相关
frameCount: number;
isMultiFrame: boolean;
frameType?: string; // 'static' | 'dynamic' | 'temporal'
// 序列信息
seriesDescription?: string;
studyDescription?: string;
modality?: string;
sopClassUID?: string;
// 图像特征
imageType?: string[];
photometricInterpretation?: string;
bitsAllocated?: number;
// 时间信息
acquisitionTime?: string;
temporalResolution?: number;
// 几何信息
pixelSpacing?: [number, number];
sliceThickness?: number;
imageOrientation?: number[];
// 增强DICOM特征
isEnhanced: boolean;
functionalGroupsInfo?: any;
// 分析元信息
analysisTimestamp: Date;
analysisVersion: string;
confidence: number; // 0-1, 分析结果的可信度
}
export class DicomMetadataAnalyzer {
private static cache = new Map<string, DicomAnalysisResult>();
private static readonly CACHE_TTL = 5 * 60 * 1000; // 5分钟缓存
/**
* 综合分析DICOM元数据
* @param imageId DICOM图像ID
* @param viewport Cornerstone viewport实例
* @returns 完整的分析结果
*/
static async analyze(imageId: string, viewport?: StackViewport): Promise<DicomAnalysisResult> {
// 检查缓存
const cached = this.getCachedResult(imageId);
if (cached) return cached;
try {
const result: DicomAnalysisResult = {
frameCount: 1,
isMultiFrame: false,
isEnhanced: false,
analysisTimestamp: new Date(),
analysisVersion: '1.0.0',
confidence: 0.9
};
// 获取基础元数据
const metadata = await this.getMetadata(imageId);
// 分析各个维度
await Promise.all([
this.analyzeFrameInfo(metadata, viewport, result),
this.analyzeSeriesInfo(metadata, result),
this.analyzeImageCharacteristics(metadata, result),
this.analyzeTemporalInfo(metadata, result),
this.analyzeGeometry(metadata, result),
this.analyzeEnhancedFeatures(metadata, result)
]);
// 缓存结果
this.cacheResult(imageId, result);
return result;
} catch (error) {
console.error('DICOM metadata analysis failed:', error);
throw new Error(`Failed to analyze DICOM metadata: ${error.message}`);
}
}
/**
* 专门用于多帧检测的快速方法
* 保持向后兼容性
*/
static async detectFrameCount(imageId: string, viewport?: StackViewport): Promise<number> {
const result = await this.analyze(imageId, viewport);
return result.frameCount;
}
/**
* 检测DICOM图像类型和能力
*/
static async detectImageCapabilities(imageId: string): Promise<{
supportsMultiFrame: boolean;
supportsDynamicPlayback: boolean;
supportsTemporalAnalysis: boolean;
recommendedViewerMode: 'single' | 'multi' | 'dynamic' | 'temporal';
}> {
const analysis = await this.analyze(imageId);
return {
supportsMultiFrame: analysis.isMultiFrame,
supportsDynamicPlayback: analysis.isMultiFrame && analysis.frameType !== 'static',
supportsTemporalAnalysis: analysis.temporalResolution != null,
recommendedViewerMode: this.determineViewerMode(analysis)
};
}
// 私有辅助方法
private static async getMetadata(imageId: string) {
const metadata = cornerstoneDICOMImageLoader.wadors.metaDataManager.get('instance', imageId);
const multiFrameModule = cornerstoneDICOMImageLoader.wadors.metaDataManager.get('multiFrameModule', imageId);
const imageModule = cornerstoneDICOMImageLoader.wadors.metaDataManager.get('imageModule', imageId);
return { metadata, multiFrameModule, imageModule };
}
private static async analyzeFrameInfo(metadataSet: any, viewport: StackViewport | undefined, result: DicomAnalysisResult) {
const { metadata, multiFrameModule } = metadataSet;
// 多种帧数检测方法
let frameCount = 1;
// 方法1:标准NumberOfFrames标签
if (metadata?.NumberOfFrames > 1) {
frameCount = metadata.NumberOfFrames;
}
// 方法2:MultiFrame模块
if (multiFrameModule?.NumberOfFrames > 1) {
frameCount = Math.max(frameCount, multiFrameModule.NumberOfFrames);
}
// 方法3:图像数据维度(需要viewport)
if (viewport) {
try {
const imageData = viewport.getImageData();
if (imageData?.dimensions?.[2] > 1) {
frameCount = Math.max(frameCount, imageData.dimensions[2]);
}
} catch (error) {
console.warn('Failed to get image dimensions:', error);
}
}
result.frameCount = frameCount;
result.isMultiFrame = frameCount > 1;
// 判断帧类型
if (frameCount > 1) {
result.frameType = this.determineFrameType(metadata);
}
}
private static async analyzeSeriesInfo(metadataSet: any, result: DicomAnalysisResult) {
const { metadata } = metadataSet;
result.seriesDescription = metadata?.SeriesDescription;
result.studyDescription = metadata?.StudyDescription;
result.modality = metadata?.Modality;
result.sopClassUID = metadata?.SOPClassUID;
}
private static async analyzeImageCharacteristics(metadataSet: any, result: DicomAnalysisResult) {
const { metadata, imageModule } = metadataSet;
result.imageType = metadata?.ImageType;
result.photometricInterpretation = imageModule?.PhotometricInterpretation;
result.bitsAllocated = imageModule?.BitsAllocated;
}
private static async analyzeTemporalInfo(metadataSet: any, result: DicomAnalysisResult) {
const { metadata } = metadataSet;
result.acquisitionTime = metadata?.AcquisitionTime;
// 其他时间相关分析...
}
private static async analyzeGeometry(metadataSet: any, result: DicomAnalysisResult) {
const { metadata, imageModule } = metadataSet;
result.pixelSpacing = imageModule?.PixelSpacing;
result.sliceThickness = metadata?.SliceThickness;
result.imageOrientation = imageModule?.ImageOrientationPatient;
}
private static async analyzeEnhancedFeatures(metadataSet: any, result: DicomAnalysisResult) {
const { metadata } = metadataSet;
// 检测Enhanced DICOM
const enhancedSOPClasses = [
'1.2.840.10008.5.1.4.1.1.4.1', // Enhanced MR Image Storage
'1.2.840.10008.5.1.4.1.1.2.1', // Enhanced CT Image Storage
'1.2.840.10008.5.1.4.1.1.6.2', // Enhanced US Volume Storage
];
result.isEnhanced = enhancedSOPClasses.includes(metadata?.SOPClassUID);
if (result.isEnhanced) {
// Enhanced DICOM特殊处理
result.functionalGroupsInfo = metadata?.SharedFunctionalGroupsSequence;
}
}
// 工具方法
private static determineFrameType(metadata: any): string {
// 根据DICOM标签判断帧类型
const imageType = metadata?.ImageType || [];
if (imageType.includes('DYNAMIC')) return 'dynamic';
if (imageType.includes('TEMPORAL')) return 'temporal';
return 'static';
}
private static determineViewerMode(analysis: DicomAnalysisResult): 'single' | 'multi' | 'dynamic' | 'temporal' {
if (!analysis.isMultiFrame) return 'single';
if (analysis.frameType === 'dynamic') return 'dynamic';
if (analysis.frameType === 'temporal') return 'temporal';
return 'multi';
}
// 缓存相关方法
private static getCachedResult(imageId: string): DicomAnalysisResult | null {
const cached = this.cache.get(imageId);
if (cached && Date.now() - cached.analysisTimestamp.getTime() < this.CACHE_TTL) {
return cached;
}
this.cache.delete(imageId);
return null;
}
private static cacheResult(imageId: string, result: DicomAnalysisResult) {
this.cache.set(imageId, result);
}
/**
* 清除缓存
*/
static clearCache() {
this.cache.clear();
}
}
initPlayback(viewportId, totalFrames) - 初始化播放状态togglePlayback(viewportId) - 切换播放/暂停setCurrentFrame(viewportId, frame) - 设置当前帧setPlaybackSpeed(viewportId, speed) - 设置播放速度toggleLoop(viewportId) - 切换循环模式nextFrame(viewportId) - 下一帧previousFrame(viewportId) - 上一帧详细职责参考: 请查看第4.1.1节《DICOM元数据分析器职责定义》,其中详细定义了分析器的核心职责、边界和协作关系。
核心特点:
核心API:
class DicomMetadataAnalyzer {
// 主要分析接口
static async analyze(imageId: string, viewport?: StackViewport): Promise<DicomAnalysisResult>
// 兼容性接口
static async detectFrameCount(imageId: string, viewport?: StackViewport): Promise<number>
// 能力评估接口
static async detectImageCapabilities(imageId: string): Promise<ImageCapabilities>
// 缓存管理
static clearCache(): void
}
职责:
核心方法:
class PlaybackController {
startPlayback(): void
stopPlayback(): void
nextFrame(): void
previousFrame(): void
setFrame(frameIndex: number): void
setPlaybackSpeed(speed: number): void
}
职责:
UI布局:
[⏯️] [⏮️] [⏭️] [📊 3/10] [🔄] [⚡1x] [📍]
播放 上一 下一 帧显示 循环 速度 拖拽
DICOM多帧检测机制
NumberOfFrames 或实际帧数基于元数据的动态渲染
架构调整策略
状态管理集成
// StackViewer组件中的异步多帧检测
const StackViewer = ({ imageUrls, viewportId, ... }) => {
const [frameInfo, setFrameInfo] = useState({
isMultiFrame: false,
totalFrames: 1,
isLoading: true
});
useEffect(() => {
const detectFrames = async () => {
try {
// 等待图像加载完成
const viewport = cornerstone.getViewport(viewportId) as cornerstone.StackViewport;
// 方案1:通过DICOM元数据检测
const imageId = imageUrls[0];
const metadata = cornerstoneDICOMImageLoader.getImageMetadata(imageId);
const numberOfFrames = metadata?.NumberOfFrames || 1;
// 方案2:通过viewport检查实际帧数
const stackData = viewport.getImageIds();
const actualFrames = stackData?.length || numberOfFrames;
// 方案3:检查图像数据维度
const imageData = viewport.getImageData();
const dimensions = imageData?.dimensions;
const frameCount = dimensions?.[2] || numberOfFrames; // Z维度为帧数
const finalFrameCount = Math.max(actualFrames, frameCount, numberOfFrames);
setFrameInfo({
isMultiFrame: finalFrameCount > 1,
totalFrames: finalFrameCount,
isLoading: false
});
// 如果是多帧,初始化播放状态
if (finalFrameCount > 1) {
dispatch(initPlayback({
viewportId,
totalFrames: finalFrameCount
}));
}
} catch (error) {
console.warn('Frame detection failed:', error);
setFrameInfo({ isMultiFrame: false, totalFrames: 1, isLoading: false });
}
};
// 延迟检测,确保图像已加载
const timer = setTimeout(detectFrames, 1500);
return () => clearTimeout(timer);
}, [imageUrls, viewportId, dispatch]);
return (
<div className="relative">
<div ref={elementRef} ... />
{/* 基于DICOM元数据检测结果显示播放控制 */}
{frameInfo.isMultiFrame && !frameInfo.isLoading && (
<FloatingPlaybackControls
viewportId={viewportId}
totalFrames={frameInfo.totalFrames}
/>
)}
{/* 可选:显示检测状态 */}
{frameInfo.isLoading && (
<div className="absolute top-2 right-2 text-white text-xs">
检测帧数...
</div>
)}
</div>
);
};
保持现有的多viewer网格布局:
┌─────────────────┬─────────────────┐
│ viewer1 │ viewer2 │
│ (单帧图像) │ (单帧图像) │
│ (无播放控制条) │ (无播放控制条) │
└─────────────────┴─────────────────┘
切换为单viewer全屏播放:
┌─────────────────────────────────────┐
│ 多帧图像播放区域 │
│ │
│ ┌─────────────────────────────┐ │
│ │ [⏯️][⏮️][⏭️] 3/50 [🔄][⚡2x] │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
空格键: 播放/暂停←/→: 上一帧/下一帧+/-: 调整播放速度L: 切换循环模式Home/End: 跳转到首帧/末帧问题重新定义:
解决方案:基于元数据的异步检测
1. 多帧检测API
// 检测DICOM文件帧数的多种方法
const detectDicomFrames = async (imageId: string, viewport: StackViewport) => {
// 方法1:DICOM标签检测
const metadata = cornerstoneDICOMImageLoader.getImageMetadata(imageId);
const numberOfFrames = metadata?.NumberOfFrames || 1;
// 方法2:图像数据维度检测
const imageData = viewport.getImageData();
const frameCountFromDimensions = imageData?.dimensions?.[2] || 1;
// 方法3:栈数据检测
const stackData = viewport.getImageIds();
const frameCountFromStack = stackData?.length || 1;
return Math.max(numberOfFrames, frameCountFromDimensions, frameCountFromStack);
};
2. 动态UI渲染
const StackViewer = ({ imageUrls, viewportId, ... }) => {
const [frameInfo, setFrameInfo] = useState({
isMultiFrame: false,
totalFrames: 1,
isDetecting: true
});
// 异步检测帧数
useEffect(() => {
const detectFrames = async () => {
const frameCount = await detectDicomFrames(imageUrls[0], viewport);
setFrameInfo({
isMultiFrame: frameCount > 1,
totalFrames: frameCount,
isDetecting: false
});
};
detectFrames();
}, [imageUrls, viewportId]);
return (
<div className="relative">
<div ref={elementRef} ... />
{/* 基于实际检测结果显示播放控制 */}
{frameInfo.isMultiFrame && (
<FloatingPlaybackControls
totalFrames={frameInfo.totalFrames}
/>
)}
</div>
);
};
viewport.setImageIdIndex() API// 播放逻辑核心代码示例
const startPlayback = () => {
if (playIntervalId) clearInterval(playIntervalId);
const intervalMs = 1000 / playbackSpeed;
playIntervalId = setInterval(() => {
let nextFrame = currentFrame + 1;
if (nextFrame >= totalFrames) {
if (isLoop) {
nextFrame = 0;
} else {
stopPlayback();
return;
}
}
viewport.setImageIdIndex(nextFrame);
dispatch(setCurrentFrame({ viewportId, frame: nextFrame }));
}, intervalMs);
};
sequenceDiagram
participant U as User
participant FC as FloatingControls
participant PC as PlaybackController
participant RS as Redux Store
participant SV as StackViewer
participant CS as Cornerstone
U->>FC: 点击播放按钮
FC->>PC: togglePlayback(viewportId)
PC->>RS: dispatch(togglePlayback)
RS->>PC: state.isPlaying = true
PC->>PC: startPlaybackTimer()
loop 播放循环
PC->>PC: nextFrame()
PC->>RS: dispatch(setCurrentFrame)
PC->>SV: setImageIdIndex(currentFrame)
SV->>CS: viewport.setImageIdIndex()
CS->>SV: 渲染新帧
FC->>FC: 更新帧数显示
end
U->>FC: 点击暂停
PC->>PC: clearInterval(timer)
PC->>RS: dispatch(togglePlayback)
RS->>PC: state.isPlaying = false
import { DicomMetadataAnalyzer } from '@/utils/dicom/DicomMetadataAnalyzer';
// 在StackViewer组件中使用
const StackViewer = ({ imageUrls, viewportId }) => {
const [analysisResult, setAnalysisResult] = useState(null);
useEffect(() => {
const analyzeImage = async () => {
try {
// 完整分析
const result = await DicomMetadataAnalyzer.analyze(imageUrls[0], viewport);
setAnalysisResult(result);
// 仅检测帧数(快速方法)
const frameCount = await DicomMetadataAnalyzer.detectFrameCount(imageUrls[0]);
console.log(`检测到 ${frameCount} 帧`);
} catch (error) {
console.error('分析失败:', error);
}
};
analyzeImage();
}, [imageUrls]);
return (
<div className="relative">
{/* 图像容器 */}
<div ref={elementRef} />
{/* 条件渲染播放控制器 */}
{analysisResult?.isMultiFrame && (
<FloatingPlaybackControls
totalFrames={analysisResult.frameCount}
frameType={analysisResult.frameType}
/>
)}
</div>
);
};
// 检测图像的播放能力
const capabilities = await DicomMetadataAnalyzer.detectImageCapabilities(imageId);
if (capabilities.supportsMultiFrame) {
// 启用多帧播放功能
enableMultiFramePlayback();
}
if (capabilities.supportsDynamicPlayback) {
// 启用动态播放功能
enableDynamicPlayback();
}
// 根据推荐的查看器模式调整UI
switch (capabilities.recommendedViewerMode) {
case 'dynamic':
showDynamicControls();
break;
case 'temporal':
showTemporalAnalysisTools();
break;
default:
showStandardControls();
}
const analyzeWithErrorHandling = async (imageId: string) => {
try {
const result = await DicomMetadataAnalyzer.analyze(imageId);
// 检查分析可信度
if (result.confidence < 0.7) {
console.warn(`分析可信度较低: ${result.confidence}`);
// 可能需要回退到简单模式
}
return result;
} catch (error) {
console.error('DICOM分析失败:', error);
// 优雅降级
return {
frameCount: 1,
isMultiFrame: false,
confidence: 0,
analysisTimestamp: new Date(),
analysisVersion: '1.0.0',
isEnhanced: false
};
}
};
// 在应用退出前清理缓存
useEffect(() => {
return () => {
DicomMetadataAnalyzer.clearCache();
};
}, []);
// 在内存紧张时清理缓存
const handleMemoryPressure = () => {
DicomMetadataAnalyzer.clearCache();
};
// 对于多个图像的批量分析
const analyzeBatch = async (imageIds: string[]) => {
const results = await Promise.all(
imageIds.map(async (imageId) => {
try {
return await DicomMetadataAnalyzer.analyze(imageId);
} catch (error) {
console.warn(`分析 ${imageId} 失败:`, error);
return null;
}
})
);
return results.filter(result => result !== null);
};
// 在Redux slice中使用分析结果
const playbackSlice = createSlice({
name: 'playback',
initialState,
reducers: {
setAnalysisResult: (state, action) => {
const { viewportId, analysisResult } = action.payload;
state[viewportId] = {
...state[viewportId],
isMultiFrame: analysisResult.isMultiFrame,
totalFrames: analysisResult.frameCount,
frameType: analysisResult.frameType,
confidence: analysisResult.confidence
};
}
}
});
// 在组件中分发分析结果
const dispatch = useDispatch();
const handleAnalysisComplete = (result: DicomAnalysisResult) => {
dispatch(setAnalysisResult({
viewportId,
analysisResult: result
}));
};
describe('DicomMetadataAnalyzer', () => {
it('应该正确检测多帧DICOM', async () => {
const mockImageId = 'wadouri:test-multi-frame.dcm';
const result = await DicomMetadataAnalyzer.analyze(mockImageId);
expect(result.isMultiFrame).toBe(true);
expect(result.frameCount).toBeGreaterThan(1);
expect(result.confidence).toBeGreaterThanOrEqual(0.5);
});
it('应该优雅处理元数据缺失', async () => {
const mockImageId = 'wadouri:corrupted.dcm';
const result = await DicomMetadataAnalyzer.analyze(mockImageId);
expect(result).toBeDefined();
expect(result.frameCount).toBe(1);
expect(result.confidence).toBeLessThan(1);
});
});
A: 检查confidence字段,如果可信度低于0.7,可能需要手动验证或使用备用检测方法。
A: 分析器会自动检测Enhanced DICOM并设置isEnhanced标志,可以根据此字段采取特殊处理。
A: 定期调用DicomMetadataAnalyzer.clearCache()清理缓存,或减少缓存TTL时间。
A: 在分析开始前设置loading状态,分析完成后清除状态:
const [isAnalyzing, setIsAnalyzing] = useState(false);
const analyzeImage = async () => {
setIsAnalyzing(true);
try {
const result = await DicomMetadataAnalyzer.analyze(imageId);
// 处理结果...
} finally {
setIsAnalyzing(false);
}
};
src/
├── states/view/
│ └── playbackSlice.ts // 播放状态管理
├── utils/dicom/ // 🆕 DICOM分析模块
│ ├── DicomMetadataAnalyzer.ts // 核心元数据分析器
│ ├── DicomFrameDetector.ts // 多帧检测器(兼容性)
│ ├── DicomCapabilityDetector.ts // 能力检测器
│ ├── DicomCacheManager.ts // 分析结果缓存管理
│ └── types/
│ ├── DicomAnalysisResult.ts // 分析结果类型定义
│ ├── DicomCapabilities.ts // 能力类型定义
│ └── index.ts // 类型导出
├── pages/view/components/
│ ├── playback/
│ │ ├── PlaybackController.tsx // 播放控制器
│ │ ├── FloatingPlaybackControls.tsx // 浮动控制条
│ │ ├── PlaybackButton.tsx // 播放按钮组件
│ │ ├── FrameCounter.tsx // 帧数显示组件
│ │ ├── SpeedControl.tsx // 速度控制组件
│ │ └── types.ts // 类型定义
│ └── viewers/
│ └── stack.image.viewer.tsx // 修改现有组件集成播放功能
在StackViewer中使用DicomMetadataAnalyzer
// StackViewer组件中的集成示例
import { DicomMetadataAnalyzer } from '@/utils/dicom/DicomMetadataAnalyzer';
const StackViewer = ({ imageUrls, viewportId, ... }) => {
const [analysisResult, setAnalysisResult] = useState(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
useEffect(() => {
const analyzeImage = async () => {
if (!imageUrls?.[0]) return;
setIsAnalyzing(true);
try {
// 使用统一的分析器
const result = await DicomMetadataAnalyzer.analyze(imageUrls[0], viewport);
setAnalysisResult(result);
// 如果是多帧,初始化播放状态
if (result.isMultiFrame) {
dispatch(initPlayback({
viewportId,
totalFrames: result.frameCount,
frameType: result.frameType,
confidence: result.confidence
}));
}
} catch (error) {
console.error('DICOM analysis failed:', error);
} finally {
setIsAnalyzing(false);
}
};
analyzeImage();
}, [imageUrls, viewportId]);
return (
<div className="relative">
<div ref={elementRef} ... />
{/* 基于分析结果显示播放控制 */}
{analysisResult?.isMultiFrame && (
<FloatingPlaybackControls
viewportId={viewportId}
totalFrames={analysisResult.frameCount}
frameType={analysisResult.frameType}
confidence={analysisResult.confidence}
/>
)}
{isAnalyzing && (
<div className="absolute top-2 right-2 text-white text-xs">
分析DICOM元数据...
</div>
)}
</div>
);
};
// 未来可以轻松扩展其他分析功能
export class DicomMetadataAnalyzer {
// 现有多帧分析
static async analyze(imageId: string): Promise<DicomAnalysisResult> { ... }
// 🆕 未来扩展:AI辅助诊断信息分析
static async analyzeAIMarkers(imageId: string): Promise<AIAnalysisResult> {
const result = await this.analyze(imageId);
// 基于元数据检测AI标记、测量数据等
return this.extractAIMarkers(result);
}
// 🆕 未来扩展:3D重建能力分析
static async analyze3DCapability(imageId: string): Promise<ThreeDCapability> {
const result = await this.analyze(imageId);
// 分析是否支持3D重建、MPR等
return this.assess3DCapability(result);
}
// 🆕 未来扩展:患者隐私信息检测
static async analyzePrivacyInfo(imageId: string): Promise<PrivacyAnalysis> {
const result = await this.analyze(imageId);
// 检测和标记患者敏感信息
return this.analyzePrivacy(result);
}
// 🆕 未来扩展:图像质量评估
static async analyzeImageQuality(imageId: string): Promise<QualityMetrics> {
const result = await this.analyze(imageId);
// 基于元数据评估图像质量
return this.assessQuality(result);
}
// 🆕 未来扩展:智能标注建议
static async suggestAnnotations(imageId: string): Promise<AnnotationSuggestions> {
const result = await this.analyze(imageId);
// 基于图像特征建议合适的标注工具
return this.generateAnnotationSuggestions(result);
}
}
风险: Cornerstone.js API变化导致兼容性问题 应对:
风险: 大型图像序列导致内存溢出 应对:
风险: 播放卡顿影响诊断效率 应对:
这个多帧DICOM图像播放架构方案具有以下优势:
✨ 架构清晰: 分层设计,职责明确,便于维护和扩展 🚀 性能优异: 基于Cornerstone.js原生能力,充分利用现有缓存机制 🎯 用户友好: 直观的播放控制界面,完整的键盘快捷键支持 🔧 易于扩展: 模块化设计,支持未来功能扩展和性能优化 🛡️ 类型安全: 完整的TypeScript支持,降低开发和维护成本 ⚡ 无缝集成: 基于现有架构扩展,最小化对现有功能的影响
该方案能够很好地满足在DICOM图像查看器中添加专业级多帧播放功能的需求,为用户提供类似医学影像工作站的专业体验,同时保持代码的可维护性和可扩展性。
| 版本 | 日期 | 更新内容 | 作者 |
|---|---|---|---|
| v1.0 | 2025-11-23 | 初始版本,基础架构设计 | 系统架构师 |
| v1.1 | 2025-11-24 | 重要更新:添加DicomMetadataAnalyzer职责定义 | 系统架构师 |
DicomMetadataAnalyzer职责定义章节 (4.1.1)
DicomMetadataAnalyzer使用指南章节 (第8章)
文档版本: v1.1 创建日期: 2025-11-23 最后更新: 2025-11-24 核心贡献: DicomMetadataAnalyzer职责明确化与使用指南完善