Spaces:
Running
on
Zero
Running
on
Zero
motionblur package
Browse files- README.md +70 -12
- requirements.txt +1 -3
- src/flair/motionblur/README.md +69 -0
- src/flair/motionblur/__init__.py +0 -0
- src/flair/motionblur/environment.yaml +20 -0
- src/flair/motionblur/example_kernel/kernel0.png +3 -0
- src/flair/motionblur/example_kernel/kernel100.png +3 -0
- src/flair/motionblur/example_kernel/kernel25.png +3 -0
- src/flair/motionblur/example_kernel/kernel50.png +3 -0
- src/flair/motionblur/example_kernel/kernel75.png +3 -0
- src/flair/motionblur/images/flag.png +3 -0
- src/flair/motionblur/images/flagBLURRED.png +3 -0
- src/flair/motionblur/images/moon.png +3 -0
- src/flair/motionblur/intensity.png +3 -0
- src/flair/motionblur/motionblur.py +419 -0
README.md
CHANGED
@@ -1,12 +1,70 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
[](link)
|
11 |
+
[](inverseFLAIR.github.io)
|
12 |
+
[](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 |
+

|
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
|
src/flair/motionblur/example_kernel/kernel100.png
ADDED
![]() |
Git LFS Details
|
src/flair/motionblur/example_kernel/kernel25.png
ADDED
![]() |
Git LFS Details
|
src/flair/motionblur/example_kernel/kernel50.png
ADDED
![]() |
Git LFS Details
|
src/flair/motionblur/example_kernel/kernel75.png
ADDED
![]() |
Git LFS Details
|
src/flair/motionblur/images/flag.png
ADDED
![]() |
Git LFS Details
|
src/flair/motionblur/images/flagBLURRED.png
ADDED
![]() |
Git LFS Details
|
src/flair/motionblur/images/moon.png
ADDED
![]() |
Git LFS Details
|
src/flair/motionblur/intensity.png
ADDED
![]() |
Git LFS Details
|
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()
|