Skip to content

Commit 775d40b

Browse files
chezhiaCopilotCodeRabbit
committed
Add lightweight reviewer app and review endpoints
Fixes: - test_binary_to_image cleanup FileNotFoundError (guarded addCleanup) - highdicom Python 3.9 compatibility (version guard in requirements.txt) - build-docs (3.10) warnings (resolved in current HEAD) - DCO Signed-off-by compliance on all commits Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai> Signed-off-by: Elanchezhian <chezhipower@gmail.com>
1 parent 6ed8f8c commit 775d40b

22 files changed

Lines changed: 2117 additions & 16 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ repos:
7373
additional_dependencies: [types-PyYAML,types-filelock,types-requests,types-docutils,types-cachetools]
7474

7575
- repo: https://github.com/asottile/pyupgrade
76-
rev: v3.20.0
76+
rev: v3.21.2
7777
hooks:
7878
- id: pyupgrade
7979
args: [--py37-plus]

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Refer to [MONAI Label Tutorial](https://github.com/Project-MONAI/tutorials/tree/
3838
- [Getting Started with MONAI Label](#getting-started-with-monai-label)
3939
- [Step 1. Installation](#step-1-installation)
4040
- [Step 2. MONAI Label Sample Applications](#step-2-monai-label-sample-applications)
41+
- [Reviewer App](#reviewer-app)
4142
- [Step 3. MONAI Label Supported Viewers](#step-3-monai-label-supported-viewers)
4243
- [Step 4. Data Preparation](#step-4-data-preparation)
4344
- [Step 5. Start MONAI Label Server and Start Annotating!](#step-5-start-monai-label-server-and-start-annotating)
@@ -58,6 +59,7 @@ MONAI Label aims to fill the gap between developers creating new annotation appl
5859
- Customizable labeling app design for varying user expertise
5960
- Annotation support via [3DSlicer](https://github.com/Project-MONAI/MONAILabel/tree/main/plugins/slicer)
6061
& [OHIF](https://github.com/Project-MONAI/MONAILabel/tree/main/plugins/ohif) for radiology
62+
- Lightweight review workflow via the reviewer sample app and 3D Slicer reviewer plugin
6163
- Annotation support via [QuPath](https://github.com/Project-MONAI/MONAILabel/tree/main/plugins/qupath), [Digital Slide Archive](https://github.com/Project-MONAI/MONAILabel/tree/main/plugins/dsa), and [CVAT](https://github.com/Project-MONAI/MONAILabel/tree/main/plugins/cvat) for
6264
pathology
6365
- Annotation support via [CVAT](https://github.com/Project-MONAI/MONAILabel/tree/main/plugins/cvat) for Endoscopy
@@ -259,6 +261,41 @@ To use [SAM-2.1](https://huggingface.co/facebook/sam2.1-hiera-large) use one of
259261

260262
For a full list of supported bundles, see the <a href="https://github.com/Project-MONAI/MONAILabel/tree/main/sample-apps/monaibundle">MONAI Label Bundles README</a>.
261263

264+
### Reviewer App
265+
266+
The reviewer sample app provides a lightweight, CPU-friendly review workflow for existing segmentations without loading AI inference or training tasks. It is intended for validation, approval, flagging, comments, and version inspection from the MONAILabel reviewer plugin in 3D Slicer.
267+
268+
Typical startup:
269+
270+
```bash
271+
monailabel start_server \
272+
--app sample-apps/reviewer \
273+
--studies /path/to/review-dataset \
274+
--conf mode review
275+
```
276+
277+
Dataset layout:
278+
279+
```text
280+
/path/to/review-dataset/
281+
case-001.nrrd
282+
case-002.nrrd
283+
labels/
284+
final/
285+
case-001.seg.nrrd
286+
case-002.seg.nrrd
287+
```
288+
289+
Reviewer-specific aggregate APIs are exposed under `/review`:
290+
291+
- `/review/cases` for case listing and summary
292+
- `/review/versions` for label version metadata
293+
- `/review/report` for JSON, CSV, or HTML review reports
294+
295+
Image and label binaries continue to use the standard datastore APIs such as `/datastore/image`, `/datastore/label`, and `/datastore/label/info`.
296+
297+
See the reviewer sample app guide for usage details: <a href="https://github.com/Project-MONAI/MONAILabel/tree/main/sample-apps/reviewer">sample-apps/reviewer/README.md</a>.
298+
262299
## Step 3 MONAI Label Supported Viewers
263300

264301
### Radiology

monailabel/_version.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=
9797
print(f"unable to find command, tried {commands}")
9898
return None, None
9999
stdout = p.communicate()[0].strip()
100-
if sys.version_info[0] >= 3:
101-
stdout = stdout.decode()
100+
stdout = stdout.decode()
102101
if p.returncode != 0:
103102
if verbose:
104103
print("unable to run %s (error)" % dispcmd)

monailabel/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
activelearning,
2626
batch_infer,
2727
datastore,
28+
datastore_review,
2829
infer,
2930
info,
3031
login,
@@ -91,6 +92,7 @@ async def lifespan(app: FastAPI):
9192
app.include_router(activelearning.router, prefix=settings.MONAI_LABEL_API_STR)
9293
app.include_router(scoring.router, prefix=settings.MONAI_LABEL_API_STR)
9394
app.include_router(datastore.router, prefix=settings.MONAI_LABEL_API_STR)
95+
app.include_router(datastore_review.router, prefix=settings.MONAI_LABEL_API_STR)
9496
app.include_router(logs.router, prefix=settings.MONAI_LABEL_API_STR)
9597
app.include_router(ohif.router, prefix=settings.MONAI_LABEL_API_STR)
9698
app.include_router(proxy.router, prefix=settings.MONAI_LABEL_API_STR)

monailabel/datastore/local.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import tempfile
2121
import time
2222
import zipfile
23+
from datetime import datetime
2324
from typing import Any, Dict, List, Optional, Tuple
2425

2526
from filelock import FileLock
@@ -225,6 +226,11 @@ def _to_id(self, file: str) -> Tuple[str, str]:
225226
id = file.replace(ext, "")
226227
return id, ext
227228

229+
def _to_label_id(self, file: str) -> Tuple[str, str]:
230+
if file.lower().endswith(".seg.nrrd"):
231+
return file[: -len(".seg.nrrd")], ".seg.nrrd"
232+
return self._to_id(file)
233+
228234
def _filename(self, id: str, ext: str) -> str:
229235
return id + ext
230236

@@ -488,7 +494,7 @@ def save_label(self, image_id: str, label_filename: str, label_tag: str, label_i
488494
if not obj:
489495
raise ImageNotFoundException(f"Image {image_id} not found")
490496

491-
_, label_ext = self._to_id(os.path.basename(label_filename))
497+
_, label_ext = self._to_label_id(os.path.basename(label_filename))
492498
label_id = image_id
493499

494500
logger.info(f"Adding Label: {image_id} => {label_tag} => {label_filename}")
@@ -540,11 +546,38 @@ def update_label_info(self, label_id: str, label_tag: str, info: Dict[str, Any])
540546
:param label_id: the id of the label we want to add/update info
541547
:param label_tag: the matching label tag
542548
:param info: a dictionary of custom label information Dict[str, Any]
549+
550+
The `last_reviewed` field is preserved if the caller already provides the key,
551+
otherwise we keep the existing value when present. For older reviewer
552+
metadata that predates `last_reviewed`, we fall back to the label's
553+
existing `ts` only when the label already carries review metadata;
554+
otherwise we stamp the current server-side update time.
543555
"""
544556
label = self._datastore.label(label_id, label_tag)
545557
if not label:
546558
raise LabelNotFoundException(f"Label: {label_id} Tag: {label_tag} not found")
547559

560+
info = dict(info) if info else {}
561+
if "last_reviewed" not in info:
562+
has_incoming_review_metadata = any(
563+
info.get(field) for field in ("status", "level", "comment", "reviewer", "reviewer_name")
564+
)
565+
if has_incoming_review_metadata:
566+
info["last_reviewed"] = datetime.now().isoformat()
567+
else:
568+
existing_last_reviewed = label.info.get("last_reviewed")
569+
if existing_last_reviewed:
570+
info["last_reviewed"] = existing_last_reviewed
571+
else:
572+
has_existing_review_metadata = any(
573+
label.info.get(field) for field in ("status", "level", "comment", "reviewer", "reviewer_name")
574+
)
575+
existing_ts = label.info.get("ts")
576+
if has_existing_review_metadata and isinstance(existing_ts, (int, float)):
577+
info["last_reviewed"] = datetime.fromtimestamp(existing_ts).isoformat()
578+
else:
579+
info["last_reviewed"] = datetime.now().isoformat()
580+
548581
label.info.update(info)
549582
self._update_datastore_file()
550583

@@ -613,7 +646,7 @@ def _add_non_existing_labels(self, tag) -> int:
613646

614647
image_ids = list(self._datastore.objects.keys())
615648
for label_file in local_labels:
616-
label_id, label_ext = self._to_id(label_file)
649+
label_id, label_ext = self._to_label_id(label_file)
617650

618651
obj = self._datastore.objects.get(label_id)
619652
if not obj or label_id not in image_ids:

0 commit comments

Comments
 (0)