| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- import React, { useEffect, useCallback, useRef } from 'react';
- import { Layout, Button, Typography, Select, message, Spin, Alert } from 'antd';
- import { ArrowLeftOutlined } from '@ant-design/icons';
- import { useDispatch } from 'react-redux';
- import { useAppSelector } from '@/states/store';
- import { useImageEnhancementSDK } from '@/hooks/useImageEnhancementSDK';
- import { selectProcessingMode } from '@/states/system/processingModeSlice';
- import { IMAGE_LOADER_PREFIX } from '@/lib/image-enhancement-sdk';
- import { switchToAdvancedProcessingPanel } from '../../../states/panelSwitchSliceForView';
- import {
- loadImageProcessingParams,
- saveProcessingParams,
- updateParameter,
- applyPreset,
- resetToPreset,
- setAlgorithm,
- setLUT,
- selectParameters,
- selectSelectedStyle,
- selectSelectedAlgorithm,
- selectSelectedLUT,
- selectIsLoading,
- selectIsSaving,
- selectIsInitialLoad,
- selectError,
- selectCurrentImageId,
- } from '../../../states/view/sliderAdjustmentPanelSlice';
- import { buildProcessedDcmUrl } from '../../../API/imageActions';
- import { updateViewerUrl } from '../../../states/view/viewerContainerSlice';
- 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;
- const { Option } = Select;
- /**
- * 滑动参数调节面板
- * 三级面板,用于调整图像处理参数
- */
- const SliderAdjustmentPanel = () => {
- const dispatch = useDispatch();
- // 从 Redux 获取状态
- const parameters = useAppSelector(selectParameters);
- const selectedStyle = useAppSelector(selectSelectedStyle);
- const selectedAlgorithm = useAppSelector(selectSelectedAlgorithm);
- const selectedLUT = useAppSelector(selectSelectedLUT);
- const isLoading = useAppSelector(selectIsLoading);
- const isSaving = useAppSelector(selectIsSaving);
- const isInitialLoad = useAppSelector(selectIsInitialLoad);
- const error = useAppSelector(selectError);
- const currentImageId = useAppSelector(selectCurrentImageId);
- // 获取处理模式配置
- const processingMode = useAppSelector(selectProcessingMode);
- const isWASMMode = processingMode === 'wasm';
- // 初始化WASM SDK(仅在WASM模式下)
- const { sdk, isSDKReady, isInitializing, error: sdkError } = useImageEnhancementSDK({
- sopInstanceUid: currentImageId || '',
- enabled: isWASMMode,
- });
- // 防抖定时器
- const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
- // 组件挂载时加载参数
- useEffect(() => {
- // 从当前选中的图像获取 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);
- }
- }, [dispatch]);
- // 监听错误
- useEffect(() => {
- if (error) {
- message.error(error);
- }
- }, [error]);
- /**
- * 返回上级面板
- */
- const handleReturn = () => {
- dispatch(switchToAdvancedProcessingPanel());
- };
- /**
- * 传统模式:防抖更新预览图像URL
- */
- const debouncedPreview = useCallback(
- (params: FullProcessingParams) => {
- // 如果是初始加载,不触发预览
- if (isInitialLoad) {
- return;
- }
- // 清除之前的定时器
- if (saveTimerRef.current) {
- clearTimeout(saveTimerRef.current);
- }
- // 设置新的定时器
- saveTimerRef.current = setTimeout(() => {
- if (currentImageId) {
- try {
- // 获取原始URL
- const dcmUrls = store.getState().viewerContainer.selectedViewers;
- if (dcmUrls.length !== 1) {
- console.error('没有选中的图像或者数量大于1,无法更新预览');
- return;
- }
- const originalUrl = dcmUrls[0];
- // 构建处理后的dcm URL(带参数)
- const processedUrl = buildProcessedDcmUrl(currentImageId, {
- contrast: params.contrast.toString(),
- detail: params.detail.toString(),
- latitude: params.latitude.toString(),
- noise: params.noise.toString(),
- });
- // 更新viewer URL以触发重新渲染
- dispatch(updateViewerUrl({
- originalUrl,
- newUrl: `dicomweb:${processedUrl}`,
- }));
- console.log('✅ [传统模式] 已更新预览图像URL:', processedUrl);
- console.log('参数:', params);
- } catch (error) {
- console.error('更新预览图像失败:', error);
- message.error('更新预览图像失败');
- }
- }
- }, 500); // 500ms 防抖延迟
- },
- [currentImageId, isInitialLoad, dispatch]
- );
- /**
- * WASM模式:防抖更新预览
- */
- const debouncedWASMPreview = useCallback(
- (params: FullProcessingParams) => {
- if (isInitialLoad || !sdk || !currentImageId) {
- return;
- }
- // 清除之前的定时器
- if (saveTimerRef.current) {
- clearTimeout(saveTimerRef.current);
- }
- // 设置新的定时器(WASM模式更短的防抖时间)
- saveTimerRef.current = setTimeout(async () => {
- try {
- console.log('🔧 [WASM模式] 开始应用参数...');
- // 1. 更新SDK参数管理器
- sdk.parameterManager.updateParameters(params);
- // 2. 生成唯一imageId(带时间戳强制重新加载)
- const timestamp = Date.now();
- const enhancedImageId = `${IMAGE_LOADER_PREFIX}${currentImageId}_${timestamp}`;
- // 3. 获取原始URL
- const dcmUrls = store.getState().viewerContainer.selectedViewers;
- if (dcmUrls.length !== 1) {
- console.error('没有选中的图像或者数量大于1');
- return;
- }
- const originalUrl = dcmUrls[0];
- // 4. 更新viewer URL以触发重新加载
- dispatch(updateViewerUrl({
- originalUrl,
- newUrl: enhancedImageId,
- }));
- console.log('✅ [WASM模式] WASM增强完成,图像已重新加载');
- console.log('参数:', params);
- } catch (error) {
- console.error('❌ [WASM模式] 参数应用失败:', error);
- message.error('WASM参数应用失败');
- }
- }, 300); // WASM模式使用更短的防抖时间
- },
- [currentImageId, isInitialLoad, sdk, dispatch]
- );
- /**
- * 参数变化处理(双模式支持)
- */
- const handleParameterChange = (name: keyof FullProcessingParams, value: number) => {
- dispatch(updateParameter({ name, value }));
- // 触发防抖预览(仅后端参数触发预览)
- const newParams = { ...parameters, [name]: value };
- // 只有后端参数(contrast, detail, latitude, noise)才触发预览
- if (['contrast', 'detail', 'latitude', 'noise', 'brightness', 'sharpness'].includes(name)) {
- // 根据处理模式选择预览函数
- if (isWASMMode) {
- debouncedWASMPreview(newParams);
- } else {
- debouncedPreview(newParams);
- }
- }
- };
- /**
- * 风格变化处理
- */
- const handleStyleChange = (value: ProcessingStyle) => {
- dispatch(applyPreset(value));
- // 风格切换后立即保存
- if (currentImageId) {
- const preset = parameters; // 应用风格后的参数会在 Redux 中更新
- setTimeout(() => {
- dispatch(
- saveProcessingParams({
- sopInstanceUid: currentImageId,
- params: {
- contrast: preset.contrast,
- detail: preset.detail,
- latitude: preset.latitude,
- noise: preset.noise,
- ww_coef: preset.brightness,
- wl_coef: preset.sharpness,
- },
- }) as any
- );
- }, 100);
- }
- };
- /**
- * 算法变化处理
- */
- const handleAlgorithmChange = (value: string) => {
- dispatch(setAlgorithm(value));
- };
- /**
- * LUT 变化处理
- */
- const handleLUTChange = (value: LUTType) => {
- dispatch(setLUT(value));
- };
- /**
- * 重置参数
- */
- const handleReset = () => {
- dispatch(resetToPreset());
- message.success('已重置为当前风格的默认参数');
- // 重置后保存
- if (currentImageId) {
- setTimeout(() => {
- dispatch(
- saveProcessingParams({
- sopInstanceUid: currentImageId,
- params: {
- contrast: parameters.contrast,
- detail: parameters.detail,
- latitude: parameters.latitude,
- noise: parameters.noise,
- ww_coef: parameters.brightness,
- wl_coef: parameters.sharpness,
- },
- }) as any
- );
- }, 100);
- }
- };
- /**
- * 手动保存参数
- */
- const handleSave = () => {
- if (currentImageId) {
- dispatch(
- saveProcessingParams({
- sopInstanceUid: currentImageId,
- params: {
- contrast: parameters.contrast,
- detail: parameters.detail,
- latitude: parameters.latitude,
- noise: parameters.noise,
- ww_coef: parameters.brightness,
- wl_coef: parameters.sharpness,
- },
- }) as any
- ).then(() => {
- message.success('参数保存成功');
- });
- }
- };
- // 清理定时器
- useEffect(() => {
- return () => {
- if (saveTimerRef.current) {
- clearTimeout(saveTimerRef.current);
- }
- };
- }, []);
- return (
- <Layout className="h-full">
- {/* 顶部导航栏 */}
- <Header
- style={{
- display: 'flex',
- alignItems: 'center',
- padding: '0 16px',
- }}
- >
- <Button
- type="text"
- icon={<ArrowLeftOutlined />}
- onClick={handleReturn}
- />
- <Title level={5} style={{ margin: 0, lineHeight: '48px' }}>
- 滑动参数调节
- </Title>
- </Header>
- {/* 主体内容 */}
- <Content
- style={{
- padding: '16px',
- maxHeight: '100%',
- overflowY: 'auto',
- position: 'relative',
- }}
- >
- {/* 处理模式状态提示 */}
- {isWASMMode && (
- <Alert
- message={
- isInitializing
- ? "🔧 WASM模式初始化中..."
- : sdkError
- ? "❌ WASM模式初始化失败"
- : isSDKReady
- ? "✅ WASM本地增强模式已启用"
- : "⏳ WASM模式准备中..."
- }
- description={
- sdkError
- ? `错误: ${sdkError.message}。将使用传统模式。`
- : isSDKReady
- ? "使用本地WASM算法处理16-bit原始数据,实时响应更快,质量更高。"
- : "正在加载WASM模块和TIF数据..."
- }
- type={sdkError ? "error" : isSDKReady ? "success" : "info"}
- showIcon
- style={{ marginBottom: 16 }}
- />
- )}
- {!isWASMMode && (
- <Alert
- message="📡 传统模式"
- description="使用后端API处理图像参数。更改设置请前往:系统设置 > 首选项 > 高级处理模式"
- type="info"
- showIcon
- closable
- style={{ marginBottom: 16 }}
- />
- )}
- {isLoading && (
- <div
- style={{
- position: 'absolute',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- zIndex: 1000,
- }}
- >
- <Spin size="large" tip="加载参数中..." />
- </div>
- )}
- <div style={{ opacity: isLoading ? 0.5 : 1 }}>
- {/* 增益滑块 */}
- <ParameterSlider
- label="增益"
- value={parameters.contrast}
- min={PARAMETER_RANGES.gain.min}
- max={PARAMETER_RANGES.gain.max}
- step={PARAMETER_RANGES.gain.step}
- range={`${PARAMETER_RANGES.gain.min}~${PARAMETER_RANGES.gain.max}`}
- onChange={(value) => handleParameterChange('contrast', value)}
- disabled={isLoading}
- />
- {/* 细节滑块 */}
- <ParameterSlider
- label="细节"
- value={parameters.detail}
- min={PARAMETER_RANGES.detail.min}
- max={PARAMETER_RANGES.detail.max}
- step={PARAMETER_RANGES.detail.step}
- range={`${PARAMETER_RANGES.detail.min}~${PARAMETER_RANGES.detail.max}`}
- onChange={(value) => handleParameterChange('detail', value)}
- disabled={isLoading}
- />
- {/* 动态范围滑块 */}
- <ParameterSlider
- label="动态范围"
- value={parameters.latitude}
- min={PARAMETER_RANGES.latitude.min}
- max={PARAMETER_RANGES.latitude.max}
- step={PARAMETER_RANGES.latitude.step}
- range={`${PARAMETER_RANGES.latitude.min}~${PARAMETER_RANGES.latitude.max}`}
- onChange={(value) => handleParameterChange('latitude', value)}
- disabled={isLoading}
- />
- {/* 噪声模式滑块 */}
- <ParameterSlider
- label="噪声模式"
- value={parameters.noise}
- min={PARAMETER_RANGES.noise.min}
- max={PARAMETER_RANGES.noise.max}
- step={PARAMETER_RANGES.noise.step}
- range={`${PARAMETER_RANGES.noise.min}~${PARAMETER_RANGES.noise.max}`}
- onChange={(value) => handleParameterChange('noise', value)}
- disabled={isLoading}
- />
- {/* 对比度滑块(前端维护) */}
- <ParameterSlider
- label="对比度"
- value={parameters.brightness}
- min={PARAMETER_RANGES.brightness.min}
- max={PARAMETER_RANGES.brightness.max}
- step={PARAMETER_RANGES.brightness.step}
- range={`${PARAMETER_RANGES.brightness.min}~${PARAMETER_RANGES.brightness.max}`}
- onChange={(value) => handleParameterChange('brightness', value)}
- disabled={isLoading}
- />
- {/* 亮度滑块(前端维护) */}
- <ParameterSlider
- label="亮度"
- value={parameters.sharpness}
- min={PARAMETER_RANGES.sharpness.min}
- max={PARAMETER_RANGES.sharpness.max}
- step={PARAMETER_RANGES.sharpness.step}
- range={`${PARAMETER_RANGES.sharpness.min}~${PARAMETER_RANGES.sharpness.max}`}
- onChange={(value) => handleParameterChange('sharpness', value)}
- disabled={isLoading}
- />
- {/* 算法选择 */}
- <div style={{ marginBottom: '16px' }}>
- <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
- 算法
- </Typography.Text>
- <Select
- value={selectedAlgorithm}
- onChange={handleAlgorithmChange}
- style={{ width: '100%' }}
- disabled={isLoading}
- >
- {ALGORITHM_OPTIONS.map((algo) => (
- <Option key={algo} value={algo}>
- {algo}
- </Option>
- ))}
- </Select>
- </div>
- {/* LUT 选择 */}
- <div style={{ marginBottom: '16px' }}>
- <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
- LUT
- </Typography.Text>
- <Select
- value={selectedLUT}
- onChange={handleLUTChange}
- style={{ width: '100%' }}
- disabled={isLoading}
- >
- {LUT_OPTIONS.map((lut) => (
- <Option key={lut} value={lut}>
- {lut}
- </Option>
- ))}
- </Select>
- </div>
- {/* 风格选择 */}
- <div style={{ marginBottom: '16px' }}>
- <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
- 风格
- </Typography.Text>
- <Select
- value={selectedStyle}
- onChange={handleStyleChange}
- style={{ width: '100%' }}
- disabled={isLoading}
- >
- <Option value="均衡">均衡</Option>
- <Option value="高对比">高对比</Option>
- <Option value="锐组织">锐组织</Option>
- <Option value="骨骼">骨骼</Option>
- </Select>
- </div>
- {/* 底部按钮 */}
- <div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
- <Button
- onClick={handleReset}
- disabled={isLoading || isSaving}
- style={{
- flex: 1,
- height: '40px',
- fontSize: '14px',
- }}
- >
- 重置参数
- </Button>
- <Button
- type="primary"
- onClick={handleSave}
- loading={isSaving}
- disabled={isLoading}
- style={{
- flex: 1,
- height: '40px',
- fontSize: '14px',
- backgroundColor: '#13c2c2',
- borderColor: '#13c2c2',
- }}
- >
- 保存参数
- </Button>
- </div>
- </div>
- </Content>
- </Layout>
- );
- };
- export default SliderAdjustmentPanel;
|