Skip to content
Merged
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
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
Loading