Compare commits

...
Author SHA1 Message Date
ohif-bot 43aa20c372 chore(version): Update package versions [skip ci] 2025-02-03 14:33:26 +00:00
ohif-bot 860f76586a chore(version): version.json [skip ci] 2025-02-03 14:33:20 +00:00
Ibrahim 9b8e5cfd1a fix(store-segmentation): storing segmentations was hitting the wrong command resulting in an undefined datasource (#4755)
[OHI-1445]
2025-02-03 09:28:58 -05:00
ohif-bot 613401e043 chore(version): Update package versions [skip ci] 2025-02-03 14:22:28 +00:00
ohif-bot 47b9a06f74 chore(version): version.json [skip ci] 2025-02-03 14:22:22 +00:00
f15eb44b4c feat: Add customization support for more UI components (#4634)
Co-authored-by: ashikcn <ashik@L1140.trenser.com>
Co-authored-by: Devu Jayalekshmi <devu.jayalekshmi@trenser.com>
Co-authored-by: Arul Mozhi <arul.mozhi@trenser.com>
2025-02-03 09:18:00 -05:00
ohif-bot 8f79ebafd9 chore(version): Update package versions [skip ci] 2025-01-31 15:12:46 +00:00
ohif-bot a3c7c2c3da chore(version): version.json [skip ci] 2025-01-31 15:12:40 +00:00
Celian-abd 32017d15a4 fix: Remove non-functional Tailwind class for SegmentationPanel (#4745) 2025-01-31 10:07:55 -05:00
ohif-bot e3967e62f2 chore(version): Update package versions [skip ci] 2025-01-31 14:47:57 +00:00
ohif-bot 09f9024728 chore(version): version.json [skip ci] 2025-01-31 14:47:51 +00:00
Alireza 0cb6106d0f fix: publish dependency (#4753) 2025-01-31 09:43:35 -05:00
ohif-bot 4dc26ae603 chore(version): Update package versions [skip ci] 2025-01-31 13:58:58 +00:00
ohif-bot 9850f92b8a chore(version): version.json [skip ci] 2025-01-31 13:58:52 +00:00
Gas Giantandscouser f10683c667 fix: typo in pet_series_module (#4748)
Co-authored-by: scouser <sharukh@Sharukhs-MacBook-Pro.local>
2025-01-31 08:54:45 -05:00
ohif-bot 2bfc790870 chore(version): Update package versions [skip ci] 2025-01-29 18:41:36 +00:00
ohif-bot 3f08a76aac chore(version): version.json [skip ci] 2025-01-29 18:41:29 +00:00
Dan Rukasandsedghi a8d84443a9 ui(components): migrates to new toolbar ui components and Numeric for Input InputRange and InputDoubleRange (#4728)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2025-01-29 13:36:52 -05:00
ohif-bot ac5ae4a29e chore(version): Update package versions [skip ci] 2025-01-29 14:19:36 +00:00
ohif-bot 6d9c701fa9 chore(version): version.json [skip ci] 2025-01-29 14:19:30 +00:00
Jaseel-trenser 054b262e9c feat(customization): enable custom onDropHandler for viewportGrid (#4641) 2025-01-29 09:14:50 -05:00
ohif-bot 3dac0f9e73 chore(version): Update package versions [skip ci] 2025-01-28 20:04:13 +00:00
ohif-bot d62c32c898 chore(version): version.json [skip ci] 2025-01-28 20:04:07 +00:00
Alireza d554f02f7c fix(dependencies): Update dcmjs library and improve documentation links (#4741) 2025-01-28 14:59:59 -05:00
ohif-bot ff6dd3a152 chore(version): Update package versions [skip ci] 2025-01-28 16:38:07 +00:00
ohif-bot a22818c1f4 chore(version): version.json [skip ci] 2025-01-28 16:38:01 +00:00
Joe Boccanfuso 6abb095b8a feat(side-panels): Added resize handle interactive feedback for side panels (#4734) 2025-01-28 11:33:53 -05:00
ohif-bot fe658c6392 chore(version): Update package versions [skip ci] 2025-01-28 13:59:39 +00:00
ohif-bot 70c94e8d5a chore(version): version.json [skip ci] 2025-01-28 13:59:33 +00:00
Devu-trenser 5addf51c45 feat(windowLevelActionMenu) customization support for windowLevelActionMenu (#4725) 2025-01-28 08:54:58 -05:00
ashik-trenserandashikcn 8e3e2085d4 fix: for initialImageIndex mismatch issue for loading SR after disabling prompts (#4732)
Co-authored-by: ashikcn <ashik@L1140.trenser.com>
2025-01-28 08:53:42 -05:00
ohif-bot 078a72996f chore(version): Update package versions [skip ci] 2025-01-27 16:59:04 +00:00
ohif-bot dc14a48fd9 chore(version): version.json [skip ci] 2025-01-27 16:58:57 +00:00
arul-trenser 61699d00b6 fix(context menu): Context menus for edge-proximate measurements are partially obscured. (#4727) 2025-01-27 11:54:29 -05:00
ohif-bot eb854ac2cb chore(version): Update package versions [skip ci] 2025-01-27 16:24:18 +00:00
ohif-bot 6c42f269a7 chore(version): version.json [skip ci] 2025-01-27 16:24:12 +00:00
Ibrahim ac6e674b4d feat(static-wado): add support for case-insensitive searching (#4603)
[OHI-1357]
2025-01-27 11:20:00 -05:00
ohif-bot 316cf0a7bd chore(version): Update package versions [skip ci] 2025-01-27 14:27:48 +00:00
ohif-bot c011236060 chore(version): version.json [skip ci] 2025-01-27 14:27:43 +00:00
dependabot[bot] 2ed7a1adfa chore(deps): bump store2 from 2.14.3 to 2.14.4 (#4737)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 09:23:38 -05:00
ohif-bot 492150478b chore(version): Update package versions [skip ci] 2025-01-24 15:27:16 +00:00
ohif-bot cb03077d37 chore(version): version.json [skip ci] 2025-01-24 15:27:10 +00:00
Alireza 28fb921089 fix(docs): image in customization (#4735) 2025-01-24 10:22:29 -05:00
ohif-bot bc175d68b6 chore(version): Update package versions [skip ci] 2025-01-24 13:46:27 +00:00
ohif-bot 884020f143 chore(version): version.json [skip ci] 2025-01-24 13:46:21 +00:00
Gas Giantandscouser d6f0092a32 feat: delete active annotation using backspace/delete key (#4722)
Co-authored-by: scouser <sharukh@Sharukhs-MacBook-Pro.local>
2025-01-24 08:42:00 -05:00
ohif-bot 01fa5d83a6 chore(version): Update package versions [skip ci] 2025-01-23 19:28:43 +00:00
ohif-bot 625919d4e4 chore(version): version.json [skip ci] 2025-01-23 19:28:36 +00:00
Alireza 55ad8efbab feat(customization): new customization service api (#4688) 2025-01-23 14:24:13 -05:00
ohif-bot fab52808cb chore(version): Update package versions [skip ci] 2025-01-23 19:18:02 +00:00
ohif-bot f5ad0de2d2 chore(version): version.json [skip ci] 2025-01-23 19:17:56 +00:00
Ibrahim d9abc3da8d feat(panels): responsive thumbnails based on panel size (#4723) 2025-01-23 14:13:44 -05:00
ohif-bot 9d07705a1f chore(version): Update package versions [skip ci] 2025-01-22 16:40:54 +00:00
ohif-bot 0966f62310 chore(version): version.json [skip ci] 2025-01-22 16:40:48 +00:00
Alireza f00d18292f fix(seg): sphere scissor on stack and cpu rendering reset properties was broken (#4721) 2025-01-22 11:36:45 -05:00
ohif-bot 3628ad84c8 chore(version): Update package versions [skip ci] 2025-01-21 16:25:19 +00:00
ohif-bot b21287e908 chore(version): version.json [skip ci] 2025-01-21 16:25:13 +00:00
Joe Boccanfuso d90a4cfb16 feat(resizable-side-panels): Make the left and right side panels (optionally) resizable. (#4672) 2025-01-21 11:20:34 -05:00
ohif-bot a518854a4c chore(version): Update package versions [skip ci] 2025-01-21 14:38:46 +00:00
ohif-bot 28f86f3cd6 chore(version): version.json [skip ci] 2025-01-21 14:38:40 +00:00
Alireza cf97fa9b7b fix(ui): Update dependencies and add missing icons (#4699) 2025-01-21 09:34:19 -05:00
ohif-bot ad7ade28ea chore(version): Update package versions [skip ci] 2025-01-21 14:12:03 +00:00
ohif-bot 9aed7b8486 chore(version): version.json [skip ci] 2025-01-21 14:11:57 +00:00
Jaseel-trenser 8aedb2ec54 fix: Inconsistent Handling of Patient Name Tag (#4703) 2025-01-21 09:07:30 -05:00
ohif-bot cbe926f52b chore(version): Update package versions [skip ci] 2025-01-20 14:17:53 +00:00
ohif-bot 855648f41f chore(version): version.json [skip ci] 2025-01-20 14:17:47 +00:00
Bez bf587070cc fix(rotation): support cycling rotateViewportCCW (#4533) 2025-01-20 09:13:43 -05:00
ohif-bot 953f36ab4c chore(version): Update package versions [skip ci] 2025-01-20 14:11:21 +00:00
ohif-bot 0911e3c0d1 chore(version): version.json [skip ci] 2025-01-20 14:11:15 +00:00
Bill Wallace 464148ece6 fix(hp): Display set should allow remembered updates (#4707) 2025-01-20 09:06:28 -05:00
ohif-bot 544dc543d9 chore(version): Update package versions [skip ci] 2025-01-17 21:48:05 +00:00
ohif-bot f5dcf25d98 chore(version): version.json [skip ci] 2025-01-17 21:47:59 +00:00
Bill Wallace 7734296d1d docs: Documentation on how to set default configuration to search for today (#4708) 2025-01-17 16:43:51 -05:00
ohif-bot 9e047784d1 chore(version): Update package versions [skip ci] 2025-01-16 16:47:32 +00:00
ohif-bot df96a4992c chore(version): version.json [skip ci] 2025-01-16 16:47:26 +00:00
Pedro H. Köhler 19931614dc fix: context menu icon (#4696) 2025-01-16 11:42:58 -05:00
ohif-bot ef477c29c5 chore(version): Update package versions [skip ci] 2025-01-16 03:33:26 +00:00
ohif-bot 6c8ebc1d5a chore(version): version.json [skip ci] 2025-01-16 03:33:19 +00:00
ec4b5a6876 fix(multiframe): handling proxies properly (#4693)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
Co-authored-by: Chao Sui <chao.sui@curvebeamai.com>
2025-01-15 22:29:06 -05:00
ohif-bot 9f8df95f3e chore(version): Update package versions [skip ci] 2025-01-15 20:21:31 +00:00
ohif-bot c718b20659 chore(version): version.json [skip ci] 2025-01-15 20:21:25 +00:00
Bill Wallaceandsedghi cded082617 fix: Having sop instance in a per-frame or shared attribute breaks load (#4560)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2025-01-15 15:16:50 -05:00
ohif-bot 27c040fdda chore(version): Update package versions [skip ci] 2025-01-14 14:07:59 +00:00
ohif-bot e923bf2bc5 chore(version): version.json [skip ci] 2025-01-14 14:07:53 +00:00
Pedro H. Köhler 2d611d06ed fix: cs dicom sr commands module (#4683) 2025-01-14 09:03:04 -05:00
Alireza c901a847af fix: bugs after multimonitor (#4680) 2025-01-13 18:32:54 -05:00
ohif-bot 7662157cbf chore(version): Update package versions [skip ci] 2025-01-13 15:54:04 +00:00
ohif-bot 161c84d875 chore(version): version.json [skip ci] 2025-01-13 15:53:58 +00:00
Bill Wallace 07c628e689 feat(multimonitor): Add simple multi-monitor support to open another study(#4178) 2025-01-13 10:49:08 -05:00
ohif-bot a4a6de07f4 chore(version): Update package versions [skip ci] 2025-01-13 14:40:08 +00:00
ohif-bot daf18f3e6b chore(version): version.json [skip ci] 2025-01-13 14:40:01 +00:00
Devu-trenser 9832dbe653 fix(toolbarService): All header tools are enabled in volume3D viewport (#4677) 2025-01-13 09:35:44 -05:00
ohif-bot f4df631a4a chore(version): Update package versions [skip ci] 2025-01-13 14:04:38 +00:00
ohif-bot bdfaa35f7f chore(version): version.json [skip ci] 2025-01-13 14:04:31 +00:00
Joeycho 9b04020a87 docs: migration docs typo (3.8 -> 3.9, useViewportGridStore()) (#4675) 2025-01-13 09:00:13 -05:00
ohif-bot 7a13c28eb3 chore(version): Update package versions [skip ci] 2025-01-10 20:09:59 +00:00
ohif-bot b9fb0ce96f chore(version): version.json [skip ci] 2025-01-10 20:09:53 +00:00
Alireza d4a4267429 feat(dev): move to rsbuild for dev - faster (#4674) 2025-01-10 15:05:35 -05:00
ohif-bot 847f1da8e0 chore(version): Update package versions [skip ci] 2025-01-10 17:05:53 +00:00
ohif-bot 71ccbe87c1 chore(version): version.json [skip ci] 2025-01-10 17:05:47 +00:00
Ibrahim a2d34c97ed fix(docker): run compression regardless of APP_CONFIG being present ( in cases such as volume mount) (#4673) 2025-01-10 12:01:15 -05:00
ohif-bot ee76912fed chore(version): Update package versions [skip ci] 2025-01-10 15:11:20 +00:00
ohif-bot e94e8439ad chore(version): version.json [skip ci] 2025-01-10 15:11:14 +00:00
Ibrahim 01924b8bf2 fix(icons): icons missing for volume presets and others (#4671) 2025-01-10 10:06:34 -05:00
ohif-bot 7d22480036 chore(version): Update package versions [skip ci] 2025-01-10 04:02:04 +00:00
ohif-bot 4e70ee7894 chore(version): version.json [skip ci] 2025-01-10 04:01:57 +00:00
Joeycho 64a72774a8 chore: delete unused and duplicate function, it exists under /utils (#4657) 2025-01-09 22:57:32 -05:00
ohif-bot 1b56b48d7b chore(version): Update package versions [skip ci] 2025-01-10 03:11:43 +00:00
ohif-bot 41e870b7db chore(version): version.json [skip ci] 2025-01-10 03:11:37 +00:00
Bill Wallace ebbc37d291 fix: orthanc datasource dev (#4663) 2025-01-09 22:07:30 -05:00
ohif-bot 29b9dc58ba chore(version): Update package versions [skip ci] 2025-01-10 03:06:19 +00:00
ohif-bot 409e06d0c6 chore(version): version.json [skip ci] 2025-01-10 03:06:13 +00:00
Bill Wallace 82440e88d5 feat: Start using group filtering to define measurements table layout (#4501) 2025-01-09 22:01:42 -05:00
ohif-bot bc73de9710 chore(version): Update package versions [skip ci] 2025-01-09 16:22:12 +00:00
ohif-bot ba8f8ba2d6 chore(version): version.json [skip ci] 2025-01-09 16:22:04 +00:00
Salim Kanoun 8e2c607904 fix: Execute HP onProtocolEnter callback after HPservice.run( (#4589) 2025-01-09 11:15:57 -05:00
ohif-bot 98f4ac2e75 chore(version): Update package versions [skip ci] 2025-01-09 15:56:12 +00:00
ohif-bot 192d8e69a9 chore(version): version.json [skip ci] 2025-01-09 15:56:05 +00:00
Salim Kanoun aef68d18b8 fix: Make StudyInstanceUID optional to retrieve a Series from DicomMetadataStore (#4644) 2025-01-09 10:51:09 -05:00
ohif-bot 2dd9b893ad chore(version): Update package versions [skip ci] 2025-01-09 14:32:47 +00:00
ohif-bot 2ebd03de07 chore(version): version.json [skip ci] 2025-01-09 14:32:41 +00:00
Bill Wallace e0088ec918 fix: Inconsistencies and update the style setting on load for embedded styles from codingValues (#4599) 2025-01-09 09:28:02 -05:00
ohif-bot 3c848c6d34 chore(version): Update package versions [skip ci] 2025-01-08 20:35:07 +00:00
ohif-bot c931b63d5e chore(version): version.json [skip ci] 2025-01-08 20:35:01 +00:00
Alireza 38edde5e37 fix(circleci): Update workflow dependencies for Docker publishing stages (#4662) 2025-01-08 15:30:24 -05:00
Alireza 66eebbed0f fix(circleci): Update workflow dependencies for Docker publishing stages (#4661) 2025-01-08 15:20:16 -05:00
ohif-bot 8ecbca700c chore(version): Update package versions [skip ci] 2025-01-08 20:02:34 +00:00
ohif-bot 1223dd00e1 chore(version): version.json [skip ci] 2025-01-08 20:02:28 +00:00
Alireza 3c6a5ef69e fix(circleci): Update workflow dependencies for Docker publishing stages (#4660) 2025-01-08 14:57:50 -05:00
ohif-bot dd5251ac95 chore(version): Update package versions [skip ci] 2025-01-08 19:43:13 +00:00
ohif-bot 71c0c18a8a chore(version): version.json [skip ci] 2025-01-08 19:43:07 +00:00
Alireza 5f6f528fbf fix(Dockerfile): Clear bun package manager cache before installation (#4659) 2025-01-08 14:38:39 -05:00
ohif-bot 60547b507f chore(version): Update package versions [skip ci] 2025-01-08 19:28:18 +00:00
ohif-bot f8b7687f94 chore(version): version.json [skip ci] 2025-01-08 19:28:12 +00:00
Dan Rukasandsedghi 230ae9758b ui(components): Viewport components added to ui-next (#4649)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2025-01-08 14:23:34 -05:00
ohif-bot 47de38f3ff chore(version): Update package versions [skip ci] 2025-01-08 19:18:23 +00:00
ohif-bot 5b454472e4 chore(version): version.json [skip ci] 2025-01-08 19:18:16 +00:00
devin-ai-integration[bot] 2f5076ece8 fix: Convert Rows and Columns to numbers before comparison (#4654) (#4656)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-01-08 14:13:28 -05:00
ohif-bot 401d5dcedd chore(version): Update package versions [skip ci] 2025-01-08 18:15:53 +00:00
ohif-bot 464976f035 chore(version): version.json [skip ci] 2025-01-08 18:15:47 +00:00
Dan Rukasandsedghi b54f2075ec ui(component): Tooltip component ui-next (#4635)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2025-01-08 13:10:57 -05:00
ohif-bot 03ad79eaed chore(version): Update package versions [skip ci] 2025-01-08 14:46:22 +00:00
ohif-bot e993b7f9e1 chore(version): version.json [skip ci] 2025-01-08 14:46:15 +00:00
Alireza 8e12021939 fix(docker): make multiarch work (#4655) 2025-01-08 09:41:47 -05:00
ohif-bot 8ad5d559bb chore(version): Update package versions [skip ci] 2025-01-08 14:05:29 +00:00
ohif-bot 20a4daa54e chore(version): version.json [skip ci] 2025-01-08 14:05:22 +00:00
Alireza 836e67a6ab fix(docker): publish manifest for multiarch and update cs3d (#4650) 2025-01-08 09:00:22 -05:00
ohif-bot 05e3f221a6 chore(version): Update package versions [skip ci] 2025-01-07 13:50:25 +00:00
ohif-bot b44373915a chore(version): version.json [skip ci] 2025-01-07 13:50:19 +00:00
Joe Boccanfuso 28cf3a17fa fix(toolbars): Fix error when filtering out duplicate buttons for a button section. (#4618) 2025-01-07 08:45:43 -05:00
ohif-bot 754d6b269d chore(version): Update package versions [skip ci] 2025-01-06 15:00:56 +00:00
ohif-bot c2c2911c6e chore(version): version.json [skip ci] 2025-01-06 15:00:50 +00:00
devin-ai-integration[bot] 4a8e8cacf2 fix: year 2025 missing in date picker (#4647)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-01-06 09:56:29 -05:00
ohif-bot a863808947 chore(version): Update package versions [skip ci] 2025-01-03 16:02:36 +00:00
ohif-bot 1935bf8c3c chore(version): version.json [skip ci] 2025-01-03 16:02:29 +00:00
nithin-trenserandIbrahim 55fe185c72 feat: Implemented CSV support for Arrow annotation. (#4623)
Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
2025-01-03 10:57:25 -05:00
arul-trenserandIbrahim e11ceb9d20 fix(context menu): restrict the context menu accessibility for locked and hidden annotations (#4625)
Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
2025-01-03 10:57:17 -05:00
Jaseel-trenserandIbrahim 6b851dfc12 fix(context menu): Implemented closing of context menu on outside click (#4627)
Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
2025-01-03 10:57:05 -05:00
ohif-bot b7fac6df3e chore(version): Update package versions [skip ci] 2025-01-03 15:48:40 +00:00
ohif-bot 63a2d3850a chore(version): version.json [skip ci] 2025-01-03 15:48:33 +00:00
Abhijith Sb 5322064e9e fix(3D rendering): disabled light sliders when shade is off (#4631) 2025-01-03 10:43:49 -05:00
nithin-trenser 860679126a feat/Added translation option for text 'segment' (#4624) 2025-01-03 10:38:34 -05:00
ohif-bot 4ef67c0cf7 chore(version): Update package versions [skip ci] 2025-01-02 19:51:17 +00:00
ohif-bot 286ae4b1f5 chore(version): version.json [skip ci] 2025-01-02 19:51:11 +00:00
Bill Wallaceandsedghi 7e43b2f768 fix: Docker build time was very slow on a tiny change (#4559)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2025-01-02 14:46:53 -05:00
ohif-bot ae48f50037 chore(version): Update package versions [skip ci] 2024-12-20 18:25:50 +00:00
ohif-bot d6a02a3d4b chore(version): version.json [skip ci] 2024-12-20 18:25:45 +00:00
Ibrahim 1992002d2d fix(tools): enable additional tools in volume viewport (#4620) 2024-12-20 13:19:59 -05:00
ohif-bot ede5724069 chore(version): Update package versions [skip ci] 2024-12-20 15:28:24 +00:00
ohif-bot 1593d9644a chore(version): version.json [skip ci] 2024-12-20 15:28:18 +00:00
Ibrahim 8d0ed80e0c fix(datasource): attach auth headers for delete requests in the dicomweb datasource (#4619) 2024-12-20 10:23:33 -05:00
ohif-bot a3d1844c2a chore(version): Update package versions [skip ci] 2024-12-20 13:17:50 +00:00
ohif-bot 8313952a8e chore(version): version.json [skip ci] 2024-12-20 13:17:44 +00:00
Ibrahim faf5515e4b fix(segmentation): black preview when loading a seg, and crash on opening panel (#4602) 2024-12-20 08:12:58 -05:00
ohif-bot 921e8f93b7 chore(version): Update package versions [skip ci] 2024-12-18 20:10:41 +00:00
ohif-bot 422ff828cd chore(version): version.json [skip ci] 2024-12-18 20:10:35 +00:00
Joe Boccanfuso 2df8e1d5cd fix(SR): Bring back the onModeEnter for the cornerstone-dicom-sr extension that was accidentally removed by PR #4586 (#4616) 2024-12-18 15:06:06 -05:00
ohif-bot 11050441e5 chore(version): Update package versions [skip ci] 2024-12-18 18:29:14 +00:00
ohif-bot 8bd659ba10 chore(version): version.json [skip ci] 2024-12-18 18:29:07 +00:00
Alireza 93d7076901 fix(icons): Add Clipboard icon and update MetadataProvider for null checks (#4615) 2024-12-18 13:24:42 -05:00
ohif-bot 0fc89c3bb6 chore(version): Update package versions [skip ci] 2024-12-18 17:18:57 +00:00
ohif-bot e2903319b7 chore(version): version.json [skip ci] 2024-12-18 17:18:51 +00:00
dependabot[bot]andAlireza 489171a14c chore(deps): bump nanoid from 3.3.7 to 3.3.8 (#4587)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-12-18 12:14:12 -05:00
ohif-bot ddb19ab3da chore(version): Update package versions [skip ci] 2024-12-18 16:38:15 +00:00
ohif-bot e90757081f chore(version): version.json [skip ci] 2024-12-18 16:38:09 +00:00
Joe Boccanfuso 4d3d5e794c feat(measurements): Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. (#4586) 2024-12-18 11:33:40 -05:00
ohif-bot 7eec015ff1 chore(version): Update package versions [skip ci] 2024-12-18 14:52:37 +00:00
ohif-bot 0e2077095d chore(version): version.json [skip ci] 2024-12-18 14:52:31 +00:00
Ceciliadrc 52cf9b1e03 fix: ohif icons in Header (#4611) 2024-12-18 09:47:38 -05:00
ohif-bot 541e12a108 chore(version): Update package versions [skip ci] 2024-12-17 18:54:44 +00:00
ohif-bot f8c89dd58a chore(version): version.json [skip ci] 2024-12-17 18:54:37 +00:00
Bill Wallace d0ccdbd68d fix: Documentation and default enabled for bulkdata load (#4607) 2024-12-17 13:49:42 -05:00
ohif-bot edefe8abda chore(version): Update package versions [skip ci] 2024-12-17 18:24:43 +00:00
ohif-bot ce0830812e chore(version): version.json [skip ci] 2024-12-17 18:24:37 +00:00
Alireza 4e2ae32874 feat: migrate icons to ui-next (#4606) 2024-12-17 13:20:19 -05:00
ohif-bot e93a9c70c5 chore(version): Update package versions [skip ci] 2024-12-17 17:39:07 +00:00
ohif-bot 5ffa751cff chore(version): version.json [skip ci] 2024-12-17 17:39:00 +00:00
Alireza 9bf24d6dc5 fix(seg): jump to the first slice in SEG and RT that has data (#4605) 2024-12-17 12:34:13 -05:00
ohif-bot 7586006e7f chore(version): Update package versions [skip ci] 2024-12-13 15:12:39 +00:00
ohif-bot 6e4f7a887a chore(version): version.json [skip ci] 2024-12-13 15:12:34 +00:00
Ibrahim 60fc7d6a11 fix(tag-browser): fix dicom tag browser not loading in segmentation mode in study panel (#4601) 2024-12-13 10:07:59 -05:00
ohif-bot 7ca9df1b2b chore(version): Update package versions [skip ci] 2024-12-11 20:30:55 +00:00
ohif-bot 2a2e96378a chore(version): version.json [skip ci] 2024-12-11 20:30:49 +00:00
Alireza 1f04d6c1be feat(node): move to node 20 (#4594) 2024-12-11 15:26:26 -05:00
ohif-bot ab41183ce5 chore(version): Update package versions [skip ci] 2024-12-11 16:53:58 +00:00
ohif-bot 414b974505 chore(version): version.json [skip ci] 2024-12-11 16:53:49 +00:00
Ibrahim 684267b19b fix(worklist): selected patient changes randomly when clicked. (#4592) 2024-12-11 11:49:15 -05:00
ohif-bot 64a9f3828b chore(version): Update package versions [skip ci] 2024-12-11 02:25:59 +00:00
ohif-bot 0f428db983 chore(version): version.json [skip ci] 2024-12-11 02:25:50 +00:00
Ibrahim 1078df99aa docs(azure): Add azure integration guide and datasource configuration (#4579) 2024-12-10 21:19:58 -05:00
ohif-bot 76fb2f56d4 chore(version): Update package versions [skip ci] 2024-12-06 02:44:11 +00:00
ohif-bot 54edfdcb04 chore(version): version.json [skip ci] 2024-12-06 02:44:03 +00:00
Alireza 6ec04ca65e fix(sr): correct jump to first image via viewRef (#4576) 2024-12-05 21:39:13 -05:00
ohif-bot 6c7f535676 chore(version): Update package versions [skip ci] 2024-12-06 02:02:37 +00:00
ohif-bot 3708917f15 chore(version): version.json [skip ci] 2024-12-06 02:02:28 +00:00
Joe Boccanfuso 6160718fd2 fix(touch): For viewport interactions use onPointerDown. (#4572) 2024-12-05 20:57:34 -05:00
ohif-bot ee9593a7e0 chore(version): Update package versions [skip ci] 2024-12-05 18:35:10 +00:00
ohif-bot 581aab67a6 chore(version): version.json [skip ci] 2024-12-05 18:35:03 +00:00
Leonardo Campos 7cc6c1484a fix(defaultRouteInit): fixes 'madeInClient' parameter when 'makeDisplaySets' is called (#4571) 2024-12-05 13:30:23 -05:00
Leonardo Campos f87c6cd2aa feat(extension): added 'extensionManager' to 'onModeEnter' parameter list (#4569) 2024-12-05 13:29:59 -05:00
ohif-bot 3ee0aafebc chore(version): Update package versions [skip ci] 2024-12-05 16:35:51 +00:00
ohif-bot ced674e48a chore(version): version.json [skip ci] 2024-12-05 16:35:44 +00:00
AlirezaandIbrahim b8e8bbe482 fix(CinePlayer): always show cine player for dynamic data (#4575)
Co-authored-by: Ibrahim <ibrahim.mdev@gmail.com>
2024-12-05 11:30:21 -05:00
ohif-bot 72a35ce905 chore(version): Update package versions [skip ci] 2024-12-03 19:32:07 +00:00
ohif-bot a9e566ffee chore(version): version.json [skip ci] 2024-12-03 19:31:59 +00:00
Leonardo Campos bca2022351 fix(WorkflowSteps): fixed how hooks are invoked + added support for 'onExit' hook (#4568) 2024-12-03 16:26:47 -03:00
ohif-bot e4595fd234 chore(version): Update package versions [skip ci] 2024-12-02 13:59:08 +00:00
ohif-bot 0ea2c7fdde chore(version): version.json [skip ci] 2024-12-02 13:59:00 +00:00
Salim Kanoun 878a1f8af8 fix : Resize was broken if 3d viewport displayed in HP (#4561) 2024-12-02 08:53:46 -05:00
ohif-bot 0a81ddb6da chore(version): Update package versions [skip ci] 2024-11-29 16:24:36 +00:00
ohif-bot e528238bc2 chore(version): version.json [skip ci] 2024-11-29 16:24:28 +00:00
Ibrahim 085590a4ca fix(cli): publish 4D preclincial mode on NPM so it can be used in the OHIF cli commands (#4557) 2024-11-29 11:19:23 -05:00
ohif-bot 96f6b0643b chore(version): Update package versions [skip ci] 2024-11-28 18:27:42 +00:00
ohif-bot 207ee65bc7 chore(version): version.json [skip ci] 2024-11-28 18:27:34 +00:00
Alireza 7624ccb5e4 fix(multiframe): metadata handling of NM studies and loading order (#4554) 2024-11-28 13:22:40 -05:00
ohif-bot 6729534a1a chore(version): Update package versions [skip ci] 2024-11-28 17:46:10 +00:00
ohif-bot 3aafcfc28d chore(version): version.json [skip ci] 2024-11-28 17:46:02 +00:00
Alireza 397fd85653 feat(segmentation): Enhance dropdown menu functionality in SegmentationTable (#4553) 2024-11-28 12:41:08 -05:00
ohif-bot 144ec791a2 chore(version): Update package versions [skip ci] 2024-11-22 20:15:57 +00:00
ohif-bot 0846375f53 chore(version): version.json [skip ci] 2024-11-22 20:15:48 +00:00
Ibrahim b9c26e775a fix(colorlut): use the correct colorlut index and update vtk (#4544) 2024-11-22 15:08:55 -05:00
ohif-bot 84e0490d76 chore(version): Update package versions [skip ci] 2024-11-22 19:51:52 +00:00
ohif-bot 00bb07a812 chore(version): version.json [skip ci] 2024-11-22 19:51:44 +00:00
Ibrahim 44648eef92 fix(modes): don't attempt to retrieve a stage index if HPs are an array (#4542) 2024-11-22 14:46:31 -05:00
ohif-bot 900a265637 chore(version): Update package versions [skip ci] 2024-11-22 17:06:15 +00:00
ohif-bot fe4e36bc3d chore(version): version.json [skip ci] 2024-11-22 17:06:07 +00:00
Bill Wallace edfaf7248d fix: Make the commands ordering the registration order of hte mode (#4492) 2024-11-22 12:00:47 -05:00
ohif-bot f0dee87882 chore(version): Update package versions [skip ci] 2024-11-22 16:05:54 +00:00
ohif-bot 03687dbfd0 chore(version): version.json [skip ci] 2024-11-22 16:05:43 +00:00
Ibrahim 27ae385fd7 fix(error-boundray): prevent stack trace from overflowing and make it scrollable (#4541) 2024-11-22 11:00:19 -05:00
ohif-bot d8ef36ed24 chore(version): Update package versions [skip ci] 2024-11-15 18:07:11 +00:00
ohif-bot dd3127568c chore(version): version.json [skip ci] 2024-11-15 18:07:03 +00:00
Ibrahim 32fe2623cf fix(viewport): set a minimum width of 5px on viewports to prevent them from turning black/ going into an unrecoverable state. (#4517)
[OHI-865] [OHI-646]
2024-11-15 13:02:13 -05:00
ohif-bot 7f3cde486b chore(version): Update package versions [skip ci] 2024-11-15 14:11:04 +00:00
ohif-bot 73ec0af4a6 chore(version): version.json [skip ci] 2024-11-15 14:10:57 +00:00
Ibrahim d05db4cb61 fix(ci): address error in merge report step of the playwright workflow (#4518)
[OHI-1333]
2024-11-15 09:06:28 -05:00
ohif-bot e85a313db4 chore(version): Update package versions [skip ci] 2024-11-14 18:12:10 +00:00
ohif-bot 7db6b8bbfe chore(version): version.json [skip ci] 2024-11-14 18:12:02 +00:00
Ibrahim 5aeed931c2 chore(CI): move tasks from yarn to bun, improve cypress tests (#4476) 2024-11-14 13:06:58 -05:00
Ștefan Silviu-Alexandru ad8f205e41 fix: avoid black images after hiding the viewports (#4502) 2024-11-14 12:51:44 -05:00
Alireza 2b62b187a0 docs: edit migration guides (#4504) 2024-11-14 12:37:04 -05:00
ohif-bot 1d9c0e530f chore(version): Update package versions [skip ci] 2024-11-13 02:37:33 +00:00
ohif-bot b5ea1b7e5b chore(version): version.json [skip ci] 2024-11-13 02:37:24 +00:00
Alireza c05e0f00c7 docs: remove banner (#4498) 2024-11-12 21:17:53 -05:00
ohif-bot b39e5c5dc9 chore(version): Update package versions [skip ci] 2024-11-12 22:50:22 +00:00
ohif-bot 88b9fe8dbe chore(version): version.json [skip ci] 2024-11-12 22:50:13 +00:00
Alireza 623d44b150 docs: update docs version (#4496) 2024-11-12 17:31:19 -05:00
ohif-bot da3207781d chore(version): Update package versions [skip ci] 2024-11-12 21:37:33 +00:00
ohif-bot 9a86abff4c chore(version): version.json [skip ci] 2024-11-12 21:37:26 +00:00
Alireza 6d0fa11ff8 docs: update to next version [BUMP BETA] (#4493) 2024-11-12 16:18:33 -05:00
ohif-bot f4775eccae chore(version): Update package versions [skip ci] 2024-11-12 18:31:31 +00:00
ohif-bot bf8fe7799c chore(version): version.json [skip ci] 2024-11-12 18:31:25 +00:00
Alireza 62b2748847 fix: Measurement Tracking: Various UI and functionality improvements (#4481) 2024-11-12 13:14:47 -05:00
Bill Wallace 4a125236dd fix: Have an addIcon that adds to both ui and ui-next (#4490) 2024-11-12 12:34:14 -05:00
Ibrahim a5f03764d1 fix(styles): several fixes for different styles (#4483) 2024-11-12 12:33:45 -05:00
Dan Rukas 1801a217e3 ui(components): updated component font sizes and design component docs (#4486) 2024-11-12 12:33:09 -05:00
ohif-bot 23a2582231 chore(version): Update package versions [skip ci] 2024-11-11 19:45:52 +00:00
ohif-bot 1105db5dd1 chore(version): version.json [skip ci] 2024-11-11 19:45:44 +00:00
Alireza 05d41c5206 fix(bugs): Update dependencies and enhance UI components (#4478) 2024-11-11 14:28:34 -05:00
ohif-bot 61e3a9cb52 chore(version): Update package versions [skip ci] 2024-11-08 15:51:44 +00:00
ohif-bot 4b69279c70 chore(version): version.json [skip ci] 2024-11-08 15:51:36 +00:00
Ibrahim 8fb8b3bfd1 fix(style): worklist shifting (#4477) 2024-11-08 10:34:22 -05:00
ohif-bot c1a196d802 chore(version): Update package versions [skip ci] 2024-11-07 05:37:54 +00:00
ohif-bot 4a564528ba chore(version): version.json [skip ci] 2024-11-07 05:37:46 +00:00
Alireza aa2b649444 fix(tmtv): fix toggle one up weird behaviours (#4473) 2024-11-07 00:20:30 -05:00
ohif-bot 3f7ae76113 chore(version): Update package versions [skip ci] 2024-11-06 16:36:09 +00:00
ohif-bot 43ad641cf1 chore(version): version.json [skip ci] 2024-11-06 16:36:00 +00:00
Leszek Grzanka c80cd6102d fix(JSONScript) update name of the script in the comments to follow renaming (#4441) 2024-11-06 11:19:20 -05:00
Alireza 3d11ef28f2 fix: build (#4471) 2024-11-06 11:16:43 -05:00
ohif-bot 69a81ae838 chore(version): Update package versions [skip ci] 2024-11-06 12:34:24 +00:00
ohif-bot 417785ecbc chore(version): version.json [skip ci] 2024-11-06 12:34:15 +00:00
Alireza 7ce7127d7c OHIF with Cornerstone3D 2.0 (#4347) 2024-11-06 07:15:27 -05:00
ohif-bot 8981a61b8c chore(version): Update package versions [skip ci] 2024-11-05 18:47:30 +00:00
ohif-bot c66c688ace chore(version): version.json [skip ci] 2024-11-05 18:47:21 +00:00
Dan Rukas d9450104af fix(ui) Study Browser small spacing changes (#4469) 2024-11-05 13:28:59 -05:00
ohif-bot 2835a555c8 chore(version): Update package versions [skip ci] 2024-10-30 14:34:08 +00:00
ohif-bot 41e8ee692b chore(version): version.json [skip ci] 2024-10-30 14:33:59 +00:00
Pedro H. Köhler 4f20523109 fix(ui): show ui notification on displaySet load error (#4447) 2024-10-30 10:15:48 -04:00
ohif-bot 0166956dd8 chore(version): Update package versions [skip ci] 2024-10-29 23:31:44 +00:00
ohif-bot 1aa2c646a6 chore(version): version.json [skip ci] 2024-10-29 23:31:37 +00:00
Pedro H. Köhler 2e96371b06 fix(ui): display error in ui while loading seg (#4433) 2024-10-29 19:14:34 -04:00
ohif-bot 543c31109b chore(version): Update package versions [skip ci] 2024-10-29 14:10:39 +00:00
ohif-bot 2d1704063c chore(version): version.json [skip ci] 2024-10-29 14:10:31 +00:00
Pedro H. Köhler 0610425740 fix(packages): http-proxy-middleware vulnerability (#4443) 2024-10-29 09:52:07 -04:00
ohif-bot ce9ff7d535 chore(version): Update package versions [skip ci] 2024-10-18 15:59:40 +00:00
ohif-bot b4918d8a33 chore(version): version.json [skip ci] 2024-10-18 15:59:32 +00:00
Ibrahim a7fd9fa5bf feat(new-study-panel): default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser (#4417) 2024-10-18 11:42:44 -04:00
ohif-bot 1d0259451f chore(version): Update package versions [skip ci] 2024-10-17 16:39:04 +00:00
ohif-bot abf2093d30 chore(version): version.json [skip ci] 2024-10-17 16:38:55 +00:00
dependabot[bot] 8fc395e68f chore(deps): bump webpack from 5.89.0 to 5.94.0 (#4354)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 12:20:33 -04:00
Ibrahim 33af9bb021 fix(tmtv): prevent fusion row in tmtv from getting inverted unexpectedly (#4420) 2024-10-17 12:18:15 -04:00
ohif-bot 8dab0883c8 chore(version): Update package versions [skip ci] 2024-10-17 14:20:54 +00:00
ohif-bot a146c9a50e chore(version): version.json [skip ci] 2024-10-17 14:20:44 +00:00
BillucandLuc Billaud 2ec4692eaf fix(createReport): early return on cancel in prompt (#4243)
Co-authored-by: Luc Billaud <luc.billaud@creatis.insa-lyon.fr>
2024-10-17 09:49:53 -04:00
Ibrahim 03aad4eba2 fix(typo): type in fourup preset (#4426) 2024-10-17 09:47:37 -04:00
dependabot[bot] f14aee2733 chore(deps): bump micromatch from 4.0.7 to 4.0.8 (#4350)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 09:46:36 -04:00
dependabot[bot] 57eb829173 chore(deps): bump express from 4.19.2 to 4.21.1 (#4422)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 09:44:37 -04:00
Ibrahim ace67b3bbb fix(hp-presets): select the active displaySet when toggling an HP preset, not a random one from the series panel (#4365) 2024-10-17 09:43:48 -04:00
Ibrahim 307b14476b fix(tools): check if seriesNumber is an undefined properly (#4338) 2024-10-17 09:43:10 -04:00
Ibrahim 433cc8089d fix(3d-viewport): exception was being thrown and 3d viewpot not getting resized. (#4366) 2024-10-17 09:42:05 -04:00
Sofien-Sellami fa2435d5e9 feat(hangingProtocols): added selection of the HangingProtocol stage from the url (#4310) 2024-10-17 09:41:41 -04:00
Ceciliadrc e8f9345b93 feat(removeButton) : add remove button from toolbar option (#4408) 2024-10-17 09:40:53 -04:00
brandon flowers e254e7bb3d chore(ui): Added Data Test Ids to Cine controls for e2e Testing (#4415) 2024-10-17 09:40:24 -04:00
Joeycho 7353f7f069 fix(sr): load existing point, if there is 2nd point in renderableData (Fix rotation in arrow annotation) (#4356) 2024-10-17 09:39:48 -04:00
Igor Octaviano 3dd0666c0c fix(dicomjson): Update getUIDsFromImageID to work with json data source + update getDisplaySetImageUIDs to work with mixed sop class json (#4322) 2024-10-17 09:38:40 -04:00
Ceciliadrc d5bcf54e23 fix(updateIndex): getNumberOfSlices is defined when used with a 3D viewport (#4424) 2024-10-17 09:36:42 -04:00
Igor Octaviano ac1cad25af feat(SR): SCOORD3D point annotations support for stack viewports (#4315) 2024-10-17 09:36:24 -04:00
Dan Rukas e0fca8c88c ui(component): Added new Toast (Sonner) component (#4412) 2024-10-17 09:30:42 -04:00
ohif-bot 43f54aa49c chore(version): Update package versions [skip ci] 2024-10-15 14:10:16 +00:00
ohif-bot 9b2e98a760 chore(version): version.json [skip ci] 2024-10-15 14:10:08 +00:00
Alireza 465df5c573 docs(ui): add base for ui component showcase in docs (#4414) 2024-10-15 09:53:26 -04:00
ohif-bot 9125cec333 chore(version): Update package versions [skip ci] 2024-10-11 13:25:01 +00:00
ohif-bot 87d623a76e chore(version): version.json [skip ci] 2024-10-11 13:24:54 +00:00
James PettsandJames Petts e849199eb0 fix(auth): oidc-react-issue (#4410)
Co-authored-by: James Petts <jamespetts@Yunu-MacBookPro-JamesPetts.local>
2024-10-11 09:09:26 -04:00
ohif-bot 0495020389 chore(version): Update package versions [skip ci] 2024-10-10 15:28:10 +00:00
ohif-bot 98ab95a743 chore(version): version.json [skip ci] 2024-10-10 15:28:02 +00:00
Ibrahim ec685ef5b9 fix(fossa): update fossa to track licenses correctly (#4411) 2024-10-10 11:12:21 -04:00
ohif-bot 70eb2aa6d7 chore(version): Update package versions [skip ci] 2024-10-08 16:41:57 +00:00
ohif-bot a1597d7bd6 chore(version): version.json [skip ci] 2024-10-08 16:41:49 +00:00
Dan Rukas 8f0035aeef ui(component): Adds flexible DataRow component (#4399) 2024-10-08 12:25:44 -04:00
ohif-bot c8923c5499 chore(version): Update package versions [skip ci] 2024-10-04 18:02:36 +00:00
ohif-bot c0c4f2398c chore(version): version.json [skip ci] 2024-10-04 18:02:29 +00:00
Ibrahim 60a8d5154a feat(tours): freeze versions and add licensings doc (#4407) 2024-10-04 13:47:32 -04:00
ohif-bot efc554bd49 chore(version): Update package versions [skip ci] 2024-10-04 17:21:26 +00:00
ohif-bot fc5f73bdfd chore(version): version.json [skip ci] 2024-10-04 17:21:18 +00:00
Ibrahim 579a592812 Merge branch 'IbrahimCSAE-feat/interactive-onboarding-guides' 2024-10-04 12:53:33 -04:00
Ibrahim ea961d6b66 add demo 2024-10-04 12:42:19 -04:00
Ibrahim 69a620ac68 read me 2024-10-04 12:37:10 -04:00
Ibrahim 8eaebd015d updates 2024-10-04 12:28:26 -04:00
Ibrahim 0326496c47 updates 2024-10-04 12:27:43 -04:00
Ibrahim 327f2cb1cf update 2024-10-04 12:16:56 -04:00
Ibrahim 478def8447 updates 2024-10-04 12:16:01 -04:00
Ibrahim 0a9cbb7589 updates 2024-10-04 12:14:31 -04:00
Ibrahim c5752e756a updates 2024-10-04 10:03:20 -04:00
Ibrahim 3b20d2b816 updates 2024-10-04 08:27:40 -04:00
Ibrahim 0d4cb69ec3 styling 2024-10-04 08:18:20 -04:00
ohif-bot 4f18a21868 chore(version): Update package versions [skip ci] 2024-10-01 20:56:33 +00:00
ohif-bot c4299de984 chore(version): version.json [skip ci] 2024-10-01 20:56:25 +00:00
Pedro H. Köhler a11cd6d6cb fix(Select): select clear button (#4398) 2024-10-01 16:42:21 -04:00
ohif-bot a72192da28 chore(version): Update package versions [skip ci] 2024-10-01 12:49:31 +00:00
ohif-bot 0dca891ff3 chore(version): version.json [skip ci] 2024-10-01 12:49:22 +00:00
Pedro H. Köhler b4f08adfb6 fix(dependent): high priority vulnerabilities (#4396) 2024-10-01 08:20:19 -04:00
ohif-bot 4d87e4dbda chore(version): Update package versions [skip ci] 2024-09-30 22:00:24 +00:00
ohif-bot 6a02f0b41c chore(version): version.json [skip ci] 2024-09-30 22:00:16 +00:00
Igor Octaviano 43efed207e fix: 🐛 Fix imports for ui-next (#4394) 2024-09-30 17:45:14 -04:00
Ibrahim 4f2583483c updates 2024-09-27 16:38:07 -04:00
Ibrahim e3f7838ce0 updates 2024-09-27 16:27:23 -04:00
Ibrahim 08d329add4 updates 2024-09-27 16:14:14 -04:00
Ibrahim 579c0c9d01 updates 2024-09-27 15:44:01 -04:00
Ibrahim ea7a3e07cb u 2024-09-27 12:58:48 -04:00
Ibrahim e91811ff23 updates 2024-09-27 12:58:39 -04:00
ohif-bot dd8e69a8cf chore(version): Update package versions [skip ci] 2024-09-27 16:33:00 +00:00
ohif-bot b1b66076b9 chore(version): version.json [skip ci] 2024-09-27 16:32:51 +00:00
Dan Rukas 80b1f54af1 ui(dropdown menu): Added DropdownMenu component in ui-next (#4387) 2024-09-27 12:17:54 -04:00
ohif-bot 75fde87b59 chore(version): Update package versions [skip ci] 2024-09-24 19:59:31 +00:00
ohif-bot 64f2f50f63 chore(version): version.json [skip ci] 2024-09-24 19:59:24 +00:00
Dan Rukas 7f17d5d733 ui(segmentation): Design Prototype for split panel component (#4378) 2024-09-24 15:44:59 -04:00
Ibrahim 9ff4ece541 updates 2024-09-22 20:47:33 -04:00
Ibrahim e534802abb updates 2024-09-22 20:41:12 -04:00
Ibrahim 51a87028c1 updates 2024-09-22 20:30:07 -04:00
Ibrahim eba85aa5b3 updates 2024-09-22 20:30:00 -04:00
Ibrahim 331778dfef Merge branch 'feat/interactive-onboarding-guides' of https://github.com/IbrahimCSAE/Viewers into local-interactive-onboarding-guides 2024-09-20 11:44:43 -04:00
Ibrahim daa5a857e4 updates 2024-09-20 11:44:36 -04:00
ohif-bot cb1f3b877b chore(version): Update package versions [skip ci] 2024-09-19 16:23:16 +00:00
ohif-bot f748d2a73a chore(version): version.json [skip ci] 2024-09-19 16:23:09 +00:00
Dan Rukas 365d824b98 fix(ui): Fixed study component open and closed feedback in Studies panel (#4384) 2024-09-19 12:07:54 -04:00
ohif-bot ee8962e9a7 chore(version): Update package versions [skip ci] 2024-09-19 04:39:08 +00:00
ohif-bot cde457c626 chore(version): version.json [skip ci] 2024-09-19 04:39:01 +00:00
Alireza 5ef102681d refactor(config): Update AWS S3 Static wado server URLs (#4381) 2024-09-19 00:23:28 -04:00
ohif-bot d55e794a86 chore(version): Update package versions [skip ci] 2024-09-17 04:08:08 +00:00
ohif-bot 4f7fa35def chore(version): version.json [skip ci] 2024-09-17 04:08:01 +00:00
Ibrahim 1f09527ae6 docs(mobile): IOS & Android Debugging Guides (#4371) 2024-09-16 23:53:10 -04:00
Ibrahim 118922f365 progress 2024-09-13 11:01:41 -04:00
ohif-bot 926f549e91 chore(version): Update package versions [skip ci] 2024-09-12 18:49:56 +00:00
ohif-bot 0d900bf4ad chore(version): version.json [skip ci] 2024-09-12 18:49:48 +00:00
Pedro H. Köhler 1bfce0a03c feat(toolbar): enable extensions to change toolbar button sections (#4367) 2024-09-12 14:34:25 -04:00
ohif-bot c08debd65a chore(version): Update package versions [skip ci] 2024-09-11 19:13:10 +00:00
ohif-bot ab5cead2af chore(version): version.json [skip ci] 2024-09-11 19:13:02 +00:00
Ibrahim 7a96406a11 feat(studies-panel): New OHIF study panel - under experimental flag (#4254) 2024-09-11 14:57:19 -04:00
ohif-bot 4432b6b420 chore(version): Update package versions [skip ci] 2024-09-05 13:31:46 +00:00
ohif-bot 7670941256 chore(version): version.json [skip ci] 2024-09-05 13:31:38 +00:00
Bill Wallace e1feffa425 fix: Add kheops integration into OHIF v3 again (#4345) 2024-09-05 09:15:48 -04:00
ohif-bot 3b47e6971c chore(version): Update package versions [skip ci] 2024-08-27 12:05:44 +00:00
ohif-bot 13ea778e61 chore(version): version.json [skip ci] 2024-08-27 12:05:36 +00:00
Igor Octaviano 14498d4e9a fix: 🐛 SeriesInstanceUID fallback + update retrieve metadata filtered to check for lazy (#4346) 2024-08-27 07:49:48 -04:00
ohif-bot 83f3bf4ec1 chore(version): Update package versions [skip ci] 2024-08-16 05:35:24 +00:00
ohif-bot 3b96c9e771 chore(version): version.json [skip ci] 2024-08-16 05:35:17 +00:00
dependabot[bot] da8bdb94a9 chore(deps): bump axios from 0.25.0 to 0.28.1 (#4337)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-16 01:09:56 -04:00
ohif-bot 1f2c7c4c57 chore(version): Update package versions [skip ci] 2024-08-16 02:11:52 +00:00
ohif-bot 5c7a8d0c56 chore(version): version.json [skip ci] 2024-08-16 02:11:45 +00:00
b2c0faf29d Test: Playwright tests for tmtv (#4331)
Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
Co-authored-by: Ibrahim <ibrahim.mdev@gmail.com>
2024-08-15 21:57:04 -04:00
ohif-bot 4068488bb2 chore(version): Update package versions [skip ci] 2024-08-15 18:52:14 +00:00
ohif-bot acc6a217c9 chore(version): version.json [skip ci] 2024-08-15 18:52:06 +00:00
Bill Wallace 7599ec9421 feat: Add CS3D WSI and Video Viewports and add annotation navigation for MPR (#4182) 2024-08-15 14:36:53 -04:00
ohif-bot b12e0211d4 chore(version): Update package versions [skip ci] 2024-08-15 15:29:35 +00:00
ohif-bot 689008eb82 chore(version): version.json [skip ci] 2024-08-15 15:29:26 +00:00
Ibrahim 7377db8d28 fix(roundNumber): handle negative numbers properly (#4336) 2024-08-15 11:13:27 -04:00
ohif-bot a2be64f899 chore(version): Update package versions [skip ci] 2024-08-08 15:50:34 +00:00
ohif-bot 5a2e88455a chore(version): version.json [skip ci] 2024-08-08 15:50:27 +00:00
Pedro H. Köhler ab6e341731 fix: unexpected mpr measurements (#4332) 2024-08-08 11:33:36 -04:00
ohif-bot 45f3a2d84d chore(version): Update package versions [skip ci] 2024-08-07 15:24:37 +00:00
ohif-bot 00f78adb1f chore(version): version.json [skip ci] 2024-08-07 15:24:29 +00:00
Salim Kanoun 8e7cc11529 fix(ui): Tailwind build errors (#4329) 2024-08-07 11:08:52 -04:00
ohif-bot c21d4848ef chore(version): Update package versions [skip ci] 2024-08-06 13:42:34 +00:00
ohif-bot 1de19a07c1 chore(version): version.json [skip ci] 2024-08-06 13:42:27 +00:00
Igor Octaviano 9b10303a2e fix(url): series query param filtering (#4328) 2024-08-06 09:26:34 -04:00
ohif-bot c6d3990341 chore(version): Update package versions [skip ci] 2024-08-02 17:01:10 +00:00
ohif-bot c42cd90c1d chore(version): version.json [skip ci] 2024-08-02 17:01:02 +00:00
Dan Rukas 90364189b8 feat(ui): Created design and added core components for ui-next (#4324) 2024-08-02 12:35:07 -04:00
ohif-bot 00707db1ad chore(version): Update package versions [skip ci] 2024-07-31 15:37:18 +00:00
ohif-bot 688d896f3e chore(version): version.json [skip ci] 2024-07-31 15:37:10 +00:00
Pedro H. Köhler 72bef63ef6 fix: customization types (#4321) 2024-07-31 11:21:51 -04:00
ohif-bot 50669867c9 chore(version): Update package versions [skip ci] 2024-07-30 16:14:37 +00:00
ohif-bot 90d974b90c chore(version): version.json [skip ci] 2024-07-30 16:14:29 +00:00
Pedro H. Köhler aa0e5a5937 fix: ip SSRF improper categorization (#4319) 2024-07-30 11:58:50 -04:00
ohif-bot dd2648a080 chore(version): Update package versions [skip ci] 2024-07-30 13:21:38 +00:00
ohif-bot 0dbf85a3d5 chore(version): version.json [skip ci] 2024-07-30 13:21:29 +00:00
Salim Kanoun d402ded8c3 fix(ui): remove border-border class (#4317) 2024-07-30 09:05:19 -04:00
ohif-bot b1514b9a3e chore(version): Update package versions [skip ci] 2024-07-27 02:41:22 +00:00
ohif-bot bbfa3c1a1f chore(version): version.json [skip ci] 2024-07-27 02:41:15 +00:00
dependabot[bot] ba8af0f35b chore(deps): bump sharp from 0.30.7 to 0.32.6 (#4314)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-26 22:26:22 -04:00
ohif-bot 5755ce5430 chore(version): Update package versions [skip ci] 2024-07-26 17:03:24 +00:00
ohif-bot a7cba1a3cf chore(version): version.json [skip ci] 2024-07-26 17:03:16 +00:00
Igor Octaviano 16b7aa4f65 fix(dicom): Update multiframe DICOM JSON parsing for correct image ID generation (#4307) 2024-07-26 12:47:49 -04:00
ohif-bot 9f0828ae27 chore(version): Update package versions [skip ci] 2024-07-26 15:20:34 +00:00
ohif-bot 8903e3c3dd chore(version): version.json [skip ci] 2024-07-26 15:20:26 +00:00
AlirezaandDan Rukas 6231629b38 chore(ui): creating a playground for the new components (#4301)
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
2024-07-26 11:04:58 -04:00
ohif-bot b9eb54c71f chore(version): Update package versions [skip ci] 2024-07-24 20:56:53 +00:00
ohif-bot 2656beef0e chore(version): version.json [skip ci] 2024-07-24 20:56:45 +00:00
Leonardo Campos fc0064fd9d feat(pmap): added support for parametric map (#4284) 2024-07-24 16:40:28 -04:00
ohif-bot 9f3fc382c3 chore(version): Update package versions [skip ci] 2024-07-23 15:43:31 +00:00
ohif-bot 640200ba7e chore(version): version.json [skip ci] 2024-07-23 15:43:23 +00:00
Leonardo Campos 246ebab6eb feat(SR): text structured report (TEXT, CODE, NUM, PNAME, DATE, TIME and DATETIME) (#4287) 2024-07-23 11:28:48 -04:00
ohif-bot 58da0324cd chore(version): Update package versions [skip ci] 2024-07-19 14:10:42 +00:00
ohif-bot c9a2496aab chore(version): version.json [skip ci] 2024-07-19 14:10:34 +00:00
Pavel Klochkov 4368eb3896 docs(resources) Update resources.md in Docs (#4290) 2024-07-19 09:57:22 -04:00
ohif-bot fc9b521fa9 chore(version): Update package versions [skip ci] 2024-07-10 15:43:46 +00:00
ohif-bot dcd1e4eb28 chore(version): version.json [skip ci] 2024-07-10 15:43:37 +00:00
Ibrahim 0968b3abb1 docs(tools): add listener helpful tip for migration (#4285) 2024-07-10 11:28:35 -04:00
ohif-bot f6cdbe46bc chore(version): Update package versions [skip ci] 2024-07-09 19:17:33 +00:00
ohif-bot d8ae7b884b chore(version): version.json [skip ci] 2024-07-09 19:17:25 +00:00
Celian-abd 38c19fab77 fix: the start/end command in TMTV for the ROIStartEndThreshold tools (#4281) 2024-07-09 14:49:42 -04:00
ohif-bot 4b91ce4df9 chore(version): Update package versions [skip ci] 2024-07-09 18:48:34 +00:00
ohif-bot 6fb7efc46d chore(version): version.json [skip ci] 2024-07-09 18:48:27 +00:00
IbrahimandAlireza aefa6d94df feat(auth): Add Authorization Code Flow and new Keycloak recipes with new video tutorials (#4234)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-07-09 14:33:37 -04:00
ohif-bot 998302eb85 chore(version): Update package versions [skip ci] 2024-07-09 15:27:38 +00:00
ohif-bot 400e914011 chore(version): version.json [skip ci] 2024-07-09 15:27:30 +00:00
Bill WallaceandIbrahimCSAE 31271aeef7 fix: Tests run against e2e config for both playwright and older tests (#4283)
Co-authored-by: IbrahimCSAE <ibrahim.mdev@gmail.com>
2024-07-09 11:13:07 -04:00
ohif-bot 993308b32b chore(version): Update package versions [skip ci] 2024-07-05 18:15:46 +00:00
ohif-bot 3a0b36d4e4 chore(version): version.json [skip ci] 2024-07-05 18:15:38 +00:00
Bill Wallace 24c511f4bc fix: webpack import bugs showing warnings on import (#4265) 2024-07-05 13:19:42 -04:00
Hemant Patel 6d11048ca5 fix(image-orientation): Prevent incorrect orientation marker display for single-slice US images (#4275) 2024-07-05 13:08:48 -04:00
Bill Wallace 6fd3c7e293 fix: Cobb angle not working in basic-test mode and open contour (#4280) 2024-07-05 12:43:18 -04:00
Bill Wallace a2084f319b feat: Add interleaved HTJ2K and volume progressive loading (#4276) 2024-07-05 12:33:07 -04:00
ohif-bot 2845f07806 chore(version): Update package versions [skip ci] 2024-07-04 02:24:23 +00:00
ohif-bot 78b814224e chore(version): version.json [skip ci] 2024-07-04 02:24:16 +00:00
Celian-abd b2c629123c fix: stdValue in TMTV mode (#4278) 2024-07-03 22:10:47 -04:00
ohif-bot 6102e5d80f chore(version): Update package versions [skip ci] 2024-07-02 14:34:25 +00:00
ohif-bot 9b9e5be477 chore(version): version.json [skip ci] 2024-07-02 14:34:18 +00:00
Ibrahim 32762e6bce test(weekly): crosshair tests (#4273) 2024-07-02 10:20:31 -04:00
ohif-bot 6faf3a17f9 chore(version): Update package versions [skip ci] 2024-07-02 13:22:18 +00:00
ohif-bot bd742d19b2 chore(version): version.json [skip ci] 2024-07-02 13:22:10 +00:00
Salim Kanoun 5531487fd6 docs : Removed deprecated useBulkDataURI (#4272) 2024-07-02 09:08:37 -04:00
ohif-bot 6c7d01a184 chore(version): Update package versions [skip ci] 2024-06-28 13:45:33 +00:00
ohif-bot 369851d87f chore(version): version.json [skip ci] 2024-06-28 13:45:27 +00:00
Alireza 16cfab2aca chore: remove polyfill io (#4270) 2024-06-28 09:31:47 -04:00
ohif-bot e60d3bc069 chore(version): Update package versions [skip ci] 2024-06-28 13:23:20 +00:00
ohif-bot a9e0cc5518 chore(version): version.json [skip ci] 2024-06-28 13:23:13 +00:00
Leonardo Campos 2048b19484 feat(studyPrefetcher): Study Prefetcher (#4206) 2024-06-28 08:58:39 -04:00
ohif-bot 257cc477cd chore(version): Update package versions [skip ci] 2024-06-28 12:29:57 +00:00
ohif-bot c9362726eb chore(version): version.json [skip ci] 2024-06-28 12:29:50 +00:00
Salim Kanoun 02eb256dc7 docs: Add prettier tailwind (#4269)
This fixes some docs issues/reformatting issues broken by changes to exclude unneeded dependencies.
2024-06-28 08:06:34 -04:00
ohif-bot 897de44856 chore(version): Update package versions [skip ci] 2024-06-27 16:52:54 +00:00
ohif-bot cfb06e458b chore(version): version.json [skip ci] 2024-06-27 16:52:47 +00:00
Salim Kanoun f660f8e970 fix(cli): missing js (#4268) 2024-06-27 12:38:43 -04:00
ohif-bot bb2f5625fc chore(version): Update package versions [skip ci] 2024-06-27 13:29:38 +00:00
ohif-bot a883f0de66 chore(version): version.json [skip ci] 2024-06-27 13:29:32 +00:00
Salim Kanoun d09f8b5ba2 fix(cli): Fix the cli utilities which require full paths
reset .js extension in import (#4267)
2024-06-27 09:15:22 -04:00
ohif-bot 976003d665 chore(version): Update package versions [skip ci] 2024-06-26 16:40:54 +00:00
ohif-bot 8714f01f9d chore(version): version.json [skip ci] 2024-06-26 16:40:47 +00:00
Bill Wallace fdf883ada8 fix(orthanc): Correct bulkdata URL handling and add configuration example PDF (#4262) 2024-06-26 12:26:00 -04:00
ohif-bot bea56d4f66 chore(version): Update package versions [skip ci] 2024-06-26 02:30:49 +00:00
ohif-bot 6c38381ac2 chore(version): version.json [skip ci] 2024-06-26 02:30:42 +00:00
Ibrahim 75ef99d3b6 test(hyrdation): weekly tests PR (#4263) 2024-06-25 22:17:12 -04:00
ohif-bot ff1d97f76e chore(version): Update package versions [skip ci] 2024-06-25 14:30:15 +00:00
ohif-bot 2155b0641d chore(version): version.json [skip ci] 2024-06-25 14:30:09 +00:00
Ibrahim c9cc372581 test(improve-setup): sharding configuration, allow accessing cornerstone methods and events in tests, and add types (#4255) 2024-06-25 10:16:51 -04:00
ohif-bot 2bcdb52db9 chore(version): Update package versions [skip ci] 2024-06-21 21:35:35 +00:00
ohif-bot 16751ee141 chore(version): version.json [skip ci] 2024-06-21 21:35:28 +00:00
Alireza 71a1e561bf chore(build): move exaca to dependencies (#4253) 2024-06-21 17:21:51 -04:00
aef00e91d6 fix(CustomViewportOverlay): pass accurate data to Custom Viewport Functions (#4224)
Co-authored-by: Michael Andersen <Michael.Andersen@rmp.uhn.ca>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2024-06-21 16:22:35 -04:00
Ibrahim d5d821464a feat(HP): Frame View HP (#4235) 2024-06-21 11:22:05 -04:00
Bill Wallace 9d0b729dc5 test: Unit test was failing (#4249) 2024-06-21 10:57:35 -04:00
Ibrahim f3bbfff09b fix(code): remove console log (#4248) 2024-06-21 10:04:02 -04:00
Bill Wallace b6136acff6 chore(build): revert webpack upgrade to fix deploy (#4244) 2024-06-21 09:30:18 -04:00
Ibrahim a433d406e2 feat(sort): custom series sort in study panel (#4214) 2024-06-20 16:58:59 -04:00
IbrahimandSalim Kanoun 6f93449149 fix(studybrowser): Differentiate recent and all in study panel based on a provided time period (#4242)
Co-authored-by: Salim Kanoun <salim.kanoun@gmail.com>
2024-06-20 08:53:56 -04:00
Bill Wallace e462fd31f7 feat: customization service append and customize functionality should run once (#4238) 2024-06-19 16:31:59 -04:00
Bill Wallace 88575c6c09 fix: Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. (#4016) 2024-06-19 09:32:00 -04:00
ohif-bot 01e200986c chore(version): Update package versions [skip ci] 2024-06-18 02:48:53 +00:00
ohif-bot 0ac7b2faaa chore(version): version.json [skip ci] 2024-06-18 02:48:45 +00:00
Bill Wallace d8f6991dbe fix: Use correct external URL for rendered responses with relative URI (#4236) 2024-06-17 22:36:03 -04:00
ohif-bot 6fc4390645 chore(version): Update package versions [skip ci] 2024-06-18 02:33:30 +00:00
ohif-bot 29d522f5f9 chore(version): version.json [skip ci] 2024-06-18 02:33:23 +00:00
Bill Wallace 2eab049d79 fix: Re-enable hpScale module (#4237) 2024-06-17 22:21:16 -04:00
ohif-bot c7a2000873 chore(version): Update package versions [skip ci] 2024-06-17 14:29:37 +00:00
ohif-bot bd6917a2ca chore(version): version.json [skip ci] 2024-06-17 14:29:30 +00:00
Alireza 097ef76655 fix(cli): version txt had a new line which it should not (#4233) 2024-06-17 10:17:38 -04:00
Michael AndersenandMichael Andersen 729efb6d76 fix(pdf-viewport): Allow Drag and Drop on PDF Viewport (#4225)
Co-authored-by: Michael Andersen <Michael.Andersen@rmp.uhn.ca>
2024-06-14 12:55:09 -04:00
ohif-bot 2f2f6b4c8d chore(version): Update package versions [skip ci] 2024-06-12 19:10:20 +00:00
ohif-bot 36a015a3ae chore(version): version.json [skip ci] 2024-06-12 19:10:12 +00:00
Joeycho 6971287460 fix(sr): rendering issue by running loadSR before updateSR (#4226) 2024-06-12 14:57:38 -04:00
ohif-bot a97666ea4e chore(version): Update package versions [skip ci] 2024-06-12 15:52:26 +00:00
ohif-bot 5a4783cb1a chore(version): version.json [skip ci] 2024-06-12 15:52:19 +00:00
Simon Stemplinger 4e87317db6 docs: broken link to DICOM JSON minimum requirements on DICOM JSON documentation page (#4228) 2024-06-12 11:39:39 -04:00
ohif-bot d5d004292e chore(version): Update package versions [skip ci] 2024-06-12 15:32:19 +00:00
ohif-bot 7ed75bc4e9 chore(version): version.json [skip ci] 2024-06-12 15:32:12 +00:00
Bill Wallace 55dcfa1f69 feat: Add customization merge, append or replace functionality (#3871) 2024-06-12 11:20:27 -04:00
ohif-bot efa6df8bfb chore(version): Update package versions [skip ci] 2024-06-12 14:48:35 +00:00
ohif-bot 58bde5982f chore(version): version.json [skip ci] 2024-06-12 14:48:28 +00:00
Ibrahim 09853abc52 tests(increase-coverage): weekly tests PR (#4221) 2024-06-12 10:36:13 -04:00
ohif-bot 727088f3dc chore(version): Update package versions [skip ci] 2024-06-08 13:02:40 +00:00
ohif-bot 89a0fae244 chore(version): version.json [skip ci] 2024-06-08 13:02:33 +00:00
Alireza 4a3b37c6bb docs(faq, migrations, orthanc): Add FAQ for dynamic measurements, migration guides, and fix orthanc docker (#4217) 2024-06-08 08:52:03 -04:00
ohif-bot f044587b73 chore(version): Update package versions [skip ci] 2024-06-07 20:20:02 +00:00
ohif-bot 7bf9e6214f chore(version): version.json [skip ci] 2024-06-07 20:19:54 +00:00
Ibrahim 33f4c18f26 fix(window-level): move window level region to more tools menu (#4215) 2024-06-07 16:08:52 -04:00
Pedro H. Köhler 319b31d9a8 chore: add extra exports to cornerstone ext (#4216) 2024-06-07 16:02:36 -04:00
ohif-bot 14221eb78e chore(version): Update package versions [skip ci] 2024-06-05 17:21:51 +00:00
ohif-bot bfb6fc5be7 chore(version): version.json [skip ci] 2024-06-05 17:21:43 +00:00
Ibrahim 9f8ad28d71 tests(increase-coverage): weekly tests PR (#4202) 2024-06-05 13:09:53 -04:00
ohif-bot f93cdf326d chore(version): Update package versions [skip ci] 2024-06-05 16:52:29 +00:00
ohif-bot 6d54132386 chore(version): version.json [skip ci] 2024-06-05 16:52:21 +00:00
Igor Octaviano 649ffab4d9 fix: get direct url pixel data should be optional for video (#4152) 2024-06-05 12:39:59 -04:00
ohif-bot 35077f1f02 chore(version): Update package versions [skip ci] 2024-06-05 16:37:26 +00:00
ohif-bot 24fe144fa8 chore(version): version.json [skip ci] 2024-06-05 16:37:18 +00:00
Ibrahim a29e94de80 fix(seg): maintain algorithm name and algorithm type when DICOM seg is exported or downloaded (#4203) 2024-06-05 12:26:17 -04:00
ohif-bot d747c97be3 chore(version): Update package versions [skip ci] 2024-06-05 14:21:01 +00:00
ohif-bot 95c47ed16c chore(version): version.json [skip ci] 2024-06-05 14:20:53 +00:00
Ibrahim 430330f7e3 fix(hydration): Maintain the same slice that the user was on pre hydration in post hydration for SR and SEG. (#4200) 2024-06-05 10:07:53 -04:00
ohif-bot ddc7559e16 chore(version): Update package versions [skip ci] 2024-06-05 14:02:25 +00:00
ohif-bot abf9197b15 chore(version): version.json [skip ci] 2024-06-05 14:02:18 +00:00
Ibrahim ab1a18af5a feat(window-level-region): add window level region tool (#4127) 2024-06-05 09:50:22 -04:00
ohif-bot 4761bbf0c8 chore(version): Update package versions [skip ci] 2024-05-31 15:26:46 +00:00
ohif-bot 8a3367279a chore(version): version.json [skip ci] 2024-05-31 15:26:40 +00:00
Alireza f85da32f34 fix(tmtv): crosshairs should not have viewport indicators (#4197) 2024-05-31 11:14:55 -04:00
ohif-bot 6e6ecd8709 chore(version): Update package versions [skip ci] 2024-05-30 18:37:49 +00:00
ohif-bot e91827f002 chore(version): version.json [skip ci] 2024-05-30 18:37:41 +00:00
Alireza 93e760937f fix(seg): should be able to navigate outside toolbox and come back later (#4196) 2024-05-30 14:25:38 -04:00
ohif-bot 5945887b22 chore(version): Update package versions [skip ci] 2024-05-30 17:18:43 +00:00
ohif-bot b8b65eac77 chore(version): version.json [skip ci] 2024-05-30 17:18:36 +00:00
Alireza 2174026789 fix: segmentation creation and segmentation mode viewport rendering (#4193) 2024-05-30 13:07:37 -04:00
Alireza d7aa386800 fix(docker): docker build was broken because of imports (#4192) 2024-05-30 13:01:14 -04:00
ohif-bot af2237e7c7 chore(version): Update package versions [skip ci] 2024-05-30 16:02:17 +00:00
ohif-bot 4dfb68f9cc chore(version): version.json [skip ci] 2024-05-30 16:02:09 +00:00
Ibrahim 19b5b1c15c fix(tmtv): side panel crashing when activeToolOptions is not an array (#4189) 2024-05-30 11:50:23 -04:00
ohif-bot c02a4f2e26 chore(version): Update package versions [skip ci] 2024-05-30 15:22:57 +00:00
ohif-bot 756c2f88e6 chore(version): version.json [skip ci] 2024-05-30 15:22:50 +00:00
Ibrahim e073d195fd fix(queryparam): set all query params to lowercase by default (#4190) 2024-05-30 11:11:08 -04:00
ohif-bot 4ce7831ea7 chore(version): Update package versions [skip ci] 2024-05-29 16:07:46 +00:00
ohif-bot fe4dfb63e4 chore(version): version.json [skip ci] 2024-05-29 16:07:38 +00:00
Ibrahim 731340d70a fix(contour): set renderFill to false for contour (#4186) 2024-05-29 11:56:03 -04:00
ohif-bot 395894c6a9 chore(version): Update package versions [skip ci] 2024-05-29 15:50:43 +00:00
ohif-bot 8c25e7d557 chore(version): version.json [skip ci] 2024-05-29 15:50:36 +00:00
Bill Wallace 18ac08ed86 feat(hp): Add displayArea option for Hanging protocols and example with Mamo(#3808) 2024-05-29 11:31:09 -04:00
ohif-bot cb30b97610 chore(version): Update package versions [skip ci] 2024-05-29 14:09:04 +00:00
ohif-bot 22d5e51751 chore(version): version.json [skip ci] 2024-05-29 14:08:56 +00:00
Alireza 75a71db7f8 fix(ultrasound): Upgrade cornerstone3D version to resolve coloring issues (#4181) 2024-05-29 09:57:50 -04:00
ohif-bot c6a3ce4804 chore(version): Update package versions [skip ci] 2024-05-29 13:24:13 +00:00
ohif-bot a389daff2f chore(version): version.json [skip ci] 2024-05-29 13:24:05 +00:00
Ibrahim 18686c2caf feat(measurements): show untracked measurements in measurement panel under additional findings (#4160) 2024-05-29 09:12:39 -04:00
ohif-bot 07eb3c527d chore(version): Update package versions [skip ci] 2024-05-28 15:25:49 +00:00
ohif-bot 96860292ea chore(version): version.json [skip ci] 2024-05-28 15:25:39 +00:00
IbrahimandAlireza 7745c092bb fix(rt): dont convert to volume for RTSTRUCT (#4157)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-05-28 11:13:14 -04:00
ohif-bot b0af8ec553 chore(version): Update package versions [skip ci] 2024-05-27 18:41:10 +00:00
ohif-bot 2114ffff41 chore(version): version.json [skip ci] 2024-05-27 18:41:02 +00:00
AlirezaandIbrahimCSAE 70f2c797f4 feat(ui): move to React 18 and base for using shadcn/ui (#4174)
Co-authored-by: IbrahimCSAE <ibrahim.mdev@gmail.com>
2024-05-27 14:30:25 -04:00
ohif-bot 72b63dcd6a chore(version): Update package versions [skip ci] 2024-05-24 16:34:01 +00:00
ohif-bot 268569e8c4 chore(version): version.json [skip ci] 2024-05-24 16:33:53 +00:00
Ibrahim 8960b89911 feat(types): typed app config (#4171) 2024-05-24 12:21:09 -04:00
ohif-bot 8c5b228d27 chore(version): Update package versions [skip ci] 2024-05-24 14:07:55 +00:00
ohif-bot a547b47e2d chore(version): version.json [skip ci] 2024-05-24 14:07:45 +00:00
dependabot[bot] 6ca158e4f6 chore(deps): bump axios from 0.28.0 to 0.28.1 in the npm_and_yarn group across 1 directory (#4173)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 09:51:12 -04:00
ohif-bot 8c6ae839ef chore(version): Update package versions [skip ci] 2024-05-24 13:35:33 +00:00
ohif-bot 1ffe71d52a chore(version): version.json [skip ci] 2024-05-24 13:35:25 +00:00
dependabot[bot] 628132eb86 chore(deps): bump axios from 0.25.0 to 0.28.0 in the npm_and_yarn group across 1 directory (#4170)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 09:24:04 -04:00
ohif-bot 9a8ee2b3fb chore(version): Update package versions [skip ci] 2024-05-24 12:48:05 +00:00
ohif-bot 023fde9b26 chore(version): version.json [skip ci] 2024-05-24 12:47:58 +00:00
dependabot[bot] 2f2df1826b chore(deps): bump the npm_and_yarn group across 2 directories with 2 updates (#4169)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 08:36:16 -04:00
ohif-bot 590a5a81d7 chore(version): Update package versions [skip ci] 2024-05-23 20:30:46 +00:00
ohif-bot c0ff0698f1 chore(version): version.json [skip ci] 2024-05-23 20:30:38 +00:00
IbrahimandAlireza 73d9e99d5d fix(crosshairs): reset angle, position, and slabthickness for crosshairs when reset viewport tool is used (#4113)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-05-23 16:18:44 -04:00
2012 changed files with 151312 additions and 32008 deletions

No files matched your search

+244 -304
View File
@@ -1,71 +1,52 @@
version: 2.1
### ABOUT
#
# This configuration powers our Circleci.io integration
#
# Note:
# Netlify works independently from this configuration to
# create pull request previews and to update `https://docs.ohif.org`
###
## https://github.com/cypress-io/circleci-orb
##
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@3.3.1
executors:
cypress-custom:
description: |
Single Docker container used to run Cypress Tests
docker:
- image: cimg/node:<< parameters.node-version >>-browsers
parameters:
node-version:
default: '18.16.1'
description: |
The version of Node to run your tests with.
type: string
defaults: &defaults
docker:
- image: cimg/node:18.18
- image: cimg/node:20.18.1
environment:
TERM: xterm # Enable colors in term
TERM: xterm
QUICK_BUILD: true
working_directory: ~/repo
commands:
install_bun:
steps:
- restore_cache:
keys:
- bun-cache-v2-{{ arch }}-latest
- run:
name: Install Bun
command: |
if [ ! -d "$HOME/.bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV
source $BASH_ENV
- save_cache:
key: bun-cache-v2-{{ arch }}-latest
paths:
- ~/.bun
jobs:
###
# Workflow: PR_CHECKS
###
UNIT_TESTS:
<<: *defaults
resource_class: large
steps:
# Update yarn
- run: yarn -v
- install_bun
- run: node --version
# Checkout code and ALL Git Tags
- checkout
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
command: bun install --no-save
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: yarn run test:unit:ci
command: bun run test:unit:ci
# platform/app
- run:
name: 'VIEWER: Combine report output'
@@ -93,35 +74,23 @@ jobs:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
###
# Workflow: DEPLOY
###
BUILD:
<<: *defaults
resource_class: large
steps:
# Checkout code and ALL Git Tags
- checkout
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- install_bun
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
command: bun install --no-save
# Build & Test
- run:
name: 'Perform the versioning before build'
command: node ./version.mjs
command: bun ./version.mjs
- run:
name: 'Build the OHIF Viewer'
command: yarn run build
command: bun run build
no_output_timeout: 45m
- run:
name: 'Upload SourceMaps, Send Deploy Notification'
@@ -143,29 +112,18 @@ jobs:
- commit.txt
- version.json
# just to make sure later on we can publish them
BUILD_PACKAGES_QUICK:
<<: *defaults
resource_class: large
steps:
- run: yarn -v
- install_bun
# Checkout code and ALL Git Tags
- checkout
- attach_workspace:
at: ~/repo
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-{{ checksum "yarn.lock" }}
command: bun install --frozen-lockfile
- run:
name: Avoid hosts unknown for github
command: |
@@ -177,44 +135,27 @@ jobs:
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
yarn run build:package-all
bun run build:package-all
- run:
name: build the other half of the packages
command: |
yarn run build:package-all-1
bun run build:package-all-1
###
# Workflow: RELEASE
###
NPM_PUBLISH:
<<: *defaults
resource_class: large
steps:
- run: yarn -v
- install_bun
# Checkout code and ALL Git Tags
- checkout
- attach_workspace:
at: ~/repo
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-{{ checksum "yarn.lock" }}
command: bun install --no-save
- run:
name: Avoid hosts unknown for github
command: |
@@ -226,18 +167,14 @@ jobs:
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
yarn run build:package-all
bun run build:package-all
- run:
name: build the other half of the packages
command: |
yarn run build:package-all-1
bun run build:package-all-1
- run:
name: increase min time out
command: |
@@ -249,14 +186,14 @@ jobs:
- run:
name: publish package versions
command: |
node ./publish-version.mjs
bun ./publish-version.mjs
- run:
name: Again set the NPM registry (was deleted in the version script)
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: publish package dist
command: |
node ./publish-package.mjs
bun ./publish-package.mjs
- persist_to_workspace:
root: ~/repo
paths:
@@ -264,13 +201,14 @@ jobs:
DOCKER_RELEASE_PUBLISH:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image from the release branch
name: Build Docker image for amd64
command: |
# This file will exist if a new version was published by
# our command in the previous job.
@@ -284,22 +222,61 @@ jobs:
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our image, auth, and push
docker build --tag ohif/app:$IMAGE_VERSION_FULL --tag ohif/app:latest .
# Build our amd64 image, auth, and push
docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-amd64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:$IMAGE_VERSION_FULL
docker push ohif/app:latest
docker push ohif/app:$IMAGE_VERSION_FULL-amd64
docker push ohif/app:latest-amd64
fi
- persist_to_workspace:
root: ~/repo
paths:
- .
DOCKER_BETA_PUBLISH:
DOCKER_RELEASE_PUBLISH_ARM:
<<: *defaults
resource_class: arm.large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image from the master branch (beta releases)
name: Build Docker image for arm64
command: |
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
exit 0
else
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our arm64 image, auth, and push
docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-arm64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:$IMAGE_VERSION_FULL-arm64
docker push ohif/app:latest-arm64
fi
- persist_to_workspace:
root: ~/repo
paths:
- .
DOCKER_BETA_PUBLISH:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build Docker image for amd64 (Beta)
command: |
echo $(ls -l)
@@ -311,107 +288,155 @@ jobs:
else
echo "Building and pushing Docker image from the master branch (beta releases)"
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our image, auth, and push
echo "starting docker build"
docker build --tag ohif/app:$IMAGE_VERSION_FULL .
# Build our amd64 image, auth, and push
docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-beta-amd64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
echo "starting docker push"
docker push ohif/app:$IMAGE_VERSION_FULL
docker push ohif/app:$IMAGE_VERSION_FULL-amd64
docker push ohif/app:latest-beta-amd64
fi
# This is copied from the Cypress orb since the default for cypress/run is node 16 and
# we migrated to 18
CYPRESS_CUSTOM_RUN:
description: |
A single, complete job to run Cypress end-to-end tests in your application.
executor: cypress-custom
parallelism: << parameters.parallelism >>
parameters:
cypress-cache-key:
default: cypress-cache-{{ arch }}-{{ checksum "package.json" }}
description: Cache key used to cache the Cypress binary.
type: string
cypress-cache-path:
default: ~/.cache/Cypress
description: |
By default, this will cache the '~/.cache/Cypress' directory so that the Cypress binary is cached. You can override this by providing your own cache path.
type: string
cypress-command:
default: npx cypress run
description: Command used to run your Cypress tests
type: string
include-branch-in-node-cache-key:
default: false
description: |
If true, this cache will only apply to runs within the same branch. (Adds -{{ .Branch }}- to the node cache key)
type: boolean
install-browsers:
default: false
description: |
Cypress runs by default in the Electron browser. Use this flag to install additional browsers to run your tests in.
This is only needed if you are passing the `--browser` flag in your `cypress-command`.
This parameter leverages the `circleci/browser-tools` orb and includes Chrome and FireFox.
If you need additional browser support you can set this to false and use an executor with a docker image
that includes the browsers of your choosing. See https://hub.docker.com/r/cypress/browsers/tags
type: boolean
install-command:
default: ''
description: Overrides the default NPM command (npm ci)
type: string
node-cache-version:
default: v1
description:
Change the default node cache version if you need to clear the cache for any reason.
type: string
package-manager:
default: npm
description: Select the default node package manager to use. NPM v5+ Required.
enum:
- npm
- yarn
- yarn-berry
type: enum
parallelism:
default: 1
description: |
Number of Circle machines to use for load balancing, min 1
(requires `parallel` and `record` flags in your `cypress-command`)
type: integer
post-install:
default: ''
description: |
Additional commands to run after running install but before verifying Cypress and saving cache.
type: string
start-command:
default: ''
description: Command used to start your local dev server for Cypress to tests against
type: string
working-directory:
default: ''
description: Directory containing package.json
type: string
DOCKER_BETA_PUBLISH_ARM:
<<: *defaults
resource_class: arm.large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build Docker image for arm64 (Beta)
command: |
echo $(ls -l)
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
echo "don't have version txt"
exit 0
else
echo "Building and pushing ARM64 Docker image from the master branch (beta releases)"
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our arm64 image, auth, and push
docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-beta-arm64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:$IMAGE_VERSION_FULL-arm64
docker push ohif/app:latest-beta-arm64
fi
CYPRESS:
<<: *defaults
resource_class: large
parallelism: 8
steps:
- install_bun
- run:
name: Install Xvfb
command: sudo apt-get update && sudo apt-get install -y xvfb
- run:
name: Start Xvfb
command: Xvfb :99 -screen 0 1920x1080x24 &
background: true
- run:
name: Export Display Variable
command: export DISPLAY=:99
- cypress/install:
cypress-cache-key: << parameters.cypress-cache-key >>
cypress-cache-path: << parameters.cypress-cache-path >>
include-branch-in-node-cache-key: << parameters.include-branch-in-node-cache-key >>
install-browsers: << parameters.install-browsers >>
install-command: << parameters.install-command >>
node-cache-version: << parameters.node-cache-version >>
package-manager: << parameters.package-manager >>
post-install: << parameters.post-install >>
working-directory: << parameters.working-directory >>
install-browsers: true
install-command: bun install --no-save
package-manager: yarn
- cypress/run-tests:
cypress-command: << parameters.cypress-command >>
start-command: << parameters.start-command >>
working-directory: << parameters.working-directory >>
cypress-command:
npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record
--browser chrome --parallel
start-command: bun run test:data && bun run test:e2e:serve
DOCKER_MULTIARCH_MANIFEST:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Create and push multi-architecture manifest
command: |
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
exit 0
else
echo "Building and pushing multi-architecture manifest from the master branch (release releases)"
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
# Create and push manifest for specific version
docker manifest create ohif/app:$IMAGE_VERSION_FULL \
--amend ohif/app:$IMAGE_VERSION_FULL-amd64 \
--amend ohif/app:$IMAGE_VERSION_FULL-arm64
docker manifest push ohif/app:$IMAGE_VERSION_FULL
# Create and push manifest for "latest" tag
docker manifest create ohif/app:latest \
--amend ohif/app:latest-amd64 \
--amend ohif/app:latest-arm64
docker manifest push ohif/app:latest
fi
DOCKER_BETA_MULTIARCH_MANIFEST:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Create and push multi-architecture manifest (Beta)
command: |
echo $(ls -l)
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
exit 0
else
echo "Building and pushing multi-architecture manifest from the master branch (beta releases)"
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
# Create and push manifest for specific beta version
docker manifest create ohif/app:$IMAGE_VERSION_FULL \
--amend ohif/app:$IMAGE_VERSION_FULL-amd64 \
--amend ohif/app:$IMAGE_VERSION_FULL-arm64
docker manifest push ohif/app:$IMAGE_VERSION_FULL
# Create and push manifest for "latest-beta" tag
docker manifest create ohif/app:latest-beta \
--amend ohif/app:latest-beta-amd64 \
--amend ohif/app:latest-beta-arm64
docker manifest push ohif/app:latest-beta
fi
workflows:
PR_CHECKS:
@@ -420,61 +445,12 @@ workflows:
filters:
branches:
ignore: master
- UNIT_TESTS:
requires:
- BUILD_PACKAGES_QUICK
- CYPRESS_CUSTOM_RUN:
- UNIT_TESTS
- CYPRESS:
name: 'Cypress Tests'
context: cypress
matrix:
parameters:
start-command:
- yarn run test:data && yarn run test:e2e:serve
install-browsers:
- true
cypress-command:
- 'npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run
--record --browser chrome --parallel'
package-manager:
- 'yarn'
cypress-cache-key:
- 'yarn-packages-{{ checksum "yarn.lock" }}'
cypress-cache-path:
- '~/.cache/Cypress'
requires:
- BUILD_PACKAGES_QUICK
# PR_OPTIONAL_VISUAL_TESTS:
# jobs:
# - AWAIT_APPROVAL:
# type: approval
# # Update hub.docker.org
# - cypress/run:
# name: 'Generate Percy Snapshots'
# executor: cypress/browsers-chrome76
# browser: chrome
# pre-steps:
# - run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on'
# yarn: true
# store_artifacts: false
# working_directory: platform/app
# build:
# yarn test:data && npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
# yarn run build
# # start server --> verify running --> percy + chrome + cypress
# command: yarn run test:e2e:dist
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
# no-workspace: true # Don't persist workspace
# post-steps:
# - store_artifacts:
# path: platform/app/cypress/screenshots
# - store_artifacts:
# path: platform/app/cypress/videos
# requires:
# - AWAIT_APPROVAL
# Our master branch deploys to viewer-dev.ohif.org, the viewer.ohif.org is
# deployed from the release branch which is more stable and less frequently updated.
# viewer-dev.ohif.org
DEPLOY_MASTER:
jobs:
- BUILD:
@@ -487,9 +463,14 @@ workflows:
- DOCKER_BETA_PUBLISH:
requires:
- NPM_PUBLISH
- DOCKER_BETA_PUBLISH_ARM:
requires:
- DOCKER_BETA_PUBLISH
- DOCKER_BETA_MULTIARCH_MANIFEST:
requires:
- DOCKER_BETA_PUBLISH_ARM
# Our release branch deploys to viewer.ohif.org and is more stable and less
# frequently updated after being tested in the staging environment.
# viewer.ohif.org
DEPLOY_RELEASE:
jobs:
- BUILD:
@@ -506,50 +487,9 @@ workflows:
- DOCKER_RELEASE_PUBLISH:
requires:
- NPM_PUBLISH
###
# Unit and E2E tests have already run for PR_CHECKS
# Re-running should not gain us any confidence here
###
# RELEASE:
# jobs:
# - NPM_PUBLISH:
# filters:
# branches:
# only: master
# - DOCS_PUBLISH:
# filters:
# branches:
# only: master
# # Update base branch snapshots
# # and record a Cypress dashboard test run
# - cypress/run:
# name: 'Generate Percy Snapshots'
# executor: cypress/browsers-chrome76
# browser: chrome
# pre-steps:
# - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
# add wait-on' # Use yarn latest
# yarn: true
# store_artifacts: false
# working_directory: platform/app
# build:
# npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js yarn run
# build
# # start server --> verify running --> percy + chrome + cypress
# command: yarn run test:e2e:dist
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
# no-workspace: true # Don't persist workspace
# post-steps:
# - store_artifacts:
# path: platform/app/cypress/screenshots
# - store_artifacts:
# path: platform/app/cypress/videos
# - store_test_results:
# path: platform/app/cypress/results
# filters:
# branches:
# only: master
# - DOCKER_MASTER_PUBLISH:
# requires:
# - NPM_PUBLISH
- DOCKER_RELEASE_PUBLISH_ARM:
requires:
- DOCKER_RELEASE_PUBLISH
- DOCKER_MULTIARCH_MANIFEST:
requires:
- DOCKER_RELEASE_PUBLISH_ARM
+3 -2
View File
@@ -1,12 +1,13 @@
server {
gzip_static always;
gzip_proxied expired no-cache no-store private auth;
gunzip on;
listen ${PORT} default_server;
listen [::]:${PORT} default_server;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -7,8 +7,6 @@ server {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
+23 -4
View File
@@ -7,11 +7,30 @@ if [ -n "$SSL_PORT" ]
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
fi
if [ -n "$APP_CONFIG" ]
then
echo "$APP_CONFIG" > /usr/share/nginx/html/app-config.js
if [ -n "$APP_CONFIG" ]; then
echo "$APP_CONFIG" > /usr/share/nginx/html${PUBLIC_URL}app-config.js
echo "Using custom APP_CONFIG environment variable"
else
echo "Not using custom APP_CONFIG"
fi
if [ -f /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then
if [ -s /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then
echo "Detected non-empty app-config.js. Ensuring .gz file is updated..."
rm -f /usr/share/nginx/html${PUBLIC_URL}app-config.js.gz
gzip /usr/share/nginx/html${PUBLIC_URL}app-config.js
touch /usr/share/nginx/html${PUBLIC_URL}app-config.js
echo "Compressed app-config.js to app-config.js.gz"
else
echo "app-config.js is empty. Skipping compression."
fi
else
echo "No app-config.js file found. Skipping compression."
fi
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
@@ -40,6 +59,6 @@ if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
fi
echo "Starting Nginx to serve the OHIF Viewer..."
echo "Starting Nginx to serve the OHIF Viewer on ${PUBLIC_URL}"
exec "$@"
+4
View File
@@ -0,0 +1,4 @@
find platform/app/dist -name "*.js" -exec gzip -9 "{}" \; -exec touch "{}" \;
find platform/app/dist -name "*.map" -exec gzip -9 "{}" \; -exec touch "{}" \;
find platform/app/dist -name "*.css" -exec gzip -9 "{}" \; -exec touch "{}" \;
find platform/app/dist -name "*.svg" -exec gzip -9 "{}" \; -exec touch "{}" \;
+4 -4
View File
@@ -8,11 +8,11 @@
# have this file as their .dockerignore.
# Output
dist/
build/
**/dist/
**/build/
# Dependencies
node_modules/
**/node_modules/
# Root
README.md
@@ -33,5 +33,5 @@ dockerfile
.scripts/
.vscode/
coverage/
docs/
platform/docs/
testdata/
+2 -1
View File
@@ -18,7 +18,8 @@
},
"rules": {
// Enforce consistent brace style for all control statements for readability
"curly": "error"
"curly": "error",
"import/no-anonymous-default-export": "off"
},
"globals": {
"cy": true,
+1 -1
View File
@@ -64,7 +64,7 @@ body:
attributes:
label: 'Node version'
description: 'Your Node.js version.'
placeholder: 'e.g., 18.16.1'
placeholder: 'e.g., 20.18.1'
validations:
required: true
- type: input
+61 -12
View File
@@ -5,23 +5,72 @@ on:
pull_request:
branches: [main, master]
jobs:
test:
playwright-tests:
timeout-minutes: 60
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.50.0-noble
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
steps:
- uses: actions/checkout@v4
- name: Install unzip
run: apt-get update && apt-get install -y unzip
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Set up Git safe directory
run: git config --global --add safe.directory /__w/Viewers/Viewers
- name: Run Playwright tests
run:
export NODE_OPTIONS="--max_old_space_size=8192" && bun x playwright test --shard=${{
matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-reports:
if: ${{ !cancelled() }}
needs: [playwright-tests]
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.48.1-focal
steps:
- name: Install unzip
run: apt-get update && apt-get install -y unzip
- uses: oven-sh/setup-bun@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 20
- name: Install dependencies
run: npm install -g yarn && yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Run Playwright tests
run: yarn test:e2e:ci
- uses: actions/upload-artifact@v4
if: always()
run: bun install --frozen-lockfile
- name: Set up Git safe directory
run: git config --global --add safe.directory /__w/Viewers/Viewers
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
name: playwright-report
path: tests/playwright-report
retention-days: 30
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: bun x playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
+3 -1
View File
@@ -10,6 +10,8 @@ junit.xml
coverage/
.docz/
.yarn/
.nx/
addOns/yarn.lock
# YALC (for Erik)
.yalc
@@ -41,7 +43,7 @@ videos/
# autogenerated files
platform/app/src/pluginImports.js
/Viewers.iml
platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
platform/app/.recipes/Nginx-Dcm4Chee/logs/*
platform/app/.recipes/OpenResty-Orthanc/logs/*
.vercel
+1 -1
View File
@@ -1 +1 @@
18.16.1
20.9.0
+18 -10
View File
@@ -4,26 +4,34 @@
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
* in the DICOM JSON Data source on docs.ohif.org
*
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>
* Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme>
*
* params:
* - studyFolder: path to the study folder
* - studyFolder: path to the study folder which contains the DICOM files
* - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance
* we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the
* example since the data is hosted on S3 and each study is in a folder. So the url in the generated
* json file for the first instance of the first series of the first study will be
* dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1
*
* as you see the dicomweb is a prefix that is used to load the data into the viewer, which is suited when
* the .dcm file is hosted statically and can be accessed via a URL (like our example above)
* However, you can specify a new scheme bellow.
*
* - outputJSONPath: path to the output JSON file
* - scheme: default dicomweb if not provided
*/
const dcmjs = require('dcmjs');
const path = require('path');
const fs = require('fs').promises;
const args = process.argv.slice(2);
const [studyDirectory, urlPrefix, outputPath] = args;
const [studyDirectory, urlPrefix, outputPath, scheme = 'dicomweb'] = args;
if (args.length !== 3) {
console.error('Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>');
if (args.length < 3 || args.length > 4) {
console.error(
'Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]'
);
process.exit(1);
}
@@ -31,7 +39,7 @@ const model = {
studies: [],
};
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme) {
try {
const files = await recursiveReadDir(studyDirectory);
console.debug('Processing...');
@@ -42,7 +50,7 @@ async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory);
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory, scheme);
processInstance(instance);
}
}
@@ -77,10 +85,10 @@ async function recursiveReadDir(dir) {
return results;
}
function createImageId(fileLocation, urlPrefix, studyDirectory) {
function createImageId(fileLocation, urlPrefix, studyDirectory, scheme) {
const relativePath = path.relative(studyDirectory, fileLocation);
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
return `dicomweb:${urlPrefix}${normalizedPath}`;
return `${scheme}:${urlPrefix}${normalizedPath}`;
}
function processInstance(instance) {
@@ -262,4 +270,4 @@ function createInstanceMetaDataMultiFrame(instance) {
return instances;
}
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath);
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme);
+1
View File
@@ -1,6 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"mikestead.dotenv",
"bungcip.better-toml",
+90 -12
View File
@@ -1,4 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.rulers": [80, 120],
// ===
// Spacing
@@ -14,23 +15,100 @@
// Event Triggers
// ===
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true,
"eslint.run": "onSave",
"eslint.validate": [
{
"language": "javascript",
"autoFix": true
},
{
"language": "javascriptreact",
"autoFix": true
}
],
"jest.autoRun": "off",
"prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"cSpell.userWords": [
"aabb",
"architectured",
"attrname",
"Barksy",
"browserslist",
"bulkdata",
"Cacheable",
"cfun",
"clonedeep",
"Colormap",
"Colormaps",
"Comlink",
"cornerstonejs",
"Crosshairs",
"datasource",
"dcmjs",
"decache",
"decached",
"decaching",
"deepmerge",
"Dicom",
"dicomweb",
"DISPLAYSETS",
"glwindow",
"grababble",
"grabbable",
"Hounsfield",
"Interactable",
"Interactor",
"istyle",
"kitware",
"labelmap",
"labelmaps",
"livewire",
"Mergeable",
"multiframe",
"nifti",
"ofun",
"OHIF",
"polylines",
"POLYSEG",
"prapogation",
"precisionmetrics",
"prefetch",
"Prescaled",
"pydicom",
"Radiopharmaceutical",
"rasterizing",
"reconstructable",
"Rehydratable",
"renderable",
"resampler",
"resemblejs",
"reslice",
"resliced",
"Reslices",
"roadmap",
"ROADMAPS",
"Segmentations",
"semibold",
"sitk",
"SUBRESOLUTION",
"suvbsa",
"suvbw",
"suvlbm",
"textbox",
"thresholded",
"thresholding",
"timepoint",
"timepoints",
"TMTV",
"TOOLGROUP",
"tqdm",
"transferables",
"typedoc",
"unsubscriptions",
"uuidv",
"viewplane",
"viewports",
"Voxel",
"Voxels",
"Vtkjs",
"wado",
"wadors",
"wadouri",
"workerpool"
]
}
+49 -13
View File
@@ -7,18 +7,20 @@ const fs = require('fs');
const webpack = require('webpack');
// ~~ PLUGINS
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ PackageJSON
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
// .rules;
// ~~ RULES
const loadShadersRule = require('./rules/loadShaders.js');
// const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js');
const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
// Only uncomment for old v2 stylus
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
@@ -55,10 +57,6 @@ if (!process.env.APP_CONFIG) {
}
module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set');
}
const mode = NODE_ENV === 'production' ? 'production' : 'development';
const isProdBuild = NODE_ENV === 'production';
const isQuickBuild = QUICK_BUILD === 'true';
@@ -96,12 +94,51 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
type: 'filesystem',
},
module: {
noParse: [/(codec)/, /(dicomicc)/],
noParse: [/(dicomicc)/],
rules: [
...(isProdBuild
? []
: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: isProdBuild ? [] : ['react-refresh/babel'],
},
},
]),
{
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
transpileJavaScriptRule(mode),
loadWebWorkersRule,
@@ -144,8 +181,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
'@state': path.resolve(__dirname, '../platform/app/src/state'),
'dicom-microscopy-viewer':
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
'@cornerstonejs/dicom-image-loader':
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js',
},
// Which directories to search when resolving modules
modules: [
@@ -173,6 +208,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
],
+1926
View File
File diff suppressed because it is too large. Load diff
+148
View File
@@ -10,6 +10,7 @@ Please find below the list of datasets used on the demo page, along with their r
## Platforms
### NIH NCI IDC
- Fedorov, A., Longabaugh, W.J., Pot, D., Clunie, D.A., Pieper, S., Aerts, H.J., Homeyer, A., Lewis, R., Akbarzadeh, A., Bontempi, D. and Clifford, W., 2021. NCI imaging data commons. Cancer research, 81(16), p.4188.
@@ -147,3 +148,150 @@ Stony Brook University COVID-19 Positive Cases (COVID-19-NY-SBU)
### 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2
https://data.kitware.com/
### 1.2.276.0.7230010.3.1.2.296485376.1.1665793212.499772
### 2.25.269859997690759739055099378767846712697
### 1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.232252967813565730694525674696
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627
### 1.3.6.1.4.1.5962.99.1.1117.5035.1620319789811.1.2.1
### 1.3.6.1.4.1.5962.99.1.1123.9231.1620326176300.1.2.1
### 1.3.6.1.4.1.5962.99.1.1126.3483.1620329455972.1.2.1
https://github.com/ImagingInformatics/hackathon-images
### 2.16.124.113543.6004.101.103.20021117.162333.1
### 2.16.124.113543.6004.101.103.20021117.190619.1
### 2.16.124.113543.6004.101.103.20021117.123455.1
### 2.16.124.113543.6004.101.103.20021117.061159.1
https://www.aapm.org/
### 1.2.840.113619.2.30.1.1762295590.1623.978668949.886
### 1.2.276.0.7230010.3.1.2.447481088.1.1669202398.851612
Custom data SPECT, specifically I123-FP-CIT (DaTSCAN) SPECT, evaluates the dopaminergic system to diagnose Parkinson's disease, especially when tremor symptoms are unclear. It helps distinguish Parkinson's disease from treatment-related tremor.
### 1.3.6.1.4.1.9328.50.1.54652
https://www.cancerimagingarchive.net/collection/rider-pilot/
Lung Image Database Consortium (LIDC). (2023) RIDER Pilot [Data set]. The Cancer Imaging Archive (TCIA). https://doi.org/10.7937/m87f-mz83
### 1.3.6.1.4.1.14519.5.2.1.331759366792756327296606233801322964986
Mayr, N., Yuh, W. T. C., Bowen, S., Harkenrider, M., Knopp, M. V., Lee, E. Y.-P., Leung, E., Lo, S. S., Small Jr., W., & Wolfson, A. H. (2023). Cervical Cancer – Tumor Heterogeneity: Serial Functional and Molecular Imaging Across the Radiation Therapy Course in Advanced Cervical Cancer (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/ERZ5-QZ59
https://www.cancerimagingarchive.net/collection/cc-tumor-heterogeneity/
### 1.3.6.1.4.1.14519.5.2.1.297577087050970310787702792940607009472
Eslick, E. M., Kipritidis, J., Gradinscak, D., Stevens, M. J., Bailey, D. L., Harris, B., Booth, J. T., & Keall, P. J. (2022). CT Ventilation as a functional imaging modality for lung cancer radiotherapy (CT-vs-PET-Ventilation-Imaging) (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/3ppx-7s22
https://www.cancerimagingarchive.net/collection/ct-vs-pet-ventilation-imaging/
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.634114621738943599785009586807
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.135953723682765205394176991681
Huang, W., Tudorica, A., Chui, S., Kemmer, K., Naik, A., Troxell, M., Oh, K., Roy, N., Afzal, A., & Holtorf, M. (2014). Variations of dynamic contrast-enhanced magnetic resonance imaging in evaluation of breast cancer therapy response: a multicenter data analysis challenge (QIN Breast DCE-MRI) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2014.a2n1ixox
https://www.cancerimagingarchive.net/collection/qin-breast-dce-mri/
### 1.3.6.1.4.1.14519.5.2.1.1.24766180081901755714059656629507905556
Cancer Moonshot Biobank. (2023). Cancer Moonshoot Biobank – Acute Myeloid Leukemia (CMB-AML) (Version 4) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/PCTE-6M66
https://www.cancerimagingarchive.net/collection/cmb-aml/
### 1.3.6.1.4.1.14519.5.2.1.3098.5025.285242291560760827564488897577
https://www.cancerimagingarchive.net/collection/anti-pd-1_lung/
Madhavi, P., Patel, S., & Tsao, A. S. (2019). Data from Anti-PD-1 Immunotherapy Lung [Data set]. The Cancer Imaging Archive. DOI: 10.7937/tcia.2019.zjjwb9ip
### 1.3.6.1.4.1.14519.5.2.1.1.84416332615988066829602832830236187384
https://www.cancerimagingarchive.net/collection/cmb-pca/
Cancer Moonshot Biobank. (2022). Cancer Moonshot Biobank – Prostate Cancer Collection (CMB-PCA) (Version 7) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/25T7-6Y12
### 1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331
Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI
https://www.cancerimagingarchive.net/collection/nsclc-radiomics/
### 1.3.6.1.4.1.14519.5.2.1.7085.2626.494695569589117268722281491772
https://www.cancerimagingarchive.net/collection/cptac-ucec/
National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2019). The Clinical Proteomic Tumor Analysis Consortium Uterine Corpus Endometrial Carcinoma Collection (CPTAC-UCEC) (Version 12) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.3R3JUISW
### 1.3.6.1.4.1.14519.5.2.1.207544490797667703011829289839681390478
https://www.cancerimagingarchive.net/collection/remind/
Juvekar, P., Dorent, R., Kögl, F., Torio, E., Barr, C., Rigolo, L., Galvin, C., Jowkar, N., Kazi, A., Haouchine, N., Cheema, H., Navab, N., Pieper, S., Wells, W. M., Bi, W. L., Golby, A., Frisken, S., & Kapur, T. (2023). The Brain Resection Multimodal Imaging Database (ReMIND) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/3RAG-D070
### 1.3.12.2.1107.5.1.4.60175.30000008042114404745300000010
Gavrielides, M. A., Kinnard, L. M., Myers, K. J., Peregoy, J., Pritchard, W. F., Zeng, R., Esparza, J., Karanian, J., & Petrick, N. (2015). Data From Phantom FDA [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/TCIA.2015.orbjkmux
https://www.cancerimagingarchive.net/collection/phantom-fda/
### 1.3.6.1.4.1.14519.5.2.1.6834.5010.992793141464713669479982159310
https://www.cancerimagingarchive.net/collection/4d-lung/
Hugo, G. D., Weiss, E., Sleeman, W. C., Balik, S., Keall, P. J., Lu, J., & Williamson, J. F. (2016). Data from 4D Lung Imaging of NSCLC Patients (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.ELN8YGLE
### 1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036
https://www.cancerimagingarchive.net/collection/rider-lung-pet-ct/
Muzi P, Wanner M, & Kinahan P. (2015). Data From RIDER Lung PET-CT. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2015.ofip7tvm
### 1.3.6.1.4.1.14519.5.2.1.9823.1001.134394060407147891170882809392
https://www.cancerimagingarchive.net/collection/prostate-mri/
Choyke P, Turkbey B, Pinto P, Merino M, Wood B. (2016). Data From PROSTATE-MRI. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2016.6046GUDv
### 1.3.6.1.4.1.14519.5.2.1.191696062987463500085282581898315738844
https://www.cancerimagingarchive.net/collection/upenn-gbm/
Bakas, S., Sako, C., Akbari, H., Bilello, M., Sotiras, A., Shukla, G., Rudie, J. D., Flores Santamaria, N., Fathi Kazerooni, A., Pati, S., Rathore, S., Mamourian, E., Ha, S. M., Parker, W., Doshi, J., Baid, U., Bergman, M., Binder, Z. A., Verma, R., … Davatzikos, C. (2021). Multi-parametric magnetic resonance imaging (mpMRI) scans for de novo Glioblastoma (GBM) patients from the University of Pennsylvania Health System (UPENN-GBM) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.709X-DN49
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.921758700577562664959693695481
https://www.cancerimagingarchive.net/collection/breast-diagnosis/
Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR
### 1.3.6.1.4.1.14519.5.2.1.1620.1225.189514895974227080410265976065
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
https://www.cancerimagingarchive.net/collection/ea1141/
### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21
From OFFIS DICOM-Team
https://www.offis.de/
OFFIS DICOM-Team
+55 -31
View File
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.7-labs
# This dockerfile is used to publish the `ohif/app` image on dockerhub.
#
# It's a good example of how to build our static application and package it
@@ -19,61 +20,84 @@
#
# syntax=docker/dockerfile:1.7-labs
# This dockerfile is used to publish the `ohif/app` image on dockerhub.
#
# It's a good example of how to build our static application and package it
# with a web server capable of hosting it as static content.
#
# docker build
# --------------
# If you would like to use this dockerfile to build and tag an image, make sure
# you set the context to the project's root directory:
# https://docs.docker.com/engine/reference/commandline/build/
#
#
# SUMMARY
# --------------
# This dockerfile is used as an input for a second stage to make things run faster.
#
# Stage 1: Build the application
# docker build -t ohif/viewer:latest .
FROM node:18.16.1-slim as json-copier
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY ["package.json", "yarn.lock", "preinstall.js", "./"]
COPY extensions /usr/src/app/extensions
COPY modes /usr/src/app/modes
COPY platform /usr/src/app/platform
# Find and remove non-package.json files
#RUN find extensions \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
#RUN find modes \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
#RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
# Copy Files
FROM node:18.16.1-slim as builder
FROM node:20.18.1-slim as builder
RUN apt-get update && apt-get install -y build-essential python3
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
RUN npm install -g bun
# RUN npm install -g lerna@7.4.2
ENV PATH=/usr/src/app/node_modules/.bin:$PATH
COPY --from=json-copier /usr/src/app .
# Do an initial install and then a final install
COPY package.json yarn.lock preinstall.js lerna.json ./
COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./
# Run the install before copying the rest of the files
RUN yarn config set workspaces-experimental true
RUN yarn install --frozen-lockfile --verbose
COPY . .
RUN bun pm cache rm
RUN bun install
# Copy the local directory
COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . .
# Do a second install to finalize things after the copy
RUN bun run show:config
RUN bun install
# To restore workspaces symlinks
RUN yarn install --frozen-lockfile --verbose
ENV PATH /usr/src/app/node_modules/.bin:$PATH
# Build here
# After install it should hopefully be stable until the local directory changes
ENV QUICK_BUILD true
# ENV GENERATE_SOURCEMAP=false
# ENV REACT_APP_CONFIG=config/default.js
ARG APP_CONFIG=config/default.js
ARG PUBLIC_URL=/
RUN yarn run build
RUN bun run show:config
RUN bun run build
# Precompress files
RUN chmod u+x .docker/compressDist.sh
RUN ./.docker/compressDist.sh
# Stage 3: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.25-alpine as final
FROM nginxinc/nginx-unprivileged:1.27-alpine as final
#RUN apk add --no-cache bash
ENV PORT=80
ARG PORT=80
ENV PORT=${PORT}
ARG PUBLIC_URL=/
ENV PUBLIC_URL=${PUBLIC_URL}
RUN rm /etc/nginx/conf.d/default.conf
USER nginx
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html${PUBLIC_URL}
COPY --from=builder /usr/src/app/platform/app/dist/index.html /usr/share/nginx/html
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
# The nginx user cannot chmod it, so change to root.
USER root
RUN chmod 666 /usr/share/nginx/html/app-config.js
RUN chown -R nginx:nginx /usr/share/nginx/html
USER nginx
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+9 -12
View File
@@ -51,7 +51,6 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |
## About
The OHIF Viewer can retrieve
@@ -119,19 +118,16 @@ This is typically where the latest development happens. Code that is in the mast
Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1`
### `release` branch - The latest stable release
### `release/*` branches - The latest stable releases
Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production.
This branch represents the latest stable version of the project that is considered ready for production. The code in this branch should be fully tested and vetted for release. Once the code in the master branch reaches a state where it's stable and ready to be released to users,
we do a comprehensive code review and QA testing. Once the code is approved,
we merge it into the release branch and tag a new release.
For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch.
Each package is tagged with version numbers, and published to npm such as `@ohif/ui@3.5.0`
Note: `master` is always ahead of `release` branch. We publish both docker builds for beta and stable releases.
Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases.
Here is a schematic representation of our development workflow:
![Alt text](platform/docs/docs/assets/img/github-readme-branches.png)
![alt text](platform/docs/docs/assets/img/github-readme-branches-Jun2024.png)
@@ -175,12 +171,13 @@ also supports a number of commands that can be found in their respective
| Yarn Commands | Description |
| ---------------------------- | ------------------------------------------------------------- |
| **Develop** | |
| `dev` or `start` | Default development experience for Viewer |
| `dev` | Default development experience for Viewer |
| `dev:fast` | Our experimental fast dev mode that uses rsbuild instead of webpack |
| `test:unit` | Jest multi-project test runner; overall coverage |
| **Deploy** | |
| `build`\* | Builds production output for our PWA Viewer | |
\* - For more information on our different builds, check out our [Deploy
\* - For more information on different builds, check out our [Deploy
Docs][deployment-docs]
## Project
@@ -337,4 +334,4 @@ MIT © [OHIF](https://github.com/OHIF)
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
<!-- prettier-ignore-end -->
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large&issueType=license)
+3
View File
@@ -0,0 +1,3 @@
# External Dependencies
This module contains optional dependencies and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component.
File diff suppressed because it is too large. Load diff
+98
View File
@@ -0,0 +1,98 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.10.0-beta.86",
"license": "MIT",
"private": true,
"engines": {
"node": ">=12",
"yarn": ">=1.19.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "32.1.1",
"clsx": "^2.1.1",
"core-js": "^3.2.1",
"moment": "^2.9.4"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@rsbuild/core": "^1.1.13",
"@rsbuild/plugin-node-polyfill": "1.2.0",
"@rsbuild/plugin-react": "^1.1.0",
"@svgr/webpack": "^8.1.0",
"@swc/helpers": "^0.5.15",
"@types/jest": "^27.5.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"autoprefixer": "^10.4.4",
"babel-eslint": "9.x",
"babel-loader": "^8.2.4",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^5.2.0",
"css-loader": "^6.8.1",
"dotenv": "^8.1.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^7.2.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-flowtype": "^7.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-tsdoc": "^0.2.11",
"eslint-webpack-plugin": "^2.5.3",
"execa": "^8.0.1",
"extract-css-chunks-webpack-plugin": "^4.5.4",
"html-webpack-plugin": "^5.3.2",
"husky": "^3.0.0",
"jest": "^29.5.0",
"jest-canvas-mock": "^2.1.0",
"jest-environment-jsdom": "^29.5.0",
"jest-junit": "^6.4.0",
"lerna": "^7.2.0",
"lint-staged": "^9.0.2",
"mini-css-extract-plugin": "^2.1.0",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"postcss": "^8.3.5",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
"postcss-preset-env": "^7.4.3",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"react-refresh": "^0.14.2",
"semver": "^7.5.1",
"serve": "^14.2.4",
"shader-loader": "^1.3.1",
"shx": "^0.3.3",
"source-map-loader": "^4.0.1",
"start-server-and-test": "^1.10.0",
"style-loader": "^1.0.0",
"stylus": "^0.59.0",
"stylus-loader": "^7.1.3",
"terser-webpack-plugin": "^5.1.4",
"typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0",
"webpack": "5.94.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "4.7.3",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.7.3",
"workbox-webpack-plugin": "^6.1.5",
"worker-loader": "^3.0.8"
},
"scripts": {
"build": "Included as direct dependency"
}
}
File diff suppressed because it is too large. Load diff
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.10.0-beta.86",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1"
}
}
+51
View File
@@ -0,0 +1,51 @@
{
"name": "ohif-monorepo-root",
"private": true,
"packageManager": "yarn@1.22.22",
"workspaces": {
"packages": [
"../platform/i18n",
"../platform/core",
"../platform/ui",
"../platform/ui-next",
"../platform/app",
"../extensions/*",
"../modes/*",
"../addOns/externals/*"
],
"nohoist": [
"**/html-minifier-terser"
]
},
"scripts": {
"preinstall": "cd .. && node preinstall.js"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0"
},
"resolutions": {
"**/@babel/runtime": "^7.20.13",
"commander": "8.3.0",
"dcmjs": "0.38.0",
"dicomweb-client": ">=0.10.4",
"nth-check": "^2.1.1",
"trim-newlines": "^5.0.0",
"glob-parent": "^6.0.2",
"trim": "^1.0.0",
"package-json": "^8.1.0",
"typescript": "5.5.4",
"sharp": "^0.32.6"
}
}
+1 -18
View File
@@ -5,23 +5,6 @@ module.exports = {
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
plugins: [
[
'inline-react-svg',
{
svgo: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
},
],
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
@@ -49,6 +32,7 @@ module.exports = {
'@babel/transform-destructuring',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-class-static-block',
],
},
production: {
@@ -67,7 +51,6 @@ module.exports = {
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
+7754
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
34c7d72142847486b98c9c52469940083eeaf87e
9b8e5cfd1a6121a58991c0f75660a2fd9913a4e7
@@ -0,0 +1,12 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const ENTRY = {
app: `${SRC_DIR}/index.tsx`,
};
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
};
@@ -0,0 +1,54 @@
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, '../');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const ENTRY = {
app: `${SRC_DIR}/index.tsx`,
};
const outputName = `ohif-${pkg.name.split('/').pop()}`;
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
return merge(commonConfig, {
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-pmap',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
new MiniCssExtractPlugin({
filename: `./dist/${outputName}.css`,
chunkFilename: `./dist/${outputName}.css`,
}),
],
});
};
File diff suppressed because it is too large. Load diff
+20
View File
@@ -0,0 +1,20 @@
MIT License
Copyright (c) 2023 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,12 @@
# dicom-pmap
## Description
DICOM PMAP read workflow. This extension will allow you to load a DICOM Parametric
Map image and display it on OHIF.
## Author
OHIF
## License
MIT
@@ -0,0 +1,44 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
'@babel/preset-typescript',
],
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};
@@ -0,0 +1,54 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.10.0-beta.86",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
"main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js",
"module": "src/index.tsx",
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-pmap": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.86",
"@ohif/extension-cornerstone": "3.10.0-beta.86",
"@ohif/extension-default": "3.10.0-beta.86",
"@ohif/i18n": "3.10.0-beta.86",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^2.17.2",
"@cornerstonejs/core": "^2.17.2",
"@kitware/vtk.js": "32.1.1",
"react-color": "^2.19.3"
}
}
@@ -0,0 +1,244 @@
import { utils } from '@ohif/core';
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
import { adaptersPMAP } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
import { dicomLoaderService } from '@ohif/extension-cornerstone';
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
function _getDisplaySetsFromSeries(
instances,
servicesManager: AppTypes.ServicesManager,
extensionManager
) {
const instance = instances[0];
const {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
SeriesDescription,
SeriesNumber,
SeriesDate,
SOPClassUID,
wadoRoot,
wadoUri,
wadoUriRoot,
} = instance;
const displaySet = {
// Parametric map use to have the same modality as its referenced volume but
// "PMAP" is used in the viewer even though this is not a valid DICOM modality
Modality: 'PMAP',
isReconstructable: true, // by default for now
displaySetInstanceUID: `pmap.${utils.guid()}`,
SeriesDescription,
SeriesNumber,
SeriesDate,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId,
SOPClassUID,
referencedImages: null,
referencedSeriesInstanceUID: null,
referencedDisplaySetInstanceUID: null,
referencedVolumeURI: null,
referencedVolumeId: null,
isDerivedDisplaySet: true,
loadStatus: {
loading: false,
loaded: false,
},
sopClassUids,
instance,
instances: [instance],
wadoRoot,
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
};
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
if (!referencedSeriesSequence) {
console.error('ReferencedSeriesSequence is missing for the parametric map');
return;
}
const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
// Does not get the referenced displaySet during parametric displaySet creation
// because it is still not available (getDisplaySetByUID returns `undefined`).
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
if (displaySet.referencedDisplaySetInstanceUID) {
return displaySetService.getDisplaySetByUID(displaySet.referencedDisplaySetInstanceUID);
}
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced displaySet is missing for the parametric map');
}
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
return referencedDisplaySet;
};
// Does not get the referenced volumeId during parametric displaySet creation because the
// referenced displaySet is still not available (getDisplaySetByUID returns `undefined`).
displaySet.getReferencedVolumeId = () => {
if (displaySet.referencedVolumeId) {
return displaySet.referencedVolumeId;
}
const referencedDisplaySet = displaySet.getReferenceDisplaySet();
const referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
const referencedVolumeId = `${VOLUME_LOADER_SCHEME}:${referencedVolumeURI}`;
displaySet.referencedVolumeURI = referencedVolumeURI;
displaySet.referencedVolumeId = referencedVolumeId;
return referencedVolumeId;
};
displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers);
return [displaySet];
}
const getRangeFromPixelData = (pixelData: Float32Array) => {
let lowest = pixelData[0];
let highest = pixelData[0];
for (let i = 1; i < pixelData.length; i++) {
if (pixelData[i] < lowest) {
lowest = pixelData[i];
}
if (pixelData[i] > highest) {
highest = pixelData[i];
}
}
return [lowest, highest];
};
async function _load(
displaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager,
headers
) {
const volumeId = `${VOLUME_LOADER_SCHEME}:${displaySet.displaySetInstanceUID}`;
const volumeLoadObject = cache.getVolumeLoadObject(volumeId);
if (volumeLoadObject) {
return volumeLoadObject.promise;
}
displaySet.loading = true;
displaySet.isLoaded = false;
// We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers.
const promise = _loadParametricMap({
extensionManager,
displaySet,
headers,
});
cache.putVolumeLoadObject(volumeId, { promise }).catch(err => {
throw err;
});
promise
.then(() => {
displaySet.loading = false;
displaySet.isLoaded = true;
// Broadcast that loading is complete
servicesManager.services.segmentationService._broadcastEvent(
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
{
pmapDisplaySet: displaySet,
}
);
})
.catch(err => {
displaySet.loading = false;
displaySet.isLoaded = false;
throw err;
});
return promise;
}
async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
const referencedVolumeId = displaySet.getReferencedVolumeId();
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
// Parametric map can be loaded only if its referenced volume exists otherwise it will fail
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the PMAP, and stack viewport PMAP is not supported yet'
);
}
const { imageIds } = cachedReferencedVolume;
const results = await adaptersPMAP.Cornerstone3D.ParametricMap.generateToolState(
imageIds,
arrayBuffer,
metaData
);
const { pixelData } = results;
const TypedArrayConstructor = pixelData.constructor;
const paramMapId = displaySet.displaySetInstanceUID;
const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, {
volumeId: paramMapId,
targetBuffer: {
type: TypedArrayConstructor.name,
},
});
const newPixelData = new TypedArrayConstructor(pixelData.length);
for (let i = 0; i < pixelData.length; i++) {
newPixelData[i] = pixelData[i] * 100;
}
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
const range = getRangeFromPixelData(newPixelData);
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
derivedVolume.metadata.voiLut = [windowLevel];
derivedVolume.loadStatus = { loaded: true };
return derivedVolume;
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
{
name: 'dicom-pmap',
sopClassUids,
getDisplaySetsFromSeries,
},
];
}
export default getSopClassHandlerModule;
@@ -0,0 +1,7 @@
import packageJson from '../package.json';
const id = packageJson.name;
const SOPClassHandlerName = 'dicom-pmap';
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
export { id, SOPClassHandlerId, SOPClassHandlerName };
@@ -0,0 +1,39 @@
import { id } from './id';
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePMAPViewport');
});
const OHIFCornerstonePMAPViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
/**
* You can remove any of the following modules if you don't need them.
*/
const extension = {
id,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
const ExtendedOHIFCornerstonePMAPViewport = props => {
return (
<OHIFCornerstonePMAPViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
};
return [{ name: 'dicom-pmap', component: ExtendedOHIFCornerstonePMAPViewport }];
},
getSopClassHandlerModule,
};
export default extension;
@@ -0,0 +1,210 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid } from '@ohif/ui-next';
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const {
displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const viewportId = viewportOptions.viewportId;
const { displaySetService, segmentationService, uiNotificationService, customizationService } =
servicesManager.services;
// PMAP viewport will always have a single display set
if (displaySets.length !== 1) {
throw new Error('PMAP viewport must have a single display set');
}
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
'ui.loadingIndicatorTotalPercent'
);
const pmapDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = pmapDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet,
pmapDisplaySet
);
referencedDisplaySetRef.current = {
displaySet: referencedDisplaySet,
metadata: referencedDisplaySetMetadata,
};
const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
// Add effect to listen for loading complete
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
setPmapIsLoading(false);
}
}
);
return () => {
unsubscribe();
};
}, [pmapDisplaySet]);
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
displaySetOptions.unshift({});
const [pmapDisplaySetOptions] = displaySetOptions;
// Make sure `options` exists
pmapDisplaySetOptions.options = pmapDisplaySetOptions.options ?? {};
Object.assign(pmapDisplaySetOptions.options, {
colormap: {
name: 'rainbow_2',
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.25, opacity: 0.25 },
{ value: 0.5, opacity: 0.5 },
{ value: 0.75, opacity: 0.75 },
{ value: 0.9, opacity: 0.99 },
],
},
voi: {
windowCenter: 50,
windowWidth: 100,
},
});
uiNotificationService.show({
title: 'Parametric Map',
type: 'warning',
message: 'The values are multiplied by 100 in the viewport for better visibility',
});
return (
<Component
{...props}
// Referenced + PMAP displaySets must be passed as parameter in this order
displaySets={[referencedDisplaySet, pmapDisplaySet]}
viewportOptions={{
viewportType: 'volume',
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
displaySetOptions={[{}, pmapDisplaySetOptions]}
></Component>
);
}, [
extensionManager,
displaySetOptions,
props,
pmapDisplaySet,
viewportOptions.orientation,
viewportOptions.viewportId,
]);
// Cleanup the PMAP viewport when the viewport is destroyed
useEffect(() => {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports.get(activeViewportId);
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
viewportGridService.setDisplaySetsForViewport({
viewportId: activeViewportId,
displaySetInstanceUIDs: [],
});
}
}
);
return () => {
onDisplaySetsRemovedSubscription.unsubscribe();
};
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
let childrenWithProps = null;
if (children && children.length) {
childrenWithProps = children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportId,
key: index,
})
);
});
}
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{pmapIsLoading && (
<LoadingIndicatorTotalPercent
className="h-full w-full"
totalNumbers={null}
percentComplete={null}
loadingText="Loading Parametric Map..."
/>
)}
{getCornerstoneViewport()}
{childrenWithProps}
</div>
</>
);
}
OHIFCornerstonePMAPViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet, pmapDisplaySet) {
const { SharedFunctionalGroupsSequence } = pmapDisplaySet.instance;
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
? SharedFunctionalGroupsSequence[0]
: SharedFunctionalGroupsSequence;
const { PixelMeasuresSequence } = SharedFunctionalGroup;
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
? PixelMeasuresSequence[0]
: PixelMeasuresSequence;
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
PatientID: image0.PatientID,
PatientName: image0.PatientName,
PatientSex: image0.PatientSex,
PatientAge: image0.PatientAge,
SliceThickness: image0.SliceThickness || SliceThickness,
StudyDate: image0.StudyDate,
SeriesDescription: image0.SeriesDescription,
SeriesInstanceUID: image0.SeriesInstanceUID,
SeriesNumber: image0.SeriesNumber,
ManufacturerModelName: image0.ManufacturerModelName,
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
};
return referencedDisplaySetMetadata;
}
export default OHIFCornerstonePMAPViewport;
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,5 @@
module.exports = {
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
@@ -37,7 +37,6 @@ module.exports = {
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.9.0-beta.16",
"version": "3.10.0-beta.86",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,16 +33,16 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.16",
"@ohif/extension-cornerstone": "3.9.0-beta.16",
"@ohif/extension-default": "3.9.0-beta.16",
"@ohif/i18n": "3.9.0-beta.16",
"@ohif/core": "3.10.0-beta.86",
"@ohif/extension-cornerstone": "3.10.0-beta.86",
"@ohif/extension-default": "3.10.0-beta.86",
"@ohif/i18n": "3.10.0-beta.86",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^10.11.0",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0"
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
@@ -0,0 +1,55 @@
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const services = servicesManager.services;
const { displaySetService, viewportGridService } = services;
const actions = {
hydrateRTSDisplaySet: ({ displaySet, viewportId }) => {
if (displaySet.Modality !== 'RTSTRUCT') {
throw new Error('Display set is not an RTSTRUCT');
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: displaySet,
type: SegmentationRepresentations.Contour,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
},
};
const definitions = {
hydrateRTSDisplaySet: {
commandFn: actions.hydrateRTSDisplaySet,
storeContexts: [],
options: {},
},
};
return {
actions,
definitions,
defaultContext: 'cornerstone-dicom-rt',
};
};
export default commandsModule;
@@ -72,22 +72,27 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
// Instead of throwing error, subscribe to display sets added
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the RT');
}
} else {
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
return referencedDisplaySet;
};
}
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
@@ -111,19 +116,13 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!rtDisplaySet.structureSet) {
const structureSet = await loadRTStruct(
extensionManager,
rtDisplaySet,
rtDisplaySet.getReferenceDisplaySet(),
headers
);
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
rtDisplaySet.structureSet = structureSet;
}
const suppressEvents = true;
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
@@ -2,6 +2,7 @@ import { id } from './id';
import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getCommandsModule from './getCommandsModule';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
@@ -24,6 +25,7 @@ const extension: Types.Extensions.Extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
getCommandsModule,
/**
* PanelModule should provide a list of panels that will be available in OHIF
@@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) {
});
}
export default async function loadRTStruct(
extensionManager,
rtStructDisplaySet,
referencedDisplaySet,
headers
) {
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
@@ -102,8 +97,6 @@ export default async function loadRTStruct(
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs =
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
// Set here is loading is asynchronous.
// If this function throws its set back to false.
@@ -133,6 +126,7 @@ export default async function loadRTStruct(
SeriesInstanceUID: instance.SeriesInstanceUID,
ROIContours: [],
visible: true,
ReferencedSOPInstanceUIDsSet: new Set(),
};
for (let i = 0; i < ROIContourSequence.length; i++) {
@@ -149,7 +143,7 @@ export default async function loadRTStruct(
const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) {
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
const { ContourData, NumberOfContourPoints, ContourGeometricType, ContourImageSequence } =
ContourSequenceArray[c];
let isSupported = false;
@@ -180,6 +174,12 @@ export default async function loadRTStruct(
type: ContourGeometricType,
isSupported,
});
if (ContourImageSequence?.ReferencedSOPInstanceUID) {
structureSet.ReferencedSOPInstanceUIDsSet.add(
ContourImageSequence?.ReferencedSOPInstanceUID
);
}
}
_setROIContourMetadata(
@@ -194,24 +194,6 @@ export default async function loadRTStruct(
return structureSet;
}
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
imageIdSopInstanceUidPairsEntry =>
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
return referencedDisplaySet.images.map(image => {
return {
imageId: image.imageId,
sopInstanceUID: image.SOPInstanceUID,
};
});
}
function _setROIContourMetadata(
structureSet,
StructureSetROISequence,
@@ -1,5 +1,5 @@
function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
@@ -10,7 +10,6 @@ function promptHydrateRT({
servicesManager,
rtDisplaySet,
viewportId,
toolGroupId = 'default',
preHydrateCallbacks,
hydrateRTDisplaySet,
}: withAppTypes) {
@@ -27,16 +26,13 @@ function promptHydrateRT({
callback();
});
window.setTimeout(async () => {
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
toolGroupId,
servicesManager,
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
}, 0);
resolve(isHydrated);
}
});
}
@@ -46,11 +42,13 @@ function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService,
const message = 'Do you want to open this Segmentation?';
const actions = [
{
id: 'no-hydrate',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
@@ -1,10 +1,13 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
import { ViewportActionArrows } from '@ohif/ui';
import { useViewportGrid } from '@ohif/ui-next';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
@@ -36,13 +39,17 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
throw new Error('RT viewport should only have a single display set');
}
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
'ui.loadingIndicatorTotalPercent'
);
const rtDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
const [selectedSegment, setSelectedSegment] = useState(1);
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the RT is opened and segments are loaded into the
// segmentation panel, and RT is also rendered on any viewport that is in the
@@ -62,7 +69,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
referencedDisplaySetRef.current = {
@@ -91,12 +101,15 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}, [viewportGrid]);
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [rtDisplaySet],
viewportId,
});
};
const hydrateRTDisplaySet = useCallback(
({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('hydrateRTSDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
[commandsManager]
);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
@@ -111,10 +124,11 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: 'volume',
viewportType: 'stack',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
@@ -143,10 +157,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
[selectedSegment, segmentationService]
);
useEffect(() => {
@@ -177,6 +191,19 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
setRtIsLoading(false);
}
if (rtDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
const { firstSegmentedSliceImageId } = rtDisplaySet;
const { presentationIds } = viewportOptions;
setPositionPresentation(presentationIds.positionPresentationId, {
viewportType: 'stack',
viewReference: {
referencedImageId: firstSegmentedSliceImageId,
},
viewPresentation: {},
});
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
@@ -239,11 +266,9 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
setToolGroupCreated(true);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
segmentationService.removeSegmentationRepresentations(viewportId);
toolGroupService.destroyToolGroup(toolGroupId);
};
@@ -254,11 +279,27 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -282,24 +323,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -318,7 +343,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
@@ -358,11 +383,6 @@ OHIFCornerstoneRTViewport.propTypes = {
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
OHIFCornerstoneRTViewport.defaultProps = {
customProps: {},
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
@@ -1,6 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Tooltip } from '@ohif/ui';
import { ViewportActionButton } from '@ohif/ui';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
@@ -8,18 +9,16 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
StatusIcon = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icon
<Icons.ByName
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
@@ -34,13 +33,7 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
@@ -49,14 +42,18 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
return (
<>
{ToolTipMessage && (
<Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea />
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
}
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,5 @@
module.exports = {
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
@@ -37,7 +37,6 @@ module.exports = {
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.9.0-beta.16",
"version": "3.10.0-beta.86",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,22 +33,22 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.16",
"@ohif/extension-cornerstone": "3.9.0-beta.16",
"@ohif/extension-default": "3.9.0-beta.16",
"@ohif/i18n": "3.9.0-beta.16",
"@ohif/core": "3.10.0-beta.86",
"@ohif/extension-cornerstone": "3.10.0-beta.86",
"@ohif/extension-default": "3.10.0-beta.86",
"@ohif/i18n": "3.10.0-beta.86",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.74.6",
"@cornerstonejs/core": "^1.74.6",
"@kitware/vtk.js": "30.4.1",
"@cornerstonejs/adapters": "^2.17.2",
"@cornerstonejs/core": "^2.17.2",
"@kitware/vtk.js": "32.1.1",
"react-color": "^2.19.3"
}
}
@@ -14,18 +14,22 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
const {
Cornerstone3D: {
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
Segmentation: { generateSegmentation },
},
} = adaptersSEG;
@@ -42,85 +46,14 @@ const commandsModule = ({
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
uiNotificationService,
segmentationService,
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
} = servicesManager.services;
} = servicesManager.services as AppTypes.Services;
const actions = {
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
getUpdatedViewportsForSegmentation,
/**
* Creates an empty segmentation for a specified viewport.
* It first checks if the display set associated with the viewport is reconstructable.
* If not, it raises a notification error. Otherwise, it creates a new segmentation
* for the display set after handling the necessary steps for making the viewport
* a volume viewport first
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the target viewport ID.
*
*/
createEmptySegmentationForViewport: async ({ viewportId }) => {
const viewport = getTargetViewport({ viewportId, viewportGridService });
// Todo: add support for multiple display sets
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet.isReconstructable) {
uiNotificationService.show({
title: 'Segmentation',
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
type: 'error',
});
return;
}
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
const currentSegmentations = segmentationService.getSegmentations();
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
displaySetInstanceUID,
{ label: `Segmentation ${currentSegmentations.length + 1}` }
);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
// Add only one segment for now
segmentationService.addSegment(segmentationId, {
toolGroupId,
segmentIndex: 1,
properties: {
label: 'Segment 1',
},
});
return segmentationId;
},
});
},
/**
* Loads segmentations for a specified viewport.
* The function prepares the viewport for rendering, then loads the segmentation details.
@@ -132,103 +65,30 @@ const commandsModule = ({
*
*/
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
// Todo: handle adding more than one segmentation
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const segmentation = segmentations[0];
const segmentationId = segmentation.id;
const label = segmentation.label;
const segments = segmentation.segments;
delete segmentation.segments;
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
segmentationId,
label,
});
if (segmentation.scalarData) {
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
labelmapVolume.scalarData.set(segmentation.scalarData);
}
segmentationService.addOrUpdateSegmentation(segmentation);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
segments.forEach(segment => {
if (segment === null) {
return;
}
segmentationService.addSegment(segmentationId, {
segmentIndex: segment.segmentIndex,
toolGroupId,
properties: {
color: segment.color,
label: segment.label,
opacity: segment.opacity,
isLocked: segment.isLocked,
visibility: segment.isVisible,
active: segmentation.activeSegmentIndex === segment.segmentIndex,
},
});
});
if (segmentation.centroidsIJK) {
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
}
return segmentationId;
},
});
},
/**
* Loads segmentation display sets for a specified viewport.
* Depending on the modality of the display set (SEG or RTSTRUCT),
* it chooses the appropriate service function to create
* the segmentation for the display set.
* The function then prepares the viewport for rendering segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
* @param params.displaySets - Array of display sets to be loaded for segmentation.
*
*/
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
// Todo: handle adding more than one segmentation
const displaySet = displaySets[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID,
loadFn: async () => {
const segDisplaySet = displaySet;
const suppressEvents = false;
const serviceFunction =
segDisplaySet.Modality === 'SEG'
? 'createSegmentationForSEGDisplaySet'
: 'createSegmentationForRTDisplaySet';
const segmentation = segmentations[0];
const segmentationId = segmentation.segmentationId;
const label = segmentation.config.label;
const segments = segmentation.config.segments;
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
const segmentation = segmentationService.getSegmentation(segmentationId);
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
return segmentationId;
},
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
await segmentationService.createLabelmapForDisplaySet(displaySet, {
segmentationId,
segments,
label,
});
segmentationService.addOrUpdateSegmentation(segmentation);
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
segmentationId,
});
return segmentationId;
},
/**
* Generates a segmentation from a given segmentation ID.
@@ -246,25 +106,61 @@ const commandsModule = ({
generateSegmentation: ({ segmentationId, options = {} }) => {
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
const { imageIds } = segmentation.representationData.Labelmap;
const segmentationVolume = cache.getVolume(segmentationId);
const referencedVolume = cache.getVolume(referencedVolumeId);
const referencedImages = referencedVolume.getCornerstoneImages();
const segImages = imageIds.map(imageId => cache.getImage(imageId));
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
const labelmaps2D = [];
// Generate fake metadata as an example
labelmapObj.metadata = [];
let z = 0;
for (const segImage of segImages) {
const segmentsOnLabelmap = new Set();
const pixelData = segImage.getPixelData();
const { rows, columns } = segImage;
// Use a single pass through the pixel data
for (let i = 0; i < pixelData.length; i++) {
const segment = pixelData[i];
if (segment !== 0) {
segmentsOnLabelmap.add(segment);
}
}
labelmaps2D[z++] = {
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
pixelData,
rows,
columns,
};
}
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
const labelmap3D = {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata: [],
labelmaps2D,
};
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
segmentationInOHIF.segments.forEach(segment => {
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
// segmentation service already has a color for each segment
if (!segment) {
return;
}
const segmentIndex = segment.segmentIndex;
const { label, color } = segment;
const { label } = segment;
const firstRepresentation = representations[0];
const color = segmentationService.getSegmentColor(
firstRepresentation.viewportId,
segmentationId,
segment.segmentIndex
);
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
color.slice(0, 3).map(value => value / 255)
@@ -273,8 +169,8 @@ const commandsModule = ({
const segmentMetadata = {
SegmentNumber: segmentIndex.toString(),
SegmentLabel: label,
SegmentAlgorithmType: 'MANUAL',
SegmentAlgorithmName: 'OHIF Brush',
SegmentAlgorithmType: segment?.algorithmType || 'MANUAL',
SegmentAlgorithmName: segment?.algorithmName || 'OHIF Brush',
RecommendedDisplayCIELabValue,
SegmentedPropertyCategoryCodeSequence: {
CodeValue: 'T-D0050',
@@ -287,12 +183,12 @@ const commandsModule = ({
CodeMeaning: 'Tissue',
},
};
labelmapObj.metadata[segmentIndex] = segmentMetadata;
labelmap3D.metadata[segmentIndex] = segmentMetadata;
});
const generatedSegmentation = generateSegmentation(
referencedImages,
labelmapObj,
labelmap3D,
metaData,
options
);
@@ -334,7 +230,7 @@ const commandsModule = ({
extensionManager,
});
if (promptResult.action !== 1 && promptResult.value) {
if (promptResult.action !== 1 && !promptResult.value) {
return;
}
@@ -440,18 +336,19 @@ const commandsModule = ({
};
const definitions = {
getUpdatedViewportsForSegmentation: {
commandFn: actions.getUpdatedViewportsForSegmentation,
},
/**
* Obsolete?
*/
loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport,
},
/**
* Obsolete?
*/
loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport,
},
createEmptySegmentationForViewport: {
commandFn: actions.createEmptySegmentationForViewport,
},
generateSegmentation: {
commandFn: actions.generateSegmentation,
},
@@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [
{
@@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: { allowUnmatchedView: true },
viewportOptions: {
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [
{
id: 'segDisplaySetId',
@@ -1,80 +0,0 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui';
import PanelSegmentation from './panels/PanelSegmentation';
const getPanelModule = ({
commandsManager,
servicesManager,
extensionManager,
configuration,
title,
}: withAppTypes) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const [appConfig] = useAppConfig();
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
);
};
const wrappedPanelSegmentationWithTools = configuration => {
const [appConfig] = useAppConfig();
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
</>
);
};
return [
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
},
];
};
export default getPanelModule;
@@ -1,6 +1,6 @@
import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools';
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
@@ -70,28 +70,32 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
const referencedDisplaySet = referencedDisplaySets[0];
if (!referencedDisplaySet) {
// subscribe to display sets added which means at some point it will be available
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
// here we can also do a little bit of search, since sometimes DICOM SEG
// does not contain the referenced display set uid , and we can just
// see which of the display sets added is more similar and assign it
// to the referencedDisplaySet
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the SEG');
}
const referencedDisplaySet = referencedDisplaySets[0];
} else {
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
displaySet.referencedVolumeId = referencedVolumeId;
return referencedDisplaySet;
};
}
displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers);
@@ -111,7 +115,7 @@ function _load(
if (
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] &&
_segmentationExists(segDisplaySet, segmentationService)
_segmentationExists(segDisplaySet)
) {
return loadPromises[SOPInstanceUID];
}
@@ -122,17 +126,21 @@ function _load(
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
});
try {
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
});
} catch (e) {
segDisplaySet.loading = false;
return reject(e);
}
}
const suppressEvents = true;
segmentationService
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
.createSegmentationForSEGDisplaySet(segDisplaySet)
.then(() => {
segDisplaySet.loading = false;
resolve();
@@ -161,20 +169,20 @@ async function _loadSegments({
const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
segDisplaySet.referencedDisplaySetInstanceUID
);
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
if (!referencedDisplaySet) {
throw new Error('referencedDisplaySet is missing for SEG');
}
const { imageIds } = cachedReferencedVolume;
const { instances: images } = referencedDisplaySet;
const imageIds = images.map(({ imageId }) => imageId);
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
@@ -226,9 +234,8 @@ async function _loadSegments({
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
function _segmentationExists(segDisplaySet) {
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
@@ -1,6 +1,15 @@
export function getToolbarModule({ servicesManager }: withAppTypes) {
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
return [
{
name: 'evaluate.cornerstone.hasSegmentation',
evaluate: ({ viewportId }) => {
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
return {
disabled: !segmentations?.length,
};
},
},
{
name: 'evaluate.cornerstone.segmentation',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
@@ -8,11 +17,10 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
// forcing the button to have black background since initially
// it is designed for the toolbox not the toolbar on top
// we should then branch the buttonSectionId to have different styles
const segmentations = segmentationService.getSegmentations();
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
if (!segmentations?.length) {
return {
disabled: true,
className: '!text-common-bright !bg-black opacity-50',
disabledText: disabledText ?? 'No segmentations available',
};
}
@@ -22,7 +30,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
if (!toolGroup) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
@@ -32,7 +39,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
if (!toolGroup.hasTool(toolName) && !toolNames) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
@@ -43,12 +49,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
return {
disabled: false,
className: isPrimaryActive
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},
@@ -3,7 +3,6 @@ import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import { getToolbarModule } from './getToolbarModule';
@@ -28,13 +27,6 @@ const extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule,
getCommandsModule,
getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
@@ -1,386 +0,0 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
import { SegmentationPanelMode } from '../types/segmentation';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next';
const components = {
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
};
export default function PanelSegmentation({
servicesManager,
commandsManager,
extensionManager,
configuration,
}: withAppTypes) {
const {
segmentationService,
viewportGridService,
uiDialogService,
displaySetService,
cornerstoneViewportService,
} = servicesManager.services;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
segmentationService.getConfiguration()
);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
useEffect(() => {
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
setSegmentationConfiguration(segmentationService.getConfiguration());
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
// temporary measure to not allow add segmentation when the selected viewport
// is stack viewport
useEffect(() => {
const handleActiveViewportChange = viewportId => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
viewportId || viewportGridService.getActiveViewportId()
);
if (!displaySetUIDs) {
return;
}
const isReconstructable =
displaySetUIDs?.some(displaySetUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
return displaySet?.isReconstructable;
}) || false;
if (isReconstructable) {
setAddSegmentationClassName('');
} else {
setAddSegmentationClassName('ohif-disabled');
}
};
// Handle initial state
handleActiveViewportChange();
const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
const subsGrid = [];
[ready, changedGrid].forEach(evt => {
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
handleActiveViewportChange(viewportId);
});
subsGrid.push(unsubscribe);
});
const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const subsData = [];
[changedData].forEach(evt => {
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => {
handleActiveViewportChange();
});
subsData.push(unsubscribe);
});
// Clean up
return () => {
subsGrid.forEach(unsub => unsub());
subsData.forEach(unsub => unsub());
};
}, []);
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
return toolGroupIds;
};
const onSegmentationAdd = async () => {
commandsManager.runCommand('createEmptySegmentationForViewport', {
viewportId: viewportGridService.getActiveViewportId(),
});
};
const onSegmentationClick = (segmentationId: string) => {
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
};
const onSegmentationDelete = (segmentationId: string) => {
segmentationService.remove(segmentationId);
};
const onSegmentAdd = segmentationId => {
segmentationService.addSegment(segmentationId);
};
const onSegmentClick = (segmentationId, segmentIndex) => {
segmentationService.setActiveSegment(segmentationId, segmentIndex);
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
// const toolGroupId =
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
});
};
const onSegmentEdit = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { label } = segment;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
});
};
const onSegmentationEdit = segmentationId => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const { label } = segmentation;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.addOrUpdateSegmentation(
{
id: segmentationId,
label,
},
false, // suppress event
true // notYetUpdatedAtSource
);
});
};
const onSegmentColorClick = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { color, opacity } = segment;
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: opacity / 255.0,
};
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
newRgbaColor.r,
newRgbaColor.g,
newRgbaColor.b,
newRgbaColor.a * 255.0,
]);
});
};
const onSegmentDelete = (segmentationId, segmentIndex) => {
segmentationService.removeSegment(segmentationId, segmentIndex);
};
// segment hide
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segmentInfo = segmentation.segments[segmentIndex];
const isVisible = !segmentInfo.isVisible;
const toolGroupIds = getToolGroupIds(segmentationId);
// Todo: right now we apply the visibility to all tool groups
toolGroupIds.forEach(toolGroupId => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
};
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
};
const onToggleSegmentationVisibility = segmentationId => {
segmentationService.toggleSegmentationVisibility(segmentationId);
const segmentation = segmentationService.getSegmentation(segmentationId);
const isVisible = segmentation.isVisible;
const segments = segmentation.segments;
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
segments.forEach((segment, segmentIndex) => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
});
};
const _setSegmentationConfiguration = useCallback(
(segmentationId, key, value) => {
segmentationService.setConfiguration({
segmentationId,
[key]: value,
});
},
[segmentationService]
);
const onSegmentationDownload = segmentationId => {
commandsManager.runCommand('downloadSegmentation', {
segmentationId,
});
};
const storeSegmentation = async segmentationId => {
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
// Show the exported report in the active viewport as read only (similar to SR)
if (displaySetInstanceUIDs) {
// clear the segmentation that we exported, similar to the storeMeasurement
// where we remove the measurements and prompt again the user if they would like
// to re-read the measurements in a SR read only viewport
segmentationService.remove(segmentationId);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs,
});
}
};
const onSegmentationDownloadRTSS = segmentationId => {
commandsManager.runCommand('downloadRTSS', {
segmentationId,
});
};
const SegmentationGroupTableComponent =
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
const allowAddSegment = configuration?.addSegment;
const onSegmentationAddWrapper =
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
? configuration?.onSegmentationAdd
: onSegmentationAdd;
return (
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAddWrapper}
addSegmentationClassName={addSegmentationClassName}
showAddSegment={allowAddSegment}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
);
}
PanelSegmentation.propTypes = {
commandsManager: PropTypes.shape({
runCommand: PropTypes.func.isRequired,
}),
servicesManager: PropTypes.shape({
services: PropTypes.shape({
segmentationService: PropTypes.shape({
getSegmentation: PropTypes.func.isRequired,
getSegmentations: PropTypes.func.isRequired,
toggleSegmentationVisibility: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
@@ -1,62 +0,0 @@
import React from 'react';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label';
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.label, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Segment',
value: { label },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
return (
<Input
label="Enter the segment label"
labelClassName="text-white text-[14px] leading-[1.2]"
autoFocus
className="border-primary-main bg-black"
type="text"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
);
},
},
});
}
}
export default callInputDialog;
@@ -1,33 +0,0 @@
// Todo: use defaults in cs3d
const initialState = {
renderOutline: true,
renderFill: true,
outlineOpacity: 0.9,
outlineWidth: 3,
fillOpacity: 0.9,
fillOpacityInactive: 0.8,
renderInactiveSegmentations: true,
};
const reducer = (state, action) => {
switch (action.type) {
case 'RENDER_OUTLINE':
return { ...state, renderOutline: action.payload.value };
case 'RENDER_FILL':
return { ...state, renderFill: action.payload.value };
case 'SET_OUTLINE_OPACITY':
return { ...state, outlineOpacity: action.payload.value };
case 'SET_OUTLINE_WIDTH':
return { ...state, outlineWidth: action.payload.value };
case 'SET_FILL_OPACITY':
return { ...state, fillOpacity: action.payload.value };
case 'SET_FILL_OPACITY_INACTIVE':
return { ...state, fillOpacityInactive: action.payload.value };
case 'RENDER_INACTIVE_SEGMENTATIONS':
return { ...state, renderInactiveSegmentations: action.payload.value };
default:
return state;
}
};
export { initialState, reducer };
@@ -1,192 +0,0 @@
import { Enums, cache } from '@cornerstonejs/core';
/**
* Updates the viewports in preparation for rendering segmentations.
* Evaluates each viewport to determine which need modifications,
* then for those viewports, changes them to a volume type and ensures
* they are ready for segmentation rendering.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport to be updated.
* @param params.loadFn - Function to load the segmentation data.
* @param params.servicesManager - The services manager.
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns Returns true upon successful update of viewports for segmentation rendering.
*/
async function updateViewportsForSegmentationRendering({
viewportId,
loadFn,
servicesManager,
referencedDisplaySetInstanceUID,
}: {
viewportId: string;
loadFn: () => Promise<string>;
servicesManager: AppTypes.ServicesManager;
referencedDisplaySetInstanceUID?: string;
}) {
const { cornerstoneViewportService, segmentationService, viewportGridService } =
servicesManager.services;
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
referencedDisplaySetInstanceUID =
referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
const updatedViewports = getUpdatedViewportsForSegmentation({
servicesManager,
viewportId,
referencedDisplaySetInstanceUID,
});
// create Segmentation callback which needs to be waited until
// the volume is created (if coming from stack)
const createSegmentationForVolume = async () => {
const segmentationId = await loadFn();
segmentationService.hydrateSegmentation(segmentationId);
};
// the reference volume that is used to draw the segmentation. so check if the
// volume exists in the cache (the target Viewport is already a volume viewport)
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
volumeId.includes(referencedDisplaySetInstanceUID)
);
updatedViewports.forEach(async viewport => {
viewport.viewportOptions = {
...viewport.viewportOptions,
viewportType: 'volume',
needsRerendering: true,
};
const viewportId = viewport.viewportId;
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const prevCamera = csViewport.getCamera();
// only run the createSegmentationForVolume for the targetViewportId
// since the rest will get handled by cornerstoneViewportService
if (volumeExists && viewportId === targetViewportId) {
await createSegmentationForVolume();
return;
}
const createNewSegmentationWhenVolumeMounts = async evt => {
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
ac.uid.includes(referencedDisplaySetInstanceUID)
);
// Note: make sure to re-grab the viewport since it might have changed
// during the time it took for the volume to be mounted, for instance
// the stack viewport has been changed to a volume viewport
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
volumeViewport.setCamera(prevCamera);
volumeViewport.element.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
if (!isTheActiveViewportVolumeMounted) {
// it means it is one of those other updated viewports so just update the camera
return;
}
if (viewportId === targetViewportId) {
await createSegmentationForVolume();
}
};
csViewport.element.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
});
// Set the displaySets for the viewports that require to be updated
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
function getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
referencedDisplaySetInstanceUID,
}: withAppTypes) {
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
servicesManager.services;
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
const referenceDisplaySetInstanceUID =
referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
targetViewportId,
referenceDisplaySetInstanceUID,
isHangingProtocolLayout
);
viewports.forEach((viewport, viewportId) => {
if (
targetViewportId === viewportId ||
updatedViewports.find(v => v.viewportId === viewportId)
) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
segmentationFrameOfReferenceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
viewportType: 'volume',
needsRerendering: true,
},
});
}
});
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
export {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
};
@@ -1,5 +1,5 @@
function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
@@ -11,7 +11,7 @@ function promptHydrateSEG({
segDisplaySet,
viewportId,
preHydrateCallbacks,
hydrateSEGDisplaySet,
hydrateCallback,
}: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
@@ -28,7 +28,7 @@ function promptHydrateSEG({
});
window.setTimeout(async () => {
const isHydrated = await hydrateSEGDisplaySet({
const isHydrated = await hydrateCallback({
segDisplaySet,
viewportId,
});
@@ -44,11 +44,13 @@ function _askHydrate(uiViewportDialogService, viewportId) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
id: 'no-hydrate',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
@@ -1,10 +1,12 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import { ViewportActionArrows } from '@ohif/ui';
import { useViewportGrid } from '@ohif/ui-next';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
@@ -29,6 +31,10 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
viewportActionCornersService,
} = servicesManager.services;
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
'ui.loadingIndicatorTotalPercent'
);
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
// SEG viewport will always have a single display set
@@ -37,11 +43,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}
const segDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
const [selectedSegment, setSelectedSegment] = useState(1);
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the SEG is opened and segments are loaded into the
// segmentation panel, and SEG is also rendered on any viewport that is in the
@@ -61,7 +67,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet,
segDisplaySet
@@ -98,18 +108,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
<Component
{...props}
displaySets={[referencedDisplaySet, segDisplaySet]}
displaySets={[segDisplaySet]}
viewportOptions={{
viewportType: 'volume',
viewportType: viewportOptions.viewportType,
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
@@ -139,12 +148,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
);
const hydrateSEG = useCallback(() => {
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
useEffect(() => {
if (segIsLoading) {
return;
@@ -155,21 +187,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateSEGDisplaySet,
hydrateCallback: hydrateSEG,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
segmentationService.clearSegmentationRepresentations(viewportId);
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
setSegIsLoading(false);
}
if (segDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
const { firstSegmentedSliceImageId } = segDisplaySet;
const { presentationIds } = viewportOptions;
setPositionPresentation(presentationIds.positionPresentationId, {
viewReference: {
referencedImageId: firstSegmentedSliceImageId,
},
});
}
}
);
@@ -223,39 +269,31 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
return;
}
// keep the already stored segmentationPresentation for this viewport in memory
// so that we can restore it after hydrating the SEG
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// always start fresh for this viewport since it is special type of viewport
// that should only show one segmentation at a time.
segmentationService.clearSegmentationRepresentations(viewportId);
// This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
// e.g., another seg displayset is dragged into the viewport
segmentationService.clearSegmentationRepresentations(viewportId);
// Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId);
};
}, []);
useEffect(() => {
setIsHydrated(segDisplaySet.isHydrated);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [segDisplaySet]);
const hydrateSEGDisplaySet = useCallback(
({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
},
[commandsManager]
);
const onStatusClick = useCallback(async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
@@ -264,16 +302,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
hydrateSEG();
}, [storePresentationState, hydrateSEG]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -292,7 +325,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
@@ -332,18 +365,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
@@ -362,18 +383,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
);
}
OHIFCornerstoneSEGViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
OHIFCornerstoneSEGViewport.defaultProps = {
customProps: {},
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
@@ -1,6 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Tooltip } from '@ohif/ui';
import { ViewportActionButton } from '@ohif/ui';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
@@ -8,18 +9,16 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
StatusIcon = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icon
<Icons.ByName
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
@@ -34,13 +33,7 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
@@ -49,14 +42,18 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
return (
<>
{ToolTipMessage && (
<Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea />
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
}
File diff suppressed because it is too large. Load diff
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.9.0-beta.16",
"version": "3.10.0-beta.86",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -34,21 +34,21 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.16",
"@ohif/extension-cornerstone": "3.9.0-beta.16",
"@ohif/extension-measurement-tracking": "3.9.0-beta.16",
"@ohif/ui": "3.9.0-beta.16",
"dcmjs": "^0.29.12",
"@ohif/core": "3.10.0-beta.86",
"@ohif/extension-cornerstone": "3.10.0-beta.86",
"@ohif/extension-measurement-tracking": "3.10.0-beta.86",
"@ohif/ui": "3.10.0-beta.86",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.2"
"react": "^18.3.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.74.6",
"@cornerstonejs/core": "^1.74.6",
"@cornerstonejs/tools": "^1.74.6",
"@cornerstonejs/adapters": "^2.17.2",
"@cornerstonejs/core": "^2.17.2",
"@cornerstonejs/tools": "^2.17.2",
"classnames": "^2.3.2"
}
}
@@ -5,12 +5,12 @@ import dcmjs from 'dcmjs';
import { adaptersSR } from '@cornerstonejs/adapters';
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { log } = OHIF;
/**
*
* @param measurementData An array of measurements from the measurements service
* that you wish to serialize.
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
@@ -41,9 +41,30 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
};
const commandsModule = (props: withAppTypes) => {
const { servicesManager } = props;
const { customizationService } = servicesManager.services;
const { servicesManager, extensionManager } = props;
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
const actions = {
changeColorMeasurement: ({ uid }) => {
// When this gets supported, it probably belongs in cornerstone, not sr
throw new Error('Unsupported operation: changeColorMeasurement');
// const { color } = measurementService.getMeasurement(uid);
// const rgbaColor = {
// r: color[0],
// g: color[1],
// b: color[2],
// a: color[3] / 255.0,
// };
// colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
// if (actionId === 'cancel') {
// return;
// }
// const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
// segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
// });
},
/**
*
* @param measurementData An array of measurements from the measurements service
@@ -53,7 +74,7 @@ const commandsModule = (props: withAppTypes) => {
* that you wish to serialize.
*/
downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
const srDataset = actions.generateReport(measurementData, additionalFindingTypes, options);
const srDataset = _generateReport(measurementData, additionalFindingTypes, options);
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
@@ -97,12 +118,11 @@ const commandsModule = (props: withAppTypes) => {
throw new Error('Invalid report, no content');
}
const onBeforeDicomStore =
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
let dicomDict;
if (typeof onBeforeDicomStore === 'function') {
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport });
}
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
@@ -123,6 +143,27 @@ const commandsModule = (props: withAppTypes) => {
throw new Error(error.message || 'Error while saving the measurements.');
}
},
/**
* Loads measurements by hydrating and loading the SR for the given display set instance UID
* and displays it in the active viewport.
*/
loadSRMeasurements: ({ displaySetInstanceUID }) => {
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
{
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
]);
}
},
};
const definitions = {
@@ -132,6 +173,9 @@ const commandsModule = (props: withAppTypes) => {
storeMeasurements: {
commandFn: actions.storeMeasurements,
},
loadSRMeasurements: {
commandFn: actions.loadSRMeasurements,
},
};
return {
@@ -0,0 +1,78 @@
import PropTypes from 'prop-types';
import React from 'react';
import { OHIFCornerstoneSRContentItem } from './OHIFCornerstoneSRContentItem';
export function OHIFCornerstoneSRContainer(props) {
const { container, nodeIndexesTree = [0], containerNumberedTree = [1] } = props;
const { ContinuityOfContent, ConceptNameCodeSequence } = container;
const { CodeMeaning } = ConceptNameCodeSequence ?? {};
let childContainerIndex = 1;
const contentItems = container.ContentSequence?.map((contentItem, i) => {
const { ValueType } = contentItem;
const childNodeLevel = [...nodeIndexesTree, i];
const key = childNodeLevel.join('.');
let Component;
let componentProps;
if (ValueType === 'CONTAINER') {
const childContainerNumberedTree = [...containerNumberedTree, childContainerIndex++];
Component = OHIFCornerstoneSRContainer;
componentProps = {
container: contentItem,
nodeIndexesTree: childNodeLevel,
containerNumberedTree: childContainerNumberedTree,
};
} else {
Component = OHIFCornerstoneSRContentItem;
componentProps = {
contentItem,
nodeIndexesTree: childNodeLevel,
continuityOfContent: ContinuityOfContent,
};
}
return (
<Component
key={key}
{...componentProps}
/>
);
});
return (
<div>
<div className="font-bold">
{containerNumberedTree.join('.')}.&nbsp;
{CodeMeaning}
</div>
<div className="ml-4 mb-2">{contentItems}</div>
</div>
);
}
OHIFCornerstoneSRContainer.propTypes = {
/**
* A tree node that may contain another container or one or more content items
* (text, code, uidref, pname, etc.)
*/
container: PropTypes.object,
/**
* A 0-based index list
*/
nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
/**
* A 1-based index list that represents a container in a multi-level numbered
* list (tree).
*
* Example:
* 1. History
* 1.1. Chief Complaint
* 1.2. Present Illness
* 1.3. Past History
* 1.4. Family History
* 2. Findings
* */
containerNumberedTree: PropTypes.arrayOf(PropTypes.number),
};
@@ -0,0 +1,63 @@
import PropTypes from 'prop-types';
import React from 'react';
import { CodeNameCodeSequenceValues } from '../enums';
import formatContentItemValue from '../utils/formatContentItem';
const EMPTY_TAG_VALUE = '[empty]';
function OHIFCornerstoneSRContentItem(props) {
const { contentItem, nodeIndexesTree, continuityOfContent } = props;
const { ConceptNameCodeSequence } = contentItem;
const { CodeValue, CodeMeaning } = ConceptNameCodeSequence;
const isChildFirstNode = nodeIndexesTree[nodeIndexesTree.length - 1] === 0;
const formattedValue = formatContentItemValue(contentItem) ?? EMPTY_TAG_VALUE;
const startWithAlphaNumCharRegEx = /^[a-zA-Z0-9]/;
const isContinuous = continuityOfContent === 'CONTINUOUS';
const isFinding = CodeValue === CodeNameCodeSequenceValues.Finding;
const addExtraSpace =
isContinuous && !isChildFirstNode && startWithAlphaNumCharRegEx.test(formattedValue?.[0]);
// Collapse sequences of white space preserving newline characters
let className = 'whitespace-pre-line';
if (CodeValue === CodeNameCodeSequenceValues.Finding) {
// Preserve spaces because it is common to see tabular text in a
// "Findings" ConceptNameCodeSequence
className = 'whitespace-pre-wrap';
}
if (isContinuous) {
return (
<>
<span
className={className}
title={CodeMeaning}
>
{addExtraSpace ? ' ' : ''}
{formattedValue}
</span>
</>
);
}
return (
<>
<div className="mb-2">
<span className="font-bold">{CodeMeaning}: </span>
{isFinding ? (
<pre>{formattedValue}</pre>
) : (
<span className={className}>{formattedValue}</span>
)}
</div>
</>
);
}
OHIFCornerstoneSRContentItem.propTypes = {
contentItem: PropTypes.object,
nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
continuityOfContent: PropTypes.string,
};
export { OHIFCornerstoneSRContentItem };
@@ -1,31 +1,25 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ExtensionManager } from '@ohif/core';
import { ExtensionManager, useToolbar } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
import { ViewportActionArrows } from '@ohif/ui';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { useViewportGrid } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRViewport(props: withAppTypes) {
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const [appConfig] = useAppConfig();
const {
displaySetService,
cornerstoneViewportService,
measurementService,
viewportActionCornersService,
} = servicesManager.services;
const { displaySetService, viewportActionCornersService } = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -36,6 +30,8 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
const srDisplaySet = displaySets[0];
const { setPositionPresentation } = usePositionPresentationStore();
const [viewportGrid, viewportGridService] = useViewportGrid();
const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1);
@@ -48,7 +44,6 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
// Optional hook into tracking extension, if present.
let trackedMeasurements;
let sendTrackedMeasurementsEvent;
const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes(
MEASUREMENT_TRACKING_EXTENSION_ID
@@ -61,29 +56,13 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
const tracked = useContext(contextModule.context);
trackedMeasurements = tracked?.[0];
sendTrackedMeasurementsEvent = tracked?.[1];
}
if (!sendTrackedMeasurementsEvent) {
// if no panels from measurement-tracking extension is used, this code will run
trackedMeasurements = null;
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
measurementService.clearMeasurements();
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
{
viewportId: activeViewportId,
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
]);
}
};
}
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
/**
* Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport,
@@ -135,30 +114,22 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
newMeasurementSelected,
displaySetService
).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => {
if (!referencedDisplaySet || !referencedDisplaySetMetadata) {
return;
}
setMeasurementSelected(newMeasurementSelected);
setActiveImageDisplaySetData(referencedDisplaySet);
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
if (
referencedDisplaySet.displaySetInstanceUID ===
activeImageDisplaySetData?.displaySetInstanceUID
) {
const { measurements } = srDisplaySet;
// it means that we have a new referenced display set, and the
// imageIdIndex will handle it by updating the viewport, but if they
// are the same we just need to use measurementService to jump to the
// new measurement
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const imageIds = csViewport.getImageIds();
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
if (imageIdIndex !== -1) {
csViewport.setImageIdIndex(imageIdIndex);
}
}
const { presentationIds } = viewportOptions;
const measurement = srDisplaySet.measurements[newMeasurementSelected];
setPositionPresentation(presentationIds.positionPresentationId, {
viewReference: {
referencedImageId: measurement.imageId,
},
});
});
},
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
@@ -180,10 +151,6 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
return null;
}
const initialImageIndex = activeImageDisplaySetData.images.findIndex(
image => image.imageId === measurement.imageId
);
return (
<Component
{...props}
@@ -208,7 +175,6 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
initialImageIndex={initialImageIndex}
isJumpToMeasurementDisabled={true}
></Component>
);
@@ -270,30 +236,31 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
updateViewport(measurementSelected);
};
loadSR();
}, [dataSource, srDisplaySet]);
}, [srDisplaySet]);
/**
* Hook to update the tracking identifiers when the selected measurement changes or
* the element changes
*/
useEffect(() => {
if (!element || !srDisplaySet.isLoaded) {
return;
}
setTrackingIdentifiers(measurementSelected);
const updateSR = async () => {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
if (!element || !srDisplaySet.isLoaded) {
return;
}
setTrackingIdentifiers(measurementSelected);
};
updateSR();
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
useEffect(() => {
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -302,8 +269,8 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
servicesManager,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
@@ -322,15 +289,7 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
isLocked,
onMeasurementChange,
sendTrackedMeasurementsEvent,
srDisplaySet,
t,
viewportActionCornersService,
viewportId,
]);
}, [isLocked, onMeasurementChange, srDisplaySet, t, viewportActionCornersService, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -361,22 +320,17 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
);
}
OHIFCornerstoneSRViewport.propTypes = {
OHIFCornerstoneSRMeasurementViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
viewportLabel: PropTypes.string,
customProps: PropTypes.object,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
OHIFCornerstoneSRViewport.defaultProps = {
customProps: {},
};
async function _getViewportReferencedDisplaySetData(
displaySet,
measurementSelected,
@@ -394,6 +348,10 @@ async function _getViewportReferencedDisplaySetData(
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
}
if (!displaySetInstanceUID) {
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const image0 = referencedDisplaySet.images[0];
@@ -419,16 +377,9 @@ function _getStatusComponent({
viewportId,
isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
servicesManager,
}) {
const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportId,
});
};
const loadStr = t('LOAD');
// 1 - Incompatible
@@ -440,7 +391,7 @@ function _getStatusComponent({
switch (state) {
case 1:
StatusIcon = () => <Icon name="status-alert" />;
StatusIcon = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => (
<div>
@@ -451,7 +402,7 @@ function _getStatusComponent({
);
break;
case 2:
StatusIcon = () => <Icon name="status-locked" />;
StatusIcon = () => <Icons.ByName name="status-locked" />;
ToolTipMessage = () => (
<div>
@@ -465,7 +416,7 @@ function _getStatusComponent({
break;
case 3:
StatusIcon = () => (
<Icon
<Icons.ByName
className="text-aqua-pale"
name="status-untracked"
/>
@@ -474,32 +425,62 @@ function _getStatusComponent({
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SR</span>
</div>
{state === 3 && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={handleMouseUp}
>
{loadStr}
const StatusArea = () => {
const { toolbarButtons: loadSRMeasurementsButtons, onInteraction } = useToolbar({
servicesManager,
buttonSection: 'loadSRMeasurements',
});
const commandOptions = {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportId,
};
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SR</span>
</div>
)}
</div>
);
{state === 3 && (
<>
{loadSRMeasurementsButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
const tool = (
<Component
key={id}
id={id}
onInteraction={args => onInteraction({ ...args, ...commandOptions })}
{...componentProps}
/>
);
return <div key={id}>{tool}</div>;
})}
</>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea />
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="start"
>
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
@@ -507,4 +488,4 @@ function _getStatusComponent({
);
}
export default OHIFCornerstoneSRViewport;
export default OHIFCornerstoneSRMeasurementViewport;
@@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ExtensionManager } from '@ohif/core';
import { OHIFCornerstoneSRContainer } from './OHIFCornerstoneSRContainer';
function OHIFCornerstoneSRTextViewport(props: withAppTypes) {
const { displaySets } = props;
const displaySet = displaySets[0];
const instance = displaySet.instances[0];
return (
<div className="relative flex h-full w-full flex-col overflow-auto p-4 text-white">
<div>
{/* The root level is always a container */}
<OHIFCornerstoneSRContainer container={instance} />
</div>
</div>
);
}
OHIFCornerstoneSRTextViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
viewportLabel: PropTypes.string,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
export default OHIFCornerstoneSRTextViewport;
@@ -0,0 +1,30 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ExtensionManager } from '@ohif/core';
import OHIFCornerstoneSRMeasurementViewport from './OHIFCornerstoneSRMeasurementViewport';
import OHIFCornerstoneSRTextViewport from './OHIFCornerstoneSRTextViewport';
function OHIFCornerstoneSRViewport(props: withAppTypes) {
const { displaySets } = props;
const { isImagingMeasurementReport } = displaySets[0];
if (isImagingMeasurementReport) {
return <OHIFCornerstoneSRMeasurementViewport {...props}></OHIFCornerstoneSRMeasurementViewport>;
}
return <OHIFCornerstoneSRTextViewport {...props}></OHIFCornerstoneSRTextViewport>;
}
OHIFCornerstoneSRViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
viewportLabel: PropTypes.string,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
export default OHIFCornerstoneSRViewport;
@@ -1,7 +0,0 @@
export default {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
};
@@ -0,0 +1,44 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
export const SCOORDTypes = {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
};
export const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
FindingSiteSCT: '363698007', // SCT
};
export const CodingSchemeDesignators = {
SRT: 'SRT',
SCT: 'SCT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
};
export const RelationshipType = {
INFERRED_FROM: 'INFERRED FROM',
CONTAINS: 'CONTAINS',
};
const enums = {
CodeNameCodeSequenceValues,
CodingSchemeDesignators,
RelationshipType,
SCOORDTypes,
};
export default enums;
@@ -1,30 +1,37 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
import isRehydratable from './utils/isRehydratable';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import { adaptersSR } from '@cornerstonejs/adapters';
import addSRAnnotation from './utils/addSRAnnotation';
import isRehydratable from './utils/isRehydratable';
import {
SOPClassHandlerName,
SOPClassHandlerId,
SOPClassHandlerId3D,
SOPClassHandlerName3D,
} from './id';
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
const { sopClassDictionary } = utils;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
type InstanceMetadata = Types.InstanceMetadata;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
// TODO ->
// Add SR thumbnail
// Make viewport
// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport.
/**
* TODO
* - [ ] Add SR thumbnail
* - [ ] Make viewport
* - [ ] Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport
*/
const sopClassUids = [
'1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR:
'1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR:
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
'1.2.840.10008.5.1.4.1.1.88.34', //COMPREHENSIVE_3D_SR:
sopClassDictionary.BasicTextSR,
sopClassDictionary.EnhancedSR,
sopClassDictionary.ComprehensiveSR,
];
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const validateSameStudyUID = (uid: string, instances): void => {
instances.forEach(it => {
if (it.StudyInstanceUID !== uid) {
@@ -34,31 +41,6 @@ const validateSameStudyUID = (uid: string, instances): void => {
});
};
const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
};
const CodingSchemeDesignators = {
SRT: 'SRT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
};
const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM',
CONTAINS: 'CONTAINS',
};
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/**
* Adds instances to the DICOM SR series, rather than creating a new
* series, so that as SR's are saved, they append to the series, and the
@@ -113,22 +95,12 @@ function _getDisplaySetsFromSeries(
} = instance;
validateSameStudyUID(instance.StudyInstanceUID, instances);
if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
) {
servicesManager.services.uiNotificationService.show({
title: 'DICOM SR',
message:
'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you’re trying to view is not supported.',
type: 'warning',
duration: 6000,
});
return [];
}
const is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR;
const isImagingMeasurementReport =
ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport;
const displaySet = {
//plugin: id,
Modality: 'SR',
displaySetInstanceUID: utils.guid(),
SeriesDescription,
@@ -137,13 +109,14 @@ function _getDisplaySetsFromSeries(
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId,
SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId,
SOPClassUID,
instances,
referencedImages: null,
measurements: null,
isDerivedDisplaySet: true,
isLoaded: false,
isImagingMeasurementReport,
sopClassUids,
instance,
addInstances,
@@ -154,12 +127,21 @@ function _getDisplaySetsFromSeries(
return [displaySet];
}
async function _load(displaySet, servicesManager: AppTypes.ServicesManager, extensionManager) {
/**
* Loads the display set with the given services and extension manager.
* @param srDisplaySet - The display set to load.
* @param servicesManager - The services manager containing displaySetService and measurementService.
* @param extensionManager - The extension manager containing data sources.
*/
async function _load(
srDisplaySet: Types.DisplaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager: AppTypes.ExtensionManager
) {
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const { ContentSequence } = displaySet.instance;
const { ContentSequence } = srDisplaySet.instance;
async function retrieveBulkData(obj, parentObj = null, key = null) {
for (const prop in obj) {
@@ -170,9 +152,9 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
} else if (prop === 'BulkDataURI') {
const value = await dataSource.retrieve.bulkDataURI({
BulkDataURI: obj[prop],
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
StudyInstanceUID: srDisplaySet.instance.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID,
SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID,
});
if (parentObj && key) {
parentObj[key] = new Float32Array(value);
@@ -181,40 +163,47 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
}
}
if (displaySet.isLoaded !== true) {
if (srDisplaySet.isLoaded !== true) {
await retrieveBulkData(ContentSequence);
}
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence);
if (srDisplaySet.isImagingMeasurementReport) {
srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence);
srDisplaySet.measurements = _getMeasurements(ContentSequence);
} else {
srDisplaySet.referencedImages = [];
srDisplaySet.measurements = [];
}
const mappings = measurementService.getSourceMappings(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
displaySet.isHydrated = false;
displaySet.isRehydratable = isRehydratable(displaySet, mappings);
displaySet.isLoaded = true;
srDisplaySet.isHydrated = false;
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
srDisplaySet.isLoaded = true;
// Check currently added displaySets and add measurements if the sources exist.
/** Check currently added displaySets and add measurements if the sources exist */
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
srDisplaySet,
activeDisplaySet,
dataSource,
servicesManager
);
});
// Subscribe to new displaySets as the source may come in after.
/** Subscribe to new displaySets as the source may come in after */
displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => {
const { displaySetsAdded } = data;
// If there are still some measurements that have not yet been loaded into cornerstone,
// See if we can load them onto any of the new displaySets.
/**
* If there are still some measurements that have not yet been loaded into cornerstone,
* See if we can load them onto any of the new displaySets.
*/
displaySetsAdded.forEach(newDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
srDisplaySet,
newDisplaySet,
dataSource,
servicesManager
@@ -223,6 +212,14 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
});
}
/**
* Checks if measurements can be added to a display set.
*
* @param srDisplaySet - The source display set containing measurements.
* @param newDisplaySet - The new display set to check if measurements can be added.
* @param dataSource - The data source used to retrieve image IDs.
* @param servicesManager - The services manager.
*/
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
@@ -230,108 +227,103 @@ function _checkIfCanAddMeasurementsToDisplaySet(
servicesManager: AppTypes.ServicesManager
) {
const { customizationService } = servicesManager.services;
let unloadedMeasurements = srDisplaySet.measurements.filter(
const unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
if (unloadedMeasurements.length === 0) {
// All already loaded!
if (
unloadedMeasurements.length === 0 ||
!(newDisplaySet instanceof ImageSet) ||
newDisplaySet.unsupported
) {
return;
}
if ((!newDisplaySet) instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
// const { sopClassUids } = newDisplaySet;
// Create a Set for faster lookups
// const sopClassUidSet = new Set(sopClassUids);
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
const imageIdMap = new Map<string, string>();
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
for (const imageId of imageIds) {
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
const key = `${SOPInstanceUID}:${frameNumber || 1}`;
imageIdMap.set(key, imageId);
}
if (!unloadedMeasurements?.length) {
return;
}
if (newDisplaySet.unsupported) {
return;
}
const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR;
const { sopClassUids } = newDisplaySet;
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
// Check if any have the newDisplaySet is the correct SOPClass.
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
measurement.coords.some(coord =>
sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID)
)
);
const onBeforeSRAddMeasurement = customizationService.getCustomization(
'onBeforeSRAddMeasurement'
);
if (unloadedMeasurements.length === 0) {
// New displaySet isn't the correct SOPClass, so can't contain the referenced images.
return;
}
const SOPInstanceUIDs = [];
unloadedMeasurements.forEach(measurement => {
const { coords } = measurement;
coords.forEach(coord => {
const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
SOPInstanceUIDs.push(SOPInstanceUID);
}
});
});
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
for (const imageId of imageIdsForDisplaySet) {
if (!unloadedMeasurements.length) {
// All measurements loaded.
return;
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
// if it is 3d SR we can just add the SR annotation
if (is3DSR) {
addSRAnnotation(measurement, null, null);
measurement.loaded = true;
continue;
}
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence;
if (!referencedSOPSequence) {
continue;
}
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
const { ReferencedSOPInstanceUID } = referencedSOPSequence;
const frame = referencedSOPSequence.ReferencedFrameNumber || 1;
const key = `${ReferencedSOPInstanceUID}:${frame}`;
const imageId = imageIdMap.get(key);
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
if (
imageId &&
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
) {
addSRAnnotation(measurement, imageId, frame);
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
const frame =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
// Update measurement properties
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
/** Update measurement properties */
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
delete measurement.coords;
unloadedMeasurements.splice(j, 1);
}
}
unloadedMeasurements.splice(j, 1);
}
}
}
/**
* Checks if a measurement references a specific SOP Instance UID.
* @param measurement - The measurement object.
* @param SOPInstanceUID - The SOP Instance UID to check against.
* @param frameNumber - The frame number to check against (optional).
* @returns True if the measurement references the specified SOP Instance UID, false otherwise.
*/
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
const { coords } = measurement;
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
// Standard. But for now, we will support only one ReferenceFrameNumber.
/**
* NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
* Standard. But for now, we will support only one ReferenceFrameNumber.
*/
const ReferencedFrameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
@@ -344,40 +336,62 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
return true;
}
}
return false;
}
/**
* Retrieves the SOP class handler module.
*
* @param {Object} options - The options for retrieving the SOP class handler module.
* @param {Object} options.servicesManager - The services manager.
* @param {Object} options.extensionManager - The extension manager.
* @returns {Array} An array containing the SOP class handler module.
*/
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
{
name: SOPClassHandlerName,
sopClassUids,
getDisplaySetsFromSeries,
},
{
name: SOPClassHandlerName3D,
sopClassUids: [sopClassDictionary.Comprehensive3DSR],
getDisplaySetsFromSeries,
},
];
}
/**
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
* @returns {Array} - The array of measurements.
*/
function _getMeasurements(ImagingMeasurementReportContentSequence) {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
);
if (!ImagingMeasurements) {
return [];
}
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
);
const mergedContentSequencesByTrackingUniqueIdentifiers =
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
@@ -386,7 +400,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
const measurement = _processMeasurement(mergedContentSequence);
if (measurement) {
measurements.push(measurement);
}
@@ -396,6 +409,12 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
return measurements;
}
/**
* Retrieves merged content sequences by tracking unique identifiers.
*
* @param {Array} MeasurementGroups - The measurement groups.
* @returns {Object} - The merged content sequences by tracking unique identifiers.
*/
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
@@ -407,7 +426,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
);
if (!TrackingUniqueIdentifierItem) {
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
}
@@ -436,6 +454,15 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
return mergedContentSequencesByTrackingUniqueIdentifiers;
}
/**
* Processes the measurement based on the merged content sequence.
* If the merged content sequence contains SCOORD or SCOORD3D value types,
* it calls the _processTID1410Measurement function.
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
*
* @param {Array<Object>} mergedContentSequence - The merged content sequence to process.
* @returns {any} - The processed measurement result.
*/
function _processMeasurement(mergedContentSequence) {
if (
mergedContentSequence.some(
@@ -448,11 +475,21 @@ function _processMeasurement(mergedContentSequence) {
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
}
/**
* Processes TID 1410 style measurements from the mergedContentSequence.
* TID 1410 style measurements have a SCOORD or SCOORD3D at the top level,
* and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D.
*
* @param mergedContentSequence - The merged content sequence containing the measurements.
* @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
*/
function _processTID1410Measurement(mergedContentSequence) {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD');
const graphicItem = mergedContentSequence.find(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
);
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
@@ -479,7 +516,6 @@ function _processTID1410Measurement(mergedContentSequence) {
NUMContentItems.forEach(item => {
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
@@ -487,12 +523,29 @@ function _processTID1410Measurement(mergedContentSequence) {
}
});
const findingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT &&
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT
);
if (findingSites.length) {
measurement.labels.push({
label: CodeNameCodeSequenceValues.FindingSiteSCT,
value: findingSites[0].ConceptCodeSequence.CodeMeaning,
});
}
return measurement;
}
/**
* Processes the non-geometrically defined measurement from the merged content sequence.
*
* @param mergedContentSequence The merged content sequence containing the measurement data.
* @returns The processed measurement object.
*/
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
const TrackingIdentifierContentItem = mergedContentSequence.find(
@@ -522,10 +575,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
value: finding.ConceptCodeSequence.CodeMeaning,
});
}
@@ -537,12 +590,13 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
FindingSite.ConceptCodeSequence.CodeValue ===
Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
);
if (cornerstoneFreeTextFindingSite) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
});
}
@@ -552,15 +606,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD') {
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
return;
}
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
if (coords) {
measurement.coords.push(coords);
}
@@ -575,59 +626,62 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
return measurement;
}
function _getCoordsFromSCOORDOrSCOORD3D(item) {
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
if (
!(
RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM ||
RelationshipType == RELATIONSHIP_TYPE.CONTAINS
)
) {
console.warn(
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM" or "CONTAINS"`
);
return;
}
/**
* Extracts coordinates from a graphic item of type SCOORD or SCOORD3D.
* @param {object} graphicItem - The graphic item containing the coordinates.
* @returns {object} - The extracted coordinates.
*/
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
const { ValueType, GraphicType, GraphicData } = graphicItem;
const coords = { ValueType, GraphicType, GraphicData };
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
if (ValueType === 'SCOORD') {
const { ReferencedSOPSequence } = item.ContentSequence;
coords.ReferencedSOPSequence = ReferencedSOPSequence;
} else if (ValueType === 'SCOORD3D') {
const { ReferencedFrameOfReferenceSequence } = item.ContentSequence;
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
}
coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence;
coords.ReferencedFrameOfReferenceSequence =
graphicItem.ReferencedFrameOfReferenceUID ||
graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence;
return coords;
}
};
/**
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
* @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object.
* @param {Object} MeasuredValueSequence - The MeasuredValueSequence object.
* @returns {Object} - An object containing the label and value.
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
* Example: { label: 'Long Axis', value: '31.00 mm' }
*/
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
return {
label: CodeMeaning,
value: `${formatedNumericValue} ${CodeValue}`,
}; // E.g. Long Axis: 31.0 mm
}
/**
* Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
* @returns {Array} - The list of referenced images.
*/
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
);
if (!ImageLibrary) {
return [];
}
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
);
if (!ImageLibraryGroup) {
return [];
}
const referencedImages = [];
@@ -651,6 +705,15 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
return referencedImages;
}
/**
* Converts a DICOM sequence to an array.
* If the sequence is null or undefined, an empty array is returned.
* If the sequence is already an array, it is returned as is.
* Otherwise, the sequence is wrapped in an array and returned.
*
* @param {any} sequence - The DICOM sequence to convert.
* @returns {any[]} - The converted array.
*/
function _getSequenceAsArray(sequence) {
if (!sequence) {
return [];
+4 -1
View File
@@ -5,4 +5,7 @@ const id = packageJson.name;
const SOPClassHandlerName = 'dicom-sr';
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
export { SOPClassHandlerName, SOPClassHandlerId, id };
const SOPClassHandlerName3D = 'dicom-sr-3d';
const SOPClassHandlerId3D = `${id}.sopClassHandlerModule.${SOPClassHandlerName3D}`;
export { SOPClassHandlerName, SOPClassHandlerId, SOPClassHandlerName3D, SOPClassHandlerId3D, id };
@@ -8,9 +8,10 @@ import { id } from './id.js';
import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
import Enums from './enums';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport');
});
const OHIFCornerstoneSRViewport = props => {
@@ -29,6 +30,7 @@ const dicomSRExtension = {
* Only required property. Should be a unique value across all extensions.
*/
id,
onModeEnter,
preRegistration,
@@ -70,4 +72,4 @@ const dicomSRExtension = {
export default dicomSRExtension;
// Put static exports here so they can be type checked
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol };
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol, Enums, toolNames };
+54 -2
View File
@@ -9,16 +9,28 @@ import {
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
import addToolInstance from './utils/addToolInstance';
import { Types } from '@ohif/core';
import toolNames from './tools/toolNames';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
/**
* @param {object} configuration
*/
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
export default function init({
configuration = {},
servicesManager,
}: Types.Extensions.ExtensionParams): void {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool);
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
@@ -28,10 +40,26 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
addToolInstance(toolNames.SRAngle, AngleTool);
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
addToolInstance(toolNames.SRSCOORD3DPoint, SCOORD3DPointTool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
const csTools3DVer1MeasurementSource = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
const { POINT } = measurementService.VALUE_TYPES;
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'SRSCOORD3DPoint',
POINT,
SRSCOOR3DProbeMapper.toAnnotation,
SRSCOOR3DProbeMapper.toMeasurement
);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
@@ -49,4 +77,28 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
SRRectangleROI: dashedLine,
global: {},
});
measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
({ viewportId, measurement, isConsumed }) => {
if (isConsumed) {
return;
}
try {
const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { viewPlaneNormal } = currentViewport.getCamera();
const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport(
currentViewport as StackViewport,
measurement.points[0],
viewPlaneNormal
);
const imageIndex = (currentViewport as StackViewport)
.getImageIds()
.indexOf(referencedImageId);
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
} catch (error) {
console.warn('Unable to jump to image based on measurement coordinate', error);
}
}
);
}
@@ -1,15 +0,0 @@
import { SOPClassHandlerId } from './id';
export default function onModeEnter({ servicesManager }) {
const { displaySetService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = [...displaySetCache.values()].filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId
);
srDisplaySets.forEach(ds => {
// New mode route, allow SRs to be hydrated again
ds.isHydrated = false;
});
}
@@ -0,0 +1,39 @@
import React from 'react';
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
import { ViewportActionButton } from '@ohif/ui';
import i18n from '@ohif/i18n';
export default function onModeEnter({ servicesManager }) {
const { displaySetService, toolbarService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = [...displaySetCache.values()].filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId || ds.SOPClassHandlerId === SOPClassHandlerId3D
);
srDisplaySets.forEach(ds => {
// New mode route, allow SRs to be hydrated again
ds.isHydrated = false;
});
toolbarService.addButtons([
{
// A base/default button for loading measurements. It is added to the toolbar below.
// Customizations to this button can be made in the mode or by another extension.
// For example, the button label can be changed and/or the command to clear
// the measurements can be dropped.
id: 'loadSRMeasurements',
component: props => (
<ViewportActionButton {...props}>{i18n.t('Common:LOAD')}</ViewportActionButton>
),
props: {
commands: ['clearMeasurements', 'loadSRMeasurements'],
},
},
]);
// The toolbar used in the viewport's status bar. Modes and extensions can further customize
// it to optionally add other buttons.
toolbarService.createButtonSection('loadSRMeasurements', ['loadSRMeasurements']);
}
@@ -7,10 +7,11 @@ import {
Types as cs3DToolsTypes,
} from '@cornerstonejs/tools';
import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule';
import SCOORD_TYPES from '../constants/scoordTypes';
import { SCOORDTypes } from '../enums';
import toolNames from './toolNames';
export default class DICOMSRDisplayTool extends AnnotationTool {
static toolName = 'DICOMSRDisplay';
static toolName = toolNames.DICOMSRDisplay;
constructor(
toolProps = {},
@@ -112,19 +113,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
let canvasCoordinatesAdapter;
switch (GraphicType) {
case SCOORD_TYPES.POINT:
case SCOORDTypes.POINT:
renderMethod = this.renderPoint;
break;
case SCOORD_TYPES.MULTIPOINT:
case SCOORDTypes.MULTIPOINT:
renderMethod = this.renderMultipoint;
break;
case SCOORD_TYPES.POLYLINE:
case SCOORDTypes.POLYLINE:
renderMethod = this.renderPolyLine;
break;
case SCOORD_TYPES.CIRCLE:
case SCOORDTypes.CIRCLE:
renderMethod = this.renderEllipse;
break;
case SCOORD_TYPES.ELLIPSE:
case SCOORDTypes.ELLIPSE:
renderMethod = this.renderEllipse;
canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners;
break;
@@ -229,7 +230,11 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
// This gives us one point for arrow
canvasCoordinates.push(viewport.worldToCanvas(point));
// We get the other point for the arrow by using the image size
if (data[1] !== undefined) {
canvasCoordinates.push(viewport.worldToCanvas(data[1]));
}
else{
// We get the other point for the arrow by using the image size
const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
let xOffset = 10;
@@ -248,6 +253,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
]);
canvasCoordinates.push(viewport.worldToCanvas(arrowEnd));
}
const arrowUID = `${index}`;
@@ -335,7 +343,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
}
const { annotationUID, data = {} } = annotation;
const { label } = data;
const { labels } = data;
const { color } = options;
let adaptedCanvasCoordinates = canvasCoordinates;
@@ -343,7 +351,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
if (typeof canvasCoordinatesAdapter === 'function') {
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
}
const textLines = this._getTextBoxLinesFromLabels(label);
const textLines = this._getTextBoxLinesFromLabels(labels);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
if (!annotation.data?.handles?.textBox?.worldPosition) {
@@ -0,0 +1,203 @@
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
import {
annotation,
drawing,
utilities,
Types as cs3DToolsTypes,
AnnotationDisplayTool,
} from '@cornerstonejs/tools';
import toolNames from './toolNames';
import { Annotation } from '@cornerstonejs/tools/dist/types/types';
export default class SCOORD3DPointTool extends AnnotationDisplayTool {
static toolName = toolNames.SRSCOORD3DPoint;
constructor(
toolProps = {},
defaultToolProps = {
configuration: {},
}
) {
super(toolProps, defaultToolProps);
}
_getTextBoxLinesFromLabels(labels) {
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 5);
const lines = [];
return lines;
}
// This tool should not inherit from AnnotationTool and we should not need
// to add the following lines.
isPointNearTool = () => null;
getHandleNearImagePoint = () => null;
renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
const { viewport } = enabledElement;
const { element } = viewport;
const annotations = annotation.state.getAnnotations(this.getToolName(), element);
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
if (!annotations?.length) {
return;
}
// Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations;
if (!viewport._actors?.size) {
return;
}
const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = {
toolGroupId: this.toolGroupId,
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
const annotationUID = annotation.annotationUID;
const { renderableData } = annotation.data;
const { POINT: points } = renderableData;
styleSpecifier.annotationUID = annotationUID;
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color = this.getStyle('color', styleSpecifier, annotation);
const options = {
color,
lineDash,
lineWidth,
};
const point = points[0][0];
// check if viewport can render it
const viewable = viewport.isReferenceViewable(
{ FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID, cameraFocalPoint: point },
{ asNearbyProjection: true }
);
if (!viewable) {
continue;
}
// render the point
const arrowPointCanvas = viewport.worldToCanvas(point);
// Todo: configure this
const arrowEndCanvas = [arrowPointCanvas[0] + 20, arrowPointCanvas[1] + 20];
const canvasCoordinates = [arrowPointCanvas, arrowEndCanvas];
drawing.drawArrow(
svgDrawingHelper,
annotationUID,
'1',
canvasCoordinates[1],
canvasCoordinates[0],
{
color: options.color,
width: options.lineWidth,
}
);
this.renderTextBox(
svgDrawingHelper,
viewport,
canvasCoordinates,
annotation,
styleSpecifier,
options
);
}
};
renderTextBox(
svgDrawingHelper,
viewport,
canvasCoordinates,
annotation,
styleSpecifier,
options = {}
) {
if (!canvasCoordinates || !annotation) {
return;
}
const { annotationUID, data = {} } = annotation;
const { labels } = data;
const textLines = [];
for (const label of labels) {
// make this generic
// fix this
if (label.label === '363698007') {
textLines.push(`Finding Site: ${label.value}`);
}
}
const { color } = options;
const adaptedCanvasCoordinates = canvasCoordinates;
// adapt coordinates if there is an adapter
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
if (!annotation.data?.handles?.textBox?.worldPosition) {
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
}
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
const textBoxUID = '1';
const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
const boundingBox = drawing.drawLinkedTextBox(
svgDrawingHelper,
annotationUID,
textBoxUID,
textLines,
textBoxPosition,
canvasCoordinates,
{},
{
...textBoxOptions,
color,
}
);
const { x: left, y: top, width, height } = boundingBox;
annotation.data.handles.textBox.worldBoundingBox = {
topLeft: viewport.canvasToWorld([left, top]),
topRight: viewport.canvasToWorld([left + width, top]),
bottomLeft: viewport.canvasToWorld([left, top + height]),
bottomRight: viewport.canvasToWorld([left + width, top + height]),
};
}
public getLinkedTextBoxStyle(
specifications: cs3DToolsTypes.AnnotationStyle.StyleSpecifier,
annotation?: Annotation
): Record<string, unknown> {
// Todo: this function can be used to set different styles for different toolMode
// for the textBox.
return {
visibility: this.getStyle('textBoxVisibility', specifications, annotation),
fontFamily: this.getStyle('textBoxFontFamily', specifications, annotation),
fontSize: this.getStyle('textBoxFontSize', specifications, annotation),
color: this.getStyle('textBoxColor', specifications, annotation),
shadow: this.getStyle('textBoxShadow', specifications, annotation),
background: this.getStyle('textBoxBackground', specifications, annotation),
lineWidth: this.getStyle('textBoxLinkLineWidth', specifications, annotation),
lineDash: this.getStyle('textBoxLinkLineDash', specifications, annotation),
};
}
}
@@ -1,7 +1,5 @@
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
const toolNames = {
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
DICOMSRDisplay: 'DICOMSRDisplay',
SRLength: 'SRLength',
SRBidirectional: 'SRBidirectional',
SREllipticalROI: 'SREllipticalROI',
@@ -11,6 +9,7 @@ const toolNames = {
SRCobbAngle: 'SRCobbAngle',
SRRectangleROI: 'SRRectangleROI',
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
SRSCOORD3DPoint: 'SRSCOORD3DPoint',
};
export default toolNames;
@@ -0,0 +1,62 @@
const SRSCOOR3DProbe = {
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Probe tool: Missing metadata or data');
return null;
}
const { toolName } = metadata;
const { points } = data.handles;
const displayText = getDisplayText(annotation);
return {
uid: annotationUID,
points,
metadata,
toolName: metadata.toolName,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType?.(toolName) ?? null,
};
},
};
function getDisplayText(annotation) {
const { data } = annotation;
if (!data) {
return [''];
}
const { labels } = data;
const displayText = [];
for (const label of labels) {
// make this generic
if (label.label === '33636980076') {
displayText.push(`Finding Site: ${label.value}`);
}
}
return displayText;
}
export default SRSCOOR3DProbe;
@@ -0,0 +1,67 @@
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData } from '@cornerstonejs/core';
import getRenderableData from './getRenderableData';
import toolNames from '../tools/toolNames';
export default function addSRAnnotation(measurement, imageId, frameNumber) {
let toolName = toolNames.DICOMSRDisplay;
const renderableData = measurement.coords.reduce((acc, coordProps) => {
acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || [];
acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId }));
return acc;
}, {});
const { TrackingUniqueIdentifier } = measurement;
const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0];
const graphicTypePoints = renderableData[graphicType];
/** TODO: Read the tool name from the DICOM SR identification type in the future. */
let frameOfReferenceUID = null;
if (imageId) {
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID;
}
if (valueType === 'SCOORD3D') {
toolName = toolNames.SRSCOORD3DPoint;
// get the ReferencedFrameOfReferenceUID from the measurement
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
}
const SRAnnotation: Types.Annotation = {
annotationUID: TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
toolName,
valueType,
graphicType,
FrameOfReferenceUID: frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
label: measurement.labels?.[0]?.value || undefined,
displayText: measurement.displayText || undefined,
handles: {
textBox: measurement.textBox ?? {},
points: graphicTypePoints[0],
},
cachedStats: {},
frameNumber,
renderableData,
TrackingUniqueIdentifier,
labels: measurement.labels,
},
};
/**
* const annotationManager = annotation.annotationState.getAnnotationManager();
* was not triggering annotation_added events.
*/
annotation.state.addAnnotation(SRAnnotation);
console.debug('Adding SR annotation:', SRAnnotation);
}
@@ -1,8 +1,14 @@
import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance(name: string, toolClass, configuration?): void {
export default function addToolInstance(name: string, toolClass, configuration = {}): void {
class InstanceClass extends toolClass {
static toolName = name;
constructor(toolProps, defaultToolProps) {
toolProps.configuration = toolProps.configuration
? { ...toolProps.configuration, ...configuration }
: configuration;
super(toolProps, defaultToolProps);
}
}
addTool(InstanceClass);
}
@@ -61,6 +61,11 @@ const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
const imageSet = new ImageSet(instances);
const instance = instances[0];
if (!instance) {
return;
}
imageSet.setAttributes({
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
SeriesDate: instance.SeriesDate,
@@ -0,0 +1,67 @@
import { utils } from '@ohif/core';
/**
* Formatters used to format each of the content items (SR "nodes") which can be
* text, code, UID ref, number, person name, date, time and date time. Each
* formatter must be a function with the following signature:
*
* [VALUE_TYPE]: (contentItem) => string
*
*/
const contentItemFormatters = {
TEXT: contentItem => contentItem.TextValue,
CODE: contentItem => contentItem.ConceptCodeSequence?.[0]?.CodeMeaning,
UIDREF: contentItem => contentItem.UID,
NUM: contentItem => {
const measuredValue = contentItem.MeasuredValueSequence?.[0];
if (!measuredValue) {
return;
}
const { NumericValue, MeasurementUnitsCodeSequence } = measuredValue;
const { CodeValue } = MeasurementUnitsCodeSequence;
return `${NumericValue} ${CodeValue}`;
},
PNAME: contentItem => {
const personName = contentItem.PersonName?.[0];
return personName ? utils.formatPN(personName) : undefined;
},
DATE: contentItem => {
const { Date } = contentItem;
return Date ? utils.formatDate(Date) : undefined;
},
TIME: contentItem => {
const { Time } = contentItem;
return Time ? utils.formatTime(Time) : undefined;
},
DATETIME: contentItem => {
const { DateTime } = contentItem;
if (typeof DateTime !== 'string') {
return;
}
// 14 characters because it should be something like 20180614113714
if (DateTime.length < 14) {
return DateTime;
}
const dicomDate = DateTime.substring(0, 8);
const dicomTime = DateTime.substring(8, 14);
const formattedDate = utils.formatDate(dicomDate);
const formattedTime = utils.formatTime(dicomTime);
return `${formattedDate} ${formattedTime}`;
},
};
function formatContentItemValue(contentItem) {
const { ValueType } = contentItem;
const fnFormat = contentItemFormatters[ValueType];
return fnFormat ? fnFormat(contentItem) : `[${ValueType} is not supported]`;
}
export { formatContentItemValue as default, formatContentItemValue };
@@ -1,3 +1,7 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
/**
* Extracts the label from the toolData imported from dcmjs. We need to do this
* as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools.
@@ -9,13 +13,15 @@
export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData;
let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT');
let freeTextLabel = findingSites.find(
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
);
if (freeTextLabel) {
return freeTextLabel.CodeMeaning;
}
if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') {
if (finding && finding.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT) {
return finding.CodeMeaning;
}
}
@@ -1,106 +1,49 @@
import { vec3 } from 'gl-matrix';
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
import toolNames from '../tools/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
import { SCOORDTypes } from '../enums';
const EPSILON = 1e-4;
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
const toolName = toolNames.DICOMSRDisplay;
const measurementData = {
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
renderableData: {},
labels: measurement.labels,
imageId,
};
measurement.coords.forEach(coord => {
const { GraphicType, GraphicData } = coord;
if (measurementData.renderableData[GraphicType] === undefined) {
measurementData.renderableData[GraphicType] = [];
const getRenderableCoords = ({ GraphicData, ValueType, imageId }) => {
const renderableData = [];
if (ValueType === 'SCOORD3D') {
for (let i = 0; i < GraphicData.length; i += 3) {
renderableData.push([GraphicData[i], GraphicData[i + 1], GraphicData[i + 2]]);
}
} else {
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [GraphicData[i], GraphicData[i + 1]]);
renderableData.push(worldPos);
}
}
return renderableData;
};
measurementData.renderableData[GraphicType].push(
_getRenderableData(GraphicType, GraphicData, imageId)
);
});
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
/**
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
* This is used before the annotation is hydrated. If hydrated the measurement will be added
* to the measurement service and will be available for the other viewports.
*/
const SRAnnotation: Types.Annotation = {
annotationUID: measurement.TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
toolName: toolName,
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
label: measurement.labels,
handles: {
textBox: measurement.textBox ?? {},
},
cachedStats: {},
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
frameNumber,
},
};
const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation(SRAnnotation);
}
function _getRenderableData(GraphicType, GraphicData, imageId) {
let renderableData: csTypes.Point3[];
function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
let renderableData = [];
switch (GraphicType) {
case SCOORD_TYPES.POINT:
case SCOORD_TYPES.MULTIPOINT:
case SCOORD_TYPES.POLYLINE:
renderableData = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
renderableData.push(worldPos);
}
case SCOORDTypes.POINT:
case SCOORDTypes.MULTIPOINT:
case SCOORDTypes.POLYLINE: {
renderableData = getRenderableCoords({ GraphicData, ValueType, imageId });
break;
case SCOORD_TYPES.CIRCLE: {
const pointsWorld = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
}
case SCOORDTypes.CIRCLE: {
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
GraphicData,
ValueType,
imageId,
});
// We do not have an explicit draw circle svg helper in Cornerstone3D at
// this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose.
const center = pointsWorld[0];
const onPerimeter = pointsWorld[1];
const radius = vec3.distance(center, onPerimeter);
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
if (!imagePlaneModule) {
throw new Error('No imagePlaneModule found');
}
@@ -115,14 +58,12 @@ function _getRenderableData(GraphicType, GraphicData, imageId) {
// we need to get major/minor axis (which are both the same size major = minor)
// first axisStart
const firstAxisStart = vec3.create();
vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius);
const firstAxisEnd = vec3.create();
vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius);
// second axisStart
const secondAxisStart = vec3.create();
vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius);
@@ -138,21 +79,16 @@ function _getRenderableData(GraphicType, GraphicData, imageId) {
break;
}
case SCOORD_TYPES.ELLIPSE: {
case SCOORDTypes.ELLIPSE: {
// GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY]
// But Cornerstone3D points are ordered as top, bottom, left, right for the
// ellipse so we need to identify if the majorAxis is horizontal or vertical
// and then choose the correct points to use for the ellipse.
const pointsWorld: csTypes.Point3[] = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
GraphicData,
ValueType,
imageId,
});
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
@@ -202,3 +138,5 @@ function _getRenderableData(GraphicType, GraphicData, imageId) {
return renderableData;
}
export default getRenderableData;
@@ -3,14 +3,12 @@ import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
const { locking } = CsAnnotation;
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
@@ -43,15 +41,15 @@ const convertSites = (codingValues, sites) => {
*
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager, appConfig }: withAppTypes,
{ servicesManager, extensionManager, commandsManager }: withAppTypes,
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0];
const { measurementService, displaySetService, customizationService } = servicesManager.services;
const codingValues = customizationService.getCustomization('codingValues', {});
const codingValues = customizationService.getCustomization('codingValues');
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// TODO -> We should define a strict versioning somewhere.
@@ -101,8 +99,7 @@ export default function hydrateStructuredReport(
metaData
);
const onBeforeSRHydration =
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
const onBeforeSRHydration = customizationService.getCustomization('onBeforeSRHydration')?.value;
if (typeof onBeforeSRHydration === 'function') {
storedMeasurementByAnnotationType = onBeforeSRHydration({
@@ -202,7 +199,11 @@ export default function hydrateStructuredReport(
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
annotation.data.finding = convertCode(codingValues, toolData.finding?.[0]);
annotation.data.findingSites = convertSites(codingValues, toolData.findingSites);
annotation.data.site = annotation.data.findingSites?.[0];
annotation.data.findingSites?.forEach(site => {
if (site.type) {
annotation.data[site.type] = site;
}
});
const matchingMapping = mappings.find(m => m.annotationType === annotationType);
@@ -214,9 +215,13 @@ export default function hydrateStructuredReport(
dataSource
);
commandsManager.runCommand('updateMeasurement', {
uid: newAnnotationUID,
code: annotation.data.finding,
});
if (disableEditing) {
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
locking.setAnnotationLocked(addedAnnotation, true);
locking.setAnnotationLocked(newAnnotationUID, true);
}
if (!imageIds.includes(imageId)) {
@@ -4,7 +4,7 @@ const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG;
/**
* Checks if the given `displaySet`can be rehydrated into the `measurementService`.
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.9.0-beta.16",
"version": "3.10.0-beta.86",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
@@ -29,22 +29,21 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.16",
"@ohif/extension-cornerstone": "3.9.0-beta.16",
"@ohif/extension-default": "3.9.0-beta.16",
"@ohif/i18n": "3.9.0-beta.16",
"@ohif/ui": "3.9.0-beta.16",
"dcmjs": "^0.29.5",
"@ohif/core": "3.10.0-beta.86",
"@ohif/extension-cornerstone": "3.10.0-beta.86",
"@ohif/extension-default": "3.10.0-beta.86",
"@ohif/i18n": "3.10.0-beta.86",
"@ohif/ui": "3.10.0-beta.86",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.2"
"react": "^18.3.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.74.6",
"@cornerstonejs/streaming-image-volume-loader": "^1.74.6",
"@cornerstonejs/tools": "^1.74.6",
"@cornerstonejs/core": "^2.17.2",
"@cornerstonejs/tools": "^2.17.2",
"classnames": "^2.3.2"
}
}
@@ -110,7 +110,12 @@ function _getTimePointsData(volume) {
return { timePoints, timePointsUnit };
}
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
function _getSegmentationData(
segmentation,
volumesTimePointsCache,
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
) {
const { displaySetService, segmentationService, viewportGridService } = servicesManager.services;
const displaySets = displaySetService.getActiveDisplaySets();
const dynamic4DDisplaySet = displaySets.find(displaySet => {
@@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
referencedDynamicVolume.imageIds[0]
);
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const maskVolumeId = segmentationVolume?.volumeId;
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
maskVolumeId: segmentation.id,
maskVolumeId,
}) as number[][];
const pixelCount = timeData.length;
@@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
return [];
}
// since we only use one segmentation representation per segmentationId
// it is fine to pick the first one
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
segmentation.id
);
const segmentationRepresentationUID =
segmentationRepresentations[0].segmentationRepresentationUID;
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
segmentationRepresentationUID
);
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
toolGroupId,
segmentationRepresentationUID,
1 // segmentIndex
);
// const color = csTools.segmentation.config.color.getSegmentIndexColor(
// segmentationRepresentationUID,
// 1 // segmentIndex
// );
const viewportId = viewportGridService.getActiveViewportId();
const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1);
const hexColor = cs.utilities.color.rgbToHex(...color);
const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]);
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
if (!timePointsData) {
@@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
};
}
function _getInstanceFromSegmentations(segmentations, displaySetService) {
function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
if (!segmentations.length) {
return;
}
const volumesTimePointsCache = new WeakMap();
const segmentationsData = segmentations.map(segmentation =>
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
);
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
@@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) {
}
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
const { segmentationService, displaySetService } = servicesManager.services;
debugger;
const { segmentationService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } =
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
if (seriesMetadata && instance) {
// An event is triggered after adding the instance and the displaySet is created
@@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const stdDeviation = Math.sqrt(variance);
return stdDeviation;
}
// Iterate through each segmentation to get the timeData and ijkCoords
segmentations.forEach((segmentation, segmentationIndex) => {
segmentations.forEach(segmentation => {
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
maskVolumeId: segmentation.id,
maskVolumeId: volume.volumeId,
}) as number[][];
if (summaryStats) {
@@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
// for each voxel in the ROI, get the value at the current time point
const voxelValues = [];
let sum = 0;
let minValue = Infinity;
let maxValue = -Infinity;
let minIndex = 0;
let maxIndex = 0;
// Single pass through the data to collect all needed values
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
voxelValues.push(timeData[voxelIndex][timeIndex]);
const value = timeData[voxelIndex][timeIndex];
voxelValues.push(value);
sum += value;
if (value < minValue) {
minValue = value;
minIndex = voxelIndex;
}
if (value > maxValue) {
maxValue = value;
maxIndex = voxelIndex;
}
}
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
const minimum = Math.min(...voxelValues);
min.push(minimum);
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
const maximum = Math.max(...voxelValues);
max.push(maximum);
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
mean.push(sum / numVoxels);
min.push(minValue);
minIJK.push(ijkCoords[minIndex]);
max.push(maxValue);
maxIJK.push(ijkCoords[maxIndex]);
std.push(calculateStandardDeviation(voxelValues));
}
@@ -340,30 +356,19 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
throw new Error('No reference display set found based on the dynamic data');
}
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
referenceDisplaySet.displaySetInstanceUID,
{ label }
const displaySet = displaySetService.getDisplaySetByUID(
referenceDisplaySet.displaySetInstanceUID
);
// Add Segmentation to all toolGroupIds in the viewer
const toolGroupIds = Array.from(
viewports.values(),
viewport => viewport.viewportOptions.toolGroupId
);
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
label,
});
const representationType = LABELMAP;
const firstViewport = viewports.values().next().value;
for (const toolGroupId of toolGroupIds) {
const hydrateSegmentation = true;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId,
hydrateSegmentation,
representationType
);
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
}
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
segmentationId,
});
return segmentationId;
},
@@ -147,8 +147,8 @@ function getFusionViewports() {
name: DEFAULT_COLORMAP,
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.1, opacity: 0.3 },
{ value: 1, opacity: 0.3 },
{ value: 0.1, opacity: 0.8 },
{ value: 1, opacity: 0.8 },
],
},
};
@@ -191,6 +191,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -240,6 +249,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -289,6 +307,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
Loaded 100 of 2012 files, more files were not shown because too many files have changed in this diff. Show more