From 666cc4b6fe2943a549748f10399f6cd3728566e7 Mon Sep 17 00:00:00 2001 From: Alejandro Aristizabal Date: Wed, 8 Nov 2023 17:42:53 -0500 Subject: [PATCH 1/5] Implement TUI for review automation --- scripts/monitor-dset.py | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/scripts/monitor-dset.py b/scripts/monitor-dset.py index 885fe9258..b71a355f1 100644 --- a/scripts/monitor-dset.py +++ b/scripts/monitor-dset.py @@ -21,6 +21,7 @@ import yaml import pandas as pd import tarfile +from subprocess import Popen from textual.app import App, ComposeResult from textual.binding import Binding @@ -41,6 +42,8 @@ NAME_HELP = "The name of the dataset to monitor" MLCUBE_HELP = "The Data Preparation MLCube UID used to create the data" +DEFAULT_SEGMENTATION = "tumorMask_fused-staple.nii.gz" +REVIEW_COMMAND = "itksnap" LISTITEM_MAX_LEN = 30 @@ -380,13 +383,28 @@ def compose(self) -> ComposeResult: yield Markdown(id="subject-comment-md") yield CopyableItem("Data path", "", id="subject-data-container") yield CopyableItem("Labels path", "", id="subject-labels-container") + with Center(id="review-buttons"): + yield Button( + "Review with ITK-SNAP (ITK-SNAP must be installed)", + variant="primary", + disabled=True, + id="review-button", + ) + yield Button.success( + "Mark as finalized (must review first)", + id="reviewed-button", + disabled=True, + ) def update_subject(self, subject: pd.Series, dset_path: str): + self.subject = subject + self.dset_path = dset_path wname = self.query_one("#subject-name", Static) wstatus = self.query_one("#subject-status", Static) wmsg = self.query_one("#subject-comment-md", Markdown) wdata = self.query_one("#subject-data-container", CopyableItem) wlabels = self.query_one("#subject-labels-container", CopyableItem) + buttons_container = self.query_one("#review-buttons", Center) labels_path = os.path.join(dset_path, "../labels") if subject["status_name"] != "DONE": @@ -399,6 +417,90 @@ def update_subject(self, subject: pd.Series, dset_path: str): wmsg.update(subject["comment"]) wdata.update(to_local_path(subject["data_path"], dset_path)) wlabels.update(to_local_path(subject["labels_path"], labels_path)) + # Hardcoding manual review behavior. This SHOULD NOT be here for general data prep monitoring. + # Additional configuration must be set to make this kind of features generic + buttons_container.display = subject["status_name"] == "MANUAL_REVIEW_REQUIRED" + self.__update_buttons() + + def __update_buttons(self): + review_button = self.query_one("#review-button", Button) + reviewed_button = self.query_one("#reviewed-button", Button) + + if self.__can_review(): + review_button.label = "Review with ITK-SNAP" + review_button.disabled = False + if self.__can_finalize(): + reviewed_button.label = "Mark as finalized" + reviewed_button.disabled = False + + def __can_review(self): + review_command_path = shutil.which(REVIEW_COMMAND) + return review_command_path is not None + + def __can_finalize(self): + labels_path = to_local_path(self.subject["labels_path"], self.dset_path) + id, tp = self.subject.name.split("|") + filename = f"{id}_{tp}_{DEFAULT_SEGMENTATION}" + under_review_filepath = os.path.join( + labels_path, + "under_review", + filename, + ) + + return os.path.exists(under_review_filepath) + + def __review(self): + review_cmd = "itksnap -g {t1c} -o {flair} {t2} {t1} -s {seg} -l {seg}" + data_path = to_local_path(self.subject["data_path"], self.dset_path) + labels_path = to_local_path(self.subject["labels_path"], self.dset_path) + id, tp = self.subject.name.split("|") + seg_file = os.path.join(labels_path, f"{id}_{tp}_{DEFAULT_SEGMENTATION}") + t1c_file = os.path.join(data_path, f"{id}_{tp}_brain_t1c.nii.gz") + t1n_file = os.path.join(data_path, f"{id}_{tp}_brain_t1n.nii.gz") + t2f_file = os.path.join(data_path, f"{id}_{tp}_brain_t2f.nii.gz") + t2w_file = os.path.join(data_path, f"{id}_{tp}_brain_t2w.nii.gz") + under_review_file = os.path.join( + labels_path, + "under_review", + seg_file, + ) + if not os.path.exists(under_review_file): + shutil.copyfile(seg_file, under_review_file) + + review_cmd = review_cmd.format( + t1c=t1c_file, + flair=t2f_file, + t2=t2w_file, + t1=t1n_file, + seg=under_review_file, + ) + with open(os.devnull, "w") as fp: + Popen(review_cmd.split(), stdout=fp) + + self.__update_buttons() + self.notify("This subject can be finalized now") + + def __finalize(self): + labels_path = to_local_path(self.subject["labels_path"], self.dset_path) + id, tp = self.subject.name.split("|") + filename = f"{id}_{tp}_{DEFAULT_SEGMENTATION}" + under_review_filepath = os.path.join( + labels_path, + "under_review", + filename, + ) + finalized_filepath = os.path.join(labels_path, "finalized", filename) + shutil.copyfile(under_review_filepath, finalized_filepath) + self.notify("Subject finalized") + + def on_button_pressed(self, event: Button.Pressed) -> None: + review_button = self.query_one("#review-button", Button) + reviewed_button = self.query_one("#reviewed-button", Button) + + if event.control == review_button: + self.__review() + elif event.control == reviewed_button: + self.__finalize() class Subjectbrowser(App): From 8cbb3e742d960a06394d8563f39e55348f6361e2 Mon Sep 17 00:00:00 2001 From: Alejandro Aristizabal Date: Wed, 8 Nov 2023 17:43:27 -0500 Subject: [PATCH 2/5] Pass label instead of segmentation file --- scripts/monitor-dset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/monitor-dset.py b/scripts/monitor-dset.py index b71a355f1..2fd8bf2c4 100644 --- a/scripts/monitor-dset.py +++ b/scripts/monitor-dset.py @@ -450,7 +450,7 @@ def __can_finalize(self): return os.path.exists(under_review_filepath) def __review(self): - review_cmd = "itksnap -g {t1c} -o {flair} {t2} {t1} -s {seg} -l {seg}" + review_cmd = "itksnap -g {t1c} -o {flair} {t2} {t1} -s {seg} -l {label}" data_path = to_local_path(self.subject["data_path"], self.dset_path) labels_path = to_local_path(self.subject["labels_path"], self.dset_path) id, tp = self.subject.name.split("|") From b5e8760b3df75d25431df032a7e1032df1999322 Mon Sep 17 00:00:00 2001 From: Alejandro Aristizabal Date: Thu, 9 Nov 2023 10:30:35 -0500 Subject: [PATCH 3/5] Add labels file --- scripts/assets/postop_gbm.label | 21 +++++++++++++++++++++ scripts/monitor-dset.py | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 scripts/assets/postop_gbm.label diff --git a/scripts/assets/postop_gbm.label b/scripts/assets/postop_gbm.label new file mode 100644 index 000000000..a1bfdc7d0 --- /dev/null +++ b/scripts/assets/postop_gbm.label @@ -0,0 +1,21 @@ +################################################ +# ITK-SnAP Label Description File +# File format: +# IDX -R- -G- -B- -A-- VIS MSH LABEL +# Fields: +# IDX: Zero-based index +# -R-: Red color component (0..255) +# -G-: Green color component (0..255) +# -B-: Blue color component (0..255) +# -A-: Label transparency (0.00 .. 1.00) +# VIS: Label visibility (0 or 1) +# IDX: Label mesh visibility (0 or 1) +# LABEL: Label description +################################################ + 0 0 0 0 0 0 0 "Background" + 1 255 0 0 1 1 1 "Necrotic Tumor Core" + 2 0 255 0 1 1 1 "Tumor Infiltration & Edema" + 3 0 0 255 1 1 1 "Enhancing Tumor Core" + 4 255 255 0 1 1 1 "Resection Cavity" + 5 0 255 255 1 1 1 "Label 5" + 6 255 0 255 1 1 1 "Label 6" diff --git a/scripts/monitor-dset.py b/scripts/monitor-dset.py index 2fd8bf2c4..ad5ad18ab 100644 --- a/scripts/monitor-dset.py +++ b/scripts/monitor-dset.py @@ -459,6 +459,7 @@ def __review(self): t1n_file = os.path.join(data_path, f"{id}_{tp}_brain_t1n.nii.gz") t2f_file = os.path.join(data_path, f"{id}_{tp}_brain_t2f.nii.gz") t2w_file = os.path.join(data_path, f"{id}_{tp}_brain_t2w.nii.gz") + label_file = os.path.join(os.path.dirname(__file__), "assets/postop_gbm.label") under_review_file = os.path.join( labels_path, "under_review", @@ -473,6 +474,7 @@ def __review(self): t2=t2w_file, t1=t1n_file, seg=under_review_file, + label=label_file, ) with open(os.devnull, "w") as fp: Popen(review_cmd.split(), stdout=fp) From 80c33a4baf7b694507f0a38fb680afc62743c65c Mon Sep 17 00:00:00 2001 From: Alejandro Aristizabal Date: Thu, 9 Nov 2023 10:35:45 -0500 Subject: [PATCH 4/5] Stop itk-snap from printint to console --- scripts/monitor-dset.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/monitor-dset.py b/scripts/monitor-dset.py index ad5ad18ab..c2dc8d8db 100644 --- a/scripts/monitor-dset.py +++ b/scripts/monitor-dset.py @@ -21,7 +21,7 @@ import yaml import pandas as pd import tarfile -from subprocess import Popen +from subprocess import Popen, DEVNULL from textual.app import App, ComposeResult from textual.binding import Binding @@ -476,8 +476,7 @@ def __review(self): seg=under_review_file, label=label_file, ) - with open(os.devnull, "w") as fp: - Popen(review_cmd.split(), stdout=fp) + Popen(review_cmd.split(), shell=False, stdout=DEVNULL, stderr=DEVNULL) self.__update_buttons() self.notify("This subject can be finalized now") From a9db77efd51810902d3b87d50da9f8e68163e9c7 Mon Sep 17 00:00:00 2001 From: Alejandro Aristizabal Date: Thu, 9 Nov 2023 10:35:50 -0500 Subject: [PATCH 5/5] Add styles to buttons --- scripts/assets/monitor-dset.tcss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/assets/monitor-dset.tcss b/scripts/assets/monitor-dset.tcss index 36ed73962..6364c07a5 100644 --- a/scripts/assets/monitor-dset.tcss +++ b/scripts/assets/monitor-dset.tcss @@ -38,6 +38,16 @@ SubjectDetails { padding: 1; } +SubjectDetails #review-buttons { + margin: 0 5; +} + +SubjectDetails Button { + text-align: center; + width: 100%; + margin: 1; +} + #subject-status { color: $accent-lighten-3; }