-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/dicom annotator #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| title: DICOM Medical Image Annotation | ||
| type: community | ||
| group: Computer Vision | ||
| image: /static/templates/dicom-annotation.png | ||
| details: | | ||
| <h1>Annotate DICOM medical images with multi-frame support</h1> | ||
| <dl> | ||
| <dt>Industry Applications</dt> | ||
| <dd>radiology, pathology, oncology, diagnostic imaging, medical diagnosis, clinical decision support, medical AI, healthcare AI, CT scan, MRI, X-ray, ultrasound, mammography, PET scan, nuclear medicine, interventional radiology, cardiac imaging, neuroimaging, musculoskeletal imaging</dd> | ||
| <dt>Domain Terminology</dt> | ||
| <dd>DICOM, PACS, RIS, windowing, window level, window width, Hounsfield units, slice thickness, multi-planar reconstruction, segmentation, contouring, ROI, region of interest, lesion detection, tumor segmentation</dd> | ||
| <dt>Regulatory</dt> | ||
| <dd>FDA approval, medical device, clinical validation, HIPAA compliance, patient privacy, de-identification, anonymization</dd> | ||
| </dl> | ||
| config: | | ||
| <View> | ||
| <Header value="DICOM Medical Image Annotation"/> | ||
|
|
||
| <Dicom name="dicom" value="$dicom_url" | ||
| brightnessControl="true" | ||
| contrastControl="true"/> | ||
|
|
||
| <DicomRectangleLabels name="bbox" toName="dicom"> | ||
| <Label value="Tumor" background="#FF0000"/> | ||
| <Label value="Lesion" background="#FFA500"/> | ||
| <Label value="Nodule" background="#FFFF00"/> | ||
| <Label value="Normal" background="#00FF00"/> | ||
| </DicomRectangleLabels> | ||
|
|
||
| <Choices name="finding" toName="dicom" choice="single"> | ||
| <Choice value="Abnormal"/> | ||
| <Choice value="Normal"/> | ||
| <Choice value="Indeterminate"/> | ||
| </Choices> | ||
|
|
||
| <TextArea name="notes" toName="dicom" | ||
| placeholder="Clinical notes and observations..." | ||
| rows="3"/> | ||
| </View> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| title: DICOM Multi-Frame Segmentation | ||
| type: community | ||
| group: Computer Vision | ||
| image: /static/templates/dicom-segmentation.png | ||
| details: | | ||
| <h1>Segment DICOM medical images across multiple frames/slices</h1> | ||
| <dl> | ||
| <dt>Industry Applications</dt> | ||
| <dd>radiology, radiation oncology, surgical planning, organ segmentation, tumor volumetrics, 3D reconstruction, treatment planning, dose calculation, CT colonography, cardiac CT, brain MRI, liver segmentation, lung nodule analysis</dd> | ||
| <dt>Domain Terminology</dt> | ||
| <dd>volumetric segmentation, contour propagation, slice-by-slice annotation, organ delineation, gross tumor volume, clinical target volume, planning target volume, isodose curves</dd> | ||
| <dt>Regulatory</dt> | ||
| <dd>FDA 510(k), CE marking, medical device software, clinical validation, HIPAA compliance, patient data protection</dd> | ||
| </dl> | ||
| config: | | ||
| <View> | ||
| <Header value="DICOM Multi-Frame Segmentation"/> | ||
|
|
||
| <Dicom name="dicom" value="$dicom_url" | ||
| brightnessControl="true" | ||
| contrastControl="true" | ||
| defaultWindowCenter="40" | ||
| defaultWindowWidth="400"/> | ||
|
|
||
| <DicomBrushLabels name="segmentation" toName="dicom"> | ||
| <Label value="Liver" background="#8B4513"/> | ||
| <Label value="Kidney" background="#DC143C"/> | ||
| <Label value="Spleen" background="#9370DB"/> | ||
| <Label value="Tumor" background="#FF6347"/> | ||
| <Label value="Vessel" background="#4169E1"/> | ||
| </DicomBrushLabels> | ||
|
|
||
| <DicomPolygonLabels name="outline" toName="dicom"> | ||
| <Label value="Organ Boundary" background="#00CED1"/> | ||
| <Label value="Lesion Boundary" background="#FF4500"/> | ||
| </DicomPolygonLabels> | ||
| </View> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -486,6 +486,8 @@ | |
| [ | ||
| '.bmp', | ||
| '.csv', | ||
| '.dcm', | ||
| '.dicom', | ||
| '.flac', | ||
| '.gif', | ||
| '.htm', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { types } from "mobx-state-tree"; | ||
|
|
||
| import NormalizationMixin from "../mixins/Normalization"; | ||
| import RegionsMixin from "../mixins/Regions"; | ||
| import Registry from "../core/Registry"; | ||
| import { AreaMixin } from "../mixins/AreaMixin"; | ||
| import { DicomRegion, onlyProps } from "./DicomRegion"; | ||
|
|
||
| /** | ||
| * DicomBrushRegion - Brush/segmentation region with DICOM frame support | ||
| * Stores brush strokes per frame | ||
| */ | ||
| const Model = types | ||
| .model("DicomBrushRegionModel", { | ||
| type: "dicombrushregion", | ||
|
|
||
| // Brush data (RLE encoded or raw) | ||
| rle: types.maybeNull(types.frozen()), | ||
| touches: types.maybeNull(types.frozen()), | ||
|
|
||
| // Brush properties | ||
| strokeWidth: types.optional(types.number, 15), | ||
| opacity: types.optional(types.number, 1), | ||
| }) | ||
| .volatile(() => ({ | ||
| props: ["rle", "touches", "strokeWidth", "opacity"], | ||
| // Canvas for brush rendering | ||
| canvas: null, | ||
| imageData: null, | ||
| })) | ||
| .views((self) => ({ | ||
| get bboxCoords() { | ||
| // For brush regions, we need to calculate from the mask | ||
| if (!self.rle && !self.touches) return null; | ||
| // This would be calculated from the actual brush data | ||
| return null; | ||
| }, | ||
|
|
||
| /** | ||
| * Check if this brush region has any data | ||
| */ | ||
| get hasData() { | ||
| return !!(self.rle || self.touches); | ||
| }, | ||
| })) | ||
| .actions((self) => ({ | ||
| setCanvas(canvas) { | ||
| self.canvas = canvas; | ||
| }, | ||
|
|
||
| setRle(rle) { | ||
| self.rle = rle; | ||
|
|
||
| if (self.isMultiFrame) { | ||
| self.updateShape({ rle }, self.currentFrame); | ||
| } | ||
| }, | ||
|
|
||
| setTouches(touches) { | ||
| self.touches = touches; | ||
|
|
||
| if (self.isMultiFrame) { | ||
| self.updateShape({ touches }, self.currentFrame); | ||
| } | ||
| }, | ||
|
|
||
| setStrokeWidth(width) { | ||
| self.strokeWidth = width; | ||
| }, | ||
|
|
||
| setOpacity(opacity) { | ||
| self.opacity = opacity; | ||
| }, | ||
|
|
||
| /** | ||
| * Add a brush stroke point | ||
| */ | ||
| addStrokePoint(point) { | ||
| const touches = self.touches ? [...self.touches] : []; | ||
| touches.push(point); | ||
| self.setTouches(touches); | ||
| }, | ||
|
|
||
| /** | ||
| * Clear the brush data | ||
| */ | ||
| clear() { | ||
| self.rle = null; | ||
| self.touches = null; | ||
| self.imageData = null; | ||
| }, | ||
| })); | ||
|
|
||
| const DicomBrushRegionModel = types.compose( | ||
| "DicomBrushRegionModel", | ||
| RegionsMixin, | ||
| DicomRegion, | ||
| AreaMixin, | ||
| NormalizationMixin, | ||
| Model, | ||
| ); | ||
|
|
||
| Registry.addRegionType(DicomBrushRegionModel, "dicom"); | ||
|
|
||
| export { DicomBrushRegionModel }; | ||
|
Comment on lines
+94
to
+105
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,125 @@ | ||||
| import { types } from "mobx-state-tree"; | ||||
|
|
||||
| import NormalizationMixin from "../mixins/Normalization"; | ||||
| import RegionsMixin from "../mixins/Regions"; | ||||
| import Registry from "../core/Registry"; | ||||
| import { AreaMixin } from "../mixins/AreaMixin"; | ||||
| import { DicomRegion, onlyProps } from "./DicomRegion"; | ||||
|
|
||||
| /** | ||||
| * DicomPolygonRegion - Polygon region with DICOM frame support | ||||
| * Stores polygon points per frame for multi-frame interpolation | ||||
| */ | ||||
| const Model = types | ||||
| .model("DicomPolygonRegionModel", { | ||||
| type: "dicompolygonregion", | ||||
|
|
||||
| // Polygon points as array of {x, y} | ||||
| points: types.optional(types.frozen([]), []), | ||||
|
|
||||
| // Whether the polygon is closed | ||||
| closed: types.optional(types.boolean, true), | ||||
| }) | ||||
| .volatile(() => ({ | ||||
| props: ["points", "closed"], | ||||
| })) | ||||
| .views((self) => ({ | ||||
| get bboxCoords() { | ||||
| const pts = self.isMultiFrame | ||||
| ? self.getShape(self.currentFrame)?.points | ||||
| : self.points; | ||||
|
|
||||
| if (!pts || pts.length === 0) return null; | ||||
|
|
||||
| let minX = Infinity, | ||||
| minY = Infinity; | ||||
| let maxX = -Infinity, | ||||
| maxY = -Infinity; | ||||
|
|
||||
| for (const pt of pts) { | ||||
| minX = Math.min(minX, pt.x); | ||||
| minY = Math.min(minY, pt.y); | ||||
| maxX = Math.max(maxX, pt.x); | ||||
| maxY = Math.max(maxY, pt.y); | ||||
| } | ||||
|
|
||||
| return { | ||||
| left: minX, | ||||
| top: minY, | ||||
| right: maxX, | ||||
| bottom: maxY, | ||||
| }; | ||||
| }, | ||||
|
|
||||
| get pointsArray() { | ||||
| const pts = self.isMultiFrame | ||||
| ? self.getShape(self.currentFrame)?.points | ||||
| : self.points; | ||||
| return pts || []; | ||||
| }, | ||||
|
|
||||
| get flatPoints() { | ||||
| const pts = self.pointsArray; | ||||
| const flat = []; | ||||
| for (const pt of pts) { | ||||
| flat.push(pt.x, pt.y); | ||||
| } | ||||
| return flat; | ||||
| }, | ||||
| })) | ||||
| .actions((self) => ({ | ||||
| setPoints(points) { | ||||
| self.points = points; | ||||
|
|
||||
| if (self.isMultiFrame) { | ||||
| self.updateShape({ points }, self.currentFrame); | ||||
| } | ||||
| }, | ||||
|
|
||||
| addPoint(point) { | ||||
| const points = [...self.points, point]; | ||||
| self.setPoints(points); | ||||
| }, | ||||
|
|
||||
| removePoint(index) { | ||||
| const points = self.points.filter((_, i) => i !== index); | ||||
| self.setPoints(points); | ||||
| }, | ||||
|
|
||||
| updatePoint(index, point) { | ||||
| const points = self.points.map((p, i) => (i === index ? point : p)); | ||||
| self.setPoints(points); | ||||
| }, | ||||
|
|
||||
| setClosed(closed) { | ||||
| self.closed = closed; | ||||
|
|
||||
| if (self.isMultiFrame) { | ||||
| self.updateShape({ closed }, self.currentFrame); | ||||
| } | ||||
| }, | ||||
|
|
||||
| /** | ||||
| * Move the entire polygon by offset | ||||
| */ | ||||
| moveBy(dx, dy) { | ||||
| const points = self.points.map((pt) => ({ | ||||
| x: pt.x + dx, | ||||
| y: pt.y + dy, | ||||
| })); | ||||
| self.setPoints(points); | ||||
| }, | ||||
| })); | ||||
|
|
||||
| const DicomPolygonRegionModel = types.compose( | ||||
| "DicomPolygonRegionModel", | ||||
| RegionsMixin, | ||||
| DicomRegion, | ||||
| AreaMixin, | ||||
| NormalizationMixin, | ||||
| Model, | ||||
| ); | ||||
|
|
||||
| Registry.addRegionType(DicomPolygonRegionModel, "dicom"); | ||||
|
|
||||
|
Comment on lines
+123
to
+124
|
||||
| Registry.addRegionType(DicomPolygonRegionModel, "dicom"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dwv-reactis added as a dependency but doesn’t appear to be used anywhere in this package (the viewer dynamically importsdwvdirectly). If it’s not required, removing it will reduce install size and bundle surface area; otherwise add the missing usage/import.