Spaces:
Sleeping
Sleeping
add dps mb
Browse files- diffusion-posterior-sampling/motionblur/README.md +69 -0
- diffusion-posterior-sampling/motionblur/__init__.py +0 -0
- diffusion-posterior-sampling/motionblur/__pycache__/__init__.cpython-38.pyc +0 -0
- diffusion-posterior-sampling/motionblur/__pycache__/motionblur.cpython-38.pyc +0 -0
- diffusion-posterior-sampling/motionblur/environment.yaml +20 -0
- diffusion-posterior-sampling/motionblur/example_kernel/kernel0.png +0 -0
- diffusion-posterior-sampling/motionblur/example_kernel/kernel100.png +0 -0
- diffusion-posterior-sampling/motionblur/example_kernel/kernel25.png +0 -0
- diffusion-posterior-sampling/motionblur/example_kernel/kernel50.png +0 -0
- diffusion-posterior-sampling/motionblur/example_kernel/kernel75.png +0 -0
- diffusion-posterior-sampling/motionblur/images/flag.png +0 -0
- diffusion-posterior-sampling/motionblur/images/flagBLURRED.png +0 -0
- diffusion-posterior-sampling/motionblur/images/moon.png +0 -0
- diffusion-posterior-sampling/motionblur/intensity.png +0 -0
- diffusion-posterior-sampling/motionblur/motionblur.py +419 -0
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()
|