juliuse commited on
Commit
b376d74
·
1 Parent(s): c7d39aa

motionblur package

Browse files
README.md CHANGED
@@ -1,12 +1,70 @@
1
- ---
2
- title: FLAIR
3
- emoji: 🎨
4
- colorFrom: indigo
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: "5.32.0"
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ # FLAIR: Flow-Based Latent Alignment for Image Restoration
4
+
5
+ **Julius Erbach<sup>1</sup>, Dominik Narnhofer<sup>1</sup>, Andreas Dombos<sup>1</sup>, Jan Eric Lenssen<sup>1</sup>, Bernt Schiele<sup>2</sup>, Konrad Schindler<sup>1</sup>**
6
+ <br>
7
+ <sup>1</sup> Photogrammetry and Remote Sensing, ETH Zurich
8
+ <sup>2</sup> Max Planck Institute for Informatics, Saarbrücken
9
+
10
+ [![Paper](https://img.shields.io/badge/arXiv-PDF-b31b1b)](link)
11
+ [![Page](https://img.shields.io/badge/Project-Page-green)](inverseFLAIR.github.io)
12
+ [![Hugging Face Space](https://img.shields.io/badge/🤗%20Hugging%20Face-Space-yellow)](link)
13
+ </div>
14
+
15
+ <p align="center">
16
+ <img src="assets/teaser3.svg" alt="teaser" width=98%"/>
17
+ </p>
18
+ <p align="center">
19
+ <emph>FLAIR</emph> is a novel approach for solving inverse imaging problems using flow-based posterior sampling.
20
+ </p>
21
+
22
+ ## Installation
23
+
24
+ 1. Clone the repository:
25
+ ```bash
26
+ git clone <your-repo-url>
27
+ cd <your-repo-name>
28
+ ```
29
+
30
+ 2. Create a virtual environment (recommended):
31
+ ```bash
32
+ python3 -m venv venv
33
+ source venv/bin/activate
34
+ ```
35
+
36
+ 3. Install the required dependencies from `requirements.txt`:
37
+ ```bash
38
+ pip install -r requirements.txt
39
+ pip install .
40
+ ```
41
+
42
+ ## Running Inference
43
+
44
+ To run inference, you can use one of the Python script run_image_inv.py with the according config file.
45
+ An example from the FFQH dataset
46
+ ```bash
47
+ python inference_scripts/run_image_inv.py --config configs/inpainting.yaml --target_file examples/girl.png --result_folder output --prompt="a high quality photo of a face"
48
+ ```
49
+ Or an example from the DIV2K dataset with captions provided by DAPE using the degraded input. The masks can be defined as rectanlge coordinates in the config file or provided as .npy file where true pixels are observed and false are masked out.
50
+
51
+ ```bash
52
+ python inference_scripts/run_image_inv.py --config configs/inpainting.yaml --target_file examples/sunflowers.png --result_folder output --prompt="a high quality photo of bloom, blue, field, flower, sky, sunflower, sunflower field, yellow" --mask_file DIV2k_mask.npy
53
+ ```
54
+
55
+ ```bash
56
+ python inference_scripts/run_image_inv.py --config configs/x12.yaml --target_file examples/sunflowers.png --result_folder output --prompt="a high quality photo of bloom, blue, field, flower, sky, sunflower, sunflower field, yellow"
57
+ ```
58
+
59
+ ## Citation
60
+
61
+ If you find this work useful in your research, please cite our paper:
62
+
63
+ ```bibtex
64
+ @article{er2025solving,
65
+ title={Solving Inverse Problems with FLAIR},
66
+ author={Erbach, Julius and Narnhofer, Dominik and Dombos, Andreas and Lenssen, Jan Eric and Schiele, Bernt and Schindler, Konrad},
67
+ journal={arXiv},
68
+ year={2025}
69
+ }
70
+ ```
requirements.txt CHANGED
@@ -15,6 +15,4 @@ opencv-python
15
  sentencepiece
16
  protobuf
17
  accelerate
18
- gradio
19
- gradio_imageslider
20
- /home/user/app/.
 
15
  sentencepiece
16
  protobuf
17
  accelerate
18
+ gradio
 
 
src/flair/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.
src/flair/motionblur/__init__.py ADDED
File without changes
src/flair/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
src/flair/motionblur/example_kernel/kernel0.png ADDED

Git LFS Details

  • SHA256: a18fa663e8546d9b5c787bc9f17af0091ad2d846c7a0a8cdfc4485394038f614
  • Pointer size: 129 Bytes
  • Size of remote file: 2.35 kB
src/flair/motionblur/example_kernel/kernel100.png ADDED

Git LFS Details

  • SHA256: 6685cf14b2d7d58cad0ffa6154073b3cb221376802303d43a241ef4bd291808c
  • Pointer size: 129 Bytes
  • Size of remote file: 3.51 kB
src/flair/motionblur/example_kernel/kernel25.png ADDED

Git LFS Details

  • SHA256: f36cdeca44bb8ea2be046f6d6eaa4e8b95c800157fc6f140b50c9714b933b0b5
  • Pointer size: 129 Bytes
  • Size of remote file: 2.3 kB
src/flair/motionblur/example_kernel/kernel50.png ADDED

Git LFS Details

  • SHA256: 52ecc9e8c05b32771060b0a17e531a89c31b8303dca50d87789c46efc79c4908
  • Pointer size: 129 Bytes
  • Size of remote file: 2.35 kB
src/flair/motionblur/example_kernel/kernel75.png ADDED

Git LFS Details

  • SHA256: 31ee0cf0cfefe84db8b77e58557361a393e5191174358a24a1a8937d2dc0faa8
  • Pointer size: 129 Bytes
  • Size of remote file: 2.2 kB
src/flair/motionblur/images/flag.png ADDED

Git LFS Details

  • SHA256: 6490a3651853f8e141d923bc3e9d770582aa5fa3ecc0cee16ecdf4825545e9fd
  • Pointer size: 129 Bytes
  • Size of remote file: 2.5 kB
src/flair/motionblur/images/flagBLURRED.png ADDED

Git LFS Details

  • SHA256: 2a5cb93489d397b1b0d34b36422fcb510ec2e162b319ac0b50b90cb591de4fc2
  • Pointer size: 130 Bytes
  • Size of remote file: 24.2 kB
src/flair/motionblur/images/moon.png ADDED

Git LFS Details

  • SHA256: c7c872c66e6508c5512875cf3995e0146fb218fd1bf97c381cf7f5876771522b
  • Pointer size: 131 Bytes
  • Size of remote file: 274 kB
src/flair/motionblur/intensity.png ADDED

Git LFS Details

  • SHA256: 49e3bfa135dc1f28bbeab834e7c2451e313e47f82b20beacca19c0dad9fd961e
  • Pointer size: 131 Bytes
  • Size of remote file: 230 kB
src/flair/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()