├── LICENSE ├── README.md ├── data ├── 10-05074_353_49_8178.png ├── HEDJitter.gif ├── affinecvimg.gif ├── blurimg.gif └── elasticimg.gif ├── dataAug_myTransforms.py └── myTransforms.py /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Augmentation-PyTorch-Transforms 2 | **Image data augmentation on-the-fly by adding new class on transforms in PyTorch and torchvision.** 3 | 4 | > Normally, we `from torchvision import transforms` for transformation, but some specific transformations (especially for **histology** image augmentation) are missing. 5 | 6 | > Thus, we add 4 new transforms class on the basic of `torchvision.transforms` pyfile, which we named as [myTransforms.py](https://github.com/gatsby2016/Augmentation-PyTorch-Transforms/blob/master/myTransforms.py). 7 | 8 | > You can call and use it in the same form as `torchvision.transforms`. Or, you can refer to [dataAug_myTransforms.py](https://github.com/gatsby2016/Augmentation-PyTorch-Transforms/blob/master/dataAug_myTransforms.py). 9 | 10 | > Also, you can check the actual effect of [myTransforms](https://github.com/gatsby2016/Augmentation-PyTorch-Transforms/blob/master/myTransforms.py) for data augmentation :) 11 | 12 | 13 | ## New transforms classes included in `myTransforms` 14 | ### **HEDJitter** 15 | Randomly perturbe the HED color space value on an RGB **pathological** image[1]. 16 | 1. Disentangle the hematoxylin and eosin color channels by color deconvolution[2] method using a fixed matrix. 17 | 2. Perturbe the hematoxylin, eosin and DAB stains independently. 18 | 3. Transform the resulting stains into regular RGB color space. 19 | 20 | **Args** 21 | - theta (float): How much to jitter HED color space, 22 | - then, alpha is chosen from a uniform distribution [1-theta, 1+theta] 23 | - betti is chosen from a uniform distribution [-theta, theta] 24 | - the jitter formula is $s' = \alpha * s + \betti$ 25 | 26 | **Example** 27 | ```python 28 | import myTransforms 29 | imagename = '../data/10-05074_353_49_8178.png' 30 | img = Image.open(imagename) # read the image 31 | 32 | preprocess = myTransforms.HEDJitter(theta=0.05) 33 | print(preprocess) 34 | 35 | HEPerimg = preprocess(img) 36 | plt.subplot(121) 37 | plt.imshow(img) 38 | plt.subplot(122) 39 | plt.imshow(HEPerimg) 40 | plt.show() 41 | ``` 42 | ![HEDjitter](https://github.com/gatsby2016/Augmentation-PyTorch-Transforms/blob/master/data/HEDJitter.gif) 43 | 44 | 45 | **References** 46 | [1]. [Tellez, D., Balkenhol, M., Otte-Höller, I., van de Loo, R., Vogels, R., Bult, P., ... & Litjens, G. (2018). Whole-slide mitosis detection in H&E breast histology using PHH3 as a reference to train distilled stain-invariant convolutional networks. IEEE transactions on medical imaging, 37(9), 2126-2136.](https://ieeexplore.ieee.org/abstract/document/8327641) 47 | [2]. [Ruifrok, A. C., & Johnston, D. A. (2001). Quantification of histochemical staining by color deconvolution. Analytical and quantitative cytology and histology, 23(4), 291-299.](https://s3.amazonaws.com/academia.edu.documents/40705455/AnalQuantCytHist-AR.pdf?response-content-disposition=inline%3B%20filename%3DQuantification_of_histochemical_staining.pdf&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIATUSBJ6BAPUHRL6A6%2F20200421%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200421T015610Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEPn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCrEZlRwVoTK5%2Fx14jlpODaSkPreJ5NY37x1uTyYAPlXQIgBZfB%2FpM4Inbb9nrIV%2BO7UCpH7m2B4xRky7dkoHmLy9sqtAMIIhAAGgwyNTAzMTg4MTEyMDAiDOVzPPqXY51w8HhKQSqRAzYnVcq6y8NUQRflE9cOTUiZ17Uh16MWC03iOPCGdSlBQBxyAwzQNqYrPFsX0WijlfQ1NsouAKa09n1syHbUMHQmczA52NthM1LVjQS%2FaB4e2QR%2B9JBNQz6Y1M2rvyy1IOcpouc%2Bb%2B4Pbl8zPxnzyxmzDf5e0VwB1l0F%2BqhQG0HCZxY8K7GssUqLaOempoDOFnpfe21HMyE3hrOqGMdA4Rp7ThBHoODkNpZc9je4v%2ByX97%2BCrOMZGX4Qrc4ZNVGk0ku9N5ly75h2qB3gsnnFmhATkKqRxNGQpqtGAPPPxy%2B4C77%2Fgeds%2Bu9v4C1dAaEwqu%2Fca%2Fc871PlZV42vLus%2FX%2FGwYcEH5tOUNdGHTDKDqPhPtS8fzKYX%2F2Q9JvogD5lTeGXtPXWJvOnGH%2F%2BNOQlRb3i3w0GWx%2BiB7AjMJVEvY9jjm3iDp2BZsxLWIc9dlX7CDKAz5qcoEd3XMsoiXhCDlwHOvx5VqDYICNrqBVRvMo5cPGui2KmoISmmdeNuyjqhBJ9%2FVq4cm1v72NT%2BIXhStHLMOaD%2BfQFOusB7%2B%2FnDDqzMUvjiFC7X1pgtrmXaTfkhMv21SGJyTvwvwPtGNh2qjBApe2dkfyFYn%2F%2BLDcVCV0574Kv3RBVTQcUd20ea1H2ZfgaCbuLFl4nhbMxbjnQgmF5anccbyCJfhHxsCWCgHZRfZR%2BwEDqMGREdHkx4R5gi5g6rsTs0iIuGM2aKpNWK%2BtBXjteH7JK1rWI6GQIxvclg2HmM3ET9gHqiirbMhVemadRloQjRjgAhYafnIu5n%2FqnbPfcDCVBlo7y2I6IofZtjrFZDfU59RS3xeqYCCunfmMvu3XOp7RzJutxEqgC4iMwCXOlLA%3D%3D&X-Amz-SignedHeaders=host&X-Amz-Signature=da6b0b590af1cc2982b418350faae562741bfea920a606337c1e3487adc24d61) 48 | 49 | 50 | ### **RandomElastic** 51 | Random Elastic transformation by *CV2* method on image by alpha, sigma parameter. 52 | **WARNING:** This transform class will spend a lot of CPU time for preprocessing. 53 | 54 | **Args** 55 | - alpha (float): alpha value for Elastic transformation, factor on dx, dy 56 | -- if alpha is 0, output is the same as origin, whatever the sigma; 57 | -- if alpha is 1, output only depends on sigma parameter; 58 | -- if alpha < 1 or > 1, it zoom in or out the sigma-relevant dx, dy. 59 | - sigma (float): sigma value for elastic transformation, should be $\in (0.05,0.1)$ 60 | - mask (PIL Image) For processing on GroundTruth of segmentation task, if not assign, set None. only used in `__call__` function 61 | 62 | **Example** 63 | ```python 64 | import myTransforms 65 | imagename = '../data/10-05074_353_49_8178.png' 66 | img = Image.open(imagename) # read the image 67 | 68 | preprocess = myTransforms.RandomElastic(alpha=2, sigma=0.06) 69 | print(preprocess) 70 | 71 | elasticimg = preprocess(img, mask=None) 72 | plt.subplot(121) 73 | plt.imshow(img) 74 | plt.subplot(122) 75 | plt.imshow(elasticimg) 76 | plt.show() 77 | ``` 78 | ![Elastic](https://github.com/gatsby2016/Augmentation-PyTorch-Transforms/blob/master/data/elasticimg.gif) 79 | 80 | 81 | **References** 82 | [affine and elastic transform](https://blog.csdn.net/maliang_1993/article/details/82020596) 83 | [cv2.warpAffine](https://blog.csdn.net/qq_27261889/article/details/80720359) 84 | [scipy.ndimage.map_coordinates](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html#scipy.ndimage.map_coordinates) 85 | 86 | 87 | ### RandomAffineCV2 88 | Random Affine transformation by CV2 method on image by alpha parameter. 89 | It is different from `torchvision.transforms.RandomAffine`, which is implemented by `PIL.Image` method. We can set BORDER_REFLECT for the area outside the transform in the output image while original `RandomAffine` can only fill by a specified value. 90 | 91 | **Args** 92 | - alpha (float): alpha value for affine transformation 93 | - mask (PIL Image) For processing on GroundTruth of segmentation task, if not assign, set None. 94 | 95 | **Example** 96 | ```python 97 | import myTransforms 98 | imagename = '../data/10-05074_353_49_8178.png' 99 | img = Image.open(imagename) # read the image 100 | 101 | preprocess = myTransforms.RandomAffineCV2(alpha=0.1)#alpha \in [0,0.15] 102 | print(preprocess) 103 | 104 | affinecvimg = preprocess(img) 105 | plt.subplot(121) 106 | plt.imshow(img) 107 | plt.subplot(122) 108 | plt.imshow(affinecvimg) 109 | plt.show() 110 | ``` 111 | ![RandomAffineCV2](https://github.com/gatsby2016/Augmentation-PyTorch-Transforms/blob/master/data/affinecvimg.gif) 112 | 113 | 114 | ### RandomGaussBlur 115 | Random Gauss Blurring on image by radius parameter. 116 | **Args** 117 | - radius (list, tuple): radius range for selecting from; you'd better set it < 2 especially for histopathological image task. 118 | 119 | **Example** 120 | ```python 121 | import myTransforms 122 | imagename = '../data/10-05074_353_49_8178.png' 123 | img = Image.open(imagename) # read the image 124 | 125 | preprocess = myTransforms.RandomGaussBlur(radius=[0.5, 1.5]) 126 | print(preprocess) 127 | 128 | blurimg = preprocess(img) 129 | plt.subplot(121) 130 | plt.imshow(img) 131 | plt.subplot(122) 132 | plt.imshow(blurimg) 133 | plt.show() 134 | ``` 135 | ![GaussianBlur](https://github.com/gatsby2016/Augmentation-PyTorch-Transforms/blob/master/data/blurimg.gif) 136 | 137 | 138 | ### AutoRandomRotation 139 | change `torchvision.transforms.RandomRotation` for auto-random select angle from [0, 90, 180, 270] for rotating the image. 140 | 141 | **Example** 142 | ```python 143 | import myTransforms 144 | imagename = '../data/10-05074_353_49_8178.png' 145 | img = Image.open(imagename) # read the image 146 | 147 | preprocess = myTransforms.AutoRandomRotation() 148 | print(preprocess) 149 | 150 | rotateimg = preprocess(img) 151 | plt.subplot(121) 152 | plt.imshow(img) 153 | plt.subplot(122) 154 | plt.imshow(rotateimg) 155 | plt.show() 156 | ``` 157 | -------------------------------------------------------------------------------- /data/10-05074_353_49_8178.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatsby2016/Augmentation-PyTorch-Transforms/1fbfcf94e134ed93eb661d6e5819856e98b7530c/data/10-05074_353_49_8178.png -------------------------------------------------------------------------------- /data/HEDJitter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatsby2016/Augmentation-PyTorch-Transforms/1fbfcf94e134ed93eb661d6e5819856e98b7530c/data/HEDJitter.gif -------------------------------------------------------------------------------- /data/affinecvimg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatsby2016/Augmentation-PyTorch-Transforms/1fbfcf94e134ed93eb661d6e5819856e98b7530c/data/affinecvimg.gif -------------------------------------------------------------------------------- /data/blurimg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatsby2016/Augmentation-PyTorch-Transforms/1fbfcf94e134ed93eb661d6e5819856e98b7530c/data/blurimg.gif -------------------------------------------------------------------------------- /data/elasticimg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatsby2016/Augmentation-PyTorch-Transforms/1fbfcf94e134ed93eb661d6e5819856e98b7530c/data/elasticimg.gif -------------------------------------------------------------------------------- /dataAug_myTransforms.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | from PIL import Image 4 | import matplotlib.pyplot as plt 5 | 6 | import myTransforms 7 | 8 | 9 | def main(img, time=0, SAVE=False): 10 | preprocess = myTransforms.Compose([ 11 | myTransforms.RandomChoice([myTransforms.RandomHorizontalFlip(p=1), 12 | myTransforms.RandomVerticalFlip(p=1), 13 | myTransforms.AutoRandomRotation()]), # above is for: randomly selecting one for process 14 | # myTransforms.RandomAffine(degrees=0, translate=[0, 0.2], scale=[0.8, 1.2], 15 | # shear=[-10, 10, -10, 10], fillcolor=(228, 218, 218)), 16 | myTransforms.ColorJitter(brightness=(0.65, 1.35), contrast=(0.5, 1.5)), 17 | myTransforms.RandomChoice([myTransforms.ColorJitter(saturation=(0, 2), hue=0.3), 18 | myTransforms.HEDJitter(theta=0.05)]), 19 | # myTransforms.ToTensor(), #operated on original image, rewrite on previous transform. 20 | # myTransforms.Normalize([0.6270, 0.5013, 0.7519], [0.1627, 0.1682, 0.0977]) 21 | ]) 22 | print(preprocess) 23 | 24 | preprocess1 = myTransforms.HEDJitter(theta=0.05) 25 | print(preprocess1) 26 | preprocess2 = myTransforms.RandomGaussBlur(radius=[0.5, 1.5]) 27 | print(preprocess2) 28 | preprocess3 = myTransforms.RandomAffineCV2(alpha=0.1) # alpha \in [0,0.15] 29 | print(preprocess3) 30 | preprocess4 = myTransforms.RandomElastic(alpha=2, sigma=0.06) 31 | print(preprocess4) 32 | 33 | composeimg = preprocess(img) 34 | HEDJitterimg = preprocess1(img) 35 | blurimg = preprocess2(img) 36 | affinecvimg = preprocess3(img) 37 | elasticimg = preprocess4(img,mask=None) 38 | 39 | if SAVE: 40 | HEDJitterimg.save('./data/HEDJitter_' + str(time) + '.png') 41 | blurimg.save('./data/blurimg_' + str(time) + '.png') 42 | affinecvimg.save('./data/affinecvimg_' + str(time) + '.png') 43 | elasticimg.save('./data/elasticimg_' + str(time) + '.png') 44 | else: 45 | plt.subplot(321) 46 | plt.imshow(img) 47 | plt.subplot(322) 48 | plt.imshow(composeimg) 49 | plt.subplot(323) 50 | plt.imshow(HEDJitterimg) 51 | plt.subplot(324) 52 | plt.imshow(blurimg) 53 | plt.subplot(325) 54 | plt.imshow(affinecvimg) 55 | plt.subplot(326) 56 | plt.imshow(elasticimg) 57 | plt.show() 58 | plt.close() 59 | 60 | 61 | if __name__ == '__main__': 62 | np.random.seed(3) 63 | random.seed(3) 64 | 65 | img = Image.open('./data/10-05074_353_49_8178.png') # read the image 66 | print('Raw image shape: ', np.array(img).shape) 67 | for ind in range(10): 68 | main(img, time=ind, SAVE=False) 69 | -------------------------------------------------------------------------------- /myTransforms.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import torch 3 | import math 4 | import sys 5 | import random 6 | try: 7 | import accimage 8 | except ImportError: 9 | accimage = None 10 | import numpy as np 11 | import numbers 12 | import types 13 | import collections 14 | import warnings 15 | 16 | from PIL import Image, ImageFilter 17 | from skimage import color 18 | import cv2 19 | from scipy.ndimage.interpolation import map_coordinates 20 | from scipy.ndimage.filters import gaussian_filter 21 | from torchvision.transforms import functional as F 22 | 23 | if sys.version_info < (3, 3): 24 | Sequence = collections.Sequence 25 | Iterable = collections.Iterable 26 | else: 27 | Sequence = collections.abc.Sequence 28 | Iterable = collections.abc.Iterable 29 | 30 | 31 | # 2020.4.19 Chaoyang 32 | # add new augmentation class *HEDJitter* for HED color space perturbation. 33 | # add new random rotation class *AutoRandomRotation* only for 0, 90,180,270 rotation. 34 | # delete Scale class because it is inapplicable now. 35 | # 2020.4.20 Chaoyang 36 | # add annotation for class *RandomAffine*, how to use it. line 1040 -- 1046 37 | # add new augmentation class *RandomGaussBlur* for gaussian blurring. 38 | # add new augmentation class *RandomAffineCV2* for affine transformation by cv2, which can \ 39 | # set BORDER_REFLECT for the area outside the transform in the output image. 40 | # add new augmentation class *RandomElastic* for elastic transformation by cv2 41 | __all__ = ["Compose", "ToTensor", "ToPILImage", "Normalize", "Resize", "CenterCrop", "Pad", 42 | "Lambda", "RandomApply", "RandomChoice", "RandomOrder", "RandomCrop", "RandomHorizontalFlip", 43 | "RandomVerticalFlip", "RandomResizedCrop", "RandomSizedCrop", "FiveCrop", "TenCrop", "LinearTransformation", 44 | "ColorJitter", "RandomRotation", "RandomAffine", "Grayscale", "RandomGrayscale", 45 | "RandomPerspective", "RandomErasing", 46 | "HEDJitter", "AutoRandomRotation", "RandomGaussBlur", "RandomAffineCV2", "RandomElastic"] 47 | 48 | _pil_interpolation_to_str = { 49 | Image.NEAREST: 'PIL.Image.NEAREST', 50 | Image.BILINEAR: 'PIL.Image.BILINEAR', 51 | Image.BICUBIC: 'PIL.Image.BICUBIC', 52 | Image.LANCZOS: 'PIL.Image.LANCZOS', 53 | Image.HAMMING: 'PIL.Image.HAMMING', 54 | Image.BOX: 'PIL.Image.BOX', 55 | } 56 | 57 | 58 | def _get_image_size(img): 59 | if F._is_pil_image(img): 60 | return img.size 61 | elif isinstance(img, torch.Tensor) and img.dim() > 2: 62 | return img.shape[-2:][::-1] 63 | else: 64 | raise TypeError("Unexpected type {}".format(type(img))) 65 | 66 | 67 | class Compose(object): 68 | """Composes several transforms together. 69 | 70 | Args: 71 | transforms (list of ``Transform`` objects): list of transforms to compose. 72 | 73 | Example: 74 | # >>> transforms.Compose([ 75 | # >>> transforms.CenterCrop(10), 76 | # >>> transforms.ToTensor(), 77 | # >>> ]) 78 | """ 79 | 80 | def __init__(self, transforms): 81 | self.transforms = transforms 82 | 83 | def __call__(self, img): 84 | for t in self.transforms: 85 | img = t(img) 86 | return img 87 | 88 | def __repr__(self): 89 | format_string = self.__class__.__name__ + '(' 90 | for t in self.transforms: 91 | format_string += '\n' 92 | format_string += ' {0}'.format(t) 93 | format_string += '\n)' 94 | return format_string 95 | 96 | 97 | class ToTensor(object): 98 | """Convert a ``PIL Image`` or ``numpy.ndarray`` to tensor. 99 | 100 | Converts a PIL Image or numpy.ndarray (H x W x C) in the range 101 | [0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0] 102 | if the PIL Image belongs to one of the modes (L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1) 103 | or if the numpy.ndarray has dtype = np.uint8 104 | 105 | In the other cases, tensors are returned without scaling. 106 | """ 107 | 108 | def __call__(self, pic): 109 | """ 110 | Args: 111 | pic (PIL Image or numpy.ndarray): Image to be converted to tensor. 112 | 113 | Returns: 114 | Tensor: Converted image. 115 | """ 116 | return F.to_tensor(pic) 117 | 118 | def __repr__(self): 119 | return self.__class__.__name__ + '()' 120 | 121 | 122 | class ToPILImage(object): 123 | """Convert a tensor or an ndarray to PIL Image. 124 | 125 | Converts a torch.*Tensor of shape C x H x W or a numpy ndarray of shape 126 | H x W x C to a PIL Image while preserving the value range. 127 | 128 | Args: 129 | mode (`PIL.Image mode`_): color space and pixel depth of input data (optional). 130 | If ``mode`` is ``None`` (default) there are some assumptions made about the input data: 131 | - If the input has 4 channels, the ``mode`` is assumed to be ``RGBA``. 132 | - If the input has 3 channels, the ``mode`` is assumed to be ``RGB``. 133 | - If the input has 2 channels, the ``mode`` is assumed to be ``LA``. 134 | - If the input has 1 channel, the ``mode`` is determined by the data type (i.e ``int``, ``float``, 135 | ``short``). 136 | 137 | .. _PIL.Image mode: https://pillow.readthedocs.io/en/latest/handbook/concepts.html#concept-modes 138 | """ 139 | def __init__(self, mode=None): 140 | self.mode = mode 141 | 142 | def __call__(self, pic): 143 | """ 144 | Args: 145 | pic (Tensor or numpy.ndarray): Image to be converted to PIL Image. 146 | 147 | Returns: 148 | PIL Image: Image converted to PIL Image. 149 | 150 | """ 151 | return F.to_pil_image(pic, self.mode) 152 | 153 | def __repr__(self): 154 | format_string = self.__class__.__name__ + '(' 155 | if self.mode is not None: 156 | format_string += 'mode={0}'.format(self.mode) 157 | format_string += ')' 158 | return format_string 159 | 160 | 161 | class Normalize(object): 162 | """Normalize a tensor image with mean and standard deviation. 163 | Given mean: ``(M1,...,Mn)`` and std: ``(S1,..,Sn)`` for ``n`` channels, this transform 164 | will normalize each channel of the input ``torch.*Tensor`` i.e. 165 | ``input[channel] = (input[channel] - mean[channel]) / std[channel]`` 166 | 167 | .. note:: 168 | This transform acts out of place, i.e., it does not mutates the input tensor. 169 | 170 | Args: 171 | mean (sequence): Sequence of means for each channel. 172 | std (sequence): Sequence of standard deviations for each channel. 173 | inplace(bool,optional): Bool to make this operation in-place. 174 | 175 | """ 176 | 177 | def __init__(self, mean, std, inplace=False): 178 | self.mean = mean 179 | self.std = std 180 | self.inplace = inplace 181 | 182 | def __call__(self, tensor): 183 | """ 184 | Args: 185 | tensor (Tensor): Tensor image of size (C, H, W) to be normalized. 186 | 187 | Returns: 188 | Tensor: Normalized Tensor image. 189 | """ 190 | return F.normalize(tensor, self.mean, self.std, self.inplace) 191 | 192 | def __repr__(self): 193 | return self.__class__.__name__ + '(mean={0}, std={1})'.format(self.mean, self.std) 194 | 195 | 196 | class Resize(object): 197 | """Resize the input PIL Image to the given size. 198 | 199 | Args: 200 | size (sequence or int): Desired output size. If size is a sequence like 201 | (h, w), output size will be matched to this. If size is an int, 202 | smaller edge of the image will be matched to this number. 203 | i.e, if height > width, then image will be rescaled to 204 | (size * height / width, size) 205 | interpolation (int, optional): Desired interpolation. Default is 206 | ``PIL.Image.BILINEAR`` 207 | """ 208 | 209 | def __init__(self, size, interpolation=Image.BILINEAR): 210 | assert isinstance(size, int) or (isinstance(size, Iterable) and len(size) == 2) 211 | self.size = size 212 | self.interpolation = interpolation 213 | 214 | def __call__(self, img): 215 | """ 216 | Args: 217 | img (PIL Image): Image to be scaled. 218 | 219 | Returns: 220 | PIL Image: Rescaled image. 221 | """ 222 | return F.resize(img, self.size, self.interpolation) 223 | 224 | def __repr__(self): 225 | interpolate_str = _pil_interpolation_to_str[self.interpolation] 226 | return self.__class__.__name__ + '(size={0}, interpolation={1})'.format(self.size, interpolate_str) 227 | 228 | 229 | class CenterCrop(object): 230 | """Crops the given PIL Image at the center. 231 | 232 | Args: 233 | size (sequence or int): Desired output size of the crop. If size is an 234 | int instead of sequence like (h, w), a square crop (size, size) is 235 | made. 236 | """ 237 | 238 | def __init__(self, size): 239 | if isinstance(size, numbers.Number): 240 | self.size = (int(size), int(size)) 241 | else: 242 | self.size = size 243 | 244 | def __call__(self, img): 245 | """ 246 | Args: 247 | img (PIL Image): Image to be cropped. 248 | 249 | Returns: 250 | PIL Image: Cropped image. 251 | """ 252 | return F.center_crop(img, self.size) 253 | 254 | def __repr__(self): 255 | return self.__class__.__name__ + '(size={0})'.format(self.size) 256 | 257 | 258 | class Pad(object): 259 | """Pad the given PIL Image on all sides with the given "pad" value. 260 | 261 | Args: 262 | padding (int or tuple): Padding on each border. If a single int is provided this 263 | is used to pad all borders. If tuple of length 2 is provided this is the padding 264 | on left/right and top/bottom respectively. If a tuple of length 4 is provided 265 | this is the padding for the left, top, right and bottom borders 266 | respectively. 267 | fill (int or tuple): Pixel fill value for constant fill. Default is 0. If a tuple of 268 | length 3, it is used to fill R, G, B channels respectively. 269 | This value is only used when the padding_mode is constant 270 | padding_mode (str): Type of padding. Should be: constant, edge, reflect or symmetric. 271 | Default is constant. 272 | 273 | - constant: pads with a constant value, this value is specified with fill 274 | 275 | - edge: pads with the last value at the edge of the image 276 | 277 | - reflect: pads with reflection of image without repeating the last value on the edge 278 | 279 | For example, padding [1, 2, 3, 4] with 2 elements on both sides in reflect mode 280 | will result in [3, 2, 1, 2, 3, 4, 3, 2] 281 | 282 | - symmetric: pads with reflection of image repeating the last value on the edge 283 | 284 | For example, padding [1, 2, 3, 4] with 2 elements on both sides in symmetric mode 285 | will result in [2, 1, 1, 2, 3, 4, 4, 3] 286 | """ 287 | 288 | def __init__(self, padding, fill=0, padding_mode='constant'): 289 | assert isinstance(padding, (numbers.Number, tuple)) 290 | assert isinstance(fill, (numbers.Number, str, tuple)) 291 | assert padding_mode in ['constant', 'edge', 'reflect', 'symmetric'] 292 | if isinstance(padding, Sequence) and len(padding) not in [2, 4]: 293 | raise ValueError("Padding must be an int or a 2, or 4 element tuple, not a " + 294 | "{} element tuple".format(len(padding))) 295 | 296 | self.padding = padding 297 | self.fill = fill 298 | self.padding_mode = padding_mode 299 | 300 | def __call__(self, img): 301 | """ 302 | Args: 303 | img (PIL Image): Image to be padded. 304 | 305 | Returns: 306 | PIL Image: Padded image. 307 | """ 308 | return F.pad(img, self.padding, self.fill, self.padding_mode) 309 | 310 | def __repr__(self): 311 | return self.__class__.__name__ + '(padding={0}, fill={1}, padding_mode={2})'.\ 312 | format(self.padding, self.fill, self.padding_mode) 313 | 314 | 315 | class Lambda(object): 316 | """Apply a user-defined lambda as a transform. 317 | 318 | Args: 319 | lambd (function): Lambda/function to be used for transform. 320 | """ 321 | 322 | def __init__(self, lambd): 323 | assert callable(lambd), repr(type(lambd).__name__) + " object is not callable" 324 | self.lambd = lambd 325 | 326 | def __call__(self, img): 327 | return self.lambd(img) 328 | 329 | def __repr__(self): 330 | return self.__class__.__name__ + '()' 331 | 332 | 333 | class RandomTransforms(object): 334 | """Base class for a list of transformations with randomness 335 | 336 | Args: 337 | transforms (list or tuple): list of transformations 338 | """ 339 | 340 | def __init__(self, transforms): 341 | assert isinstance(transforms, (list, tuple)) 342 | self.transforms = transforms 343 | 344 | def __call__(self, *args, **kwargs): 345 | raise NotImplementedError() 346 | 347 | def __repr__(self): 348 | format_string = self.__class__.__name__ + '(' 349 | for t in self.transforms: 350 | format_string += '\n' 351 | format_string += ' {0}'.format(t) 352 | format_string += '\n)' 353 | return format_string 354 | 355 | 356 | class RandomApply(RandomTransforms): 357 | """Apply randomly a list of transformations with a given probability 358 | 359 | Args: 360 | transforms (list or tuple): list of transformations 361 | p (float): probability 362 | """ 363 | 364 | def __init__(self, transforms, p=0.5): 365 | super(RandomApply, self).__init__(transforms) 366 | self.p = p 367 | 368 | def __call__(self, img): 369 | if self.p < random.random(): 370 | return img 371 | for t in self.transforms: 372 | img = t(img) 373 | return img 374 | 375 | def __repr__(self): 376 | format_string = self.__class__.__name__ + '(' 377 | format_string += '\n p={}'.format(self.p) 378 | for t in self.transforms: 379 | format_string += '\n' 380 | format_string += ' {0}'.format(t) 381 | format_string += '\n)' 382 | return format_string 383 | 384 | 385 | class RandomOrder(RandomTransforms): 386 | """Apply a list of transformations in a random order 387 | """ 388 | def __call__(self, img): 389 | order = list(range(len(self.transforms))) 390 | random.shuffle(order) 391 | for i in order: 392 | img = self.transforms[i](img) 393 | return img 394 | 395 | 396 | class RandomChoice(RandomTransforms): 397 | """Apply single transformation randomly picked from a list 398 | """ 399 | def __call__(self, img): 400 | t = random.choice(self.transforms) 401 | return t(img) 402 | 403 | 404 | class RandomCrop(object): 405 | """Crop the given PIL Image at a random location. 406 | 407 | Args: 408 | size (sequence or int): Desired output size of the crop. If size is an 409 | int instead of sequence like (h, w), a square crop (size, size) is 410 | made. 411 | padding (int or sequence, optional): Optional padding on each border 412 | of the image. Default is None, i.e no padding. If a sequence of length 413 | 4 is provided, it is used to pad left, top, right, bottom borders 414 | respectively. If a sequence of length 2 is provided, it is used to 415 | pad left/right, top/bottom borders, respectively. 416 | pad_if_needed (boolean): It will pad the image if smaller than the 417 | desired size to avoid raising an exception. Since cropping is done 418 | after padding, the padding seems to be done at a random offset. 419 | fill: Pixel fill value for constant fill. Default is 0. If a tuple of 420 | length 3, it is used to fill R, G, B channels respectively. 421 | This value is only used when the padding_mode is constant 422 | padding_mode: Type of padding. Should be: constant, edge, reflect or symmetric. Default is constant. 423 | 424 | - constant: pads with a constant value, this value is specified with fill 425 | 426 | - edge: pads with the last value on the edge of the image 427 | 428 | - reflect: pads with reflection of image (without repeating the last value on the edge) 429 | 430 | padding [1, 2, 3, 4] with 2 elements on both sides in reflect mode 431 | will result in [3, 2, 1, 2, 3, 4, 3, 2] 432 | 433 | - symmetric: pads with reflection of image (repeating the last value on the edge) 434 | 435 | padding [1, 2, 3, 4] with 2 elements on both sides in symmetric mode 436 | will result in [2, 1, 1, 2, 3, 4, 4, 3] 437 | 438 | """ 439 | 440 | def __init__(self, size, padding=None, pad_if_needed=False, fill=0, padding_mode='constant'): 441 | if isinstance(size, numbers.Number): 442 | self.size = (int(size), int(size)) 443 | else: 444 | self.size = size 445 | self.padding = padding 446 | self.pad_if_needed = pad_if_needed 447 | self.fill = fill 448 | self.padding_mode = padding_mode 449 | 450 | @staticmethod 451 | def get_params(img, output_size): 452 | """Get parameters for ``crop`` for a random crop. 453 | 454 | Args: 455 | img (PIL Image): Image to be cropped. 456 | output_size (tuple): Expected output size of the crop. 457 | 458 | Returns: 459 | tuple: params (i, j, h, w) to be passed to ``crop`` for random crop. 460 | """ 461 | w, h = _get_image_size(img) 462 | th, tw = output_size 463 | if w == tw and h == th: 464 | return 0, 0, h, w 465 | 466 | i = random.randint(0, h - th) 467 | j = random.randint(0, w - tw) 468 | return i, j, th, tw 469 | 470 | def __call__(self, img): 471 | """ 472 | Args: 473 | img (PIL Image): Image to be cropped. 474 | 475 | Returns: 476 | PIL Image: Cropped image. 477 | """ 478 | if self.padding is not None: 479 | img = F.pad(img, self.padding, self.fill, self.padding_mode) 480 | 481 | # pad the width if needed 482 | if self.pad_if_needed and img.size[0] < self.size[1]: 483 | img = F.pad(img, (self.size[1] - img.size[0], 0), self.fill, self.padding_mode) 484 | # pad the height if needed 485 | if self.pad_if_needed and img.size[1] < self.size[0]: 486 | img = F.pad(img, (0, self.size[0] - img.size[1]), self.fill, self.padding_mode) 487 | 488 | i, j, h, w = self.get_params(img, self.size) 489 | 490 | return F.crop(img, i, j, h, w) 491 | 492 | def __repr__(self): 493 | return self.__class__.__name__ + '(size={0}, padding={1})'.format(self.size, self.padding) 494 | 495 | 496 | class RandomHorizontalFlip(object): 497 | """Horizontally flip the given PIL Image randomly with a given probability. 498 | 499 | Args: 500 | p (float): probability of the image being flipped. Default value is 0.5 501 | """ 502 | 503 | def __init__(self, p=0.5): 504 | self.p = p 505 | 506 | def __call__(self, img): 507 | """ 508 | Args: 509 | img (PIL Image): Image to be flipped. 510 | 511 | Returns: 512 | PIL Image: Randomly flipped image. 513 | """ 514 | if random.random() < self.p: 515 | return F.hflip(img) 516 | return img 517 | 518 | def __repr__(self): 519 | return self.__class__.__name__ + '(p={})'.format(self.p) 520 | 521 | 522 | class RandomVerticalFlip(object): 523 | """Vertically flip the given PIL Image randomly with a given probability. 524 | 525 | Args: 526 | p (float): probability of the image being flipped. Default value is 0.5 527 | """ 528 | 529 | def __init__(self, p=0.5): 530 | self.p = p 531 | 532 | def __call__(self, img): 533 | """ 534 | Args: 535 | img (PIL Image): Image to be flipped. 536 | 537 | Returns: 538 | PIL Image: Randomly flipped image. 539 | """ 540 | if random.random() < self.p: 541 | return F.vflip(img) 542 | return img 543 | 544 | def __repr__(self): 545 | return self.__class__.__name__ + '(p={})'.format(self.p) 546 | 547 | 548 | class RandomPerspective(object): 549 | """Performs Perspective transformation of the given PIL Image randomly with a given probability. 550 | 551 | Args: 552 | interpolation : Default- Image.BICUBIC 553 | 554 | p (float): probability of the image being perspectively transformed. Default value is 0.5 555 | 556 | distortion_scale(float): it controls the degree of distortion and ranges from 0 to 1. Default value is 0.5. 557 | 558 | """ 559 | 560 | def __init__(self, distortion_scale=0.5, p=0.5, interpolation=Image.BICUBIC): 561 | self.p = p 562 | self.interpolation = interpolation 563 | self.distortion_scale = distortion_scale 564 | 565 | def __call__(self, img): 566 | """ 567 | Args: 568 | img (PIL Image): Image to be Perspectively transformed. 569 | 570 | Returns: 571 | PIL Image: Random perspectivley transformed image. 572 | """ 573 | if not F._is_pil_image(img): 574 | raise TypeError('img should be PIL Image. Got {}'.format(type(img))) 575 | 576 | if random.random() < self.p: 577 | width, height = img.size 578 | startpoints, endpoints = self.get_params(width, height, self.distortion_scale) 579 | return F.perspective(img, startpoints, endpoints, self.interpolation) 580 | return img 581 | 582 | @staticmethod 583 | def get_params(width, height, distortion_scale): 584 | """Get parameters for ``perspective`` for a random perspective transform. 585 | 586 | Args: 587 | width : width of the image. 588 | height : height of the image. 589 | 590 | Returns: 591 | List containing [top-left, top-right, bottom-right, bottom-left] of the original image, 592 | List containing [top-left, top-right, bottom-right, bottom-left] of the transformed image. 593 | """ 594 | half_height = int(height / 2) 595 | half_width = int(width / 2) 596 | topleft = (random.randint(0, int(distortion_scale * half_width)), 597 | random.randint(0, int(distortion_scale * half_height))) 598 | topright = (random.randint(width - int(distortion_scale * half_width) - 1, width - 1), 599 | random.randint(0, int(distortion_scale * half_height))) 600 | botright = (random.randint(width - int(distortion_scale * half_width) - 1, width - 1), 601 | random.randint(height - int(distortion_scale * half_height) - 1, height - 1)) 602 | botleft = (random.randint(0, int(distortion_scale * half_width)), 603 | random.randint(height - int(distortion_scale * half_height) - 1, height - 1)) 604 | startpoints = [(0, 0), (width - 1, 0), (width - 1, height - 1), (0, height - 1)] 605 | endpoints = [topleft, topright, botright, botleft] 606 | return startpoints, endpoints 607 | 608 | def __repr__(self): 609 | return self.__class__.__name__ + '(p={})'.format(self.p) 610 | 611 | 612 | class RandomResizedCrop(object): 613 | """Crop the given PIL Image to random size and aspect ratio. 614 | 615 | A crop of random size (default: of 0.08 to 1.0) of the original size and a random 616 | aspect ratio (default: of 3/4 to 4/3) of the original aspect ratio is made. This crop 617 | is finally resized to given size. 618 | This is popularly used to train the Inception networks. 619 | 620 | Args: 621 | size: expected output size of each edge 622 | scale: range of size of the origin size cropped 623 | ratio: range of aspect ratio of the origin aspect ratio cropped 624 | interpolation: Default: PIL.Image.BILINEAR 625 | """ 626 | 627 | def __init__(self, size, scale=(0.08, 1.0), ratio=(3. / 4., 4. / 3.), interpolation=Image.BILINEAR): 628 | if isinstance(size, tuple): 629 | self.size = size 630 | else: 631 | self.size = (size, size) 632 | if (scale[0] > scale[1]) or (ratio[0] > ratio[1]): 633 | warnings.warn("range should be of kind (min, max)") 634 | 635 | self.interpolation = interpolation 636 | self.scale = scale 637 | self.ratio = ratio 638 | 639 | @staticmethod 640 | def get_params(img, scale, ratio): 641 | """Get parameters for ``crop`` for a random sized crop. 642 | 643 | Args: 644 | img (PIL Image): Image to be cropped. 645 | scale (tuple): range of size of the origin size cropped 646 | ratio (tuple): range of aspect ratio of the origin aspect ratio cropped 647 | 648 | Returns: 649 | tuple: params (i, j, h, w) to be passed to ``crop`` for a random 650 | sized crop. 651 | """ 652 | width, height = _get_image_size(img) 653 | area = height * width 654 | 655 | for attempt in range(10): 656 | target_area = random.uniform(*scale) * area 657 | log_ratio = (math.log(ratio[0]), math.log(ratio[1])) 658 | aspect_ratio = math.exp(random.uniform(*log_ratio)) 659 | 660 | w = int(round(math.sqrt(target_area * aspect_ratio))) 661 | h = int(round(math.sqrt(target_area / aspect_ratio))) 662 | 663 | if 0 < w <= width and 0 < h <= height: 664 | i = random.randint(0, height - h) 665 | j = random.randint(0, width - w) 666 | return i, j, h, w 667 | 668 | # Fallback to central crop 669 | in_ratio = float(width) / float(height) 670 | if (in_ratio < min(ratio)): 671 | w = width 672 | h = int(round(w / min(ratio))) 673 | elif (in_ratio > max(ratio)): 674 | h = height 675 | w = int(round(h * max(ratio))) 676 | else: # whole image 677 | w = width 678 | h = height 679 | i = (height - h) // 2 680 | j = (width - w) // 2 681 | return i, j, h, w 682 | 683 | def __call__(self, img): 684 | """ 685 | Args: 686 | img (PIL Image): Image to be cropped and resized. 687 | 688 | Returns: 689 | PIL Image: Randomly cropped and resized image. 690 | """ 691 | i, j, h, w = self.get_params(img, self.scale, self.ratio) 692 | return F.resized_crop(img, i, j, h, w, self.size, self.interpolation) 693 | 694 | def __repr__(self): 695 | interpolate_str = _pil_interpolation_to_str[self.interpolation] 696 | format_string = self.__class__.__name__ + '(size={0}'.format(self.size) 697 | format_string += ', scale={0}'.format(tuple(round(s, 4) for s in self.scale)) 698 | format_string += ', ratio={0}'.format(tuple(round(r, 4) for r in self.ratio)) 699 | format_string += ', interpolation={0})'.format(interpolate_str) 700 | return format_string 701 | 702 | 703 | class RandomSizedCrop(RandomResizedCrop): 704 | """ 705 | Note: This transform is deprecated in favor of RandomResizedCrop. 706 | """ 707 | def __init__(self, *args, **kwargs): 708 | warnings.warn("The use of the transforms.RandomSizedCrop transform is deprecated, " + 709 | "please use transforms.RandomResizedCrop instead.") 710 | super(RandomSizedCrop, self).__init__(*args, **kwargs) 711 | 712 | 713 | class FiveCrop(object): 714 | """Crop the given PIL Image into four corners and the central crop 715 | 716 | .. Note:: 717 | This transform returns a tuple of images and there may be a mismatch in the number of 718 | inputs and targets your Dataset returns. See below for an example of how to deal with 719 | this. 720 | 721 | Args: 722 | size (sequence or int): Desired output size of the crop. If size is an ``int`` 723 | instead of sequence like (h, w), a square crop of size (size, size) is made. 724 | 725 | Example: 726 | # >>> transform = Compose([ 727 | # >>> FiveCrop(size), # this is a list of PIL Images 728 | # >>> Lambda(lambda crops: torch.stack([ToTensor()(crop) for crop in crops])) # returns a 4D tensor 729 | # >>> ]) 730 | # >>> #In your test loop you can do the following: 731 | # >>> input, target = batch # input is a 5d tensor, target is 2d 732 | # >>> bs, ncrops, c, h, w = input.size() 733 | # >>> result = model(input.view(-1, c, h, w)) # fuse batch size and ncrops 734 | # >>> result_avg = result.view(bs, ncrops, -1).mean(1) # avg over crops 735 | """ 736 | 737 | def __init__(self, size): 738 | self.size = size 739 | if isinstance(size, numbers.Number): 740 | self.size = (int(size), int(size)) 741 | else: 742 | assert len(size) == 2, "Please provide only two dimensions (h, w) for size." 743 | self.size = size 744 | 745 | def __call__(self, img): 746 | return F.five_crop(img, self.size) 747 | 748 | def __repr__(self): 749 | return self.__class__.__name__ + '(size={0})'.format(self.size) 750 | 751 | 752 | class TenCrop(object): 753 | """Crop the given PIL Image into four corners and the central crop plus the flipped version of 754 | these (horizontal flipping is used by default) 755 | 756 | .. Note:: 757 | This transform returns a tuple of images and there may be a mismatch in the number of 758 | inputs and targets your Dataset returns. See below for an example of how to deal with 759 | this. 760 | 761 | Args: 762 | size (sequence or int): Desired output size of the crop. If size is an 763 | int instead of sequence like (h, w), a square crop (size, size) is 764 | made. 765 | vertical_flip (bool): Use vertical flipping instead of horizontal 766 | 767 | Example: 768 | # >>> transform = Compose([ 769 | # >>> TenCrop(size), # this is a list of PIL Images 770 | # >>> Lambda(lambda crops: torch.stack([ToTensor()(crop) for crop in crops])) # returns a 4D tensor 771 | # >>> ]) 772 | # >>> #In your test loop you can do the following: 773 | # >>> input, target = batch # input is a 5d tensor, target is 2d 774 | # >>> bs, ncrops, c, h, w = input.size() 775 | # >>> result = model(input.view(-1, c, h, w)) # fuse batch size and ncrops 776 | # >>> result_avg = result.view(bs, ncrops, -1).mean(1) # avg over crops 777 | """ 778 | 779 | def __init__(self, size, vertical_flip=False): 780 | self.size = size 781 | if isinstance(size, numbers.Number): 782 | self.size = (int(size), int(size)) 783 | else: 784 | assert len(size) == 2, "Please provide only two dimensions (h, w) for size." 785 | self.size = size 786 | self.vertical_flip = vertical_flip 787 | 788 | def __call__(self, img): 789 | return F.ten_crop(img, self.size, self.vertical_flip) 790 | 791 | def __repr__(self): 792 | return self.__class__.__name__ + '(size={0}, vertical_flip={1})'.format(self.size, self.vertical_flip) 793 | 794 | 795 | class LinearTransformation(object): 796 | """Transform a tensor image with a square transformation matrix and a mean_vector computed 797 | offline. 798 | Given transformation_matrix and mean_vector, will flatten the torch.*Tensor and 799 | subtract mean_vector from it which is then followed by computing the dot 800 | product with the transformation matrix and then reshaping the tensor to its 801 | original shape. 802 | 803 | Applications: 804 | whitening transformation: Suppose X is a column vector zero-centered data. 805 | Then compute the data covariance matrix [D x D] with torch.mm(X.t(), X), 806 | perform SVD on this matrix and pass it as transformation_matrix. 807 | 808 | Args: 809 | transformation_matrix (Tensor): tensor [D x D], D = C x H x W 810 | mean_vector (Tensor): tensor [D], D = C x H x W 811 | """ 812 | 813 | def __init__(self, transformation_matrix, mean_vector): 814 | if transformation_matrix.size(0) != transformation_matrix.size(1): 815 | raise ValueError("transformation_matrix should be square. Got " + 816 | "[{} x {}] rectangular matrix.".format(*transformation_matrix.size())) 817 | 818 | if mean_vector.size(0) != transformation_matrix.size(0): 819 | raise ValueError("mean_vector should have the same length {}".format(mean_vector.size(0)) + 820 | " as any one of the dimensions of the transformation_matrix [{} x {}]" 821 | .format(transformation_matrix.size())) 822 | 823 | self.transformation_matrix = transformation_matrix 824 | self.mean_vector = mean_vector 825 | 826 | def __call__(self, tensor): 827 | """ 828 | Args: 829 | tensor (Tensor): Tensor image of size (C, H, W) to be whitened. 830 | 831 | Returns: 832 | Tensor: Transformed image. 833 | """ 834 | if tensor.size(0) * tensor.size(1) * tensor.size(2) != self.transformation_matrix.size(0): 835 | raise ValueError("tensor and transformation matrix have incompatible shape." + 836 | "[{} x {} x {}] != ".format(*tensor.size()) + 837 | "{}".format(self.transformation_matrix.size(0))) 838 | flat_tensor = tensor.view(1, -1) - self.mean_vector 839 | transformed_tensor = torch.mm(flat_tensor, self.transformation_matrix) 840 | tensor = transformed_tensor.view(tensor.size()) 841 | return tensor 842 | 843 | def __repr__(self): 844 | format_string = self.__class__.__name__ + '(transformation_matrix=' 845 | format_string += (str(self.transformation_matrix.tolist()) + ')') 846 | format_string += (", (mean_vector=" + str(self.mean_vector.tolist()) + ')') 847 | return format_string 848 | 849 | 850 | class ColorJitter(object): 851 | """Randomly change the brightness, contrast and saturation of an image. 852 | 853 | Args: 854 | brightness (float or tuple of float (min, max)): How much to jitter brightness. 855 | brightness_factor is chosen uniformly from [max(0, 1 - brightness), 1 + brightness] 856 | or the given [min, max]. Should be non negative numbers. 857 | contrast (float or tuple of float (min, max)): How much to jitter contrast. 858 | contrast_factor is chosen uniformly from [max(0, 1 - contrast), 1 + contrast] 859 | or the given [min, max]. Should be non negative numbers. 860 | saturation (float or tuple of float (min, max)): How much to jitter saturation. 861 | saturation_factor is chosen uniformly from [max(0, 1 - saturation), 1 + saturation] 862 | or the given [min, max]. Should be non negative numbers. 863 | hue (float or tuple of float (min, max)): How much to jitter hue. 864 | hue_factor is chosen uniformly from [-hue, hue] or the given [min, max]. 865 | Should have 0<= hue <= 0.5 or -0.5 <= min <= max <= 0.5. 866 | """ 867 | def __init__(self, brightness=0, contrast=0, saturation=0, hue=0): 868 | self.brightness = self._check_input(brightness, 'brightness') 869 | self.contrast = self._check_input(contrast, 'contrast') 870 | self.saturation = self._check_input(saturation, 'saturation') 871 | self.hue = self._check_input(hue, 'hue', center=0, bound=(-0.5, 0.5), 872 | clip_first_on_zero=False) 873 | 874 | def _check_input(self, value, name, center=1, bound=(0, float('inf')), clip_first_on_zero=True): 875 | if isinstance(value, numbers.Number): 876 | if value < 0: 877 | raise ValueError("If {} is a single number, it must be non negative.".format(name)) 878 | value = [center - value, center + value] 879 | if clip_first_on_zero: 880 | value[0] = max(value[0], 0) 881 | elif isinstance(value, (tuple, list)) and len(value) == 2: 882 | if not bound[0] <= value[0] <= value[1] <= bound[1]: 883 | raise ValueError("{} values should be between {}".format(name, bound)) 884 | else: 885 | raise TypeError("{} should be a single number or a list/tuple with lenght 2.".format(name)) 886 | 887 | # if value is 0 or (1., 1.) for brightness/contrast/saturation 888 | # or (0., 0.) for hue, do nothing 889 | if value[0] == value[1] == center: 890 | value = None 891 | return value 892 | 893 | @staticmethod 894 | def get_params(brightness, contrast, saturation, hue): 895 | """Get a randomized transform to be applied on image. 896 | 897 | Arguments are same as that of __init__. 898 | 899 | Returns: 900 | Transform which randomly adjusts brightness, contrast and 901 | saturation in a random order. 902 | """ 903 | transforms = [] 904 | 905 | if brightness is not None: 906 | brightness_factor = random.uniform(brightness[0], brightness[1]) 907 | transforms.append(Lambda(lambda img: F.adjust_brightness(img, brightness_factor))) 908 | 909 | if contrast is not None: 910 | contrast_factor = random.uniform(contrast[0], contrast[1]) 911 | transforms.append(Lambda(lambda img: F.adjust_contrast(img, contrast_factor))) 912 | 913 | if saturation is not None: 914 | saturation_factor = random.uniform(saturation[0], saturation[1]) 915 | transforms.append(Lambda(lambda img: F.adjust_saturation(img, saturation_factor))) 916 | 917 | if hue is not None: 918 | hue_factor = random.uniform(hue[0], hue[1]) 919 | transforms.append(Lambda(lambda img: F.adjust_hue(img, hue_factor))) 920 | 921 | random.shuffle(transforms) 922 | transform = Compose(transforms) 923 | 924 | return transform 925 | 926 | def __call__(self, img): 927 | """ 928 | Args: 929 | img (PIL Image): Input image. 930 | 931 | Returns: 932 | PIL Image: Color jittered image. 933 | """ 934 | transform = self.get_params(self.brightness, self.contrast, 935 | self.saturation, self.hue) 936 | return transform(img) 937 | 938 | def __repr__(self): 939 | format_string = self.__class__.__name__ + '(' 940 | format_string += 'brightness={0}'.format(self.brightness) 941 | format_string += ', contrast={0}'.format(self.contrast) 942 | format_string += ', saturation={0}'.format(self.saturation) 943 | format_string += ', hue={0})'.format(self.hue) 944 | return format_string 945 | 946 | 947 | class RandomRotation(object): 948 | """Rotate the image by angle. 949 | 950 | Args: 951 | degrees (sequence or float or int): Range of degrees to select from. 952 | If degrees is a number instead of sequence like (min, max), the range of degrees 953 | will be (-degrees, +degrees). 954 | resample ({PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC}, optional): 955 | An optional resampling filter. See `filters`_ for more information. 956 | If omitted, or if the image has mode "1" or "P", it is set to PIL.Image.NEAREST. 957 | expand (bool, optional): Optional expansion flag. 958 | If true, expands the output to make it large enough to hold the entire rotated image. 959 | If false or omitted, make the output image the same size as the input image. 960 | Note that the expand flag assumes rotation around the center and no translation. 961 | center (2-tuple, optional): Optional center of rotation. 962 | Origin is the upper left corner. 963 | Default is the center of the image. 964 | fill (3-tuple or int): RGB pixel fill value for area outside the rotated image. 965 | If int, it is used for all channels respectively. 966 | 967 | .. _filters: https://pillow.readthedocs.io/en/latest/handbook/concepts.html#filters 968 | 969 | """ 970 | 971 | def __init__(self, degrees, resample=False, expand=False, center=None, fill=0): 972 | if isinstance(degrees, numbers.Number): 973 | if degrees < 0: 974 | raise ValueError("If degrees is a single number, it must be positive.") 975 | self.degrees = (-degrees, degrees) 976 | else: 977 | if len(degrees) != 2: 978 | raise ValueError("If degrees is a sequence, it must be of len 2.") 979 | self.degrees = degrees 980 | 981 | self.resample = resample 982 | self.expand = expand 983 | self.center = center 984 | self.fill = fill 985 | 986 | @staticmethod 987 | def get_params(degrees): 988 | """Get parameters for ``rotate`` for a random rotation. 989 | 990 | Returns: 991 | sequence: params to be passed to ``rotate`` for random rotation. 992 | """ 993 | angle = random.uniform(degrees[0], degrees[1]) 994 | 995 | return angle 996 | 997 | def __call__(self, img): 998 | """ 999 | Args: 1000 | img (PIL Image): Image to be rotated. 1001 | 1002 | Returns: 1003 | PIL Image: Rotated image. 1004 | """ 1005 | 1006 | angle = self.get_params(self.degrees) 1007 | 1008 | return F.rotate(img, angle, self.resample, self.expand, self.center, self.fill) 1009 | 1010 | def __repr__(self): 1011 | format_string = self.__class__.__name__ + '(degrees={0}'.format(self.degrees) 1012 | format_string += ', resample={0}'.format(self.resample) 1013 | format_string += ', expand={0}'.format(self.expand) 1014 | if self.center is not None: 1015 | format_string += ', center={0}'.format(self.center) 1016 | format_string += ')' 1017 | return format_string 1018 | 1019 | 1020 | class RandomAffine(object): 1021 | """Random affine transformation of the image keeping center invariant 1022 | 1023 | Args: 1024 | degrees (sequence or float or int): Range of degrees to select from. 1025 | If degrees is a number instead of sequence like (min, max), the range of degrees 1026 | will be (-degrees, +degrees). Set to 0 to deactivate rotations. 1027 | translate (tuple, optional): tuple of maximum absolute fraction for horizontal 1028 | and vertical translations. For example translate=(a, b), then horizontal shift 1029 | is randomly sampled in the range -img_width * a < dx < img_width * a and vertical shift is 1030 | randomly sampled in the range -img_height * b < dy < img_height * b. Will not translate by default. 1031 | scale (tuple, optional): scaling factor interval, e.g (a, b), then scale is 1032 | randomly sampled from the range a <= scale <= b. Will keep original scale by default. 1033 | shear (sequence or float or int, optional): Range of degrees to select from. 1034 | If shear is a number, a shear parallel to the x axis in the range (-shear, +shear) 1035 | will be apllied. Else if shear is a tuple or list of 2 values a shear parallel to the x axis in the 1036 | range (shear[0], shear[1]) will be applied. Else if shear is a tuple or list of 4 values, 1037 | a x-axis shear in (shear[0], shear[1]) and y-axis shear in (shear[2], shear[3]) will be applied. 1038 | Will not apply shear by default 1039 | resample ({PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC}, optional): 1040 | An optional resampling filter. See `filters`_ for more information. 1041 | If omitted, or if the image has mode "1" or "P", it is set to PIL.Image.NEAREST. 1042 | fillcolor (tuple or int): Optional fill color (Tuple for RGB Image And int for grayscale) for the area 1043 | outside the transform in the output image.(Pillow>=5.0.0) 1044 | 1045 | .. _filters: https://pillow.readthedocs.io/en/latest/handbook/concepts.html#filters 1046 | 1047 | """ 1048 | # degree: rotate for the image; \in [-180, 180]; 旋转 1049 | # translate: translation for the image, \in [0,1] 平移 1050 | # scale: scale the image with center invariant, better \in (0,2] 放缩 1051 | # shear: shear the image with dx or dy, w\in [-180, 180] 扭曲 1052 | # eg. 1053 | # preprocess1 = myTransforms.RandomAffine(degrees=0, translate=[0, 0.2], scale=[0.8, 1.2], 1054 | # shear=[-10, 10, -10, 10], fillcolor=(228, 218, 218)) 1055 | def __init__(self, degrees, translate=None, scale=None, shear=None, resample=False, fillcolor=0): 1056 | if isinstance(degrees, numbers.Number): 1057 | if degrees < 0: 1058 | raise ValueError("If degrees is a single number, it must be positive.") 1059 | self.degrees = (-degrees, degrees) 1060 | else: 1061 | assert isinstance(degrees, (tuple, list)) and len(degrees) == 2, \ 1062 | "degrees should be a list or tuple and it must be of length 2." 1063 | self.degrees = degrees 1064 | 1065 | if translate is not None: 1066 | assert isinstance(translate, (tuple, list)) and len(translate) == 2, \ 1067 | "translate should be a list or tuple and it must be of length 2." 1068 | for t in translate: 1069 | if not (0.0 <= t <= 1.0): 1070 | raise ValueError("translation values should be between 0 and 1") 1071 | self.translate = translate 1072 | 1073 | if scale is not None: 1074 | assert isinstance(scale, (tuple, list)) and len(scale) == 2, \ 1075 | "scale should be a list or tuple and it must be of length 2." 1076 | for s in scale: 1077 | if s <= 0: 1078 | raise ValueError("scale values should be positive") 1079 | self.scale = scale 1080 | 1081 | if shear is not None: 1082 | if isinstance(shear, numbers.Number): 1083 | if shear < 0: 1084 | raise ValueError("If shear is a single number, it must be positive.") 1085 | self.shear = (-shear, shear) 1086 | else: 1087 | assert isinstance(shear, (tuple, list)) and \ 1088 | (len(shear) == 2 or len(shear) == 4), \ 1089 | "shear should be a list or tuple and it must be of length 2 or 4." 1090 | # X-Axis shear with [min, max] 1091 | if len(shear) == 2: 1092 | self.shear = [shear[0], shear[1], 0., 0.] 1093 | elif len(shear) == 4: 1094 | self.shear = [s for s in shear] 1095 | else: 1096 | self.shear = shear 1097 | 1098 | self.resample = resample 1099 | self.fillcolor = fillcolor 1100 | 1101 | @staticmethod 1102 | def get_params(degrees, translate, scale_ranges, shears, img_size): 1103 | """Get parameters for affine transformation 1104 | 1105 | Returns: 1106 | sequence: params to be passed to the affine transformation 1107 | """ 1108 | angle = random.uniform(degrees[0], degrees[1]) 1109 | if translate is not None: 1110 | max_dx = translate[0] * img_size[0] 1111 | max_dy = translate[1] * img_size[1] 1112 | translations = (np.round(random.uniform(-max_dx, max_dx)), 1113 | np.round(random.uniform(-max_dy, max_dy))) 1114 | else: 1115 | translations = (0, 0) 1116 | 1117 | if scale_ranges is not None: 1118 | scale = random.uniform(scale_ranges[0], scale_ranges[1]) 1119 | else: 1120 | scale = 1.0 1121 | 1122 | if shears is not None: 1123 | if len(shears) == 2: 1124 | shear = [random.uniform(shears[0], shears[1]), 0.] 1125 | elif len(shears) == 4: 1126 | shear = [random.uniform(shears[0], shears[1]), 1127 | random.uniform(shears[2], shears[3])] 1128 | else: 1129 | shear = 0.0 1130 | 1131 | return angle, translations, scale, shear 1132 | 1133 | def __call__(self, img): 1134 | """ 1135 | img (PIL Image): Image to be transformed. 1136 | 1137 | Returns: 1138 | PIL Image: Affine transformed image. 1139 | """ 1140 | ret = self.get_params(self.degrees, self.translate, self.scale, self.shear, img.size) 1141 | return F.affine(img, *ret, resample=self.resample, fillcolor=self.fillcolor) 1142 | 1143 | def __repr__(self): 1144 | s = '{name}(degrees={degrees}' 1145 | if self.translate is not None: 1146 | s += ', translate={translate}' 1147 | if self.scale is not None: 1148 | s += ', scale={scale}' 1149 | if self.shear is not None: 1150 | s += ', shear={shear}' 1151 | if self.resample > 0: 1152 | s += ', resample={resample}' 1153 | if self.fillcolor != 0: 1154 | s += ', fillcolor={fillcolor}' 1155 | s += ')' 1156 | d = dict(self.__dict__) 1157 | d['resample'] = _pil_interpolation_to_str[d['resample']] 1158 | return s.format(name=self.__class__.__name__, **d) 1159 | 1160 | 1161 | class Grayscale(object): 1162 | """Convert image to grayscale. 1163 | 1164 | Args: 1165 | num_output_channels (int): (1 or 3) number of channels desired for output image 1166 | 1167 | Returns: 1168 | PIL Image: Grayscale version of the input. 1169 | - If num_output_channels == 1 : returned image is single channel 1170 | - If num_output_channels == 3 : returned image is 3 channel with r == g == b 1171 | 1172 | """ 1173 | 1174 | def __init__(self, num_output_channels=1): 1175 | self.num_output_channels = num_output_channels 1176 | 1177 | def __call__(self, img): 1178 | """ 1179 | Args: 1180 | img (PIL Image): Image to be converted to grayscale. 1181 | 1182 | Returns: 1183 | PIL Image: Randomly grayscaled image. 1184 | """ 1185 | return F.to_grayscale(img, num_output_channels=self.num_output_channels) 1186 | 1187 | def __repr__(self): 1188 | return self.__class__.__name__ + '(num_output_channels={0})'.format(self.num_output_channels) 1189 | 1190 | 1191 | class RandomGrayscale(object): 1192 | """Randomly convert image to grayscale with a probability of p (default 0.1). 1193 | 1194 | Args: 1195 | p (float): probability that image should be converted to grayscale. 1196 | 1197 | Returns: 1198 | PIL Image: Grayscale version of the input image with probability p and unchanged 1199 | with probability (1-p). 1200 | - If input image is 1 channel: grayscale version is 1 channel 1201 | - If input image is 3 channel: grayscale version is 3 channel with r == g == b 1202 | 1203 | """ 1204 | 1205 | def __init__(self, p=0.1): 1206 | self.p = p 1207 | 1208 | def __call__(self, img): 1209 | """ 1210 | Args: 1211 | img (PIL Image): Image to be converted to grayscale. 1212 | 1213 | Returns: 1214 | PIL Image: Randomly grayscaled image. 1215 | """ 1216 | num_output_channels = 1 if img.mode == 'L' else 3 1217 | if random.random() < self.p: 1218 | return F.to_grayscale(img, num_output_channels=num_output_channels) 1219 | return img 1220 | 1221 | def __repr__(self): 1222 | return self.__class__.__name__ + '(p={0})'.format(self.p) 1223 | 1224 | 1225 | class RandomErasing(object): 1226 | """ Randomly selects a rectangle region in an image and erases its pixels. 1227 | 'Random Erasing Data Augmentation' by Zhong et al. 1228 | See https://arxiv.org/pdf/1708.04896.pdf 1229 | Args: 1230 | p: probability that the random erasing operation will be performed. 1231 | scale: range of proportion of erased area against input image. 1232 | ratio: range of aspect ratio of erased area. 1233 | value: erasing value. Default is 0. If a single int, it is used to 1234 | erase all pixels. If a tuple of length 3, it is used to erase 1235 | R, G, B channels respectively. 1236 | If a str of 'random', erasing each pixel with random values. 1237 | inplace: boolean to make this transform inplace. Default set to False. 1238 | 1239 | Returns: 1240 | Erased Image. 1241 | # Examples: 1242 | >>> transform = transforms.Compose([ 1243 | >>> transforms.RandomHorizontalFlip(), 1244 | >>> transforms.ToTensor(), 1245 | >>> transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), 1246 | >>> transforms.RandomErasing(), 1247 | >>> ]) 1248 | """ 1249 | 1250 | def __init__(self, p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False): 1251 | assert isinstance(value, (numbers.Number, str, tuple, list)) 1252 | if (scale[0] > scale[1]) or (ratio[0] > ratio[1]): 1253 | warnings.warn("range should be of kind (min, max)") 1254 | if scale[0] < 0 or scale[1] > 1: 1255 | raise ValueError("range of scale should be between 0 and 1") 1256 | if p < 0 or p > 1: 1257 | raise ValueError("range of random erasing probability should be between 0 and 1") 1258 | 1259 | self.p = p 1260 | self.scale = scale 1261 | self.ratio = ratio 1262 | self.value = value 1263 | self.inplace = inplace 1264 | 1265 | @staticmethod 1266 | def get_params(img, scale, ratio, value=0): 1267 | """Get parameters for ``erase`` for a random erasing. 1268 | 1269 | Args: 1270 | img (Tensor): Tensor image of size (C, H, W) to be erased. 1271 | scale: range of proportion of erased area against input image. 1272 | ratio: range of aspect ratio of erased area. 1273 | 1274 | Returns: 1275 | tuple: params (i, j, h, w, v) to be passed to ``erase`` for random erasing. 1276 | """ 1277 | img_c, img_h, img_w = img.shape 1278 | area = img_h * img_w 1279 | 1280 | for attempt in range(10): 1281 | erase_area = random.uniform(scale[0], scale[1]) * area 1282 | aspect_ratio = random.uniform(ratio[0], ratio[1]) 1283 | 1284 | h = int(round(math.sqrt(erase_area * aspect_ratio))) 1285 | w = int(round(math.sqrt(erase_area / aspect_ratio))) 1286 | 1287 | if h < img_h and w < img_w: 1288 | i = random.randint(0, img_h - h) 1289 | j = random.randint(0, img_w - w) 1290 | if isinstance(value, numbers.Number): 1291 | v = value 1292 | elif isinstance(value, torch._six.string_classes): 1293 | v = torch.empty([img_c, h, w], dtype=torch.float32).normal_() 1294 | elif isinstance(value, (list, tuple)): 1295 | v = torch.tensor(value, dtype=torch.float32).view(-1, 1, 1).expand(-1, h, w) 1296 | return i, j, h, w, v 1297 | 1298 | # Return original image 1299 | return 0, 0, img_h, img_w, img 1300 | 1301 | def __call__(self, img): 1302 | """ 1303 | Args: 1304 | img (Tensor): Tensor image of size (C, H, W) to be erased. 1305 | 1306 | Returns: 1307 | img (Tensor): Erased Tensor image. 1308 | """ 1309 | if random.uniform(0, 1) < self.p: 1310 | x, y, h, w, v = self.get_params(img, scale=self.scale, ratio=self.ratio, value=self.value) 1311 | return F.erase(img, x, y, h, w, v, self.inplace) 1312 | return img 1313 | 1314 | 1315 | class HEDJitter(object): 1316 | """Randomly perturbe the HED color space value an RGB image. 1317 | First, it disentangled the hematoxylin and eosin color channels by color deconvolution method using a fixed matrix. 1318 | Second, it perturbed the hematoxylin, eosin and DAB stains independently. 1319 | Third, it transformed the resulting stains into regular RGB color space. 1320 | Args: 1321 | theta (float): How much to jitter HED color space, 1322 | alpha is chosen from a uniform distribution [1-theta, 1+theta] 1323 | betti is chosen from a uniform distribution [-theta, theta] 1324 | the jitter formula is **s' = \alpha * s + \betti** 1325 | """ 1326 | def __init__(self, theta=0.): # HED_light: theta=0.05; HED_strong: theta=0.2 1327 | assert isinstance(theta, numbers.Number), "theta should be a single number." 1328 | self.theta = theta 1329 | self.alpha = np.random.uniform(1-theta, 1+theta, (1, 3)) 1330 | self.betti = np.random.uniform(-theta, theta, (1, 3)) 1331 | 1332 | @staticmethod 1333 | def adjust_HED(img, alpha, betti): 1334 | img = np.array(img) 1335 | 1336 | s = np.reshape(color.rgb2hed(img), (-1, 3)) 1337 | ns = alpha * s + betti # perturbations on HED color space 1338 | nimg = color.hed2rgb(np.reshape(ns, img.shape)) 1339 | 1340 | imin = nimg.min() 1341 | imax = nimg.max() 1342 | rsimg = (255 * (nimg - imin) / (imax - imin)).astype('uint8') # rescale to [0,255] 1343 | # transfer to PIL image 1344 | return Image.fromarray(rsimg) 1345 | 1346 | def __call__(self, img): 1347 | return self.adjust_HED(img, self.alpha, self.betti) 1348 | 1349 | def __repr__(self): 1350 | format_string = self.__class__.__name__ + '(' 1351 | format_string += 'theta={0}'.format(self.theta) 1352 | format_string += ',alpha={0}'.format(self.alpha) 1353 | format_string += ',betti={0}'.format(self.betti) 1354 | return format_string 1355 | 1356 | 1357 | class AutoRandomRotation(object): 1358 | """auto randomly select angle 0, 90, 180 or 270 for rotating the image. 1359 | 1360 | Args: 1361 | resample ({PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC}, optional): 1362 | An optional resampling filter. See `filters`_ for more information. 1363 | If omitted, or if the image has mode "1" or "P", it is set to PIL.Image.NEAREST. 1364 | expand (bool, optional): Optional expansion flag. 1365 | If true, expands the output to make it large enough to hold the entire rotated image. 1366 | If false or omitted, make the output image the same size as the input image. 1367 | Note that the expand flag assumes rotation around the center and no translation. 1368 | center (2-tuple, optional): Optional center of rotation. 1369 | Origin is the upper left corner. 1370 | Default is the center of the image. 1371 | fill (3-tuple or int): RGB pixel fill value for area outside the rotated image. 1372 | If int, it is used for all channels respectively. 1373 | """ 1374 | 1375 | def __init__(self, degree=None, resample=False, expand=True, center=None, fill=0): 1376 | if degree is None: 1377 | self.degrees = random.choice([0, 90, 180, 270]) 1378 | else: 1379 | assert degree in [0, 90, 180, 270], 'degree must be in [0, 90, 180, 270]' 1380 | self.degrees = degree 1381 | 1382 | self.resample = resample 1383 | self.expand = expand 1384 | self.center = center 1385 | self.fill = fill 1386 | 1387 | def __call__(self, img): 1388 | return F.rotate(img, self.degrees, self.resample, self.expand, self.center, self.fill) 1389 | 1390 | def __repr__(self): 1391 | format_string = self.__class__.__name__ + '(degrees={0}'.format(self.degrees) 1392 | format_string += ', resample={0}'.format(self.resample) 1393 | format_string += ', expand={0}'.format(self.expand) 1394 | if self.center is not None: 1395 | format_string += ', center={0}'.format(self.center) 1396 | format_string += ')' 1397 | return format_string 1398 | 1399 | 1400 | class RandomGaussBlur(object): 1401 | """Random GaussBlurring on image by radius parameter. 1402 | Args: 1403 | radius (list, tuple): radius range for selecting from; you'd better set it < 2 1404 | """ 1405 | def __init__(self, radius=None): 1406 | if radius is not None: 1407 | assert isinstance(radius, (tuple, list)) and len(radius) == 2, \ 1408 | "radius should be a list or tuple and it must be of length 2." 1409 | self.radius = random.uniform(radius[0], radius[1]) 1410 | else: 1411 | self.radius = 0.0 1412 | 1413 | def __call__(self, img): 1414 | return img.filter(ImageFilter.GaussianBlur(radius=self.radius)) 1415 | 1416 | def __repr__(self): 1417 | return self.__class__.__name__ + '(Gaussian Blur radius={0})'.format(self.radius) 1418 | 1419 | 1420 | class RandomAffineCV2(object): 1421 | """Random Affine transformation by CV2 method on image by alpha parameter. 1422 | Args: 1423 | alpha (float): alpha value for affine transformation 1424 | mask (PIL Image) in __call__, if not assign, set None. 1425 | """ 1426 | def __init__(self, alpha): 1427 | assert isinstance(alpha, numbers.Number), "alpha should be a single number." 1428 | assert 0. <= alpha <= 0.15, \ 1429 | "In pathological image, alpha should be in (0,0.15), you can change in myTransform.py" 1430 | self.alpha = alpha 1431 | 1432 | @staticmethod 1433 | def affineTransformCV2(img, alpha, mask=None): 1434 | alpha = img.shape[1] * alpha 1435 | if mask is not None: 1436 | mask = np.array(mask).astype(np.uint8) 1437 | img = np.concatenate((img, mask[..., None]), axis=2) 1438 | 1439 | imgsize = img.shape[:2] 1440 | center = np.float32(imgsize) // 2 1441 | censize = min(imgsize) // 3 1442 | pts1 = np.float32([center+censize, [center[0]+censize, center[1]-censize], center-censize]) # raw point 1443 | pts2 = pts1 + np.random.uniform(-alpha, alpha, size=pts1.shape).astype(np.float32) # output point 1444 | M = cv2.getAffineTransform(pts1, pts2) # affine matrix 1445 | img = cv2.warpAffine(img, M, imgsize[::-1], 1446 | flags=cv2.INTER_NEAREST, borderMode=cv2.BORDER_REFLECT_101) 1447 | if mask is not None: 1448 | return Image.fromarray(img[..., :3]), Image.fromarray(img[..., 3]) 1449 | else: 1450 | return Image.fromarray(img) 1451 | 1452 | def __call__(self, img, mask=None): 1453 | return self.affineTransformCV2(np.array(img), self.alpha, mask) 1454 | 1455 | def __repr__(self): 1456 | return self.__class__.__name__ + '(alpha value={0})'.format(self.alpha) 1457 | 1458 | 1459 | class RandomElastic(object): 1460 | """Random Elastic transformation by CV2 method on image by alpha, sigma parameter. 1461 | # you can refer to: https://blog.csdn.net/qq_27261889/article/details/80720359 1462 | # https://blog.csdn.net/maliang_1993/article/details/82020596 1463 | # https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html#scipy.ndimage.map_coordinates 1464 | Args: 1465 | alpha (float): alpha value for Elastic transformation, factor 1466 | if alpha is 0, output is original whatever the sigma; 1467 | if alpha is 1, output only depends on sigma parameter; 1468 | if alpha < 1 or > 1, it zoom in or out the sigma's Relevant dx, dy. 1469 | sigma (float): sigma value for Elastic transformation, should be \ in (0.05,0.1) 1470 | mask (PIL Image) in __call__, if not assign, set None. 1471 | """ 1472 | def __init__(self, alpha, sigma): 1473 | assert isinstance(alpha, numbers.Number) and isinstance(sigma, numbers.Number), \ 1474 | "alpha and sigma should be a single number." 1475 | assert 0.05 <= sigma <= 0.1, \ 1476 | "In pathological image, sigma should be in (0.05,0.1)" 1477 | self.alpha = alpha 1478 | self.sigma = sigma 1479 | 1480 | @staticmethod 1481 | def RandomElasticCV2(img, alpha, sigma, mask=None): 1482 | alpha = img.shape[1] * alpha 1483 | sigma = img.shape[1] * sigma 1484 | if mask is not None: 1485 | mask = np.array(mask).astype(np.uint8) 1486 | img = np.concatenate((img, mask[..., None]), axis=2) 1487 | 1488 | shape = img.shape 1489 | 1490 | dx = gaussian_filter((np.random.rand(*shape) * 2 - 1), sigma) * alpha 1491 | dy = gaussian_filter((np.random.rand(*shape) * 2 - 1), sigma) * alpha 1492 | # dz = np.zeros_like(dx) 1493 | 1494 | x, y, z = np.meshgrid(np.arange(shape[1]), np.arange(shape[0]), np.arange(shape[2])) 1495 | indices = np.reshape(y + dy, (-1, 1)), np.reshape(x + dx, (-1, 1)), np.reshape(z, (-1, 1)) 1496 | 1497 | img = map_coordinates(img, indices, order=0, mode='reflect').reshape(shape) 1498 | if mask is not None: 1499 | return Image.fromarray(img[..., :3]), Image.fromarray(img[..., 3]) 1500 | else: 1501 | return Image.fromarray(img) 1502 | 1503 | def __call__(self, img, mask=None): 1504 | return self.RandomElasticCV2(np.array(img), self.alpha, self.sigma, mask) 1505 | 1506 | def __repr__(self): 1507 | format_string = self.__class__.__name__ + '(alpha value={0})'.format(self.alpha) 1508 | format_string += ', sigma={0}'.format(self.sigma) 1509 | format_string += ')' 1510 | return format_string 1511 | 1512 | 1513 | --------------------------------------------------------------------------------