SliderAdjustmentPanel.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  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. });
  123. // 更新viewer URL以触发重新渲染
  124. dispatch(updateViewerUrl({
  125. originalUrl,
  126. newUrl: `dicomweb:${processedUrl}`,
  127. }));
  128. console.log('✅ [传统模式] 已更新预览图像URL:', processedUrl);
  129. console.log('参数:', params);
  130. } catch (error) {
  131. console.error('更新预览图像失败:', error);
  132. message.error('更新预览图像失败');
  133. }
  134. }
  135. }, 500); // 500ms 防抖延迟
  136. },
  137. [currentImageId, isInitialLoad, dispatch]
  138. );
  139. /**
  140. * WASM模式:防抖更新预览
  141. */
  142. const debouncedWASMPreview = useCallback(
  143. (params: FullProcessingParams) => {
  144. if (isInitialLoad || !sdk || !currentImageId) {
  145. return;
  146. }
  147. // 清除之前的定时器
  148. if (saveTimerRef.current) {
  149. clearTimeout(saveTimerRef.current);
  150. }
  151. // 设置新的定时器(WASM模式更短的防抖时间)
  152. saveTimerRef.current = setTimeout(async () => {
  153. try {
  154. console.log('🔧 [WASM模式] 开始应用参数...');
  155. // 1. 更新SDK参数管理器
  156. sdk.parameterManager.updateParameters(params);
  157. // 2. 生成唯一imageId(带时间戳强制重新加载)
  158. const timestamp = Date.now();
  159. const enhancedImageId = `${IMAGE_LOADER_PREFIX}${currentImageId}_${timestamp}`;
  160. // 3. 获取原始URL
  161. const dcmUrls = store.getState().viewerContainer.selectedViewers;
  162. if (dcmUrls.length !== 1) {
  163. console.error('没有选中的图像或者数量大于1');
  164. return;
  165. }
  166. const originalUrl = dcmUrls[0];
  167. // 4. 更新viewer URL以触发重新加载
  168. dispatch(updateViewerUrl({
  169. originalUrl,
  170. newUrl: enhancedImageId,
  171. }));
  172. console.log('✅ [WASM模式] WASM增强完成,图像已重新加载');
  173. console.log('参数:', params);
  174. } catch (error) {
  175. console.error('❌ [WASM模式] 参数应用失败:', error);
  176. message.error('WASM参数应用失败');
  177. }
  178. }, 300); // WASM模式使用更短的防抖时间
  179. },
  180. [currentImageId, isInitialLoad, sdk, dispatch]
  181. );
  182. /**
  183. * 参数变化处理(双模式支持)
  184. */
  185. const handleParameterChange = (name: keyof FullProcessingParams, value: number) => {
  186. dispatch(updateParameter({ name, value }));
  187. // 触发防抖预览(仅后端参数触发预览)
  188. const newParams = { ...parameters, [name]: value };
  189. // 只有后端参数(contrast, detail, latitude, noise)才触发预览
  190. if (['contrast', 'detail', 'latitude', 'noise', 'brightness', 'sharpness'].includes(name)) {
  191. // 根据处理模式选择预览函数
  192. if (isWASMMode) {
  193. debouncedWASMPreview(newParams);
  194. } else {
  195. debouncedPreview(newParams);
  196. }
  197. }
  198. };
  199. /**
  200. * 风格变化处理
  201. */
  202. const handleStyleChange = (value: ProcessingStyle) => {
  203. dispatch(applyPreset(value));
  204. // 风格切换后立即保存
  205. if (currentImageId) {
  206. const preset = parameters; // 应用风格后的参数会在 Redux 中更新
  207. setTimeout(() => {
  208. dispatch(
  209. saveProcessingParams({
  210. sopInstanceUid: currentImageId,
  211. params: {
  212. contrast: preset.contrast,
  213. detail: preset.detail,
  214. latitude: preset.latitude,
  215. noise: preset.noise,
  216. ww_coef: preset.brightness,
  217. wl_coef: preset.sharpness,
  218. },
  219. }) as any
  220. );
  221. }, 100);
  222. }
  223. };
  224. /**
  225. * 算法变化处理
  226. */
  227. const handleAlgorithmChange = (value: string) => {
  228. dispatch(setAlgorithm(value));
  229. };
  230. /**
  231. * LUT 变化处理
  232. */
  233. const handleLUTChange = (value: LUTType) => {
  234. dispatch(setLUT(value));
  235. };
  236. /**
  237. * 重置参数
  238. */
  239. const handleReset = () => {
  240. dispatch(resetToPreset());
  241. message.success('已重置为当前风格的默认参数');
  242. // 重置后保存
  243. if (currentImageId) {
  244. setTimeout(() => {
  245. dispatch(
  246. saveProcessingParams({
  247. sopInstanceUid: currentImageId,
  248. params: {
  249. contrast: parameters.contrast,
  250. detail: parameters.detail,
  251. latitude: parameters.latitude,
  252. noise: parameters.noise,
  253. ww_coef: parameters.brightness,
  254. wl_coef: parameters.sharpness,
  255. },
  256. }) as any
  257. );
  258. }, 100);
  259. }
  260. };
  261. /**
  262. * 手动保存参数
  263. */
  264. const handleSave = () => {
  265. if (currentImageId) {
  266. dispatch(
  267. saveProcessingParams({
  268. sopInstanceUid: currentImageId,
  269. params: {
  270. contrast: parameters.contrast,
  271. detail: parameters.detail,
  272. latitude: parameters.latitude,
  273. noise: parameters.noise,
  274. ww_coef: parameters.brightness,
  275. wl_coef: parameters.sharpness,
  276. },
  277. }) as any
  278. ).then(() => {
  279. message.success('参数保存成功');
  280. });
  281. }
  282. };
  283. // 清理定时器
  284. useEffect(() => {
  285. return () => {
  286. if (saveTimerRef.current) {
  287. clearTimeout(saveTimerRef.current);
  288. }
  289. };
  290. }, []);
  291. return (
  292. <Layout className="h-full">
  293. {/* 顶部导航栏 */}
  294. <Header
  295. style={{
  296. display: 'flex',
  297. alignItems: 'center',
  298. padding: '0 16px',
  299. }}
  300. >
  301. <Button
  302. type="text"
  303. icon={<ArrowLeftOutlined />}
  304. onClick={handleReturn}
  305. />
  306. <Title level={5} style={{ margin: 0, lineHeight: '48px' }}>
  307. 滑动参数调节
  308. </Title>
  309. </Header>
  310. {/* 主体内容 */}
  311. <Content
  312. style={{
  313. padding: '16px',
  314. maxHeight: '100%',
  315. overflowY: 'auto',
  316. position: 'relative',
  317. }}
  318. >
  319. {/* 处理模式状态提示 */}
  320. {isWASMMode && (
  321. <Alert
  322. message={
  323. isInitializing
  324. ? "🔧 WASM模式初始化中..."
  325. : sdkError
  326. ? "❌ WASM模式初始化失败"
  327. : isSDKReady
  328. ? "✅ WASM本地增强模式已启用"
  329. : "⏳ WASM模式准备中..."
  330. }
  331. description={
  332. sdkError
  333. ? `错误: ${sdkError.message}。将使用传统模式。`
  334. : isSDKReady
  335. ? "使用本地WASM算法处理16-bit原始数据,实时响应更快,质量更高。"
  336. : "正在加载WASM模块和TIF数据..."
  337. }
  338. type={sdkError ? "error" : isSDKReady ? "success" : "info"}
  339. showIcon
  340. style={{ marginBottom: 16 }}
  341. />
  342. )}
  343. {!isWASMMode && (
  344. <Alert
  345. message="📡 传统模式"
  346. description="使用后端API处理图像参数。更改设置请前往:系统设置 > 首选项 > 高级处理模式"
  347. type="info"
  348. showIcon
  349. closable
  350. style={{ marginBottom: 16 }}
  351. />
  352. )}
  353. {isLoading && (
  354. <div
  355. style={{
  356. position: 'absolute',
  357. top: '50%',
  358. left: '50%',
  359. transform: 'translate(-50%, -50%)',
  360. zIndex: 1000,
  361. }}
  362. >
  363. <Spin size="large" tip="加载参数中..." />
  364. </div>
  365. )}
  366. <div style={{ opacity: isLoading ? 0.5 : 1 }}>
  367. {/* 增益滑块 */}
  368. <ParameterSlider
  369. label="增益"
  370. value={parameters.contrast}
  371. min={PARAMETER_RANGES.gain.min}
  372. max={PARAMETER_RANGES.gain.max}
  373. step={PARAMETER_RANGES.gain.step}
  374. range={`${PARAMETER_RANGES.gain.min}~${PARAMETER_RANGES.gain.max}`}
  375. onChange={(value) => handleParameterChange('contrast', value)}
  376. disabled={isLoading}
  377. />
  378. {/* 细节滑块 */}
  379. <ParameterSlider
  380. label="细节"
  381. value={parameters.detail}
  382. min={PARAMETER_RANGES.detail.min}
  383. max={PARAMETER_RANGES.detail.max}
  384. step={PARAMETER_RANGES.detail.step}
  385. range={`${PARAMETER_RANGES.detail.min}~${PARAMETER_RANGES.detail.max}`}
  386. onChange={(value) => handleParameterChange('detail', value)}
  387. disabled={isLoading}
  388. />
  389. {/* 动态范围滑块 */}
  390. <ParameterSlider
  391. label="动态范围"
  392. value={parameters.latitude}
  393. min={PARAMETER_RANGES.latitude.min}
  394. max={PARAMETER_RANGES.latitude.max}
  395. step={PARAMETER_RANGES.latitude.step}
  396. range={`${PARAMETER_RANGES.latitude.min}~${PARAMETER_RANGES.latitude.max}`}
  397. onChange={(value) => handleParameterChange('latitude', value)}
  398. disabled={isLoading}
  399. />
  400. {/* 噪声模式滑块 */}
  401. <ParameterSlider
  402. label="噪声模式"
  403. value={parameters.noise}
  404. min={PARAMETER_RANGES.noise.min}
  405. max={PARAMETER_RANGES.noise.max}
  406. step={PARAMETER_RANGES.noise.step}
  407. range={`${PARAMETER_RANGES.noise.min}~${PARAMETER_RANGES.noise.max}`}
  408. onChange={(value) => handleParameterChange('noise', value)}
  409. disabled={isLoading}
  410. />
  411. {/* 对比度滑块(前端维护) */}
  412. <ParameterSlider
  413. label="对比度"
  414. value={parameters.brightness}
  415. min={PARAMETER_RANGES.brightness.min}
  416. max={PARAMETER_RANGES.brightness.max}
  417. step={PARAMETER_RANGES.brightness.step}
  418. range={`${PARAMETER_RANGES.brightness.min}~${PARAMETER_RANGES.brightness.max}`}
  419. onChange={(value) => handleParameterChange('brightness', value)}
  420. disabled={isLoading}
  421. />
  422. {/* 亮度滑块(前端维护) */}
  423. <ParameterSlider
  424. label="亮度"
  425. value={parameters.sharpness}
  426. min={PARAMETER_RANGES.sharpness.min}
  427. max={PARAMETER_RANGES.sharpness.max}
  428. step={PARAMETER_RANGES.sharpness.step}
  429. range={`${PARAMETER_RANGES.sharpness.min}~${PARAMETER_RANGES.sharpness.max}`}
  430. onChange={(value) => handleParameterChange('sharpness', value)}
  431. disabled={isLoading}
  432. />
  433. {/* 算法选择 */}
  434. <div style={{ marginBottom: '16px' }}>
  435. <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
  436. 算法
  437. </Typography.Text>
  438. <Select
  439. value={selectedAlgorithm}
  440. onChange={handleAlgorithmChange}
  441. style={{ width: '100%' }}
  442. disabled={isLoading}
  443. >
  444. {ALGORITHM_OPTIONS.map((algo) => (
  445. <Option key={algo} value={algo}>
  446. {algo}
  447. </Option>
  448. ))}
  449. </Select>
  450. </div>
  451. {/* LUT 选择 */}
  452. <div style={{ marginBottom: '16px' }}>
  453. <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
  454. LUT
  455. </Typography.Text>
  456. <Select
  457. value={selectedLUT}
  458. onChange={handleLUTChange}
  459. style={{ width: '100%' }}
  460. disabled={isLoading}
  461. >
  462. {LUT_OPTIONS.map((lut) => (
  463. <Option key={lut} value={lut}>
  464. {lut}
  465. </Option>
  466. ))}
  467. </Select>
  468. </div>
  469. {/* 风格选择 */}
  470. <div style={{ marginBottom: '16px' }}>
  471. <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
  472. 风格
  473. </Typography.Text>
  474. <Select
  475. value={selectedStyle}
  476. onChange={handleStyleChange}
  477. style={{ width: '100%' }}
  478. disabled={isLoading}
  479. >
  480. <Option value="均衡">均衡</Option>
  481. <Option value="高对比">高对比</Option>
  482. <Option value="锐组织">锐组织</Option>
  483. <Option value="骨骼">骨骼</Option>
  484. </Select>
  485. </div>
  486. {/* 底部按钮 */}
  487. <div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
  488. <Button
  489. onClick={handleReset}
  490. disabled={isLoading || isSaving}
  491. style={{
  492. flex: 1,
  493. height: '40px',
  494. fontSize: '14px',
  495. }}
  496. >
  497. 重置参数
  498. </Button>
  499. <Button
  500. type="primary"
  501. onClick={handleSave}
  502. loading={isSaving}
  503. disabled={isLoading}
  504. style={{
  505. flex: 1,
  506. height: '40px',
  507. fontSize: '14px',
  508. backgroundColor: '#13c2c2',
  509. borderColor: '#13c2c2',
  510. }}
  511. >
  512. 保存参数
  513. </Button>
  514. </div>
  515. </div>
  516. </Content>
  517. </Layout>
  518. );
  519. };
  520. export default SliderAdjustmentPanel;