diff --git a/scripts/monitor/rano_monitor/__main__.py b/scripts/monitor/rano_monitor/__main__.py index 1e50d4e4f..37a19e807 100644 --- a/scripts/monitor/rano_monitor/__main__.py +++ b/scripts/monitor/rano_monitor/__main__.py @@ -8,6 +8,8 @@ STAGES_HELP, DSET_LOC_HELP, OUT_HELP, + REVIEW_COMMAND, + REVIEW_CMD_HELP, ) from rano_monitor.dataset_browser import DatasetBrowser from rano_monitor.handlers import InvalidHandler @@ -21,7 +23,7 @@ app = typer.Typer() -def run_dset_app(dset_path, stages_path, output_path): +def run_dset_app(dset_path, stages_path, output_path, review_cmd): report_path = os.path.join(dset_path, "report.yaml") dset_data_path = os.path.join(dset_path, "data") invalid_path = os.path.join(dset_path, "metadata/.invalid.txt") @@ -48,6 +50,7 @@ def run_dset_app(dset_path, stages_path, output_path): invalid_path, invalid_watchdog, prompt_watchdog, + review_cmd, ) observer = Observer() @@ -60,7 +63,7 @@ def run_dset_app(dset_path, stages_path, output_path): observer.stop() -def run_tarball_app(tarball_path): +def run_tarball_app(tarball_path, review_cmd): folder_name = f".{os.path.basename(tarball_path).split('.')[0]}" contents_path = os.path.join(os.path.dirname(tarball_path), folder_name) if not os.path.exists(contents_path): @@ -72,7 +75,7 @@ def run_tarball_app(tarball_path): contents_path = os.path.join(contents_path, "review_cases") reviewed_watchdog = TarballReviewedHandler(contents_path, t_app) - t_app.set_vars(contents_path) + t_app.set_vars(contents_path, review_cmd) observer = Observer() observer.schedule(reviewed_watchdog, path=contents_path, recursive=True) @@ -94,10 +97,13 @@ def main( help=DSET_LOC_HELP, ), output_path: str = Option(None, "-o", "--out", help=OUT_HELP), + itksnap_executable: str = Option( + REVIEW_COMMAND, "-i", "--itksnap", help=REVIEW_CMD_HELP + ), ): if dataset_uid.endswith(".tar.gz"): # TODO: implement tarball_app - run_tarball_app(dataset_uid) + run_tarball_app(dataset_uid, itksnap_executable) return elif dataset_uid.isdigit(): # Only import medperf dependencies if the user intends to use medperf @@ -115,7 +121,7 @@ def main( "Please ensure the passed dataset UID/path is correct" ) - run_dset_app(dset_path, stages_path, output_path) + run_dset_app(dset_path, stages_path, output_path, itksnap_executable) if __name__ == "__main__": diff --git a/scripts/monitor/rano_monitor/constants.py b/scripts/monitor/rano_monitor/constants.py index 7330ce36c..2515cde27 100644 --- a/scripts/monitor/rano_monitor/constants.py +++ b/scripts/monitor/rano_monitor/constants.py @@ -24,6 +24,7 @@ REVIEW_FILENAME = "review_cases.tar.gz" REVIEWED_FILENAME = "reviewed_cases.tar.gz" REVIEW_COMMAND = "itksnap" +REVIEW_CMD_HELP = f"path or name of the command that launches itksnap. Defaults to '{REVIEW_COMMAND}'" MANUAL_REVIEW_STAGE = 5 DONE_STAGE = 8 LISTITEM_MAX_LEN = 30 diff --git a/scripts/monitor/rano_monitor/dataset_browser.py b/scripts/monitor/rano_monitor/dataset_browser.py index 431ce191b..47bd12b54 100644 --- a/scripts/monitor/rano_monitor/dataset_browser.py +++ b/scripts/monitor/rano_monitor/dataset_browser.py @@ -49,6 +49,7 @@ def set_vars( invalid_path, invalid_watchdog, prompt_watchdog, + review_cmd, ): self.dset_data_path = dset_data_path self.stages_path = stages_path @@ -56,6 +57,7 @@ def set_vars( self.invalid_path = invalid_path self.invalid_watchdog = invalid_watchdog self.prompt_watchdog = prompt_watchdog + self.review_cmd = review_cmd def update_invalid(self, invalid_subjects): self.invalid_subjects = invalid_subjects @@ -107,6 +109,7 @@ def on_mount(self): # Set invalid path for subject view subject_details = self.query_one("#details", SubjectDetails) subject_details.set_invalid_path(self.invalid_path) + subject_details.review_cmd = self.review_cmd # Set dataset path to listview listview = self.query_one("#subjects-list", ListView) diff --git a/scripts/monitor/rano_monitor/tarball_browser.py b/scripts/monitor/rano_monitor/tarball_browser.py index 4c81aaa60..ebea23d0f 100644 --- a/scripts/monitor/rano_monitor/tarball_browser.py +++ b/scripts/monitor/rano_monitor/tarball_browser.py @@ -44,8 +44,9 @@ class TarballBrowser(App): subjects = var([]) - def set_vars(self, contents_path): + def set_vars(self, contents_path, review_cmd): self.contents_path = contents_path + self.review_cmd = review_cmd self.subjects_list = self.__get_subjects() def __get_subjects(self): @@ -55,7 +56,9 @@ def __get_subjects(self): for subject in subjects: subject_path = os.path.join(self.contents_path, subject) timepoints = os.listdir(subject_path) - timepoints = [timepoint for timepoint in timepoints if not timepoint.startswith(".")] + timepoints = [ + timepoint for timepoint in timepoints if not timepoint.startswith(".") + ] subject_timepoint_list += [(subject, tp) for tp in timepoints] return subject_timepoint_list @@ -76,6 +79,7 @@ def compose(self) -> ComposeResult: subject_view = TarballSubjectView() subject_view.subject = f"{id}|{tp}" subject_view.contents_path = self.contents_path + subject_view.review_cmd = self.review_cmd yield subject_view def on_mount(self): @@ -84,7 +88,7 @@ def on_mount(self): def update_subjects_status(self): subject_views = self.query(TarballSubjectView) editor_msg = self.query_one("#review-msg", Static) - editor_msg.display = "none" if is_editor_installed() else "block" + editor_msg.display = "none" if is_editor_installed(self.review_cmd) else "block" for subject_view in subject_views: subject_view.update_status() diff --git a/scripts/monitor/rano_monitor/utils.py b/scripts/monitor/rano_monitor/utils.py index c38930824..1c61ea97f 100644 --- a/scripts/monitor/rano_monitor/utils.py +++ b/scripts/monitor/rano_monitor/utils.py @@ -9,7 +9,6 @@ import pandas as pd import yaml from rano_monitor.constants import ( - REVIEW_COMMAND, BRAINMASK_BAK, DEFAULT_SEGMENTATION, BRAINMASK, @@ -22,8 +21,8 @@ ) -def is_editor_installed(): - review_command_path = shutil.which(REVIEW_COMMAND) +def is_editor_installed(review_command): + review_command_path = shutil.which(review_command) return review_command_path is not None @@ -34,10 +33,10 @@ def get_hash(filepath: str): return file_hash -def run_editor(t1c, flair, t2, t1, seg, label, cmd=REVIEW_COMMAND): +def run_editor(t1c, flair, t2, t1, seg, label, cmd): review_cmd = "{cmd} -g {t1c} -o {flair} {t2} {t1} -s {seg} -l {label}" review_cmd = review_cmd.format( - cmd=REVIEW_COMMAND, + cmd=cmd, t1c=t1c, flair=flair, t2=t2, @@ -48,7 +47,9 @@ def run_editor(t1c, flair, t2, t1, seg, label, cmd=REVIEW_COMMAND): Popen(review_cmd.split(), shell=False, stdout=DEVNULL, stderr=DEVNULL) -def review_tumor(subject: str, data_path: str, labels_path: str): +def review_tumor( + subject: str, data_path: str, labels_path: str, review_cmd: str +): ( t1c_file, t1n_file, @@ -65,10 +66,18 @@ def review_tumor(subject: str, data_path: str, labels_path: str): if not is_nifti and not is_under_review: shutil.copyfile(seg_file, under_review_file) - run_editor(t1c_file, t2f_file, t2w_file, t1n_file, under_review_file, label_file) + run_editor( + t1c_file, + t2f_file, + t2w_file, + t1n_file, + under_review_file, + label_file, + cmd=review_cmd, + ) -def review_brain(subject, labels_path, data_path=None): +def review_brain(subject, labels_path, review_cmd, data_path=None): ( t1c_file, t1n_file, @@ -82,7 +91,9 @@ def review_brain(subject, labels_path, data_path=None): if not os.path.exists(backup_path): shutil.copyfile(seg_file, backup_path) - run_editor(t1c_file, t2f_file, t2w_file, t1n_file, seg_file, label_file) + run_editor( + t1c_file, t2f_file, t2w_file, t1n_file, seg_file, label_file, cmd=review_cmd + ) def finalize(subject: str, labels_path: str): diff --git a/scripts/monitor/rano_monitor/widgets/subject_details.py b/scripts/monitor/rano_monitor/widgets/subject_details.py index 1cf0f66c1..eea9b2695 100644 --- a/scripts/monitor/rano_monitor/widgets/subject_details.py +++ b/scripts/monitor/rano_monitor/widgets/subject_details.py @@ -25,6 +25,7 @@ class SubjectDetails(Static): invalid_subjects = set() subject = pd.Series() dset_path = "" + review_cmd = None # This will be assigned after initialized def compose(self) -> ComposeResult: with Center(id="subject-title"): @@ -51,10 +52,7 @@ def compose(self) -> ComposeResult: id="reviewed-button", disabled=True, ) - yield Static( - "If brain mask is not correct", - id="brianmask-review-header" - ) + yield Static("If brain mask is not correct", id="brianmask-review-header") yield Button( "Brain mask not available", disabled=True, @@ -134,7 +132,7 @@ def __update_buttons(self): brainmask_button = self.query_one("#brainmask-review-button", Button) valid_btn = self.query_one("#valid-btn", Button) - if is_editor_installed(): + if is_editor_installed(self.review_cmd): review_msg.display = "none" review_button.disabled = False if self.__can_finalize(): @@ -175,7 +173,7 @@ def __review_tumor(self): data_path = to_local_path(data_path, self.dset_path) labels_path = self.subject["labels_path"] labels_path = to_local_path(labels_path, self.dset_path) - review_tumor(subject, data_path, labels_path) + review_tumor(subject, data_path, labels_path, review_cmd=self.review_cmd) self.__update_buttons() self.notify("This subject can be finalized now") @@ -183,7 +181,7 @@ def __review_brainmask(self): subject = self.subject.name labels_path = self.subject["labels_path"] labels_path = to_local_path(labels_path, self.dset_path) - review_brain(subject, labels_path) + review_brain(subject, labels_path, review_cmd=self.review_cmd) self.__update_buttons() def __finalize(self): diff --git a/scripts/monitor/rano_monitor/widgets/tarball_subject_view.py b/scripts/monitor/rano_monitor/widgets/tarball_subject_view.py index f0773ad33..d61f8cf5f 100644 --- a/scripts/monitor/rano_monitor/widgets/tarball_subject_view.py +++ b/scripts/monitor/rano_monitor/widgets/tarball_subject_view.py @@ -18,6 +18,7 @@ class TarballSubjectView(Static): subject = reactive("") contents_path = reactive("") + review_cmd = None # This will be assigned after initialized def compose(self) -> ComposeResult: with Horizontal(classes="subject-item"): @@ -64,7 +65,7 @@ def __update_buttons(self): tumor_btn = self.query_one(".tumor-btn", Button) finalize_btn = self.query_one(".finalize-btn", Button) brain_btn = self.query_one(".brain-btn", Button) - if is_editor_installed(): + if is_editor_installed(self.review_cmd): tumor_btn.disabled = False if self.__can_finalize(): finalize_btn.disabled = False @@ -87,19 +88,19 @@ def __can_review_brain(self): id, tp = self.subject.split("|") filepath = os.path.join(self.contents_path, id, tp, BRAINMASK) - return os.path.exists(filepath) and is_editor_installed() + return os.path.exists(filepath) and is_editor_installed(self.review_cmd) def __review_tumor(self): id, tp = self.subject.split("|") data_path = os.path.join(self.contents_path, id, tp, "brain_scans") labels_path = os.path.join(self.contents_path, id, tp) - review_tumor(self.subject, data_path, labels_path) + review_tumor(self.subject, data_path, labels_path, review_cmd=self.review_cmd) def __review_brainmask(self): id, tp = self.subject.split("|") data_path = os.path.join(self.contents_path, id, tp, "raw_scans") labels_path = os.path.join(self.contents_path, id, tp) - review_brain(self.subject, labels_path, data_path) + review_brain(self.subject, labels_path, self.review_cmd, data_path) def __finalize(self): id, tp = self.subject.split("|") @@ -119,3 +120,23 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.__finalize() self.__update_buttons() + + def __brain_has_been_reviewed(self): + id, tp = self.subject.split("|") + brainpath = os.path.join(self.contents_path, id, tp, BRAINMASK) + backup_brainpath = os.path.join(self.contents_path, id, tp, BRAINMASK_BAK) + + if not os.path.exists(backup_brainpath): + return False + + brain_hash = get_hash(brainpath) + backup_hash = get_hash(backup_brainpath) + return brain_hash != backup_hash + + def __tumor_has_been_finalized(self): + id, tp = self.subject.split("|") + finalized_tumor_path = os.path.join(self.contents_path, id, tp, "finalized") + finalized_files = os.listdir(finalized_tumor_path) + finalized_files = [file for file in finalized_files if not file.startswith(".")] + + return len(finalized_files) > 0