Skip to content

Commit da88508

Browse files
committed
Init commit
1 parent 3ee6c66 commit da88508

12 files changed

+310
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
.DS_Store

README.md

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
# Blur Generator
3+
4+
Generate blur on image.
5+
6+
There are 3 types of blur can be used with `motion`, `lens`, or `gaussian`.
7+
8+
We can use the results on model training or something.
9+
10+
## Usage
11+
12+
```bash
13+
usage: main.py [-h] [--input INPUT] [--output OUTPUT] [--type TYPE] [--motion_blur_size MOTION_BLUR_SIZE]
14+
[--motion_blur_angle MOTION_BLUR_ANGLE] [--lens_radius LENS_RADIUS] [--lens_components LENS_COMPONENTS]
15+
[--lens_exposure_gamma LENS_EXPOSURE_GAMMA] [--gaussian_kernel GAUSSIAN_KERNEL]
16+
17+
optional arguments:
18+
-h, --help show this help message and exit
19+
--input INPUT Specific path of image as `input`.
20+
--output OUTPUT Specific path for `output`. Default is `./result.png`.
21+
--type TYPE Blur type of `motion`, `lens`, or `gaussian`. Default is `motion`.
22+
--motion_blur_size MOTION_BLUR_SIZE
23+
Size for motion blur. Default is 100.
24+
--motion_blur_angle MOTION_BLUR_ANGLE
25+
Angle for motion blur. Default is 30.
26+
--lens_radius LENS_RADIUS
27+
Radius for lens blur. Default is 5.
28+
--lens_components LENS_COMPONENTS
29+
Components for lens blur. Default is 4.
30+
--lens_exposure_gamma LENS_EXPOSURE_GAMMA
31+
Exposure gamma for lens blur. Default is 2.
32+
--gaussian_kernel GAUSSIAN_KERNEL
33+
Kernel for gaussian. Default is 100.
34+
```
35+
36+
## Results
37+
38+
* Original image
39+
40+
![original image](./doc/test.png)
41+
42+
* Motion blur
43+
44+
`python3 main.py --type motion --input ./doc/test.png --output ./doc/motion.png`
45+
46+
![motion blur image](./doc/motion.png)
47+
48+
* Lens blur
49+
50+
`python3 main.py --type lens --input ./doc/test.png --output ./doc/lens.png`
51+
52+
![lens blur image](./doc/lens.png)
53+
54+
* Gaussian blur
55+
56+
`python3 main.py --type gaussian --input ./doc/test.png --output ./doc/gaussian.png`
57+
58+
![gaussian blur image](./doc/gaussian.png)
59+

blur_tools/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Blur maker init
3+
"""
4+
5+
from .motion_blur import motion_blur
6+
from .lens_blur import lens_blur
7+
from .gaussian_blur import gaussian_blur

blur_tools/gaussian_blur.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
Gaussian blur generator
3+
"""
4+
5+
import cv2
6+
7+
def gaussian_blur(img, kernel, sigma=5):
8+
'''Gaussian blur generator'''
9+
if kernel % 2 == 0:
10+
kernel += 1
11+
kernel_size = (kernel, kernel)
12+
dst = cv2.GaussianBlur(img, kernel_size, sigma)
13+
return dst

blur_tools/lens_blur.py

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Lens blur generator
3+
4+
"""
5+
6+
import math
7+
from functools import reduce
8+
9+
import cv2
10+
import numpy as np
11+
from scipy import signal
12+
13+
# These scales bring the size of the below components to roughly the specified radius - I just hard coded these
14+
kernel_scales = [1.4,1.2,1.2,1.2,1.2,1.2]
15+
16+
# Kernel parameters a, b, A, B
17+
# These parameters are drawn from <http://yehar.com/blog/?p=1495>
18+
kernel_params = [
19+
# 1-component
20+
[[0.862325, 1.624835, 0.767583, 1.862321]],
21+
22+
# 2-components
23+
[[0.886528, 5.268909, 0.411259, -0.548794],
24+
[1.960518, 1.558213, 0.513282, 4.56111]],
25+
26+
# 3-components
27+
[[2.17649, 5.043495, 1.621035, -2.105439],
28+
[1.019306, 9.027613, -0.28086, -0.162882],
29+
[2.81511, 1.597273, -0.366471, 10.300301]],
30+
31+
# 4-components
32+
[[4.338459, 1.553635, -5.767909, 46.164397],
33+
[3.839993, 4.693183, 9.795391, -15.227561],
34+
[2.791880, 8.178137, -3.048324, 0.302959],
35+
[1.342190, 12.328289, 0.010001, 0.244650]],
36+
37+
# 5-components
38+
[[4.892608, 1.685979, -22.356787, 85.91246],
39+
[4.71187, 4.998496, 35.918936, -28.875618],
40+
[4.052795, 8.244168, -13.212253, -1.578428],
41+
[2.929212, 11.900859, 0.507991, 1.816328],
42+
[1.512961, 16.116382, 0.138051, -0.01]],
43+
44+
# 6-components
45+
[[5.143778, 2.079813, -82.326596, 111.231024],
46+
[5.612426, 6.153387, 113.878661, 58.004879],
47+
[5.982921, 9.802895, 39.479083, -162.028887],
48+
[6.505167, 11.059237, -71.286026, 95.027069],
49+
[3.869579, 14.81052, 1.405746, -3.704914],
50+
[2.201904, 19.032909, -0.152784, -0.107988]]]
51+
52+
# Obtain specific parameters and scale for a given component count
53+
def get_parameters(component_count = 2):
54+
parameter_index = max(0, min(component_count - 1, len(kernel_params)))
55+
parameter_dictionaries = [dict(zip(['a','b','A','B'], b)) for b in kernel_params[parameter_index]]
56+
return (parameter_dictionaries, kernel_scales[parameter_index])
57+
58+
# Produces a complex kernel of a given radius and scale (adjusts radius to be more accurate)
59+
# a and b are parameters of this complex kernel
60+
def complex_kernel_1d(radius, scale, a, b):
61+
kernel_radius = radius
62+
kernel_size = kernel_radius * 2 + 1
63+
ax = np.arange(-kernel_radius, kernel_radius + 1., dtype=np.float32)
64+
ax = ax * scale * (1 / kernel_radius)
65+
kernel_complex = np.zeros((kernel_size), dtype=np.complex64)
66+
kernel_complex.real = np.exp(-a * (ax**2)) * np.cos(b * (ax**2))
67+
kernel_complex.imag = np.exp(-a * (ax**2)) * np.sin(b * (ax**2))
68+
return kernel_complex.reshape((1, kernel_size))
69+
70+
def normalise_kernels(kernels, params):
71+
# Normalises with respect to A*real+B*imag
72+
total = 0
73+
74+
for k,p in zip(kernels, params):
75+
# 1D kernel - applied in 2D
76+
for i in range(k.shape[1]):
77+
for j in range(k.shape[1]):
78+
# Complex multiply and weighted sum
79+
total += p['A'] * (k[0,i].real*k[0,j].real - k[0,i].imag*k[0,j].imag) + p['B'] * (k[0,i].real*k[0,j].imag + k[0,i].imag*k[0,j].real)
80+
81+
scalar = 1 / math.sqrt(total)
82+
kernels = np.asarray(kernels) * scalar
83+
84+
return kernels
85+
86+
# Combine the real and imaginary parts of an image, weighted by A and B
87+
def weighted_sum(kernel, params):
88+
return np.add(kernel.real * params['A'], kernel.imag * params['B'])
89+
90+
# Produce a 2D kernel by self-multiplying a 1d kernel. This would be slower to use
91+
# than the separable approach, mostly for visualisation below
92+
def multiply_kernel(kernel):
93+
kernel_size = kernel.shape[1]
94+
a = np.repeat(kernel, kernel_size, 0)
95+
b = np.repeat(kernel.transpose(), kernel_size, 1)
96+
return np.multiply(a,b)
97+
98+
99+
def lens_blur(img, radius=3, components=5, exposure_gamma=5):
100+
101+
img = np.ascontiguousarray(img.transpose(2,0,1), dtype=np.float32)
102+
103+
104+
# Obtain component parameters / scale values
105+
parameters, scale = get_parameters(component_count = components)
106+
107+
# Create each component for size radius, using scale and other component parameters
108+
components = [complex_kernel_1d(radius, scale, component_params['a'], component_params['b']) for component_params in parameters]
109+
110+
# Normalise all kernels together (the combination of all applied kernels in 2D must sum to 1)
111+
components = normalise_kernels(components, parameters)
112+
113+
# Increase exposure to highlight bright spots
114+
img = np.power(img, exposure_gamma)
115+
116+
# Process RGB channels for all components
117+
component_output = list()
118+
for component, component_params in zip(components, parameters):
119+
channels = list()
120+
for channel in range(img.shape[0]):
121+
inter = signal.convolve2d(img[channel], component, boundary='symm', mode='same')
122+
channels.append(signal.convolve2d(inter, component.transpose(), boundary='symm', mode='same'))
123+
124+
# The final component output is a stack of RGB, with weighted sums of real and imaginary parts
125+
component_image = np.stack([weighted_sum(channel, component_params) for channel in channels])
126+
component_output.append(component_image)
127+
128+
# Add all components together
129+
output_image = reduce(np.add, component_output)
130+
131+
# Reverse exposure
132+
output_image = np.clip(output_image, 0, None)
133+
output_image = np.power(output_image, 1.0/exposure_gamma)
134+
135+
# Avoid out of range values - generally this only occurs with small negatives
136+
# due to imperfect complex kernels
137+
output_image = np.clip(output_image, 0, 1)
138+
139+
#output_image *= 255
140+
#output_image = output_image.transpose(1,2,0).astype(np.uint8)
141+
output_image = output_image.transpose(1,2,0)
142+
return output_image

blur_tools/motion_blur.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Motion blur generator
3+
4+
"""
5+
6+
from random import randint
7+
8+
import cv2
9+
import numpy as np
10+
11+
def motion_blur(img, size=None, angle=None):
12+
'''Motion blur generator'''
13+
if size is None:
14+
size = randint(20, 80)
15+
if angle is None:
16+
angle = randint(15, 30)
17+
18+
k = np.zeros((size, size), dtype=np.float32)
19+
k[(size-1)//2, :] = np.ones(size, dtype=np.float32)
20+
k = cv2.warpAffine(k, cv2.getRotationMatrix2D((size/2-0.5, size/2-0.5), angle, 1.0), (size, size))
21+
k = k * (1.0/np.sum(k))
22+
23+
return cv2.filter2D(img, -1, k)

doc/gaussian.png

92 KB
Loading

doc/lens.png

60.4 KB
Loading

doc/motion.png

83.4 KB
Loading

doc/test.png

27.3 KB
Loading

main.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Blur Maker
3+
4+
"""
5+
import argparse
6+
7+
from pathlib import Path
8+
9+
import cv2
10+
11+
from blur_tools import motion_blur, lens_blur, gaussian_blur
12+
13+
14+
if __name__ == "__main__":
15+
16+
parser = argparse.ArgumentParser()
17+
18+
parser.add_argument('--input', type=str, default=None, help='Specific path of image as `input`.')
19+
parser.add_argument('--output', type=str, default='./result.png', help='Specific path for `output`. Default is `./result.png`.')
20+
21+
parser.add_argument('--type', type=str, default='motion', help='Blur type of `motion`, `lens`, or `gaussian`. Default is `motion`.')
22+
23+
parser.add_argument('--motion_blur_size', type=int, default=100, help='Size for motion blur. Default is 100.')
24+
parser.add_argument('--motion_blur_angle', type=int, default=30, help='Angle for motion blur. Default is 30.')
25+
26+
parser.add_argument('--lens_radius', type=int, default=5, help='Radius for lens blur. Default is 5.')
27+
parser.add_argument('--lens_components', type=int, default=4, help='Components for lens blur. Default is 4.')
28+
parser.add_argument('--lens_exposure_gamma', type=int, default=2, help='Exposure gamma for lens blur. Default is 2.')
29+
30+
parser.add_argument('--gaussian_kernel', type=int, default=100, help='Kernel for gaussian. Default is 100.')
31+
32+
args = parser.parse_args()
33+
34+
if args.input:
35+
img_path = Path(args.input)
36+
if img_path.is_file():
37+
if img_path.suffix in ['.jpg', '.jpeg', '.png']:
38+
39+
img = cv2.imread(img_path.absolute().as_posix())
40+
img = img / 255.
41+
42+
if args.type not in ['motion', 'lens', 'gaussian']:
43+
print('No type has been selected. Please specific `motion`, `lens`, or `gaussian`.')
44+
else:
45+
if args.type == 'motion':
46+
result = motion_blur(img, size=args.motion_blur_size, angle=args.motion_blur_angle)
47+
48+
elif args.type == 'lens':
49+
result = lens_blur(img, radius=5, components=4, exposure_gamma=2)
50+
51+
elif args.type == 'gaussian':
52+
result = gaussian_blur(img, 100)
53+
54+
cv2.imwrite(args.output, result*255)
55+
56+
else:
57+
print('Only support common types of image `.jpg` and `.png`.')
58+
59+
else:
60+
print('File not exists!')
61+
else:
62+
print('Please specific image for input.')

requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
opencv-python
3+

0 commit comments

Comments
 (0)