SliderAdjustmentPanel.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. import React, { useEffect, useCallback, useRef } from 'react';
  2. import { Layout, Button, Typography, Select, message, Spin, Alert } from 'antd';
  3. import { ArrowLeftOutlined } from '@ant-design/icons';
  4. import { useDispatch } from 'react-redux';
  5. import { useAppSelector } from '@/states/store';
  6. import { useImageEnhancementSDK } from '@/hooks/useImageEnhancementSDK';
  7. import { selectProcessingMode } from '@/states/system/processingModeSlice';
  8. import { IMAGE_LOADER_PREFIX } from '@/lib/image-enhancement-sdk';
  9. import { switchToAdvancedProcessingPanel } from '../../../states/panelSwitchSliceForView';
  10. import {
  11. loadImageProcessingParams,
  12. saveProcessingParams,
  13. updateParameter,
  14. applyPreset,
  15. resetToPreset,
  16. setAlgorithm,
  17. setLUT,
  18. selectParameters,
  19. selectSelectedStyle,
  20. selectSelectedAlgorithm,
  21. selectSelectedLUT,
  22. selectIsLoading,
  23. selectIsSaving,
  24. selectIsInitialLoad,
  25. selectError,
  26. selectCurrentImageId,
  27. } from '../../../states/view/sliderAdjustmentPanelSlice';
  28. import { buildProcessedDcmUrl } from '../../../API/imageActions';
  29. import { updateViewerUrl } from '../../../states/view/viewerContainerSlice';
  30. import { PARAMETER_RANGES, ALGORITHM_OPTIONS } from '../../../domain/processingPresets';
  31. import { LUT_OPTIONS } from '../../../domain/lutConfig';
  32. import type { ProcessingStyle, LUTType, FullProcessingParams } from '../../../types/imageProcessing';
  33. import ParameterSlider from './ParameterSlider';
  34. import store from '@/states/store';
  35. import { getSopInstanceUidFromUrl } from '@/API/bodyPosition';
  36. const { Header, Content } = Layout;
  37. const { Title } = Typography;
  38. const { Option } = Select;
  39. /**
  40. * 滑动参数调节面板
  41. * 三级面板,用于调整图像处理参数
  42. */
  43. const SliderAdjustmentPanel = () => {
  44. const dispatch = useDispatch();
  45. // 从 Redux 获取状态
  46. const parameters = useAppSelector(selectParameters);
  47. const selectedStyle = useAppSelector(selectSelectedStyle);
  48. const selectedAlgorithm = useAppSelector(selectSelectedAlgorithm);
  49. const selectedLUT = useAppSelector(selectSelectedLUT);
  50. const isLoading = useAppSelector(selectIsLoading);
  51. const isSaving = useAppSelector(selectIsSaving);
  52. const isInitialLoad = useAppSelector(selectIsInitialLoad);
  53. const error = useAppSelector(selectError);
  54. const currentImageId = useAppSelector(selectCurrentImageId);
  55. // 获取处理模式配置
  56. const processingMode = useAppSelector(selectProcessingMode);
  57. const isWASMMode = processingMode === 'wasm';
  58. // 初始化WASM SDK(仅在WASM模式下)
  59. const { sdk, isSDKReady, isInitializing, error: sdkError } = useImageEnhancementSDK({
  60. sopInstanceUid: currentImageId || '',
  61. enabled: isWASMMode,
  62. });
  63. // 防抖定时器
  64. const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
  65. // 组件挂载时加载参数
  66. useEffect(() => {
  67. // 从当前选中的图像获取 sopInstanceUid
  68. //
  69. const dcmUrls = store.getState().viewerContainer.selectedViewers;
  70. //正常情况下只会得到一个选中的图像,否则进入不了滑杆调参页面
  71. if (dcmUrls.length !== 1) {
  72. console.error('没有选中的图像或者数量大于1,无法加载参数,异常现象');
  73. return;
  74. }
  75. const sopInstanceUid = getSopInstanceUidFromUrl(dcmUrls[0]);
  76. if (sopInstanceUid) {
  77. dispatch(loadImageProcessingParams(sopInstanceUid) as any);
  78. }
  79. }, [dispatch]);
  80. // 监听错误
  81. useEffect(() => {
  82. if (error) {
  83. message.error(error);
  84. }
  85. }, [error]);
  86. /**
  87. * 返回上级面板
  88. */
  89. const handleReturn = () => {
  90. dispatch(switchToAdvancedProcessingPanel());
  91. };
  92. /**
  93. * 传统模式:防抖更新预览图像URL
  94. */
  95. const debouncedPreview = useCallback(
  96. (params: FullProcessingParams) => {
  97. // 如果是初始加载,不触发预览
  98. if (isInitialLoad) {
  99. return;
  100. }
  101. // 清除之前的定时器
  102. if (saveTimerRef.current) {
  103. clearTimeout(saveTimerRef.current);
  104. }
  105. // 设置新的定时器
  106. saveTimerRef.current = setTimeout(() => {
  107. if (currentImageId) {
  108. try {
  109. // 获取原始URL
  110. const dcmUrls = store.getState().viewerContainer.selectedViewers;
  111. if (dcmUrls.length !== 1) {
  112. console.error('没有选中的图像或者数量大于1,无法更新预览');
  113. return;
  114. }
  115. const originalUrl = dcmUrls[0];
  116. // 构建处理后的dcm URL(带参数)
  117. const processedUrl = buildProcessedDcmUrl(currentImageId, {
  118. contrast: params.contrast.toString(),
  119. detail: params.detail.toString(),
  120. latitude: params.latitude.toString(),
  121. noise: params.noise.toString(),
  122. ww_coef: params.brightness.toString(),
  123. wl_coef: params.sharpness.toString(),
  124. });
  125. // 更新viewer URL以触发重新渲染
  126. dispatch(updateViewerUrl({
  127. originalUrl,
  128. newUrl: `dicomweb:${processedUrl}`,
  129. }));
  130. console.log('✅ [传统模式] 已更新预览图像URL:', processedUrl);
  131. console.log('参数:', params);
  132. } catch (error) {
  133. console.error('更新预览图像失败:', error);
  134. message.error('更新预览图像失败');
  135. }
  136. }
  137. }, 500); // 500ms 防抖延迟
  138. },
  139. [currentImageId, isInitialLoad, dispatch]
  140. );
  141. /**
  142. * WASM模式:防抖更新预览
  143. */
  144. const debouncedWASMPreview = useCallback(
  145. (params: FullProcessingParams) => {
  146. if (isInitialLoad || !sdk || !currentImageId) {
  147. return;
  148. }
  149. // 清除之前的定时器
  150. if (saveTimerRef.current) {
  151. clearTimeout(saveTimerRef.current);
  152. }
  153. // 设置新的定时器(WASM模式更短的防抖时间)
  154. saveTimerRef.current = setTimeout(async () => {
  155. try {
  156. console.log('🔧 [WASM模式] 开始应用参数...');
  157. // 1. 更新SDK参数管理器
  158. sdk.parameterManager.updateParameters(params);
  159. // 2. 生成唯一imageId(带时间戳强制重新加载)
  160. const timestamp = Date.now();
  161. const enhancedImageId = `${IMAGE_LOADER_PREFIX}${currentImageId}_${timestamp}`;
  162. // 3. 获取原始URL
  163. const dcmUrls = store.getState().viewerContainer.selectedViewers;
  164. if (dcmUrls.length !== 1) {
  165. console.error('没有选中的图像或者数量大于1');
  166. return;
  167. }
  168. const originalUrl = dcmUrls[0];
  169. // 4. 更新viewer URL以触发重新加载
  170. dispatch(updateViewerUrl({
  171. originalUrl,
  172. newUrl: enhancedImageId,
  173. }));
  174. console.log('✅ [WASM模式] WASM增强完成,图像已重新加载');
  175. console.log('参数:', params);
  176. } catch (error) {
  177. console.error('❌ [WASM模式] 参数应用失败:', error);
  178. message.error('WASM参数应用失败');
  179. }
  180. }, 300); // WASM模式使用更短的防抖时间
  181. },
  182. [currentImageId, isInitialLoad, sdk, dispatch]
  183. );
  184. /**
  185. * 参数变化处理(双模式支持)
  186. */
  187. const handleParameterChange = (name: keyof FullProcessingParams, value: number) => {
  188. dispatch(updateParameter({ name, value }));
  189. // 触发防抖预览(仅后端参数触发预览)
  190. const newParams = { ...parameters, [name]: value };
  191. // 只有后端参数(contrast, detail, latitude, noise)才触发预览
  192. if (['contrast', 'detail', 'latitude', 'noise', 'brightness', 'sharpness'].includes(name)) {
  193. // 根据处理模式选择预览函数
  194. if (isWASMMode) {
  195. debouncedWASMPreview(newParams);
  196. } else {
  197. debouncedPreview(newParams);
  198. }
  199. }
  200. };
  201. /**
  202. * 风格变化处理
  203. */
  204. const handleStyleChange = (value: ProcessingStyle) => {
  205. dispatch(applyPreset(value));
  206. // 风格切换仅更新前端预览,不保存到后端
  207. };
  208. /**
  209. * 算法变化处理
  210. */
  211. const handleAlgorithmChange = (value: string) => {
  212. dispatch(setAlgorithm(value));
  213. };
  214. /**
  215. * LUT 变化处理
  216. */
  217. const handleLUTChange = (value: LUTType) => {
  218. dispatch(setLUT(value));
  219. };
  220. /**
  221. * 重置参数
  222. */
  223. const handleReset = () => {
  224. dispatch(resetToPreset());
  225. message.success('已重置为当前风格的默认参数');
  226. // 重置参数仅更新前端预览,不保存到后端
  227. };
  228. /**
  229. * 手动保存参数
  230. */
  231. const handleSave = () => {
  232. if (currentImageId) {
  233. dispatch(
  234. saveProcessingParams({
  235. sopInstanceUid: currentImageId,
  236. params: {
  237. contrast: parameters.contrast,
  238. detail: parameters.detail,
  239. latitude: parameters.latitude,
  240. noise: parameters.noise,
  241. ww_coef: parameters.brightness,
  242. wl_coef: parameters.sharpness,
  243. },
  244. }) as any
  245. ).then(() => {
  246. message.success('参数保存成功');
  247. });
  248. }
  249. };
  250. // 清理定时器
  251. useEffect(() => {
  252. return () => {
  253. if (saveTimerRef.current) {
  254. clearTimeout(saveTimerRef.current);
  255. }
  256. };
  257. }, []);
  258. return (
  259. <Layout className="h-full">
  260. {/* 顶部导航栏 */}
  261. <Header
  262. style={{
  263. display: 'flex',
  264. alignItems: 'center',
  265. padding: '0 16px',
  266. }}
  267. >
  268. <Button
  269. type="text"
  270. icon={<ArrowLeftOutlined />}
  271. onClick={handleReturn}
  272. />
  273. <Title level={5} style={{ margin: 0, lineHeight: '48px' }}>
  274. 滑动参数调节
  275. </Title>
  276. </Header>
  277. {/* 主体内容 */}
  278. <Content
  279. style={{
  280. padding: '16px',
  281. maxHeight: '100%',
  282. overflowY: 'auto',
  283. position: 'relative',
  284. }}
  285. >
  286. {/* 处理模式状态提示 */}
  287. {isWASMMode && (
  288. <Alert
  289. message={
  290. isInitializing
  291. ? "🔧 WASM模式初始化中..."
  292. : sdkError
  293. ? "❌ WASM模式初始化失败"
  294. : isSDKReady
  295. ? "✅ WASM本地增强模式已启用"
  296. : "⏳ WASM模式准备中..."
  297. }
  298. description={
  299. sdkError
  300. ? `错误: ${sdkError.message}。将使用传统模式。`
  301. : isSDKReady
  302. ? "使用本地WASM算法处理16-bit原始数据,实时响应更快,质量更高。"
  303. : "正在加载WASM模块和TIF数据..."
  304. }
  305. type={sdkError ? "error" : isSDKReady ? "success" : "info"}
  306. showIcon
  307. style={{ marginBottom: 16 }}
  308. />
  309. )}
  310. {!isWASMMode && (
  311. <Alert
  312. message="📡 传统模式"
  313. description="使用后端API处理图像参数。更改设置请前往:系统设置 > 首选项 > 高级处理模式"
  314. type="info"
  315. showIcon
  316. closable
  317. style={{ marginBottom: 16 }}
  318. />
  319. )}
  320. {isLoading && (
  321. <div
  322. style={{
  323. position: 'absolute',
  324. top: '50%',
  325. left: '50%',
  326. transform: 'translate(-50%, -50%)',
  327. zIndex: 1000,
  328. }}
  329. >
  330. <Spin size="large" tip="加载参数中..." />
  331. </div>
  332. )}
  333. <div style={{ opacity: isLoading ? 0.5 : 1 }}>
  334. {/* 增益滑块 */}
  335. <ParameterSlider
  336. label="增益"
  337. value={parameters.contrast}
  338. min={PARAMETER_RANGES.gain.min}
  339. max={PARAMETER_RANGES.gain.max}
  340. step={PARAMETER_RANGES.gain.step}
  341. range={`${PARAMETER_RANGES.gain.min}~${PARAMETER_RANGES.gain.max}`}
  342. onChange={(value) => handleParameterChange('contrast', value)}
  343. disabled={isLoading}
  344. />
  345. {/* 细节滑块 */}
  346. <ParameterSlider
  347. label="细节"
  348. value={parameters.detail}
  349. min={PARAMETER_RANGES.detail.min}
  350. max={PARAMETER_RANGES.detail.max}
  351. step={PARAMETER_RANGES.detail.step}
  352. range={`${PARAMETER_RANGES.detail.min}~${PARAMETER_RANGES.detail.max}`}
  353. onChange={(value) => handleParameterChange('detail', value)}
  354. disabled={isLoading}
  355. />
  356. {/* 动态范围滑块 */}
  357. <ParameterSlider
  358. label="动态范围"
  359. value={parameters.latitude}
  360. min={PARAMETER_RANGES.latitude.min}
  361. max={PARAMETER_RANGES.latitude.max}
  362. step={PARAMETER_RANGES.latitude.step}
  363. range={`${PARAMETER_RANGES.latitude.min}~${PARAMETER_RANGES.latitude.max}`}
  364. onChange={(value) => handleParameterChange('latitude', value)}
  365. disabled={isLoading}
  366. />
  367. {/* 噪声模式滑块 */}
  368. <ParameterSlider
  369. label="噪声模式"
  370. value={parameters.noise}
  371. min={PARAMETER_RANGES.noise.min}
  372. max={PARAMETER_RANGES.noise.max}
  373. step={PARAMETER_RANGES.noise.step}
  374. range={`${PARAMETER_RANGES.noise.min}~${PARAMETER_RANGES.noise.max}`}
  375. onChange={(value) => handleParameterChange('noise', value)}
  376. disabled={isLoading}
  377. />
  378. {/* 对比度滑块(前端维护) */}
  379. <ParameterSlider
  380. label="对比度"
  381. value={parameters.brightness}
  382. min={PARAMETER_RANGES.brightness.min}
  383. max={PARAMETER_RANGES.brightness.max}
  384. step={PARAMETER_RANGES.brightness.step}
  385. range={`${PARAMETER_RANGES.brightness.min}~${PARAMETER_RANGES.brightness.max}`}
  386. onChange={(value) => handleParameterChange('brightness', value)}
  387. disabled={isLoading}
  388. />
  389. {/* 亮度滑块(前端维护) */}
  390. <ParameterSlider
  391. label="亮度"
  392. value={parameters.sharpness}
  393. min={PARAMETER_RANGES.sharpness.min}
  394. max={PARAMETER_RANGES.sharpness.max}
  395. step={PARAMETER_RANGES.sharpness.step}
  396. range={`${PARAMETER_RANGES.sharpness.min}~${PARAMETER_RANGES.sharpness.max}`}
  397. onChange={(value) => handleParameterChange('sharpness', value)}
  398. disabled={isLoading}
  399. />
  400. {/* 算法选择 */}
  401. <div style={{ marginBottom: '16px' }}>
  402. <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
  403. 算法
  404. </Typography.Text>
  405. <Select
  406. value={selectedAlgorithm}
  407. onChange={handleAlgorithmChange}
  408. style={{ width: '100%' }}
  409. disabled={isLoading}
  410. >
  411. {ALGORITHM_OPTIONS.map((algo) => (
  412. <Option key={algo} value={algo}>
  413. {algo}
  414. </Option>
  415. ))}
  416. </Select>
  417. </div>
  418. {/* LUT 选择 */}
  419. <div style={{ marginBottom: '16px' }}>
  420. <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
  421. LUT
  422. </Typography.Text>
  423. <Select
  424. value={selectedLUT}
  425. onChange={handleLUTChange}
  426. style={{ width: '100%' }}
  427. disabled={isLoading}
  428. >
  429. {LUT_OPTIONS.map((lut) => (
  430. <Option key={lut} value={lut}>
  431. {lut}
  432. </Option>
  433. ))}
  434. </Select>
  435. </div>
  436. {/* 风格选择 */}
  437. <div style={{ marginBottom: '16px' }}>
  438. <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
  439. 风格
  440. </Typography.Text>
  441. <Select
  442. value={selectedStyle}
  443. onChange={handleStyleChange}
  444. style={{ width: '100%' }}
  445. disabled={isLoading}
  446. >
  447. <Option value="均衡">均衡</Option>
  448. <Option value="高对比">高对比</Option>
  449. <Option value="锐组织">锐组织</Option>
  450. <Option value="骨骼">骨骼</Option>
  451. </Select>
  452. </div>
  453. {/* 底部按钮 */}
  454. <div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
  455. <Button
  456. onClick={handleReset}
  457. disabled={isLoading || isSaving}
  458. style={{
  459. flex: 1,
  460. height: '40px',
  461. fontSize: '14px',
  462. }}
  463. >
  464. 重置参数
  465. </Button>
  466. <Button
  467. type="primary"
  468. onClick={handleSave}
  469. loading={isSaving}
  470. disabled={isLoading}
  471. style={{
  472. flex: 1,
  473. height: '40px',
  474. fontSize: '14px',
  475. backgroundColor: '#13c2c2',
  476. borderColor: '#13c2c2',
  477. }}
  478. >
  479. 保存参数
  480. </Button>
  481. </div>
  482. </div>
  483. </Content>
  484. </Layout>
  485. );
  486. };
  487. export default SliderAdjustmentPanel;