Skip to content

Commit

Permalink
Merge pull request #4 from NatLee/feat/add-support-for-lens-radius
Browse files Browse the repository at this point in the history
Feat/add support for lens radius
  • Loading branch information
NatLee authored May 16, 2024
2 parents c2e6989 + 08b83b4 commit 77a3c47
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 71 deletions.
37 changes: 1 addition & 36 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: [3.7, 3.8, 3.9, '3.10', '3.11']
python: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12']
os:
- name: Linux
runs-on: ubuntu-latest
Expand All @@ -37,38 +37,3 @@ jobs:
- name: Run Tox
# Run tox using the version of Python in `PATH`
run: tox -e py

build36:
runs-on: ${{ matrix.os.runs-on }}
strategy:
matrix:
python-version: [ 3.6 ]
os:
- name: Linux
runs-on: ubuntu-20.04
python_platform: linux
- name: Windows
runs-on: windows-latest
python_platform: win32
- name: macOS
runs-on: macos-latest
python_platform: darwin
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.6
uses: actions/setup-python@v4
with:
python-version: 3.6
- name: install-dependencies
run: |
python -m pip install --upgrade pip
pip install opencv-python==4.6.0.66
- name: Test with unittest
run: |
python setup.py test
#- name: Install Tox and other packages
#run: pip install tox
#- name: Run Tox
# Run tox using the version of Python in `PATH`
#run: python -m tox -e py
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ usage: blurgenerator [-h] [--input INPUT] [--input_depth_map INPUT_DEPTH_MAP] [-
--motion_blur_angle MOTION_BLUR_ANGLE
Angle for motion blur. Default is 30.
--lens_radius LENS_RADIUS
Radius for lens blur. Default is 5.
Radius for lens blur. Default is 5.0.
--lens_components LENS_COMPONENTS
Components for lens blur. Default is 4.
--lens_exposure_gamma LENS_EXPOSURE_GAMMA
Expand Down
1 change: 1 addition & 0 deletions reinstall.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#!/bin/bash
pip uninstall -y blurgenerator
python setup.py clean --all install clean --all
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
'Topic :: Software Development :: Build Tools',

'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
Expand All @@ -39,7 +38,7 @@
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
],
python_requires='>=3.6',
python_requires='>=3.7',
install_requires=['opencv-python'],
extras_require={
'dev': ['check-manifest'],
Expand Down
2 changes: 1 addition & 1 deletion src/blurgenerator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def main():
parser.add_argument('--motion_blur_size', type=int, default=100, help='Size for motion blur. Default is 100.')
parser.add_argument('--motion_blur_angle', type=int, default=30, help='Angle for motion blur. Default is 30.')

parser.add_argument('--lens_radius', type=int, default=5, help='Radius for lens blur. Default is 5.')
parser.add_argument('--lens_radius', type=float, default=5.0, help='Radius for lens blur. Default is 5.0.')
parser.add_argument('--lens_components', type=int, default=4, help='Components for lens blur. Default is 4.')
parser.add_argument('--lens_exposure_gamma', type=int, default=2, help='Exposure gamma for lens blur. Default is 2.')

Expand Down
78 changes: 48 additions & 30 deletions src/blurgenerator/lens_blur.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Lens blur generator
"""
from typing import Tuple, Dict, List
import os
import math
from functools import reduce
Expand Down Expand Up @@ -50,76 +51,93 @@
[2.201904, 19.032909, -0.152784, -0.107988]]]

# Obtain specific parameters and scale for a given component count
def get_parameters(component_count = 2):
def get_parameters(component_count: int = 2) -> Tuple[List[Dict[str, float]], float]:
"""
Obtain specific parameters and scale for a given component count.
"""
parameter_index = max(0, min(component_count - 1, len(kernel_params)))
parameter_dictionaries = [dict(zip(['a','b','A','B'], b)) for b in kernel_params[parameter_index]]
return (parameter_dictionaries, kernel_scales[parameter_index])
parameter_dictionaries = [dict(zip(['a', 'b', 'A', 'B'], b)) for b in kernel_params[parameter_index]]
return parameter_dictionaries, kernel_scales[parameter_index]

# Produces a complex kernel of a given radius and scale (adjusts radius to be more accurate)
# a and b are parameters of this complex kernel
def complex_kernel_1d(radius, scale, a, b):
kernel_radius = radius
def complex_kernel_1d(radius: float, scale: float, a: float, b: float) -> np.ndarray:
"""
Produces a complex kernel of a given radius and scale (adjusts radius to be more accurate).
"""
kernel_radius = int(math.ceil(radius))
kernel_size = kernel_radius * 2 + 1
ax = np.arange(-kernel_radius, kernel_radius + 1., dtype=np.float32)
ax = ax * scale * (1 / kernel_radius)
kernel_complex = np.zeros((kernel_size), dtype=np.complex64)
ax = np.linspace(-radius, radius, kernel_size, dtype=np.float32)
ax = ax * scale * (1 / radius)
kernel_complex = np.zeros((kernel_size,), dtype=np.complex64)
kernel_complex.real = np.exp(-a * (ax**2)) * np.cos(b * (ax**2))
kernel_complex.imag = np.exp(-a * (ax**2)) * np.sin(b * (ax**2))
return kernel_complex.reshape((1, kernel_size))

def normalise_kernels(kernels, params):
# Normalises with respect to A*real+B*imag

def normalise_kernels(kernels: List[np.ndarray], params: List[Dict[str, float]]) -> np.ndarray:
"""
Normalises the kernels with respect to A*real + B*imag.
"""
total = 0

for k,p in zip(kernels, params):
# 1D kernel - applied in 2D
for k, p in zip(kernels, params):
for i in range(k.shape[1]):
for j in range(k.shape[1]):
# Complex multiply and weighted sum
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)
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)

scalar = 1 / math.sqrt(total)
kernels = np.asarray(kernels) * scalar

return kernels

# Combine the real and imaginary parts of an image, weighted by A and B
def weighted_sum(kernel, params):
def weighted_sum(kernel: np.ndarray, params: Dict[str, float]) -> np.ndarray:
"""
Combine the real and imaginary parts of an image, weighted by A and B.
"""
return np.add(kernel.real * params['A'], kernel.imag * params['B'])

# Produce a 2D kernel by self-multiplying a 1d kernel. This would be slower to use
# than the separable approach, mostly for visualisation below
def multiply_kernel(kernel):
def multiply_kernel(kernel: np.ndarray) -> np.ndarray:
"""
Produce a 2D kernel by self-multiplying a 1D kernel.
"""
kernel_size = kernel.shape[1]
a = np.repeat(kernel, kernel_size, 0)
b = np.repeat(kernel.transpose(), kernel_size, 1)
return np.multiply(a,b)
return np.multiply(a, b)

# ----------------------------------------------------------------

def filter_task(idx, channel, img_channel, component, component_params):
def filter_task(idx: int, channel: int, img_channel: np.ndarray, component: np.ndarray, component_params: Dict[str, float]) -> Tuple[int, int, np.ndarray]:
"""
https://github.com/Davide-sd/GIMP-lens-blur/blob/master/GIMP-lens-blur.py#L188
Perform convolution with the complex kernel components on the image channel.
"""
component_real = np.real(component)
component_imag = np.imag(component)

component_real_t = component_real.transpose()
component_imag_t = component_imag.transpose()
# first convolution
inter_real = cv2.filter2D(img_channel, -1, component_real)
inter_imag = cv2.filter2D(img_channel, -1, component_imag)
# second convolution (see NOTE above, here inter_ is f, component_ is g)
final_1 = cv2.filter2D(inter_real, -1, component_real_t)
final_2 = cv2.filter2D(inter_real, -1, component_imag_t)
final_3 = cv2.filter2D(inter_imag, -1, component_real_t)
final_4 = cv2.filter2D(inter_imag, -1, component_imag_t)

inter_real = cv2.filter2D(img_channel, -1, component_real, borderType=cv2.BORDER_REPLICATE)
inter_imag = cv2.filter2D(img_channel, -1, component_imag, borderType=cv2.BORDER_REPLICATE)

final_1 = cv2.filter2D(inter_real, -1, component_real_t, borderType=cv2.BORDER_REPLICATE)
final_2 = cv2.filter2D(inter_real, -1, component_imag_t, borderType=cv2.BORDER_REPLICATE)
final_3 = cv2.filter2D(inter_imag, -1, component_real_t, borderType=cv2.BORDER_REPLICATE)
final_4 = cv2.filter2D(inter_imag, -1, component_imag_t, borderType=cv2.BORDER_REPLICATE)

final = final_1 - final_4 + 1j * (final_2 + final_3)
weight_sum = weighted_sum(final, component_params)
# return index, channel No. and sum of weights

return idx, channel, weight_sum

def lens_blur(img, radius=3, components=5, exposure_gamma=5):
def lens_blur(img: np.ndarray, radius: float = 3.0, components: int = 5, exposure_gamma: float = 5.0) -> np.ndarray:
"""
Apply lens blur to the input image.
"""

img = img/255.

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py{36, 37, 38, 39, 310, 311}
envlist = py{37, 38, 39, 310, 311, 312}
minversion = 3.28.0
isolated_build = true

Expand Down

0 comments on commit 77a3c47

Please sign in to comment.