Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ ignore = [
"BLE001", # blind `except Exception`
"E501", # line too long (the formatter handles what it can)
"E741", # ambiguous variable name (`l` for label is idiomatic here)
"F811", # redefinition (triggered by the __main__ demo blocks)
"FURB171", # membership test against a single-item container
"N801", # class name not CapWords (transform names mirror nnU-Net's)
"N802", # function name not lowercase
Expand Down Expand Up @@ -220,6 +219,12 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# the drop-in contract with nnUNetTrainer.
"smauglab/trainers/**" = ["B008"]

[tool.ruff.lint.isort]
# scripts/_common.py is imported by the standalone scripts as a bare `_common`
# (running `python scripts/foo.py` puts scripts/ on sys.path). Without this, isort
# files it under third-party and sorts it in among monai/torch/wandb.
known-first-party = ["_common"]

[tool.ruff.lint.mccabe]
max-complexity = 20

Expand Down
108 changes: 55 additions & 53 deletions smauglab/utils/utils.py → scripts/_common.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import argparse
"""Helpers shared by the standalone scripts in this directory.

These used to live in `smauglab/utils/utils.py` and so shipped in the wheel, but
nothing under `smauglab/` ever imported them once the `__main__` demo blocks were
removed -- they are MONAI-training and data-loading support for `train_monai.py` and
`generate_augmentations.py`, not part of the augmentation library. `config2parser`
and `sig_fn` came along too and had no callers at all; they are gone.

`smauglab/utils/image.py` deliberately did NOT move: five modules in the sibling
segtransferaug repository import `smauglab.utils.image.Image`, so it is a real part
of the public API despite having no in-package consumer.

Scripts here are run as `python scripts/<name>.py`, which puts this directory on
sys.path, so `from _common import ...` resolves.
"""

from __future__ import annotations

import json
import os

import numpy as np
from progress.bar import Bar


def fetch_image_config(config_data, split="TRAINING"):
"""
def fetch_image_config(config_data: dict, split: str = "TRAINING") -> tuple[list[dict], list]:
"""Resolve a data config's image/label pairs for one split.

:param config_data: Config dict where every label used for TRAINING, VALIDATION and/or TESTING has its path specified
:param split: Split of the data needed in the config file ('TRAINING', 'VALIDATION', 'TESTING').
:return: out_list: list of dictionary with image and label paths (like monai load_decathlon_datalist)
Expand All @@ -28,35 +46,26 @@ def fetch_image_config(config_data, split="TRAINING"):

err = []
out_list = []
for di in dict_list:
for i, di in enumerate(dict_list):
input_img_path = os.path.join(config_data["DATASETS_PATH"], di["IMAGE"])
input_seg_path = os.path.join(config_data["DATASETS_PATH"], di["LABEL"])
if not os.path.exists(input_img_path):
err.append([input_img_path, "path error"])
else:
out_list.append({"image": os.path.abspath(input_img_path), "segmentation": os.path.abspath(input_seg_path)})

# Plot progress
bar.suffix = f"{dict_list.index(di) + 1}/{len(dict_list)}"
# Plot progress. Indexing by enumerate, not dict_list.index(di): the latter is
# a linear scan per item (quadratic overall) and reports the wrong number
# whenever two entries are equal.
bar.suffix = f"{i + 1}/{len(dict_list)}"
bar.next()
bar.finish()
return out_list, err


def config2parser(config_path):
"""
Create a parser object from a json file
"""
# Read json file and create a dictionary
with open(config_path) as file:
config_dict = json.load(file)

return argparse.Namespace(**config_dict)
def parser2config(args, path_out: str) -> None:
"""Extract the parameters from an input parser to create a config json file.


def parser2config(args, path_out):
"""
Extract the parameters from an input parser to create a config json file
:param args: parser arguments
:param path_out: path out of the config file
"""
Expand All @@ -78,31 +87,25 @@ def parser2config(args, path_out):
outfile.write(json_object)


def tuple_type_int(strings):
"""
Copied from https://stackoverflow.com/questions/33564246/passing-a-tuple-as-command-line-argument
"""
def tuple_type_int(strings: str) -> tuple[int, ...]:
"""Copied from https://stackoverflow.com/questions/33564246/passing-a-tuple-as-command-line-argument"""
strings = strings.replace("(", "").replace(")", "")
mapped_int = map(int, strings.split(","))
return tuple(mapped_int)
return tuple(map(int, strings.split(",")))


def tuple_type_float(strings):
"""
Copied from https://stackoverflow.com/questions/33564246/passing-a-tuple-as-command-line-argument
"""
def tuple_type_float(strings: str) -> tuple[float, ...]:
"""Copied from https://stackoverflow.com/questions/33564246/passing-a-tuple-as-command-line-argument"""
strings = strings.replace("(", "").replace(")", "")
mapped_float = map(float, strings.split(","))
return tuple(mapped_float)
return tuple(map(float, strings.split(",")))


def tuple2string(t):
def tuple2string(t) -> str:
return str(t).replace(" ", "").replace("(", "").replace(")", "").replace(",", "-")


def adjust_learning_rate(optimizer, lr, gamma):
"""
Sets the learning rate to the initial LR decayed by schedule
def adjust_learning_rate(optimizer, lr: float, gamma: float) -> float:
"""Set the learning rate to the initial LR decayed by schedule.

Copied from https://github.com/spinalcordtoolbox/disc-labeling-hourglass
"""
lr *= gamma
Expand All @@ -111,35 +114,32 @@ def adjust_learning_rate(optimizer, lr, gamma):
return lr


def compute_dsc(gt_mask, pred_mask, sigmoid=False):
"""
def compute_dsc(gt_mask, pred_mask, sigmoid: bool = False):
"""Dice similarity coefficient.

:param gt_mask: Ground truth mask used as the reference
:param pred_mask: Prediction mask
:param sigmoid: Apply sigmoid on prediction if True (default=False)

:return: dsc=2*intersection/(number of non zero pixels)
"""
if sigmoid:
pred_mask = sig_fn(pred_mask)
pred_mask = 1 / (1 + np.exp(-pred_mask))
numerator = 2 * (gt_mask * pred_mask).sum()
denominator = gt_mask.sum() + pred_mask.sum()
if denominator == 0:
# Both ground truth and prediction are empty
return 0
else:
return numerator / denominator

return numerator / denominator

def sig_fn(z):
return 1 / (1 + np.exp(-z))


def get_validation_image(in_img, target_img, pred_img, sigmoid=False):
def get_validation_image(in_img, target_img, pred_img, sigmoid: bool = False):
"""Stack input / target / prediction mid-slices into one image for logging."""
in_img = in_img.data.cpu().numpy()
target_img = target_img.data.cpu().numpy()
pred_img = pred_img.data.cpu().numpy()
if sigmoid:
pred_img = sig_fn(pred_img)
pred_img = 1 / (1 + np.exp(-pred_img))
in_all = []
target_all = []
pred_all = []
Expand All @@ -156,9 +156,9 @@ def get_validation_image(in_img, target_img, pred_img, sigmoid=False):
y_pred = y_pred[shape[0] // 2, :, :]

# Normalize intensity
x = normalize(x) * 255
y = normalize(y) * 255
y_pred = normalize(y_pred) * 255
x = normalize_percentile(x) * 255
y = normalize_percentile(y) * 255
y_pred = normalize_percentile(y_pred) * 255

# Regroup batch
in_all.append(x)
Expand All @@ -176,11 +176,13 @@ def get_validation_image(in_img, target_img, pred_img, sigmoid=False):
return img_result, target_line_arr, pred_line_arr


def normalize(arr):
"""
Normalize image using percentiles
def normalize_percentile(arr: np.ndarray) -> np.ndarray:
"""Rescale using the 10th/90th percentiles.

Renamed from `normalize`: three functions in this repository shared that name and
two of them computed something else (min-max, in the GPU demo blocks). The name now
says which one this is. See `normalize_minmax` in demo_augmentations.py.
"""
# Use 10th percentile
p10 = np.percentile(arr, 10)
p90 = np.percentile(arr, 90)
return (arr - p10) / (p90 - p10 + 0.00001)
Loading