stack.image.viewer.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import React, { useEffect, useRef } from 'react';
  2. import * as cornerstone from '@cornerstonejs/core';
  3. import type { Types } from '@cornerstonejs/core';
  4. import * as cornerstoneTools from '@cornerstonejs/tools';
  5. import * as cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
  6. import { useSelector } from 'react-redux';
  7. import { RootState } from '@/states/store';
  8. import { SystemMode } from '@/states/systemModeSlice';
  9. import store from '@/states/store';
  10. import { clearAction } from '@/states/view/functionAreaSlice';
  11. import { useDispatch } from 'react-redux';
  12. const {
  13. PanTool,
  14. WindowLevelTool,
  15. StackScrollTool,
  16. ZoomTool,
  17. LabelTool,
  18. ToolGroupManager,
  19. Enums: csToolsEnums,
  20. PlanarRotateTool,
  21. } = cornerstoneTools;
  22. const { MouseBindings } = csToolsEnums;
  23. let toolGroup: cornerstoneTools.Types.IToolGroup;
  24. let currentViewportId: string;
  25. function registerTools(viewportId, renderingEngineId) {
  26. // Add tools to Cornerstone3D
  27. cornerstoneTools.addTool(PanTool);
  28. cornerstoneTools.addTool(WindowLevelTool);
  29. cornerstoneTools.addTool(StackScrollTool);
  30. cornerstoneTools.addTool(ZoomTool);
  31. cornerstoneTools.addTool(LabelTool);
  32. cornerstoneTools.addTool(PlanarRotateTool);
  33. // Define a tool group
  34. const toolGroupId = 'STACK_TOOL_GROUP_ID';
  35. const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId);
  36. if (!toolGroupTmp) {
  37. return;
  38. }
  39. toolGroup = toolGroupTmp;
  40. // Add tools to the tool group
  41. toolGroup.addTool(PanTool.toolName);
  42. toolGroup.addTool(WindowLevelTool.toolName);
  43. toolGroup.addTool(StackScrollTool.toolName);
  44. toolGroup.addTool(ZoomTool.toolName);
  45. toolGroup.addTool(LabelTool.toolName);
  46. toolGroup.addTool(PlanarRotateTool.toolName);
  47. // Set the LabelTool as active
  48. // toolGroup.setToolActive(LabelTool.toolName, {
  49. // bindings: [
  50. // {
  51. // mouseButton: MouseBindings.Primary, // Left Click
  52. // },
  53. // ],
  54. // });
  55. // Set the initial state of the tools
  56. // toolGroup.setToolActive(WindowLevelTool.toolName, {
  57. // bindings: [
  58. // {
  59. // mouseButton: MouseBindings.Primary, // Left Click
  60. // },
  61. // ],
  62. // });
  63. toolGroup.setToolActive(PanTool.toolName, {
  64. bindings: [
  65. {
  66. mouseButton: MouseBindings.Auxiliary, // Middle Click
  67. },
  68. ],
  69. });
  70. toolGroup.setToolActive(ZoomTool.toolName, {
  71. bindings: [
  72. {
  73. mouseButton: MouseBindings.Secondary, // Right Click
  74. },
  75. ],
  76. });
  77. toolGroup.setToolActive(StackScrollTool.toolName, {
  78. bindings: [
  79. {
  80. mouseButton: MouseBindings.Wheel, // Mouse Wheel
  81. },
  82. ],
  83. });
  84. toolGroup.addViewport(viewportId, renderingEngineId);
  85. }
  86. function addLMark(): void {
  87. // Implement the logic to add an L mark
  88. console.log('Adding L Mark');
  89. toolGroup.setToolActive(LabelTool.toolName, {
  90. bindings: [
  91. // {
  92. // mouseButton: MouseBindings.Primary, // Left Click
  93. // },
  94. ],
  95. });
  96. const position: Types.Point3 = [100, 100, 0]; // Example position
  97. const text = 'L'; // Predefined text
  98. LabelTool.hydrate(currentViewportId, position, text);
  99. toolGroup.setToolPassive(LabelTool.toolName, {
  100. removeAllBindings: true,
  101. });
  102. // const enabledElement = cornerstone.getEnabledElementByViewportId(currentViewportId);
  103. // cursors.elementCursor.resetElementCursor(elementRef.current as HTMLDivElement);
  104. }
  105. function addRLabel(viewportId) {
  106. toolGroup.setToolActive(LabelTool.toolName, {
  107. bindings: [],
  108. });
  109. const element = document.getElementById(viewportId);
  110. const elementHeight = element ? element.getBoundingClientRect().height : 0;
  111. const position: Types.Point3 = [100, elementHeight / 2, 0]; // Example position
  112. const text = 'R'; // Predefined text
  113. LabelTool.hydrate(viewportId, position, text);
  114. toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true });
  115. }
  116. function deleteSelectedMark(): void {
  117. const viewport =
  118. cornerstone.getEnabledElementByViewportId(currentViewportId).viewport;
  119. const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
  120. for (const annotation of allAnnotations) {
  121. if (annotation.data.text === 'L' || annotation.data.text === 'R') {
  122. cornerstoneTools.annotation.state.removeAnnotation(
  123. annotation.annotationUID!
  124. );
  125. }
  126. }
  127. viewport.render();
  128. }
  129. function HorizontalFlip(): void {
  130. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  131. .viewport as cornerstone.StackViewport;
  132. // 切换水平翻转状态
  133. const { flipHorizontal } = viewport.getCamera();
  134. viewport.setCamera({ flipHorizontal: !flipHorizontal });
  135. console.log('Flipping Image Horizontally');
  136. }
  137. function VerticalFlip(): void {
  138. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  139. .viewport as cornerstone.StackViewport;
  140. // 切换竖直翻转状态
  141. const { flipVertical } = viewport.getCamera();
  142. viewport.setCamera({ flipVertical: !flipVertical });
  143. }
  144. function ApplyColormap(): void {
  145. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  146. .viewport as cornerstone.StackViewport;
  147. // Implement the logic to apply colormap
  148. viewport.setProperties({ colormap: { name: 'hsv' } });
  149. viewport.render();
  150. console.log('Applying Colormap');
  151. }
  152. function RotateCounterclockwise90(): void {
  153. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  154. .viewport as cornerstone.StackViewport;
  155. // let { rotation } = viewport.getViewPresentation();
  156. // const currentRotataion = viewport.getRotation();
  157. // viewport.rotation(22)
  158. // console.log(`rotation:${rotation}`)
  159. // if(!!rotation){
  160. // rotation=0;
  161. // }
  162. // viewport.setViewPresentation({ rotation: rotation ?? 0 + 30 });
  163. // // // Implement the logic to rotate the image counterclockwise
  164. // // viewport.setCamera({ rotation: -90 });
  165. // viewport.render();
  166. // // 1. 获取当前的视图表现状态(一个完整的对象)
  167. // const currentViewPresentation = viewport.getViewPresentation();
  168. // // 2. 创建一个新的对象,基于当前状态,只更新你需要修改的属性
  169. // // 使用展开运算符 (...) 来复制所有现有属性
  170. // const newViewPresentation = {
  171. // ...currentViewPresentation, // 保留所有原有属性,如 scale, translation, flip 等
  172. // rotation: currentViewPresentation.rotation ?? + 30, // 只覆盖 rotation 属性
  173. // };
  174. // // 3. 将完整的新状态对象设置回 viewport
  175. // viewport.setViewPresentation(newViewPresentation);
  176. // viewport.render();
  177. //----------------------------------------
  178. // // 获取当前的完整视图状态
  179. // const currentViewState = viewport.getViewPresentation();
  180. // // 创建新的视图状态对象,保持所有其他属性不变
  181. // const newViewState = {
  182. // ...currentViewState, // 保留所有现有属性
  183. // rotation: (currentViewState.rotation ?? 0 + 30) % 360 // 只修改rotation
  184. // };
  185. // // 设置新的视图状态
  186. // viewport.setViewPresentation(newViewState);
  187. // viewport.render();
  188. //-----------------------------------------
  189. // 获取当前相机
  190. const camera = viewport.getCamera();
  191. // // 计算新的旋转角度(当前角度 + 90度)
  192. // const newRotation = (camera.rotation ?? 0) + 90;
  193. // // 转换为弧度
  194. // const radians = newRotation * Math.PI / 180;
  195. // // 计算新的viewUp向量(顺时针旋转90度)
  196. // const newViewUp:[number, number, number] = [
  197. // Math.cos(radians), // X分量
  198. // Math.sin(radians), // Y分量
  199. // 0 // Z分量
  200. // ];
  201. // // 设置新的相机参数
  202. // viewport.setCamera({
  203. // ...camera, // 保持其他相机参数不变
  204. // viewUp: newViewUp, // 更新向上向量
  205. // rotation: newRotation // 更新旋转角度(可选,但推荐)
  206. // });
  207. // 计算新的旋转角度(当前角度 + 90度)
  208. const newRotation = (camera.rotation ?? 0) + 90;
  209. // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度!
  210. const ninetyDegreesRadians = (90 * Math.PI) / 180;
  211. // 获取当前的viewUp向量
  212. const currentViewUp = camera.viewUp || [0, 1, 0];
  213. // 计算旋转后的viewUp向量(这才是正确的相对旋转)
  214. const newViewUp: [number, number, number] = [
  215. currentViewUp[0] * Math.cos(ninetyDegreesRadians) -
  216. currentViewUp[1] * Math.sin(ninetyDegreesRadians),
  217. currentViewUp[0] * Math.sin(ninetyDegreesRadians) +
  218. currentViewUp[1] * Math.cos(ninetyDegreesRadians),
  219. 0,
  220. ];
  221. // 设置新的相机参数
  222. viewport.setCamera({
  223. ...camera,
  224. viewUp: newViewUp,
  225. rotation: newRotation % 360, // 确保角度在0-359范围内
  226. });
  227. viewport.render();
  228. console.log('Rotating Image Counterclockwise 90°');
  229. }
  230. function RotateClockwise90(): void {
  231. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  232. .viewport as cornerstone.StackViewport;
  233. const camera = viewport.getCamera();
  234. // 计算新的旋转角度(当前角度 + 90度)
  235. const newRotation = (camera.rotation ?? 0) - 90;
  236. // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度!
  237. const ninetyDegreesRadians = (90 * Math.PI) / 180;
  238. // 获取当前的viewUp向量
  239. const currentViewUp = camera.viewUp || [0, 1, 0];
  240. // 计算旋转后的viewUp向量(这才是正确的相对旋转)
  241. const newViewUp: [number, number, number] = [
  242. currentViewUp[0] * Math.cos(ninetyDegreesRadians) -
  243. currentViewUp[1] * Math.sin(ninetyDegreesRadians),
  244. currentViewUp[0] * Math.sin(ninetyDegreesRadians) +
  245. currentViewUp[1] * Math.cos(ninetyDegreesRadians),
  246. 0,
  247. ];
  248. // 设置新的相机参数
  249. viewport.setCamera({
  250. ...camera,
  251. viewUp: newViewUp,
  252. rotation: newRotation % 360, // 确保角度在0-359范围内
  253. });
  254. viewport.render();
  255. console.log('Rotating Image Clockwise 90°');
  256. }
  257. function ResetImage(): void {
  258. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  259. .viewport as cornerstone.StackViewport;
  260. // Implement the logic to reset the image
  261. // Resets the viewport's camera
  262. viewport.resetCamera();
  263. // Resets the viewport's properties
  264. viewport.resetProperties();
  265. viewport.render();
  266. console.log('Resetting Image');
  267. }
  268. function InvertImage(): void {
  269. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  270. .viewport as cornerstone.StackViewport;
  271. // Implement the logic to invert the image
  272. const invert = !viewport.getProperties().invert;
  273. viewport.setProperties({ invert });
  274. viewport.render();
  275. console.log('Inverting Image');
  276. }
  277. const StackViewer = ({
  278. imageIndex = 0,
  279. imageUrls = [],
  280. }: {
  281. imageIndex?: number;
  282. imageUrls?: string[];
  283. }) => {
  284. const elementRef = useRef<HTMLDivElement>(null);
  285. const action = useSelector((state: RootState) => state.functionArea.action);
  286. const dispatch = useDispatch();
  287. useEffect(() => {
  288. const setup = async () => {
  289. // 初始化 Cornerstone
  290. cornerstone.init();
  291. cornerstoneTools.init();
  292. const state = store.getState();
  293. console.log(`当前系统模式:${state.systemMode.mode}`);
  294. const token =
  295. state.systemMode.mode === SystemMode.Emergency
  296. ? state.product.guest
  297. : state.userInfo.token;
  298. console.log(`token stack.image.viewer: ${token}`);
  299. cornerstoneDICOMImageLoader.init({
  300. maxWebWorkers: navigator.hardwareConcurrency || 1,
  301. errorInterceptor: (error) => {
  302. if (error.status === 401) {
  303. console.error('Authentication failed. Please refresh the token.');
  304. }
  305. console.error(`请求dcm文件出错:${error}`);
  306. },
  307. beforeSend: (xhr, imageId, defaultHeaders) => {
  308. return {
  309. ...defaultHeaders,
  310. Authorization: `Bearer ${token}`,
  311. Language: 'en',
  312. Product: 'DROS',
  313. Source: 'Electron',
  314. };
  315. },
  316. });
  317. // Instantiate a rendering engine
  318. const renderingEngineId = 'myRenderingEngine';
  319. const renderingEngine = new cornerstone.RenderingEngine(
  320. renderingEngineId
  321. );
  322. const viewportId = 'CT_AXIAL_STACK';
  323. currentViewportId = viewportId;
  324. const viewportInput: cornerstone.Types.PublicViewportInput = {
  325. viewportId,
  326. element: elementRef.current!,
  327. type: cornerstone.Enums.ViewportType.STACK,
  328. };
  329. renderingEngine.enableElement(viewportInput);
  330. registerTools(viewportId, renderingEngineId);
  331. // Get the stack viewport that was created
  332. const viewport = renderingEngine.getViewport(
  333. viewportId
  334. ) as cornerstone.Types.IStackViewport;
  335. // 给定一个dcm文件路径,加载并显示出来
  336. await viewport.setStack(imageUrls, imageIndex);
  337. viewport.render();
  338. };
  339. setup();
  340. }, [elementRef, imageIndex]);
  341. useEffect(() => {
  342. if (action) {
  343. // Handle the action
  344. switch (action) {
  345. case 'Add L Mark':
  346. addLMark();
  347. break;
  348. case 'Add R Mark':
  349. // Implement the logic to add an R mark
  350. addRLabel(currentViewportId);
  351. console.log('Adding R Mark');
  352. break;
  353. case 'Delete Selected Mark': {
  354. deleteSelectedMark();
  355. break;
  356. }
  357. case 'Horizontal Flip': {
  358. HorizontalFlip();
  359. break;
  360. }
  361. case 'Vertical Flip': {
  362. VerticalFlip();
  363. break;
  364. }
  365. case 'Rotate Counterclockwise 90': {
  366. RotateCounterclockwise90();
  367. break;
  368. }
  369. case 'Rotate Clockwise 90':
  370. RotateClockwise90();
  371. break;
  372. case 'Rotate Any Angle':
  373. // Implement the logic to rotate the image by any angle
  374. {
  375. const planar = toolGroup.getToolInstance(PlanarRotateTool.toolName); // Reset rotation angle
  376. const isActive = planar.mode === csToolsEnums.ToolModes.Active;
  377. console.log(
  378. `PlanarRotateTool is currently ${isActive ? 'active' : 'inactive'}`
  379. );
  380. if (isActive) {
  381. toolGroup.setToolPassive(PlanarRotateTool.toolName, {
  382. removeAllBindings: true,
  383. });
  384. } else {
  385. toolGroup.setToolActive(PlanarRotateTool.toolName, {
  386. bindings: [
  387. {
  388. mouseButton: MouseBindings.Primary, // Left Click
  389. },
  390. ],
  391. });
  392. }
  393. console.log('Rotating Image by Any Angle');
  394. }
  395. break;
  396. case 'Crop Image':
  397. // Implement the logic to crop the image
  398. console.log('Cropping Image');
  399. break;
  400. case 'Delete Digital Mask':
  401. // Implement the logic to delete the digital mask
  402. console.log('Deleting Digital Mask');
  403. break;
  404. case 'Adjust Brightness and Contrast':
  405. // Implement the logic to adjust brightness and contrast
  406. console.log('Adjusting Brightness and Contrast');
  407. break;
  408. case 'Crop Selected Area':
  409. // Implement the logic to crop the selected area
  410. console.log('Cropping Selected Area');
  411. break;
  412. case 'Delete Mask':
  413. // Implement the logic to delete the mask
  414. console.log('Deleting Mask');
  415. break;
  416. case 'Image Comparison':
  417. // Implement the logic for image comparison
  418. console.log('Comparing Images');
  419. break;
  420. case 'Invert Contrast':
  421. // Implement the logic to invert the contrast
  422. {
  423. const viewport =
  424. cornerstone.getEnabledElementByViewportId(
  425. currentViewportId
  426. ).viewport;
  427. const targetBool = !viewport.getProperties().invert;
  428. viewport.setProperties({
  429. invert: targetBool,
  430. });
  431. viewport.render();
  432. }
  433. console.log('Inverting Contrast');
  434. break;
  435. case '1x1 Layout':
  436. // Implement the logic for 1x1 layout
  437. console.log('Setting 1x1 Layout');
  438. break;
  439. case '1x2 Layout':
  440. // Implement the logic for 1x2 layout
  441. console.log('Setting 1x2 Layout');
  442. break;
  443. case '2x2 Layout':
  444. // Implement the logic for 2x2 layout
  445. console.log('Setting 2x2 Layout');
  446. break;
  447. case '4x4 Layout':
  448. // Implement the logic for 4x4 layout
  449. console.log('Setting 4x4 Layout');
  450. break;
  451. case 'Magnifier':
  452. // Implement the logic for magnifier
  453. console.log('Activating Magnifier');
  454. break;
  455. case 'Fit Size':
  456. // Implement the logic to fit the image size
  457. console.log('Fitting Image Size');
  458. break;
  459. case 'Original Size':
  460. // Implement the logic to set the image to original size
  461. console.log('Setting Image to Original Size');
  462. break;
  463. case 'Zoom Image':
  464. // Implement the logic to zoom the image
  465. console.log('Zooming Image');
  466. break;
  467. case 'Reset Cursor':
  468. // Implement the logic to reset the cursor
  469. console.log('Resetting Cursor');
  470. break;
  471. case 'Pan':
  472. // Implement the logic to pan the image
  473. console.log('Panning Image');
  474. break;
  475. case 'Invert Image':
  476. InvertImage();
  477. break;
  478. case 'Reset Image':
  479. ResetImage();
  480. break;
  481. case 'Snapshot':
  482. // Implement the logic to take a snapshot
  483. console.log('Taking Snapshot');
  484. break;
  485. case 'Advanced Processing':
  486. // Implement the logic for advanced processing
  487. console.log('Performing Advanced Processing');
  488. break;
  489. case 'Musician':
  490. // Implement the logic for musician
  491. console.log('Activating Musician');
  492. break;
  493. case 'Image Measurement':
  494. // Implement the logic for image measurement
  495. console.log('Measuring Image');
  496. break;
  497. case 'More':
  498. // Implement the logic for more options
  499. console.log('Showing More Options');
  500. break;
  501. case 'Apply Colormap':
  502. ApplyColormap();
  503. break;
  504. default:
  505. break;
  506. }
  507. dispatch(clearAction()); //清理后可连续同一个action触发响应
  508. }
  509. }, [action]);
  510. return (
  511. <div
  512. ref={elementRef}
  513. style={{ width: '100%', height: '100%', backgroundColor: '#000' }}
  514. />
  515. );
  516. };
  517. export default StackViewer;