1# Copyright 2019 Huawei Technologies Co., Ltd 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# ============================================================================== 15""" 16The module transforms.py_transform is implemented based on Python. It provides common 17operations including OneHotOp. 18""" 19import json 20import sys 21import numpy as np 22 23from .validators import check_one_hot_op, check_compose_list, check_random_apply, check_transforms_list, \ 24 check_compose_call 25from . import py_transforms_util as util 26from .c_transforms import TensorOperation 27 28 29def not_random(function): 30 """ 31 Specify the function as "not random", i.e., it produces deterministic result. 32 A Python function can only be cached after it is specified as "not random". 33 """ 34 function.random = False 35 return function 36 37 38class PyTensorOperation: 39 """ 40 Base Python Tensor Operations class 41 """ 42 43 def to_json(self): 44 """ 45 Base to_json for Python tensor operations class 46 """ 47 json_obj = {} 48 json_trans = {} 49 if "transforms" in self.__dict__.keys(): 50 # operations which have transforms as input, need to call _to_json() for each transform to serialize 51 json_list = [] 52 for transform in self.transforms: 53 json_list.append(json.loads(transform.to_json())) 54 json_trans["transforms"] = json_list 55 self.__dict__.pop("transforms") 56 if "output_type" in self.__dict__.keys(): 57 json_trans["output_type"] = np.dtype( 58 self.__dict__["output_type"]).name 59 self.__dict__.pop("output_type") 60 json_obj["tensor_op_params"] = self.__dict__ 61 # append transforms to the tensor_op_params of the operation 62 json_obj["tensor_op_params"].update(json_trans) 63 json_obj["tensor_op_name"] = self.__class__.__name__ 64 json_obj["python_module"] = self.__class__.__module__ 65 return json.dumps(json_obj) 66 67 @classmethod 68 def from_json(cls, json_string): 69 """ 70 Base from_json for Python tensor operations class 71 """ 72 json_obj = json.loads(json_string) 73 new_op = cls.__new__(cls) 74 new_op.__dict__ = json_obj 75 if "transforms" in json_obj.keys(): 76 # operations which have transforms as input, need to call _from_json() for each transform to deseriallize 77 transforms = [] 78 for json_op in json_obj["transforms"]: 79 transforms.append(getattr( 80 sys.modules[json_op["python_module"]], json_op["tensor_op_name"]).from_json( 81 json.dumps(json_op["tensor_op_params"]))) 82 new_op.transforms = transforms 83 if "output_type" in json_obj.keys(): 84 output_type = np.dtype(json_obj["output_type"]) 85 new_op.output_type = output_type 86 return new_op 87 88 89class OneHotOp(PyTensorOperation): 90 """ 91 Apply one hot encoding transformation to the input label, make label be more smoothing and continuous. 92 93 Args: 94 num_classes (int): Number of classes of objects in dataset. 95 It should be larger than the largest label number in the dataset. 96 smoothing_rate (float, optional): Adjustable hyperparameter for label smoothing level. 97 (Default=0.0 means no smoothing is applied.) 98 99 Examples: 100 >>> # Assume that dataset has 10 classes, thus the label ranges from 0 to 9 101 >>> transforms_list = [py_transforms.OneHotOp(num_classes=10, smoothing_rate=0.1)] 102 >>> transform = py_transforms.Compose(transforms_list) 103 >>> mnist_dataset = mnist_dataset.map(input_columns=["label"], operations=transform) 104 """ 105 106 @check_one_hot_op 107 def __init__(self, num_classes, smoothing_rate=0.0): 108 self.num_classes = num_classes 109 self.smoothing_rate = smoothing_rate 110 self.random = False 111 112 def __call__(self, label): 113 """ 114 Call method. 115 116 Args: 117 label (numpy.ndarray): label to be applied label smoothing. 118 119 Returns: 120 label (numpy.ndarray), label after being Smoothed. 121 """ 122 return util.one_hot_encoding(label, self.num_classes, self.smoothing_rate) 123 124 125class Compose(PyTensorOperation): 126 """ 127 Compose a list of transforms. 128 129 .. Note:: 130 Compose takes a list of transformations either provided in py_transforms or from user-defined implementation; 131 each can be an initialized transformation class or a lambda function, as long as the output from the last 132 transformation is a single tensor of type numpy.ndarray. See below for an example of how to use Compose 133 with py_transforms classes and check out FiveCrop or TenCrop for the use of them in conjunction with lambda 134 functions. 135 136 Args: 137 transforms (list): List of transformations to be applied. 138 139 Examples: 140 >>> image_folder_dataset_dir = "/path/to/image_folder_dataset_directory" 141 >>> # create a dataset that reads all files in dataset_dir with 8 threads 142 >>> image_folder_dataset = ds.ImageFolderDataset(image_folder_dataset_dir, num_parallel_workers=8) 143 >>> # create a list of transformations to be applied to the image data 144 >>> transform = py_transforms.Compose([py_vision.Decode(), 145 ... py_vision.RandomHorizontalFlip(0.5), 146 ... py_vision.ToTensor(), 147 ... py_vision.Normalize((0.491, 0.482, 0.447), (0.247, 0.243, 0.262)), 148 ... py_vision.RandomErasing()]) 149 >>> # apply the transform to the dataset through dataset.map function 150 >>> image_folder_dataset = image_folder_dataset.map(operations=transform, input_columns=["image"]) 151 >>> 152 >>> # Compose is also be invoked implicitly, by just passing in a list of ops 153 >>> # the above example then becomes: 154 >>> transforms_list = [py_vision.Decode(), 155 ... py_vision.RandomHorizontalFlip(0.5), 156 ... py_vision.ToTensor(), 157 ... py_vision.Normalize((0.491, 0.482, 0.447), (0.247, 0.243, 0.262)), 158 ... py_vision.RandomErasing()] 159 >>> 160 >>> # apply the transform to the dataset through dataset.map() 161 >>> image_folder_dataset_1 = image_folder_dataset_1.map(operations=transforms_list, input_columns=["image"]) 162 >>> 163 >>> # Certain C++ and Python ops can be combined, but not all of them 164 >>> # An example of combined operations 165 >>> arr = [0, 1] 166 >>> dataset = ds.NumpySlicesDataset(arr, column_names=["cols"], shuffle=False) 167 >>> transformed_list = [py_transforms.OneHotOp(2), c_transforms.Mask(c_transforms.Relational.EQ, 1)] 168 >>> dataset = dataset.map(operations=transformed_list, input_columns=["cols"]) 169 >>> 170 >>> # Here is an example of mixing vision ops 171 >>> import numpy as np 172 >>> op_list=[c_vision.Decode(), 173 ... c_vision.Resize((224, 244)), 174 ... py_vision.ToPIL(), 175 ... np.array, # need to convert PIL image to a NumPy array to pass it to C++ operation 176 ... c_vision.Resize((24, 24))] 177 >>> image_folder_dataset = image_folder_dataset.map(operations=op_list, input_columns=["image"]) 178 """ 179 180 @check_compose_list 181 def __init__(self, transforms): 182 self.transforms = transforms 183 if all(hasattr(transform, "random") and not transform.random for transform in self.transforms): 184 self.random = False 185 186 @check_compose_call 187 def __call__(self, *args): 188 """ 189 Call method. 190 191 Returns: 192 lambda function, Lambda function that takes in an args to apply transformations on. 193 """ 194 return util.compose(self.transforms, *args) 195 196 @staticmethod 197 def reduce(operations): 198 """ 199 Wraps adjacent Python operations in a Compose to allow mixing of Python and C++ operations. 200 201 Args: 202 operations (list): list of tensor operations. 203 204 Returns: 205 list, the reduced list of operations. 206 """ 207 if len(operations) == 1: 208 if str(operations).find("c_transform") >= 0 or isinstance(operations[0], TensorOperation): 209 return operations 210 return [util.FuncWrapper(operations[0])] 211 212 new_ops, start_ind, end_ind = [], 0, 0 213 for i, op in enumerate(operations): 214 if str(op).find("c_transform") >= 0: 215 # reset counts 216 if start_ind != end_ind: 217 new_ops.append(Compose(operations[start_ind:end_ind])) 218 new_ops.append(op) 219 start_ind, end_ind = i + 1, i + 1 220 else: 221 end_ind += 1 222 # do additional check in case the last operation is a Python operation 223 if start_ind != end_ind: 224 new_ops.append(Compose(operations[start_ind:end_ind])) 225 return new_ops 226 227 228class RandomApply(PyTensorOperation): 229 """ 230 Randomly perform a series of transforms with a given probability. 231 232 Args: 233 transforms (list): List of transformations to apply. 234 prob (float, optional): The probability to apply the transformation list (default=0.5). 235 236 Examples: 237 >>> from mindspore.dataset.transforms.py_transforms import Compose 238 >>> transforms_list = [py_vision.RandomHorizontalFlip(0.5), 239 ... py_vision.Normalize((0.491, 0.482, 0.447), (0.247, 0.243, 0.262)), 240 ... py_vision.RandomErasing()] 241 >>> transforms = Compose([py_vision.Decode(), 242 ... py_transforms.RandomApply(transforms_list, prob=0.6), 243 ... py_vision.ToTensor()]) 244 >>> image_folder_dataset = image_folder_dataset.map(operations=transforms, input_columns=["image"]) 245 """ 246 247 @check_random_apply 248 def __init__(self, transforms, prob=0.5): 249 self.prob = prob 250 self.transforms = transforms 251 252 def __call__(self, img): 253 """ 254 Call method. 255 256 Args: 257 img (PIL image): Image to be randomly applied a list transformations. 258 259 Returns: 260 img (PIL image), Transformed image. 261 """ 262 return util.random_apply(img, self.transforms, self.prob) 263 264 265class RandomChoice(PyTensorOperation): 266 """ 267 Randomly select one transform from a series of transforms and applies that on the image. 268 269 Args: 270 transforms (list): List of transformations to be chosen from to apply. 271 272 Examples: 273 >>> from mindspore.dataset.transforms.py_transforms import Compose 274 >>> transforms_list = [py_vision.RandomHorizontalFlip(0.5), 275 ... py_vision.Normalize((0.491, 0.482, 0.447), (0.247, 0.243, 0.262)), 276 ... py_vision.RandomErasing()] 277 >>> transforms = Compose([py_vision.Decode(), 278 ... py_transforms.RandomChoice(transforms_list), 279 ... py_vision.ToTensor()]) 280 >>> image_folder_dataset = image_folder_dataset.map(operations=transforms, input_columns=["image"]) 281 """ 282 283 @check_transforms_list 284 def __init__(self, transforms): 285 self.transforms = transforms 286 287 def __call__(self, img): 288 """ 289 Call method. 290 291 Args: 292 img (PIL image): Image to be applied transformation. 293 294 Returns: 295 img (PIL image), Transformed image. 296 """ 297 return util.random_choice(img, self.transforms) 298 299 300class RandomOrder(PyTensorOperation): 301 """ 302 Perform a series of transforms to the input PIL image in a random order. 303 304 Args: 305 transforms (list): List of the transformations to apply. 306 307 Examples: 308 >>> from mindspore.dataset.transforms.py_transforms import Compose 309 >>> transforms_list = [py_vision.RandomHorizontalFlip(0.5), 310 ... py_vision.Normalize((0.491, 0.482, 0.447), (0.247, 0.243, 0.262)), 311 ... py_vision.RandomErasing()] 312 >>> transforms = Compose([py_vision.Decode(), 313 ... py_transforms.RandomOrder(transforms_list), 314 ... py_vision.ToTensor()]) 315 >>> image_folder_dataset = image_folder_dataset.map(operations=transforms, input_columns=["image"]) 316 """ 317 318 @check_transforms_list 319 def __init__(self, transforms): 320 self.transforms = transforms 321 322 def __call__(self, img): 323 """ 324 Call method. 325 326 Args: 327 img (PIL image): Image to apply transformations in a random order. 328 329 Returns: 330 img (PIL image), Transformed image. 331 """ 332 return util.random_order(img, self.transforms) 333