Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
2 changes: 2 additions & 0 deletions label_studio/core/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@
[
'.bmp',
'.csv',
'.dcm',
'.dicom',
'.flac',
'.gif',
'.htm',
Expand Down
5 changes: 3 additions & 2 deletions web/libs/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"colormap": "^2.3.2",
"d3": "^5.16.0",
"date-fns": "^2.20.1",
"dwv": "^0.33.0",
"dwv-react": "^0.15.0",
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dwv-react is added as a dependency but doesn’t appear to be used anywhere in this package (the viewer dynamically imports dwv directly). If it’s not required, removing it will reduce install size and bundle surface area; otherwise add the missing usage/import.

Suggested change
"dwv-react": "^0.15.0",

Copilot uses AI. Check for mistakes.
"emoji-regex": "^7.0.3",
"insert-after": "^0.1.4",
"keymaster": "^1.6.2",
Expand All @@ -35,12 +37,11 @@
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
"sanitize-html": "^2.12.1",

"webfft": "^1.0.3",
"xpath-range": "^1.1.1"
},
"main": "src/index.js",
"devDependencies": {
"babel-plugin-istanbul": "^7.0.0"
}
}
}
105 changes: 105 additions & 0 deletions web/libs/editor/src/regions/DicomBrushRegion.js
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
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This region model is registered via Registry.addRegionType but doesn’t register a view with Registry.addTag(...). If instantiated, it will fail to render with Tree.renderItem due to the missing view for DicomBrushRegionModel. Please add the view registration (and ensure the view works with the DICOM object’s coordinate system).

Copilot uses AI. Check for mistakes.
125 changes: 125 additions & 0 deletions web/libs/editor/src/regions/DicomPolygonRegion.js
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
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This region model is registered via Registry.addRegionType but lacks a corresponding Registry.addTag(...) view registration. If it’s created/deserialized, Tree.renderItem will fail because there’s no view for DicomPolygonRegionModel. Register a view component for this model (or explicitly prevent it from being selected/created).

Suggested change
Registry.addRegionType(DicomPolygonRegionModel, "dicom");

Copilot uses AI. Check for mistakes.
export { DicomPolygonRegionModel };
Loading