LituRout commited on
Commit
a8a10a3
1 Parent(s): 0de10e5

add dps mb

Browse files
diffusion-posterior-sampling/motionblur/README.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MotionBlur
2
+
3
+ Generate authentic motion blur kernels (point spread functions) and apply them to images en masse.
4
+
5
+ Very efficient thanks to numpy's FFT based convolution and the optimised procedural generation of kernels. Intuitive API.
6
+
7
+ # Description
8
+
9
+ After installation, import the `Kernel` class from `motionblur.py` and use to your liking.
10
+
11
+ Here is how:
12
+
13
+ Initialise a `Kernel` instance with the parameters `size` (size of kernel matrix in pixels - as a tuple of integers) and `intensity`.
14
+
15
+ Intensity determines how non-linear and shaken the motion blur is. It must have a value between 0 and 1.
16
+ Zero is a linear motion and 1 a highly non-linear and often self intersecting motion.
17
+
18
+ ![Effect of intensity](./intensity.png)
19
+
20
+ Once a kernel is initialised, you can utilise a range of properties to make us of it.
21
+
22
+ ```python
23
+ # Initialise Kernel
24
+ kernel = Kernel(size=(100, 100), intensity=0.2)
25
+
26
+ # Display kernel
27
+ kernel.displayKernel()
28
+
29
+ # Get kernel as numpy array
30
+ kernel.kernelMatrix
31
+
32
+ # Save kernel as image. (Do not show kernel, just save.)
33
+ kernel.displayKernel(save_to="./my_file.png", show=False)
34
+
35
+ # load image or get image path
36
+ image1_path = "./image1.png"
37
+ image2 = PIL.Image.open("./image2.png")
38
+
39
+ # apply motion blur (returns PIL.Image instance of blurred image)
40
+ blurred1 = kernel.applyTo(image1_path)
41
+
42
+ blurred2 = kernel.applyTo(image2)
43
+
44
+ # if you need the dimension of the blurred image to be the same
45
+ # as the original image, pass `keep_image_dim=True`
46
+ blurred_same = kernel.applyTo(image2, keep_image_dim=True)
47
+
48
+ # show result
49
+ blurred1.show()
50
+
51
+ # or save to file
52
+ blurred2.save("./output2.png", "PNG")
53
+ ```
54
+
55
+
56
+ # Installation
57
+
58
+ In order to set up the necessary environment:
59
+
60
+ 1. create an environment `MotionBlur` with the help of conda,
61
+ ```
62
+ conda env create - f environment.yaml
63
+ ```
64
+ 2. activate the new environment with
65
+ ```
66
+ conda activate MotionBlur
67
+ ```
68
+
69
+ Or simply install numpy, pillow and scipy manually.
diffusion-posterior-sampling/motionblur/__init__.py ADDED
File without changes
diffusion-posterior-sampling/motionblur/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (171 Bytes). View file
 
diffusion-posterior-sampling/motionblur/__pycache__/motionblur.cpython-38.pyc ADDED
Binary file (10.5 kB). View file
 
diffusion-posterior-sampling/motionblur/environment.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: MotionBlur
2
+ channels:
3
+ - defaults
4
+ - conda-forge
5
+ dependencies:
6
+ - python>=3.6
7
+ - pip
8
+ - numpy
9
+ - scipy
10
+ - Pillow
11
+
12
+ # for development only (could also be kept in a separate environment file)
13
+ - pytest
14
+ - pytest-cov
15
+ - tox
16
+ - pre_commit
17
+ - nbdime
18
+ - nbstripout
19
+ - sphinx
20
+ - recommonmark
diffusion-posterior-sampling/motionblur/example_kernel/kernel0.png ADDED
diffusion-posterior-sampling/motionblur/example_kernel/kernel100.png ADDED
diffusion-posterior-sampling/motionblur/example_kernel/kernel25.png ADDED
diffusion-posterior-sampling/motionblur/example_kernel/kernel50.png ADDED
diffusion-posterior-sampling/motionblur/example_kernel/kernel75.png ADDED
diffusion-posterior-sampling/motionblur/images/flag.png ADDED
diffusion-posterior-sampling/motionblur/images/flagBLURRED.png ADDED
diffusion-posterior-sampling/motionblur/images/moon.png ADDED
diffusion-posterior-sampling/motionblur/intensity.png ADDED
diffusion-posterior-sampling/motionblur/motionblur.py ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from PIL import Image, ImageDraw, ImageFilter
3
+ from numpy.random import uniform, triangular, beta
4
+ from math import pi
5
+ from pathlib import Path
6
+ from scipy.signal import convolve
7
+
8
+ # tiny error used for nummerical stability
9
+ eps = 0.1
10
+
11
+
12
+ def softmax(x):
13
+ """Compute softmax values for each sets of scores in x."""
14
+ e_x = np.exp(x - np.max(x))
15
+ return e_x / e_x.sum()
16
+
17
+
18
+ def norm(lst: list) -> float:
19
+ """[summary]
20
+ L^2 norm of a list
21
+ [description]
22
+ Used for internals
23
+ Arguments:
24
+ lst {list} -- vector
25
+ """
26
+ if not isinstance(lst, list):
27
+ raise ValueError("Norm takes a list as its argument")
28
+
29
+ if lst == []:
30
+ return 0
31
+
32
+ return (sum((i**2 for i in lst)))**0.5
33
+
34
+
35
+ def polar2z(r: np.ndarray, θ: np.ndarray) -> np.ndarray:
36
+ """[summary]
37
+ Takes a list of radii and angles (radians) and
38
+ converts them into a corresponding list of complex
39
+ numbers x + yi.
40
+ [description]
41
+
42
+ Arguments:
43
+ r {np.ndarray} -- radius
44
+ θ {np.ndarray} -- angle
45
+
46
+ Returns:
47
+ [np.ndarray] -- list of complex numbers r e^(i theta) as x + iy
48
+ """
49
+ return r * np.exp(1j * θ)
50
+
51
+
52
+ class Kernel(object):
53
+ """[summary]
54
+ Class representing a motion blur kernel of a given intensity.
55
+
56
+ [description]
57
+ Keyword Arguments:
58
+ size {tuple} -- Size of the kernel in px times px
59
+ (default: {(100, 100)})
60
+
61
+ intensity {float} -- Float between 0 and 1.
62
+ Intensity of the motion blur.
63
+
64
+ : 0 means linear motion blur and 1 is a highly non linear
65
+ and often convex motion blur path. (default: {0})
66
+
67
+ Attribute:
68
+ kernelMatrix -- Numpy matrix of the kernel of given intensity
69
+
70
+ Properties:
71
+ applyTo -- Applies kernel to image
72
+ (pass as path, pillow image or np array)
73
+
74
+ Raises:
75
+ ValueError
76
+ """
77
+
78
+ def __init__(self, size: tuple = (100, 100), intensity: float=0):
79
+
80
+ # checking if size is correctly given
81
+ if not isinstance(size, tuple):
82
+ raise ValueError("Size must be TUPLE of 2 positive integers")
83
+ elif len(size) != 2 or type(size[0]) != type(size[1]) != int:
84
+ raise ValueError("Size must be tuple of 2 positive INTEGERS")
85
+ elif size[0] < 0 or size[1] < 0:
86
+ raise ValueError("Size must be tuple of 2 POSITIVE integers")
87
+
88
+ # check if intensity is float (int) between 0 and 1
89
+ if type(intensity) not in [int, float, np.float32, np.float64]:
90
+ raise ValueError("Intensity must be a number between 0 and 1")
91
+ elif intensity < 0 or intensity > 1:
92
+ raise ValueError("Intensity must be a number between 0 and 1")
93
+
94
+ # saving args
95
+ self.SIZE = size
96
+ self.INTENSITY = intensity
97
+
98
+ # deriving quantities
99
+
100
+ # we super size first and then downscale at the end for better
101
+ # anti-aliasing
102
+ self.SIZEx2 = tuple([2 * i for i in size])
103
+ self.x, self.y = self.SIZEx2
104
+
105
+ # getting length of kernel diagonal
106
+ self.DIAGONAL = (self.x**2 + self.y**2)**0.5
107
+
108
+ # flag to see if kernel has been calculated already
109
+ self.kernel_is_generated = False
110
+
111
+ def _createPath(self):
112
+ """[summary]
113
+ creates a motion blur path with the given intensity.
114
+ [description]
115
+ Proceede in 5 steps
116
+ 1. Get a random number of random step sizes
117
+ 2. For each step get a random angle
118
+ 3. combine steps and angles into a sequence of increments
119
+ 4. create path out of increments
120
+ 5. translate path to fit the kernel dimensions
121
+
122
+ NOTE: "random" means random but might depend on the given intensity
123
+ """
124
+
125
+ # first we find the lengths of the motion blur steps
126
+ def getSteps():
127
+ """[summary]
128
+ Here we calculate the length of the steps taken by
129
+ the motion blur
130
+ [description]
131
+ We want a higher intensity lead to a longer total motion
132
+ blur path and more different steps along the way.
133
+
134
+ Hence we sample
135
+
136
+ MAX_PATH_LEN =[U(0,1) + U(0, intensity^2)] * diagonal * 0.75
137
+
138
+ and each step: beta(1, 30) * (1 - self.INTENSITY + eps) * diagonal)
139
+ """
140
+
141
+ # getting max length of blur motion
142
+ self.MAX_PATH_LEN = 0.75 * self.DIAGONAL * \
143
+ (uniform() + uniform(0, self.INTENSITY**2))
144
+
145
+ # getting step
146
+ steps = []
147
+
148
+ while sum(steps) < self.MAX_PATH_LEN:
149
+
150
+ # sample next step
151
+ step = beta(1, 30) * (1 - self.INTENSITY + eps) * self.DIAGONAL
152
+ if step < self.MAX_PATH_LEN:
153
+ steps.append(step)
154
+
155
+ # note the steps and the total number of steps
156
+ self.NUM_STEPS = len(steps)
157
+ self.STEPS = np.asarray(steps)
158
+
159
+ def getAngles():
160
+ """[summary]
161
+ Gets an angle for each step
162
+ [description]
163
+ The maximal angle should be larger the more
164
+ intense the motion is. So we sample it from a
165
+ U(0, intensity * pi)
166
+
167
+ We sample "jitter" from a beta(2,20) which is the probability
168
+ that the next angle has a different sign than the previous one.
169
+ """
170
+
171
+ # same as with the steps
172
+
173
+ # first we get the max angle in radians
174
+ self.MAX_ANGLE = uniform(0, self.INTENSITY * pi)
175
+
176
+ # now we sample "jitter" which is the probability that the
177
+ # next angle has a different sign than the previous one
178
+ self.JITTER = beta(2, 20)
179
+
180
+ # initialising angles (and sign of angle)
181
+ angles = [uniform(low=-self.MAX_ANGLE, high=self.MAX_ANGLE)]
182
+
183
+ while len(angles) < self.NUM_STEPS:
184
+
185
+ # sample next angle (absolute value)
186
+ angle = triangular(0, self.INTENSITY *
187
+ self.MAX_ANGLE, self.MAX_ANGLE + eps)
188
+
189
+ # with jitter probability change sign wrt previous angle
190
+ if uniform() < self.JITTER:
191
+ angle *= - np.sign(angles[-1])
192
+ else:
193
+ angle *= np.sign(angles[-1])
194
+
195
+ angles.append(angle)
196
+
197
+ # save angles
198
+ self.ANGLES = np.asarray(angles)
199
+
200
+ # Get steps and angles
201
+ getSteps()
202
+ getAngles()
203
+
204
+ # Turn them into a path
205
+ ####
206
+
207
+ # we turn angles and steps into complex numbers
208
+ complex_increments = polar2z(self.STEPS, self.ANGLES)
209
+
210
+ # generate path as the cumsum of these increments
211
+ self.path_complex = np.cumsum(complex_increments)
212
+
213
+ # find center of mass of path
214
+ self.com_complex = sum(self.path_complex) / self.NUM_STEPS
215
+
216
+ # Shift path s.t. center of mass lies in the middle of
217
+ # the kernel and a apply a random rotation
218
+ ###
219
+
220
+ # center it on COM
221
+ center_of_kernel = (self.x + 1j * self.y) / 2
222
+ self.path_complex -= self.com_complex
223
+
224
+ # randomly rotate path by an angle a in (0, pi)
225
+ self.path_complex *= np.exp(1j * uniform(0, pi))
226
+
227
+ # center COM on center of kernel
228
+ self.path_complex += center_of_kernel
229
+
230
+ # convert complex path to final list of coordinate tuples
231
+ self.path = [(i.real, i.imag) for i in self.path_complex]
232
+
233
+ def _createKernel(self, save_to: Path=None, show: bool=False):
234
+ """[summary]
235
+ Finds a kernel (psf) of given intensity.
236
+ [description]
237
+ use displayKernel to actually see the kernel.
238
+
239
+ Keyword Arguments:
240
+ save_to {Path} -- Image file to save the kernel to. {None}
241
+ show {bool} -- shows kernel if true
242
+ """
243
+
244
+ # check if we haven't already generated a kernel
245
+ if self.kernel_is_generated:
246
+ return None
247
+
248
+ # get the path
249
+ self._createPath()
250
+
251
+ # Initialise an image with super-sized dimensions
252
+ # (pillow Image object)
253
+ self.kernel_image = Image.new("RGB", self.SIZEx2)
254
+
255
+ # ImageDraw instance that is linked to the kernel image that
256
+ # we can use to draw on our kernel_image
257
+ self.painter = ImageDraw.Draw(self.kernel_image)
258
+
259
+ # draw the path
260
+ self.painter.line(xy=self.path, width=int(self.DIAGONAL / 150))
261
+
262
+ # applying gaussian blur for realism
263
+ self.kernel_image = self.kernel_image.filter(
264
+ ImageFilter.GaussianBlur(radius=int(self.DIAGONAL * 0.01)))
265
+
266
+ # Resize to actual size
267
+ self.kernel_image = self.kernel_image.resize(
268
+ self.SIZE, resample=Image.LANCZOS)
269
+
270
+ # convert to gray scale
271
+ self.kernel_image = self.kernel_image.convert("L")
272
+
273
+ # flag that we have generated a kernel
274
+ self.kernel_is_generated = True
275
+
276
+ def displayKernel(self, save_to: Path=None, show: bool=True):
277
+ """[summary]
278
+ Finds a kernel (psf) of given intensity.
279
+ [description]
280
+ Saves the kernel to save_to if needed or shows it
281
+ is show true
282
+
283
+ Keyword Arguments:
284
+ save_to {Path} -- Image file to save the kernel to. {None}
285
+ show {bool} -- shows kernel if true
286
+ """
287
+
288
+ # generate kernel if needed
289
+ self._createKernel()
290
+
291
+ # save if needed
292
+ if save_to is not None:
293
+
294
+ save_to_file = Path(save_to)
295
+
296
+ # save Kernel image
297
+ self.kernel_image.save(save_to_file)
298
+ else:
299
+ # Show kernel
300
+ self.kernel_image.show()
301
+
302
+ @property
303
+ def kernelMatrix(self) -> np.ndarray:
304
+ """[summary]
305
+ Kernel matrix of motion blur of given intensity.
306
+ [description]
307
+ Once generated, it stays the same.
308
+ Returns:
309
+ numpy ndarray
310
+ """
311
+
312
+ # generate kernel if needed
313
+ self._createKernel()
314
+ kernel = np.asarray(self.kernel_image, dtype=np.float32)
315
+ kernel /= np.sum(kernel)
316
+
317
+ return kernel
318
+
319
+ @kernelMatrix.setter
320
+ def kernelMatrix(self, *kargs):
321
+ raise NotImplementedError("Can't manually set kernel matrix yet")
322
+
323
+ def applyTo(self, image, keep_image_dim: bool = False) -> Image:
324
+ """[summary]
325
+ Applies kernel to one of the following:
326
+
327
+ 1. Path to image file
328
+ 2. Pillow image object
329
+ 3. (H,W,3)-shaped numpy array
330
+ [description]
331
+
332
+ Arguments:
333
+ image {[str, Path, Image, np.ndarray]}
334
+ keep_image_dim {bool} -- If true, then we will
335
+ conserve the image dimension after blurring
336
+ by using "same" convolution instead of "valid"
337
+ convolution inside the scipy convolve function.
338
+
339
+ Returns:
340
+ Image -- [description]
341
+ """
342
+ # calculate kernel if haven't already
343
+ self._createKernel()
344
+
345
+ def applyToPIL(image: Image, keep_image_dim: bool = False) -> Image:
346
+ """[summary]
347
+ Applies the kernel to an PIL.Image instance
348
+ [description]
349
+ converts to RGB and applies the kernel to each
350
+ band before recombining them.
351
+ Arguments:
352
+ image {Image} -- Image to convolve
353
+ keep_image_dim {bool} -- If true, then we will
354
+ conserve the image dimension after blurring
355
+ by using "same" convolution instead of "valid"
356
+ convolution inside the scipy convolve function.
357
+
358
+ Returns:
359
+ Image -- blurred image
360
+ """
361
+ # convert to RGB
362
+ image = image.convert(mode="RGB")
363
+
364
+ conv_mode = "valid"
365
+ if keep_image_dim:
366
+ conv_mode = "same"
367
+
368
+ result_bands = ()
369
+
370
+ for band in image.split():
371
+
372
+ # convolve each band individually with kernel
373
+ result_band = convolve(
374
+ band, self.kernelMatrix, mode=conv_mode).astype("uint8")
375
+
376
+ # collect bands
377
+ result_bands += result_band,
378
+
379
+ # stack bands back together
380
+ result = np.dstack(result_bands)
381
+
382
+ # Get image
383
+ return Image.fromarray(result)
384
+
385
+ # If image is Path
386
+ if isinstance(image, str) or isinstance(image, Path):
387
+
388
+ # open image as Image class
389
+ image_path = Path(image)
390
+ image = Image.open(image_path)
391
+
392
+ return applyToPIL(image, keep_image_dim)
393
+
394
+ elif isinstance(image, Image.Image):
395
+
396
+ # apply kernel
397
+ return applyToPIL(image, keep_image_dim)
398
+
399
+ elif isinstance(image, np.ndarray):
400
+
401
+ # ASSUMES we have an array of the form (H, W, 3)
402
+ ###
403
+
404
+ # initiate Image object from array
405
+ image = Image.fromarray(image)
406
+
407
+ return applyToPIL(image, keep_image_dim)
408
+
409
+ else:
410
+
411
+ raise ValueError("Cannot apply kernel to this type.")
412
+
413
+
414
+ if __name__ == '__main__':
415
+ image = Image.open("./images/moon.png")
416
+ image.show()
417
+ k = Kernel()
418
+
419
+ k.applyTo(image, keep_image_dim=True).show()