Browse Source

实现滑杆移动后触发后端修改图像,目前还没有实现前端预览;实现修改图像后,保存参数

sw 1 day ago
parent
commit
f5bd98a944

+ 365 - 0
docs/实现/滑动参数调节面板功能.md

@@ -0,0 +1,365 @@
+# 滑动参数调节面板功能实现文档
+
+## 功能概述
+
+在"图像处理"面板(AdvancedProcessingPanel)上添加一个"高级"按钮,点击后打开"滑动参数调节"面板(SliderAdjustmentPanel)。这是一个三级面板,点击返回按钮后会返回到"图像处理"面板。
+
+## 面板层级结构
+
+```
+OperationPanel (二级面板)
+  └── AdvancedProcessingPanel (图像处理 - 三级面板)
+        └── SliderAdjustmentPanel (滑动参数调节 - 四级面板)
+```
+
+## 功能特性
+
+### 1. 参数调整
+- **增益** (contrast): -5~45,步长0.1
+- **细节** (detail): -0.5~24.5,步长0.1
+- **动态范围** (latitude): -5~20,步长0.1
+- **噪声模式** (noise): 0~20,步长0.1
+- **对比度** (brightness): -8~8,步长0.1(仅前端维护)
+- **亮度** (sharpness): -8~8,步长0.1(仅前端维护)
+
+### 2. 下拉选择
+- **算法选择**: RSymphony(默认)
+- **LUT选择**: SIGMOID(默认)、LINER 等
+- **风格选择**: 均衡、高对比、锐组织、骨骼
+
+### 3. 实时预览(新增)
+- 滑块调整后自动获取预览图像(500ms防抖)
+- 使用 Blob URL 实现即时预览
+- 仅后端参数触发预览(contrast, detail, latitude, noise)
+- 前端参数调整不触发后端请求
+
+### 4. 手动操作
+- **重置参数**: 重置为当前风格的默认值
+- **保存参数**: 永久保存到dcm文件
+
+## 技术实现
+
+### 1. 文件结构
+
+```
+src/
+├── types/
+│   └── imageProcessing.ts                    # 类型定义
+├── domain/
+│   ├── lutConfig.ts                          # LUT配置
+│   └── processingPresets.ts                  # 风格预设配置
+├── API/
+│   └── imageActions.ts                       # 新增API接口
+├── states/
+│   ├── panelSwitchSliceForView.ts           # 面板切换状态(已修改)
+│   ├── store.ts                              # Redux Store(已修改)
+│   └── view/
+│       └── sliderAdjustmentPanelSlice.ts    # 滑动参数调节面板状态
+└── pages/
+    └── view/
+        └── components/
+            ├── ParameterSlider.tsx           # 参数滑块组件
+            ├── SliderAdjustmentPanel.tsx    # 滑动参数调节面板
+            ├── AdvancedProcessingPanel.tsx  # 高级处理面板(已修改)
+            └── OperationPanel.tsx            # 操作面板(已修改)
+```
+
+### 2. 核心组件
+
+#### ParameterSlider 组件
+- 可复用的参数滑块组件
+- 包含:标签、范围提示、当前值显示、滑块、加减按钮
+- 支持自定义范围、步长、禁用状态
+
+#### SliderAdjustmentPanel 组件
+- 主要面板组件
+- 集成6个参数滑块
+- 集成算法、LUT、风格下拉选择
+- 实现防抖自动保存
+- 提供重置和手动保存按钮
+
+### 3. 状态管理
+
+#### sliderAdjustmentPanelSlice
+- **状态**:
+  - `currentImageId`: 当前图像ID
+  - `parameters`: 当前参数值
+  - `selectedStyle`: 当前风格
+  - `selectedAlgorithm`: 当前算法
+  - `selectedLUT`: 当前LUT
+  - `isLoading`: 加载状态
+  - `isSaving`: 保存状态
+  - `isInitialLoad`: 初始加载标志
+  - `error`: 错误信息
+
+- **异步操作**:
+  - `loadImageProcessingParams`: 加载图像处理参数
+  - `saveProcessingParams`: 保存图像处理参数
+
+- **同步操作**:
+  - `setCurrentImageId`: 设置当前图像ID
+  - `updateParameter`: 更新单个参数
+  - `applyPreset`: 应用风格预设
+  - `resetToPreset`: 重置为当前风格默认值
+  - `setAlgorithm`: 设置算法
+  - `setLUT`: 设置LUT
+  - `clearError`: 清除错误
+  - `resetState`: 重置状态
+
+#### panelSwitchSliceForView(修改)
+- 新增 `SliderAdjustmentPanel` 面板类型
+- 新增 `switchToSliderAdjustmentPanel` action
+
+### 4. API接口
+
+#### getImageProcessingParams
+```typescript
+GET /auth/image/{sopInstanceUid}/preparation
+```
+获取当前图像处理参数(增益、细节、动态范围、噪声模式)
+
+#### getProcessedDcm(新增)
+```typescript
+GET /auth/image/{sopInstanceUid}/proc?contrast=5.0&detail=9.0&latitude=25.0&noise=12.0
+```
+获取应用指定参数后的处理后dcm文件(用于实时预览)
+- **返回**: 直接返回dcm文件的二进制数据(Blob)
+- **用途**: 滑块调整时实时预览
+- **机制**: 
+  1. 前端调用API获取Blob
+  2. 使用 `URL.createObjectURL(blob)` 创建临时URL
+  3. 更新Viewer显示新图像
+  4. 适当时机调用 `URL.revokeObjectURL()` 释放内存
+
+#### saveImageProcessingParams
+```typescript
+POST /auth/image/{sopInstanceUid}/proc
+Body: {
+  contrast: number,   // 增益
+  detail: number,     // 细节
+  latitude: number,   // 动态范围
+  noise: number       // 噪声模式
+}
+```
+保存图像处理参数到DICOM文件(永久保存)
+- **用途**: 点击"保存参数"按钮时调用
+- **效果**: 将处理后的dcm永久保存到文件系统
+
+### 5. 配置文件
+
+#### processingPresets.ts
+```typescript
+export const PROCESSING_PRESETS: Record<ProcessingStyle, PresetConfig> = {
+  '均衡': {
+    name: '均衡',
+    params: { contrast: 5.0, detail: 9.0, latitude: 25.0, noise: 12.0, brightness: -2.0, sharpness: 1.0 }
+  },
+  '高对比': {
+    name: '高对比',
+    params: { contrast: 10.0, detail: 5.0, latitude: 15.0, noise: 8.0, brightness: 0.0, sharpness: 2.0 }
+  },
+  // ...
+};
+```
+
+#### lutConfig.ts
+```typescript
+export const LUT_OPTIONS: LUTType[] = [
+  'SIGMOID',
+  'LINER',
+  // ...
+];
+```
+
+## 用户交互流程
+
+### 1. 进入滑动参数调节面板
+```
+用户在"图像处理"面板 
+  → 点击"高级"按钮 
+  → 进入"滑动参数调节"面板
+  → 自动加载当前图像的参数
+```
+
+### 2. 调整参数(实时预览)
+```
+用户拖动滑块或点击加减按钮
+  → 参数值实时更新到Redux
+  → 触发500ms防抖定时器
+  → 调用GET API获取预览dcm(Blob)
+  → 创建Blob URL
+  → 刷新Viewer显示
+```
+
+### 3. 保存参数(永久保存)
+```
+用户点击"保存参数"按钮
+  → 调用POST API永久保存
+  → 成功提示
+```
+
+### 4. 切换风格
+```
+用户选择不同风格
+  → 应用对应的预设参数
+  → 立即保存到后端(POST)
+```
+
+### 5. 返回上级面板
+```
+用户点击返回按钮
+  → 返回到"图像处理"面板
+```
+
+## 数据流
+
+### 实时预览流程
+```
+用户拖动滑块
+  ↓
+UI组件 (SliderAdjustmentPanel)
+  ↓
+Redux Action (updateParameter)
+  ↓
+Redux State 更新
+  ↓
+防抖处理 (500ms)
+  ↓
+API调用 GET /proc (getProcessedDcm)
+  ↓
+获取Blob数据
+  ↓
+创建Blob URL
+  ↓
+更新Viewer显示(TODO)
+```
+
+### 永久保存流程
+```
+用户点击"保存参数"
+  ↓
+UI组件 (SliderAdjustmentPanel)
+  ↓
+Redux Thunk (saveProcessingParams)
+  ↓
+API调用 POST /proc (saveImageProcessingParams)
+  ↓
+后端永久保存到dcm文件
+  ↓
+成功提示
+```
+
+## 注意事项
+
+### 1. 参数分类
+- **后端参数** (4个): contrast, detail, latitude, noise
+  - 会保存到DICOM文件
+  - 通过API接口同步
+  
+- **前端参数** (2个): brightness, sharpness
+  - 仅在前端维护
+  - 不会保存到后端
+
+### 2. 防抖机制
+- 参数调整触发500ms防抖
+- 避免频繁调用预览API
+- 初始加载时不触发预览
+- 前端参数(brightness, sharpness)不触发后端请求
+
+### 3. 错误处理
+- API调用失败时显示错误提示
+- 加载失败时使用默认值
+- 错误信息存储在Redux state中
+
+### 4. 待完善功能
+- 当前图像ID获取逻辑(TODO)
+- 需要从ViewerContainer或其他地方获取当前选中图像的sopInstanceUid
+
+## 测试步骤
+
+### 1. 面板切换测试
+- [ ] 从"图像处理"面板点击"高级"按钮
+- [ ] 验证进入"滑动参数调节"面板
+- [ ] 点击返回按钮
+- [ ] 验证返回到"图像处理"面板
+
+### 2. 参数调整测试
+- [ ] 拖动各个参数滑块
+- [ ] 验证参数值实时更新
+- [ ] 验证UI显示正确
+- [ ] 等待500ms后验证API调用
+
+### 3. 风格切换测试
+- [ ] 选择不同的风格
+- [ ] 验证参数值立即更新
+- [ ] 验证立即触发保存
+
+### 4. 手动操作测试
+- [ ] 点击"重置参数"按钮
+- [ ] 验证参数恢复到当前风格默认值
+- [ ] 点击"保存参数"按钮
+- [ ] 验证手动触发保存
+
+### 5. 边界条件测试
+- [ ] 参数值达到最大值/最小值时的表现
+- [ ] 网络请求失败时的错误处理
+- [ ] 快速切换参数时的防抖效果
+
+## 潜在问题与解决方案
+
+### 1. 图像ID获取
+**问题**: 当前代码中sopInstanceUid获取逻辑未实现
+**解决方案**: 需要从ViewerContainer或selectedBodyPosition中获取当前选中图像的ID
+
+### 2. 参数同步
+**问题**: 前端参数(brightness, sharpness)与后端参数(contrast, detail等)的同步
+**解决方案**: 
+- 后端参数通过API同步
+- 前端参数仅在Redux中维护
+- 加载时只更新后端返回的4个参数
+
+### 3. Blob URL内存管理
+**问题**: 每次预览都创建新的Blob URL,可能导致内存泄漏
+**解决方案**: 
+- 在创建新URL前,先revoke旧的URL
+- 组件卸载时清理所有Blob URL
+- 考虑使用useRef存储当前的blobUrl
+
+### 4. 并发请求
+**问题**: 用户快速切换参数可能导致多次API调用
+**解决方案**: 使用500ms防抖机制,合并多次操作为一次请求
+
+## 扩展建议
+
+1. **参数预览**: 在调整参数时实时预览效果
+2. **参数历史**: 记录参数调整历史,支持撤销/重做
+3. **自定义风格**: 允许用户保存自定义的参数组合为新风格
+4. **参数对比**: 支持查看调整前后的参数对比
+5. **批量应用**: 支持将当前参数应用到多张图像
+
+## 更新日志
+
+### 2025-01-22
+- ✅ 完成类型定义
+- ✅ 完成配置文件创建
+- ✅ 完成API接口扩展(包括getProcessedDcm)
+- ✅ 完成状态管理实现
+- ✅ 完成UI组件开发
+- ✅ 完成面板集成
+- ✅ 实现实时预览API调用逻辑
+- ⏳ 待完成:获取当前图像ID逻辑
+- ⏳ 待完成:Cornerstone图像刷新实现
+- ⏳ 待完成:Blob URL内存管理
+- ⏳ 待完成:功能测试
+
+### 实时预览实现细节
+**已实现**:
+1. 新增 `getProcessedDcm` API - 获取处理后的dcm Blob
+2. 修改滑块逻辑 - 调用预览API而非保存API
+3. 区分前端参数和后端参数 - 只有后端参数触发预览
+4. 防抖机制 - 500ms延迟避免频繁请求
+
+**待实现**:
+1. Blob URL → Cornerstone 图像刷新
+2. Blob URL 内存管理(revoke机制)
+3. 获取当前图像ID的完整逻辑

+ 31 - 0
src/API/bodyPosition.ts

@@ -22,3 +22,34 @@ export function getExposedImageUrl(sopInstanceUid: string): string {
 export function getDcmImageUrl(sopInstanceUid: string): string {
   return `dicomweb:${API_BASE_URL}pub/dcm/${sopInstanceUid}.dcm`;
 }
+export function getSopInstanceUidFromUrl(url: string): string {
+  if (!url.startsWith('dicomweb:')) {
+    throw new Error('Invalid URL format: must start with "dicomweb:"');
+  }
+
+  const path = url.substring('dicomweb:'.length);
+  const segments = path.split('/').filter(Boolean); // 移除空段
+
+  // 查找 'dcm' 段的位置
+  const dcmIndex = segments.indexOf('dcm');
+  if (dcmIndex === -1) {
+    throw new Error('Invalid path format: expected "/dcm/{sopInstanceUid}.dcm" segment');
+  }
+
+  // 下一个段应该是 {sopInstanceUid}.dcm
+  if (dcmIndex + 1 >= segments.length) {
+    throw new Error('Invalid path format: missing file name after "dcm"');
+  }
+
+  const filename = segments[dcmIndex + 1];
+  if (!filename.endsWith('.dcm')) {
+    throw new Error('Invalid file extension: expected ".dcm"');
+  }
+
+  const sopInstanceUid = filename.slice(0, -4); // 移除 .dcm
+  if (!sopInstanceUid) {
+    throw new Error('Empty SOP Instance UID');
+  }
+
+  return sopInstanceUid;
+}

+ 59 - 0
src/API/imageActions.ts

@@ -235,6 +235,20 @@ export interface SaveProcessingParamsRequest {
   noise: number;
 }
 
+/**
+ * 获取处理的dcm
+ */
+export interface GetProcessedDcmRequest {
+  /** 增益 */
+  contrast: string;
+  /** 细节 */
+  detail: string;
+  /** 动态范围 */
+  latitude: string;
+  /** 噪声模式 */
+  noise: string;
+}
+
 /**
  * 保存图像处理参数响应类型
  */
@@ -249,6 +263,51 @@ export interface SaveProcessingParamsResponse {
   data: Record<string, never>;
 }
 
+/**
+ * 获取处理后的 dcm 文件
+ * 实时获取应用指定参数后的 dcm 文件(用于预览)
+ * 
+ * @param sopInstanceUid 图像实例 UID (SOP Instance UID)
+ * @param params 图像处理参数
+ * @returns 处理后的 dcm 文件 Blob
+ * @throws 当获取失败时抛出错误
+ * 
+ * @example
+ * ```typescript
+ * const dcmBlob = await getProcessedDcm('1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671', {
+ *   contrast: 5.0,
+ *   detail: 9.0,
+ *   latitude: 25.0,
+ *   noise: 12.0
+ * });
+ * const blobUrl = URL.createObjectURL(dcmBlob);
+ * ```
+ */
+export const getProcessedDcm = async (
+  sopInstanceUid: string,
+  params: GetProcessedDcmRequest
+): Promise<Blob> => {
+  try {
+    const response = await axiosInstance.get(
+      `/auth/image/${sopInstanceUid}/proc`,
+      {
+        params: {
+          contrast: params.contrast,
+          detail: params.detail,
+          latitude: params.latitude,
+          noise: params.noise,
+        },
+        responseType: 'blob', // 重要:指定响应类型为blob
+      }
+    );
+
+    return response.data;
+  } catch (error) {
+    console.error('Error getting processed dcm:', error);
+    throw error;
+  }
+};
+
 /**
  * 保存图像处理参数
  * 保存应用增益、细节、动态范围、噪声模式后的 dcm

+ 47 - 21
src/pages/view/components/SliderAdjustmentPanel.tsx

@@ -22,10 +22,13 @@ import {
   selectError,
   selectCurrentImageId,
 } from '../../../states/view/sliderAdjustmentPanelSlice';
+import { getProcessedDcm } from '../../../API/imageActions';
 import { PARAMETER_RANGES, ALGORITHM_OPTIONS } from '../../../domain/processingPresets';
 import { LUT_OPTIONS } from '../../../domain/lutConfig';
 import type { ProcessingStyle, LUTType, FullProcessingParams } from '../../../types/imageProcessing';
 import ParameterSlider from './ParameterSlider';
+import store from '@/states/store';
+import { getSopInstanceUidFromUrl } from '@/API/bodyPosition';
 
 const { Header, Content } = Layout;
 const { Title } = Typography;
@@ -54,9 +57,16 @@ const SliderAdjustmentPanel = () => {
   
   // 组件挂载时加载参数
   useEffect(() => {
-    // TODO: 从当前选中的图像获取 sopInstanceUid
-    // 这里需要从 ViewerContainer 或其他地方获取当前图像ID
-    const sopInstanceUid = ''; // 临时占位
+    // 从当前选中的图像获取 sopInstanceUid
+    // 
+    const dcmUrls = store.getState().viewerContainer.selectedViewers;
+    //正常情况下只会得到一个选中的图像,否则进入不了滑杆调参页面
+    if(dcmUrls.length !== 1){
+      console.error('没有选中的图像或者数量大于1,无法加载参数,异常现象');
+      return;
+    }
+
+    const sopInstanceUid = getSopInstanceUidFromUrl(dcmUrls[0]); 
     
     if (sopInstanceUid) {
       dispatch(loadImageProcessingParams(sopInstanceUid) as any);
@@ -78,11 +88,11 @@ const SliderAdjustmentPanel = () => {
   };
   
   /**
-   * 防抖保存参数
+   * 防抖获取预览图像
    */
-  const debouncedSave = useCallback(
+  const debouncedPreview = useCallback(
     (params: FullProcessingParams) => {
-      // 如果是初始加载,不触发保存
+      // 如果是初始加载,不触发预览
       if (isInitialLoad) {
         return;
       }
@@ -93,23 +103,36 @@ const SliderAdjustmentPanel = () => {
       }
       
       // 设置新的定时器
-      saveTimerRef.current = setTimeout(() => {
+      saveTimerRef.current = setTimeout(async () => {
         if (currentImageId) {
-          dispatch(
-            saveProcessingParams({
-              sopInstanceUid: currentImageId,
-              params: {
-                contrast: params.contrast,
-                detail: params.detail,
-                latitude: params.latitude,
-                noise: params.noise,
-              },
-            }) as any
-          );
+          try {
+            // 调用GET API获取处理后的dcm
+            const dcmBlob = await getProcessedDcm(currentImageId, {
+              contrast: params.contrast.toString(),
+              detail: params.detail.toString(),
+              latitude: params.latitude.toString(),
+              noise: params.noise.toString(),
+            });
+            
+            // 创建Blob URL
+            const blobUrl = URL.createObjectURL(dcmBlob);
+            
+            // TODO: 这里需要刷新Cornerstone显示
+            // 目前先在控制台输出,后续需要实现图像刷新逻辑
+            console.log('预览图像URL:', blobUrl);
+            console.log('参数:', params);
+            
+            // 提示:需要在这里更新Viewer显示
+            message.info('预览图像已生成(待实现图像刷新)');
+            
+          } catch (error) {
+            console.error('获取预览图像失败:', error);
+            message.error('获取预览图像失败');
+          }
         }
       }, 500); // 500ms 防抖延迟
     },
-    [dispatch, currentImageId, isInitialLoad]
+    [currentImageId, isInitialLoad]
   );
   
   /**
@@ -117,9 +140,12 @@ const SliderAdjustmentPanel = () => {
    */
   const handleParameterChange = (name: keyof FullProcessingParams, value: number) => {
     dispatch(updateParameter({ name, value }));
-    // 触发防抖保存
+    // 触发防抖预览(仅后端参数触发预览)
     const newParams = { ...parameters, [name]: value };
-    debouncedSave(newParams);
+    // 只有后端参数(contrast, detail, latitude, noise)才触发预览
+    if (['contrast', 'detail', 'latitude', 'noise'].includes(name)) {
+      debouncedPreview(newParams);
+    }
   };
   
   /**