pytorch-image-models
534 строки · 18.4 Кб
1import math
2import numbers
3import random
4import warnings
5from typing import List, Sequence, Tuple, Union
6
7import torch
8import torchvision.transforms.functional as F
9try:
10from torchvision.transforms.functional import InterpolationMode
11has_interpolation_mode = True
12except ImportError:
13has_interpolation_mode = False
14from PIL import Image
15import numpy as np
16
17__all__ = [
18"ToNumpy", "ToTensor", "str_to_interp_mode", "str_to_pil_interp", "interp_mode_to_str",
19"RandomResizedCropAndInterpolation", "CenterCropOrPad", "center_crop_or_pad", "crop_or_pad",
20"RandomCropOrPad", "RandomPad", "ResizeKeepRatio", "TrimBorder"
21]
22
23
24class ToNumpy:
25
26def __call__(self, pil_img):
27np_img = np.array(pil_img, dtype=np.uint8)
28if np_img.ndim < 3:
29np_img = np.expand_dims(np_img, axis=-1)
30np_img = np.rollaxis(np_img, 2) # HWC to CHW
31return np_img
32
33
34class ToTensor:
35""" ToTensor with no rescaling of values"""
36def __init__(self, dtype=torch.float32):
37self.dtype = dtype
38
39def __call__(self, pil_img):
40return F.pil_to_tensor(pil_img).to(dtype=self.dtype)
41
42
43# Pillow is deprecating the top-level resampling attributes (e.g., Image.BILINEAR) in
44# favor of the Image.Resampling enum. The top-level resampling attributes will be
45# removed in Pillow 10.
46if hasattr(Image, "Resampling"):
47_pil_interpolation_to_str = {
48Image.Resampling.NEAREST: 'nearest',
49Image.Resampling.BILINEAR: 'bilinear',
50Image.Resampling.BICUBIC: 'bicubic',
51Image.Resampling.BOX: 'box',
52Image.Resampling.HAMMING: 'hamming',
53Image.Resampling.LANCZOS: 'lanczos',
54}
55else:
56_pil_interpolation_to_str = {
57Image.NEAREST: 'nearest',
58Image.BILINEAR: 'bilinear',
59Image.BICUBIC: 'bicubic',
60Image.BOX: 'box',
61Image.HAMMING: 'hamming',
62Image.LANCZOS: 'lanczos',
63}
64
65_str_to_pil_interpolation = {b: a for a, b in _pil_interpolation_to_str.items()}
66
67
68if has_interpolation_mode:
69_torch_interpolation_to_str = {
70InterpolationMode.NEAREST: 'nearest',
71InterpolationMode.BILINEAR: 'bilinear',
72InterpolationMode.BICUBIC: 'bicubic',
73InterpolationMode.BOX: 'box',
74InterpolationMode.HAMMING: 'hamming',
75InterpolationMode.LANCZOS: 'lanczos',
76}
77_str_to_torch_interpolation = {b: a for a, b in _torch_interpolation_to_str.items()}
78else:
79_pil_interpolation_to_torch = {}
80_torch_interpolation_to_str = {}
81
82
83def str_to_pil_interp(mode_str):
84return _str_to_pil_interpolation[mode_str]
85
86
87def str_to_interp_mode(mode_str):
88if has_interpolation_mode:
89return _str_to_torch_interpolation[mode_str]
90else:
91return _str_to_pil_interpolation[mode_str]
92
93
94def interp_mode_to_str(mode):
95if has_interpolation_mode:
96return _torch_interpolation_to_str[mode]
97else:
98return _pil_interpolation_to_str[mode]
99
100
101_RANDOM_INTERPOLATION = (str_to_interp_mode('bilinear'), str_to_interp_mode('bicubic'))
102
103
104def _setup_size(size, error_msg="Please provide only two dimensions (h, w) for size."):
105if isinstance(size, numbers.Number):
106return int(size), int(size)
107
108if isinstance(size, Sequence) and len(size) == 1:
109return size[0], size[0]
110
111if len(size) != 2:
112raise ValueError(error_msg)
113
114return size
115
116
117class RandomResizedCropAndInterpolation:
118"""Crop the given PIL Image to random size and aspect ratio with random interpolation.
119
120A crop of random size (default: of 0.08 to 1.0) of the original size and a random
121aspect ratio (default: of 3/4 to 4/3) of the original aspect ratio is made. This crop
122is finally resized to given size.
123This is popularly used to train the Inception networks.
124
125Args:
126size: expected output size of each edge
127scale: range of size of the origin size cropped
128ratio: range of aspect ratio of the origin aspect ratio cropped
129interpolation: Default: PIL.Image.BILINEAR
130"""
131
132def __init__(
133self,
134size,
135scale=(0.08, 1.0),
136ratio=(3. / 4., 4. / 3.),
137interpolation='bilinear',
138):
139if isinstance(size, (list, tuple)):
140self.size = tuple(size)
141else:
142self.size = (size, size)
143if (scale[0] > scale[1]) or (ratio[0] > ratio[1]):
144warnings.warn("range should be of kind (min, max)")
145
146if interpolation == 'random':
147self.interpolation = _RANDOM_INTERPOLATION
148else:
149self.interpolation = str_to_interp_mode(interpolation)
150self.scale = scale
151self.ratio = ratio
152
153@staticmethod
154def get_params(img, scale, ratio):
155"""Get parameters for ``crop`` for a random sized crop.
156
157Args:
158img (PIL Image): Image to be cropped.
159scale (tuple): range of size of the origin size cropped
160ratio (tuple): range of aspect ratio of the origin aspect ratio cropped
161
162Returns:
163tuple: params (i, j, h, w) to be passed to ``crop`` for a random
164sized crop.
165"""
166img_w, img_h = F.get_image_size(img)
167area = img_w * img_h
168
169for attempt in range(10):
170target_area = random.uniform(*scale) * area
171log_ratio = (math.log(ratio[0]), math.log(ratio[1]))
172aspect_ratio = math.exp(random.uniform(*log_ratio))
173
174target_w = int(round(math.sqrt(target_area * aspect_ratio)))
175target_h = int(round(math.sqrt(target_area / aspect_ratio)))
176if target_w <= img_w and target_h <= img_h:
177i = random.randint(0, img_h - target_h)
178j = random.randint(0, img_w - target_w)
179return i, j, target_h, target_w
180
181# Fallback to central crop
182in_ratio = img_w / img_h
183if in_ratio < min(ratio):
184target_w = img_w
185target_h = int(round(target_w / min(ratio)))
186elif in_ratio > max(ratio):
187target_h = img_h
188target_w = int(round(target_h * max(ratio)))
189else: # whole image
190target_w = img_w
191target_h = img_h
192i = (img_h - target_h) // 2
193j = (img_w - target_w) // 2
194return i, j, target_h, target_w
195
196def __call__(self, img):
197"""
198Args:
199img (PIL Image): Image to be cropped and resized.
200
201Returns:
202PIL Image: Randomly cropped and resized image.
203"""
204i, j, h, w = self.get_params(img, self.scale, self.ratio)
205if isinstance(self.interpolation, (tuple, list)):
206interpolation = random.choice(self.interpolation)
207else:
208interpolation = self.interpolation
209return F.resized_crop(img, i, j, h, w, self.size, interpolation)
210
211def __repr__(self):
212if isinstance(self.interpolation, (tuple, list)):
213interpolate_str = ' '.join([interp_mode_to_str(x) for x in self.interpolation])
214else:
215interpolate_str = interp_mode_to_str(self.interpolation)
216format_string = self.__class__.__name__ + '(size={0}'.format(self.size)
217format_string += ', scale={0}'.format(tuple(round(s, 4) for s in self.scale))
218format_string += ', ratio={0}'.format(tuple(round(r, 4) for r in self.ratio))
219format_string += ', interpolation={0})'.format(interpolate_str)
220return format_string
221
222
223def center_crop_or_pad(
224img: torch.Tensor,
225output_size: Union[int, List[int]],
226fill: Union[int, Tuple[int, int, int]] = 0,
227padding_mode: str = 'constant',
228) -> torch.Tensor:
229"""Center crops and/or pads the given image.
230
231If the image is torch Tensor, it is expected
232to have [..., H, W] shape, where ... means an arbitrary number of leading dimensions.
233If image size is smaller than output size along any edge, image is padded with 0 and then center cropped.
234
235Args:
236img (PIL Image or Tensor): Image to be cropped.
237output_size (sequence or int): (height, width) of the crop box. If int or sequence with single int,
238it is used for both directions.
239fill (int, Tuple[int]): Padding color
240
241Returns:
242PIL Image or Tensor: Cropped image.
243"""
244output_size = _setup_size(output_size)
245crop_height, crop_width = output_size
246_, image_height, image_width = F.get_dimensions(img)
247
248if crop_width > image_width or crop_height > image_height:
249padding_ltrb = [
250(crop_width - image_width) // 2 if crop_width > image_width else 0,
251(crop_height - image_height) // 2 if crop_height > image_height else 0,
252(crop_width - image_width + 1) // 2 if crop_width > image_width else 0,
253(crop_height - image_height + 1) // 2 if crop_height > image_height else 0,
254]
255img = F.pad(img, padding_ltrb, fill=fill, padding_mode=padding_mode)
256_, image_height, image_width = F.get_dimensions(img)
257if crop_width == image_width and crop_height == image_height:
258return img
259
260crop_top = int(round((image_height - crop_height) / 2.0))
261crop_left = int(round((image_width - crop_width) / 2.0))
262return F.crop(img, crop_top, crop_left, crop_height, crop_width)
263
264
265class CenterCropOrPad(torch.nn.Module):
266"""Crops the given image at the center.
267If the image is torch Tensor, it is expected
268to have [..., H, W] shape, where ... means an arbitrary number of leading dimensions.
269If image size is smaller than output size along any edge, image is padded with 0 and then center cropped.
270
271Args:
272size (sequence or int): Desired output size of the crop. If size is an
273int instead of sequence like (h, w), a square crop (size, size) is
274made. If provided a sequence of length 1, it will be interpreted as (size[0], size[0]).
275"""
276
277def __init__(
278self,
279size: Union[int, List[int]],
280fill: Union[int, Tuple[int, int, int]] = 0,
281padding_mode: str = 'constant',
282):
283super().__init__()
284self.size = _setup_size(size)
285self.fill = fill
286self.padding_mode = padding_mode
287
288def forward(self, img):
289"""
290Args:
291img (PIL Image or Tensor): Image to be cropped.
292
293Returns:
294PIL Image or Tensor: Cropped image.
295"""
296return center_crop_or_pad(img, self.size, fill=self.fill, padding_mode=self.padding_mode)
297
298def __repr__(self) -> str:
299return f"{self.__class__.__name__}(size={self.size})"
300
301
302def crop_or_pad(
303img: torch.Tensor,
304top: int,
305left: int,
306height: int,
307width: int,
308fill: Union[int, Tuple[int, int, int]] = 0,
309padding_mode: str = 'constant',
310) -> torch.Tensor:
311""" Crops and/or pads image to meet target size, with control over fill and padding_mode.
312"""
313_, image_height, image_width = F.get_dimensions(img)
314right = left + width
315bottom = top + height
316if left < 0 or top < 0 or right > image_width or bottom > image_height:
317padding_ltrb = [
318max(-left + min(0, right), 0),
319max(-top + min(0, bottom), 0),
320max(right - max(image_width, left), 0),
321max(bottom - max(image_height, top), 0),
322]
323img = F.pad(img, padding_ltrb, fill=fill, padding_mode=padding_mode)
324
325top = max(top, 0)
326left = max(left, 0)
327return F.crop(img, top, left, height, width)
328
329
330class RandomCropOrPad(torch.nn.Module):
331""" Crop and/or pad image with random placement within the crop or pad margin.
332"""
333
334def __init__(
335self,
336size: Union[int, List[int]],
337fill: Union[int, Tuple[int, int, int]] = 0,
338padding_mode: str = 'constant',
339):
340super().__init__()
341self.size = _setup_size(size)
342self.fill = fill
343self.padding_mode = padding_mode
344
345@staticmethod
346def get_params(img, size):
347_, image_height, image_width = F.get_dimensions(img)
348delta_height = image_height - size[0]
349delta_width = image_width - size[1]
350top = int(math.copysign(random.randint(0, abs(delta_height)), delta_height))
351left = int(math.copysign(random.randint(0, abs(delta_width)), delta_width))
352return top, left
353
354def forward(self, img):
355"""
356Args:
357img (PIL Image or Tensor): Image to be cropped.
358
359Returns:
360PIL Image or Tensor: Cropped image.
361"""
362top, left = self.get_params(img, self.size)
363return crop_or_pad(
364img,
365top=top,
366left=left,
367height=self.size[0],
368width=self.size[1],
369fill=self.fill,
370padding_mode=self.padding_mode,
371)
372
373def __repr__(self) -> str:
374return f"{self.__class__.__name__}(size={self.size})"
375
376
377class RandomPad:
378def __init__(self, input_size, fill=0):
379self.input_size = input_size
380self.fill = fill
381
382@staticmethod
383def get_params(img, input_size):
384width, height = F.get_image_size(img)
385delta_width = max(input_size[1] - width, 0)
386delta_height = max(input_size[0] - height, 0)
387pad_left = random.randint(0, delta_width)
388pad_top = random.randint(0, delta_height)
389pad_right = delta_width - pad_left
390pad_bottom = delta_height - pad_top
391return pad_left, pad_top, pad_right, pad_bottom
392
393def __call__(self, img):
394padding = self.get_params(img, self.input_size)
395img = F.pad(img, padding, self.fill)
396return img
397
398
399class ResizeKeepRatio:
400""" Resize and Keep Aspect Ratio
401"""
402
403def __init__(
404self,
405size,
406longest=0.,
407interpolation='bilinear',
408random_scale_prob=0.,
409random_scale_range=(0.85, 1.05),
410random_scale_area=False,
411random_aspect_prob=0.,
412random_aspect_range=(0.9, 1.11),
413):
414"""
415
416Args:
417size:
418longest:
419interpolation:
420random_scale_prob:
421random_scale_range:
422random_scale_area:
423random_aspect_prob:
424random_aspect_range:
425"""
426if isinstance(size, (list, tuple)):
427self.size = tuple(size)
428else:
429self.size = (size, size)
430if interpolation == 'random':
431self.interpolation = _RANDOM_INTERPOLATION
432else:
433self.interpolation = str_to_interp_mode(interpolation)
434self.longest = float(longest)
435self.random_scale_prob = random_scale_prob
436self.random_scale_range = random_scale_range
437self.random_scale_area = random_scale_area
438self.random_aspect_prob = random_aspect_prob
439self.random_aspect_range = random_aspect_range
440
441@staticmethod
442def get_params(
443img,
444target_size,
445longest,
446random_scale_prob=0.,
447random_scale_range=(1.0, 1.33),
448random_scale_area=False,
449random_aspect_prob=0.,
450random_aspect_range=(0.9, 1.11)
451):
452"""Get parameters
453"""
454img_h, img_w = img_size = F.get_dimensions(img)[1:]
455target_h, target_w = target_size
456ratio_h = img_h / target_h
457ratio_w = img_w / target_w
458ratio = max(ratio_h, ratio_w) * longest + min(ratio_h, ratio_w) * (1. - longest)
459
460if random_scale_prob > 0 and random.random() < random_scale_prob:
461ratio_factor = random.uniform(random_scale_range[0], random_scale_range[1])
462if random_scale_area:
463# make ratio factor equivalent to RRC area crop where < 1.0 = area zoom,
464# otherwise like affine scale where < 1.0 = linear zoom out
465ratio_factor = 1. / math.sqrt(ratio_factor)
466ratio_factor = (ratio_factor, ratio_factor)
467else:
468ratio_factor = (1., 1.)
469
470if random_aspect_prob > 0 and random.random() < random_aspect_prob:
471log_aspect = (math.log(random_aspect_range[0]), math.log(random_aspect_range[1]))
472aspect_factor = math.exp(random.uniform(*log_aspect))
473aspect_factor = math.sqrt(aspect_factor)
474# currently applying random aspect adjustment equally to both dims,
475# could change to keep output sizes above their target where possible
476ratio_factor = (ratio_factor[0] / aspect_factor, ratio_factor[1] * aspect_factor)
477
478size = [round(x * f / ratio) for x, f in zip(img_size, ratio_factor)]
479return size
480
481def __call__(self, img):
482"""
483Args:
484img (PIL Image): Image to be cropped and resized.
485
486Returns:
487PIL Image: Resized, padded to at least target size, possibly cropped to exactly target size
488"""
489size = self.get_params(
490img, self.size, self.longest,
491self.random_scale_prob, self.random_scale_range, self.random_scale_area,
492self.random_aspect_prob, self.random_aspect_range
493)
494if isinstance(self.interpolation, (tuple, list)):
495interpolation = random.choice(self.interpolation)
496else:
497interpolation = self.interpolation
498img = F.resize(img, size, interpolation)
499return img
500
501def __repr__(self):
502if isinstance(self.interpolation, (tuple, list)):
503interpolate_str = ' '.join([interp_mode_to_str(x) for x in self.interpolation])
504else:
505interpolate_str = interp_mode_to_str(self.interpolation)
506format_string = self.__class__.__name__ + '(size={0}'.format(self.size)
507format_string += f', interpolation={interpolate_str}'
508format_string += f', longest={self.longest:.3f}'
509format_string += f', random_scale_prob={self.random_scale_prob:.3f}'
510format_string += f', random_scale_range=(' \
511f'{self.random_scale_range[0]:.3f}, {self.random_aspect_range[1]:.3f})'
512format_string += f', random_aspect_prob={self.random_aspect_prob:.3f}'
513format_string += f', random_aspect_range=(' \
514f'{self.random_aspect_range[0]:.3f}, {self.random_aspect_range[1]:.3f}))'
515return format_string
516
517
518class TrimBorder(torch.nn.Module):
519
520def __init__(
521self,
522border_size: int,
523):
524super().__init__()
525self.border_size = border_size
526
527def forward(self, img):
528w, h = F.get_image_size(img)
529top = left = self.border_size
530top = min(top, h)
531left = min(left, h)
532height = max(0, h - 2 * self.border_size)
533width = max(0, w - 2 * self.border_size)
534return F.crop(img, top, left, height, width)