Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3322 Optimize performance of the astype usage in arrays #3338

Merged
merged 16 commits into from
Nov 18, 2021
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
6 changes: 3 additions & 3 deletions monai/apps/deepedit/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ def __call__(self, data):
factor = np.divide(current_shape, d["image_meta_dict"]["dim"][1:4])
pos_clicks, neg_clicks = d["foreground"], d["background"]

pos = np.multiply(pos_clicks, factor).astype(int).tolist() if len(pos_clicks) else []
neg = np.multiply(neg_clicks, factor).astype(int).tolist() if len(neg_clicks) else []
pos = np.multiply(pos_clicks, factor).astype(int, copy=False).tolist() if len(pos_clicks) else []
wyli marked this conversation as resolved.
Show resolved Hide resolved
neg = np.multiply(neg_clicks, factor).astype(int, copy=False).tolist() if len(neg_clicks) else []

d[self.guidance] = [pos, neg]
return d
Expand Down Expand Up @@ -158,7 +158,7 @@ def _apply(self, guidance, discrepancy):
guidance[0].append([-1] * len(neg))
guidance[1].append(neg)

return json.dumps(np.asarray(guidance).astype(int).tolist())
return json.dumps(np.asarray(guidance, dtype=int).tolist())

def __call__(self, data):
d = dict(data)
Expand Down
22 changes: 11 additions & 11 deletions monai/apps/deepgrow/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def _apply(self, label, sid):
def __call__(self, data):
d = dict(data)
self.randomize(data)
d[self.guidance] = json.dumps(self._apply(d[self.label], self.sid).astype(int).tolist())
d[self.guidance] = json.dumps(self._apply(d[self.label], self.sid).astype(int, copy=False).tolist())
return d


Expand Down Expand Up @@ -323,7 +323,7 @@ def _apply(self, guidance, discrepancy):
guidance[0].append([-1] * len(neg))
guidance[1].append(neg)

return json.dumps(np.asarray(guidance).astype(int).tolist())
return json.dumps(np.asarray(guidance, dtype=int).tolist())

def __call__(self, data):
d = dict(data)
Expand Down Expand Up @@ -420,8 +420,8 @@ def __call__(self, data):
d[self.source_key], self.select_fn, self.channel_indices, self.margin
)

center = list(np.mean([box_start, box_end], axis=0).astype(int))
current_size = list(np.subtract(box_end, box_start).astype(int))
center = list(np.mean([box_start, box_end], axis=0).astype(int, copy=False))
current_size = list(np.subtract(box_end, box_start).astype(int, copy=False))

if np.all(np.less(current_size, self.spatial_size)):
cropper = SpatialCrop(roi_center=center, roi_size=self.spatial_size)
Expand Down Expand Up @@ -528,9 +528,9 @@ def _apply(self, pos_clicks, neg_clicks, factor, slice_num):
guidance = [pos, neg, slice_idx]
else:
if len(pos_clicks):
pos = np.multiply(pos_clicks, factor).astype(int).tolist()
pos = np.multiply(pos_clicks, factor).astype(int, copy=False).tolist()
if len(neg_clicks):
neg = np.multiply(neg_clicks, factor).astype(int).tolist()
neg = np.multiply(neg_clicks, factor).astype(int, copy=False).tolist()
guidance = [pos, neg]
return guidance

Expand All @@ -555,7 +555,7 @@ def __call__(self, data):
fg_bg_clicks = []
for key in [self.foreground, self.background]:
clicks = d[key]
clicks = list(np.array(clicks).astype(int))
clicks = list(np.array(clicks, dtype=int))
if self.depth_first:
for i in range(len(clicks)):
clicks[i] = list(np.roll(clicks[i], 1))
Expand Down Expand Up @@ -651,10 +651,10 @@ def __call__(self, data):
guidance = d[self.guidance]
original_spatial_shape = d[first_key].shape[1:] # type: ignore
box_start, box_end = self.bounding_box(np.array(guidance[0] + guidance[1]), original_spatial_shape)
center = list(np.mean([box_start, box_end], axis=0).astype(int))
center = list(np.mean([box_start, box_end], axis=0).astype(int, copy=False))
spatial_size = self.spatial_size

box_size = list(np.subtract(box_end, box_start).astype(int))
box_size = list(np.subtract(box_end, box_start).astype(int, copy=False))
spatial_size = spatial_size[-len(box_size) :]

if len(spatial_size) < len(box_size):
Expand Down Expand Up @@ -738,8 +738,8 @@ def __call__(self, data):
factor = np.divide(current_shape, cropped_shape)

pos_clicks, neg_clicks = guidance[0], guidance[1]
pos = np.multiply(pos_clicks, factor).astype(int).tolist() if len(pos_clicks) else []
neg = np.multiply(neg_clicks, factor).astype(int).tolist() if len(neg_clicks) else []
pos = np.multiply(pos_clicks, factor).astype(int, copy=False).tolist() if len(pos_clicks) else []
neg = np.multiply(neg_clicks, factor).astype(int, copy=False).tolist() if len(neg_clicks) else []

d[self.guidance] = [pos, neg]
return d
Expand Down
6 changes: 3 additions & 3 deletions monai/apps/pathology/transforms/stain/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _deconvolution_extract_stain(self, image: np.ndarray) -> np.ndarray:

# reshape image and calculate absorbance
image = image.reshape((-1, 3))
image = image.astype(np.float32) + 1.0
image = image.astype(np.float32, copy=False) + 1.0
absorbance = -np.log(image.clip(max=self.tli) / self.tli)

# remove transparent pixels
Expand All @@ -76,7 +76,7 @@ def _deconvolution_extract_stain(self, image: np.ndarray) -> np.ndarray:
raise ValueError("All pixels of the input image are below the absorbance threshold.")

# compute eigenvectors
_, eigvecs = np.linalg.eigh(np.cov(absorbance_hat.T).astype(np.float32))
_, eigvecs = np.linalg.eigh(np.cov(absorbance_hat.T).astype(np.float32, copy=False))

# project on the plane spanned by the eigenvectors corresponding to the two largest eigenvalues
t_hat = absorbance_hat.dot(eigvecs[:, 1:3])
Expand Down Expand Up @@ -186,7 +186,7 @@ def __call__(self, image: np.ndarray) -> np.ndarray:
conc = np.linalg.lstsq(he, y, rcond=None)[0]

# normalize stain concentrations
max_conc = np.array([np.percentile(conc[0, :], 99), np.percentile(conc[1, :], 99)], dtype=np.float32)
max_conc = np.asarray([np.percentile(conc[0, :], 99), np.percentile(conc[1, :], 99)], dtype=np.float32)
tmp = np.divide(max_conc, self.max_cref, dtype=np.float32)
image_c = np.divide(conc, tmp[:, np.newaxis], dtype=np.float32)

Expand Down
14 changes: 7 additions & 7 deletions monai/data/nifti_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def write_nifti(

if np.allclose(affine, target_affine, atol=1e-3):
# no affine changes, save (data, affine)
results_img = nib.Nifti1Image(data.astype(output_dtype), to_affine_nd(3, target_affine))
results_img = nib.Nifti1Image(data.astype(output_dtype, copy=False), to_affine_nd(3, target_affine))
nib.save(results_img, file_name)
return

Expand All @@ -128,7 +128,7 @@ def write_nifti(
data = nib.orientations.apply_orientation(data, ornt_transform)
_affine = affine @ nib.orientations.inv_ornt_aff(ornt_transform, data_shape)
if np.allclose(_affine, target_affine, atol=1e-3) or not resample:
results_img = nib.Nifti1Image(data.astype(output_dtype), to_affine_nd(3, _affine)) # type: ignore
results_img = nib.Nifti1Image(data.astype(output_dtype, copy=False), to_affine_nd(3, _affine)) # type: ignore
nib.save(results_img, file_name)
return

Expand All @@ -147,8 +147,8 @@ def write_nifti(
data_np: np.ndarray = data.reshape(list(spatial_shape) + [-1]) # type: ignore
data_np = np.moveaxis(data_np, -1, 0) # channel first for pytorch
data_torch = affine_xform(
torch.as_tensor(np.ascontiguousarray(data_np).astype(dtype)).unsqueeze(0),
torch.as_tensor(np.ascontiguousarray(transform).astype(dtype)),
torch.as_tensor(np.ascontiguousarray(data_np, dtype=dtype)).unsqueeze(0),
torch.as_tensor(np.ascontiguousarray(transform, dtype=dtype)),
spatial_size=output_spatial_shape_[:3],
)
data_np = data_torch.squeeze(0).detach().cpu().numpy()
Expand All @@ -158,12 +158,12 @@ def write_nifti(
while len(output_spatial_shape_) < len(data.shape):
output_spatial_shape_ = output_spatial_shape_ + [1]
data_torch = affine_xform(
torch.as_tensor(np.ascontiguousarray(data).astype(dtype)[None, None]),
torch.as_tensor(np.ascontiguousarray(transform).astype(dtype)),
torch.as_tensor(np.ascontiguousarray(data, dtype=dtype)[None, None]),
torch.as_tensor(np.ascontiguousarray(transform, dtype=dtype)),
spatial_size=output_spatial_shape_[: len(data.shape)],
)
data_np = data_torch.squeeze(0).squeeze(0).detach().cpu().numpy()

results_img = nib.Nifti1Image(data_np.astype(output_dtype), to_affine_nd(3, target_affine))
results_img = nib.Nifti1Image(data_np.astype(output_dtype, copy=False), to_affine_nd(3, target_affine))
nib.save(results_img, file_name)
return
6 changes: 3 additions & 3 deletions monai/data/png_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ def write_png(
if scale is not None:
data = np.clip(data, 0.0, 1.0) # type: ignore # png writer only can scale data in range [0, 1]
if scale == np.iinfo(np.uint8).max:
data = (scale * data).astype(np.uint8)
data = (scale * data).astype(np.uint8, copy=False)
elif scale == np.iinfo(np.uint16).max:
data = (scale * data).astype(np.uint16)
data = (scale * data).astype(np.uint16, copy=False)
else:
raise ValueError(f"Unsupported scale: {scale}, available options are [255, 65535]")

# PNG data must be int number
if data.dtype not in (np.uint8, np.uint16): # type: ignore
data = data.astype(np.uint8)
data = data.astype(np.uint8, copy=False)

data = np.moveaxis(data, 0, 1)
img = Image.fromarray(data)
Expand Down
4 changes: 2 additions & 2 deletions monai/data/synthetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def create_test_image_2d(
else:
image[circle] = rs.random() * 0.5 + 0.5

labels = np.ceil(image).astype(np.int32)
labels = np.ceil(image).astype(np.int32, copy=False)

norm = rs.uniform(0, num_seg_classes * noise_max, size=image.shape)
noisyimage: np.ndarray = rescale_array(np.maximum(image, norm)) # type: ignore
Expand Down Expand Up @@ -148,7 +148,7 @@ def create_test_image_3d(
else:
image[circle] = rs.random() * 0.5 + 0.5

labels = np.ceil(image).astype(np.int32)
labels = np.ceil(image).astype(np.int32, copy=False)
wyli marked this conversation as resolved.
Show resolved Hide resolved

norm = rs.uniform(0, num_seg_classes * noise_max, size=image.shape)
noisyimage: np.ndarray = rescale_array(np.maximum(image, norm)) # type: ignore
Expand Down
4 changes: 3 additions & 1 deletion monai/data/test_time_augmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from monai.transforms.utils import allow_missing_keys_mode, convert_inverse_interp_mode
from monai.utils.enums import CommonKeys, TraceKeys
from monai.utils.module import optional_import
from monai.utils.type_conversion import convert_data_type

if TYPE_CHECKING:
from tqdm import tqdm
Expand Down Expand Up @@ -215,7 +216,8 @@ def __call__(
return output

# calculate metrics
mode = np.array(torch.mode(torch.Tensor(output.astype(np.int64)), dim=0).values)
output_t, *_ = convert_data_type(output, output_type=torch.Tensor, dtype=np.int64)
mode: np.ndarray = np.asarray(torch.mode(output_t, dim=0).values) # type: ignore
mean: np.ndarray = np.mean(output, axis=0) # type: ignore
std: np.ndarray = np.std(output, axis=0) # type: ignore
vvc: float = (np.std(output) / np.mean(output)).item()
Expand Down
4 changes: 2 additions & 2 deletions monai/data/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ def compute_shape_offset(
# different orientation, the min is the origin
corners = corners[:-1] / corners[-1]
offset = np.min(corners, 1)
return out_shape.astype(int), offset
return out_shape.astype(int, copy=False), offset


def to_affine_nd(r: Union[np.ndarray, int], affine: np.ndarray) -> np.ndarray:
Expand Down Expand Up @@ -1143,7 +1143,7 @@ def convert_tables_to_dicts(
# convert data types
types = {k: v["type"] for k, v in col_types.items() if v is not None and "type" in v}
if types:
data_ = data_.astype(dtype=types)
data_ = data_.astype(dtype=types, copy=False)
data: List[Dict] = data_.to_dict(orient="records")

# group columns to generate new column
Expand Down
26 changes: 18 additions & 8 deletions monai/transforms/intensity/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import torch

from monai.config import DtypeLike
from monai.config.type_definitions import NdarrayOrTensor, NdarrayTensor
from monai.config.type_definitions import NdarrayOrTensor
from monai.data.utils import get_random_patch, get_valid_patch_size
from monai.networks.layers import GaussianFilter, HilbertTransform, SavitzkyGolayFilter
from monai.transforms.transform import RandomizableTransform, Transform
Expand Down Expand Up @@ -86,23 +86,27 @@ class RandGaussianNoise(RandomizableTransform):
prob: Probability to add Gaussian noise.
mean: Mean or “centre” of the distribution.
std: Standard deviation (spread) of distribution.
dtype: output data type, if None, same as input image. defaults to float32.

"""

backend = [TransformBackends.TORCH, TransformBackends.NUMPY]

def __init__(self, prob: float = 0.1, mean: float = 0.0, std: float = 0.1) -> None:
def __init__(self, prob: float = 0.1, mean: float = 0.0, std: float = 0.1, dtype: DtypeLike = np.float32) -> None:
RandomizableTransform.__init__(self, prob)
self.mean = mean
self.std = std
self.dtype = dtype
self.noise: Optional[np.ndarray] = None

def randomize(self, img: NdarrayOrTensor, mean: Optional[float] = None) -> None:
super().randomize(None)
if not self._do_transform:
return None
rand_std = self.R.uniform(0, self.std)
self.noise = self.R.normal(self.mean if mean is None else mean, rand_std, size=img.shape)
noise = self.R.normal(self.mean if mean is None else mean, rand_std, size=img.shape)
# noise is float64 array, convert to the output dtype to save memory
self.noise, *_ = convert_data_type(noise, dtype=self.dtype) # type: ignore

def __call__(self, img: NdarrayOrTensor, mean: Optional[float] = None, randomize: bool = True) -> NdarrayOrTensor:
"""
Expand All @@ -116,6 +120,7 @@ def __call__(self, img: NdarrayOrTensor, mean: Optional[float] = None, randomize

if self.noise is None:
raise RuntimeError("please call the `randomize()` function first.")
img, *_ = convert_data_type(img, dtype=self.dtype)
noise, *_ = convert_to_dst_type(self.noise, img)
return img + noise

Expand All @@ -141,6 +146,8 @@ class RandRicianNoise(RandomizableTransform):
histogram.
sample_std: If True, sample the spread of the Gaussian distributions
uniformly from 0 to std.
dtype: output data type, if None, same as input image. defaults to float32.

"""

backend = [TransformBackends.TORCH, TransformBackends.NUMPY]
Expand All @@ -153,6 +160,7 @@ def __init__(
channel_wise: bool = False,
relative: bool = False,
sample_std: bool = True,
dtype: DtypeLike = np.float32,
) -> None:
RandomizableTransform.__init__(self, prob)
self.prob = prob
Expand All @@ -161,23 +169,24 @@ def __init__(
self.channel_wise = channel_wise
self.relative = relative
self.sample_std = sample_std
self.dtype = dtype
self._noise1: NdarrayOrTensor
self._noise2: NdarrayOrTensor

def _add_noise(self, img: NdarrayTensor, mean: float, std: float):
def _add_noise(self, img: NdarrayOrTensor, mean: float, std: float):
dtype_np = get_equivalent_dtype(img.dtype, np.ndarray)
im_shape = img.shape
_std = self.R.uniform(0, std) if self.sample_std else std
self._noise1 = self.R.normal(mean, _std, size=im_shape).astype(dtype_np)
self._noise2 = self.R.normal(mean, _std, size=im_shape).astype(dtype_np)
self._noise1 = self.R.normal(mean, _std, size=im_shape).astype(dtype_np, copy=False)
self._noise2 = self.R.normal(mean, _std, size=im_shape).astype(dtype_np, copy=False)
if isinstance(img, torch.Tensor):
n1 = torch.tensor(self._noise1, device=img.device)
n2 = torch.tensor(self._noise2, device=img.device)
return torch.sqrt((img + n1) ** 2 + n2 ** 2)

return np.sqrt((img + self._noise1) ** 2 + self._noise2 ** 2)

def __call__(self, img: NdarrayTensor, randomize: bool = True) -> NdarrayTensor:
def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor:
"""
Apply the transform to `img`.
"""
Expand All @@ -187,6 +196,7 @@ def __call__(self, img: NdarrayTensor, randomize: bool = True) -> NdarrayTensor:
if not self._do_transform:
return img

img, *_ = convert_data_type(img, dtype=self.dtype)
if self.channel_wise:
_mean = ensure_tuple_rep(self.mean, len(img))
_std = ensure_tuple_rep(self.std, len(img))
Expand Down Expand Up @@ -1942,7 +1952,7 @@ def _transform_holes(self, img: np.ndarray):
ret = img
else:
if isinstance(fill_value, (tuple, list)):
ret = self.R.uniform(fill_value[0], fill_value[1], size=img.shape).astype(img.dtype)
ret = self.R.uniform(fill_value[0], fill_value[1], size=img.shape).astype(img.dtype, copy=False)
else:
ret = np.full_like(img, fill_value)
for h in self.hole_coords:
Expand Down
Loading