Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,13 @@
*.xml
*.pyc
*.iml
.idea/*
.idea/*
*.suo
*.db
*.ipch
*.sqlite
*.tlog
*.log
*.pdb
*.sln
*.vcxproj*
52 changes: 27 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,8 @@
# Image Smoothing Algorithm Based on Gradient Analysis
This repository contains C++ and Python 3.6 implementation of an image smoothing algorithm that was proposed in this [publication](https://ieeexplore.ieee.org/document/9117646) in IEEE conference "2020 Ural Symposium on Biomedical Engineering, Radioelectronics and Information Technology (USBEREIT)".
This repository contains C++ and Python 3 implementation of an image smoothing algorithm that was proposed in this [publication](https://ieeexplore.ieee.org/document/9117646).

![example1](/images/example.png)

![example1](/images/example1.jpg)

## General idea
In this paper image smoothing algorithm based on gradient analysis is proposed. Our algorithm uses filtering and to achieve edge-preserving smoothing it uses two components of gradient vectors: their magnitudes (or lengths) and directions. Our method discriminates between two types of boundaries in given neighborhood: regular and irregular ones.
![boundaries](/images/boundaries.png)
Regular boundaries have small deviations of gradient angles and the opposite for irregular ones. To measure closeness of angles cosine of doubled difference is used. As additional measure that helps to discriminate the types of boundaries inverted gradient values were used.
![gradients](/images/gradients.png)
When gradient magnitudes are inverted bigger values refer to textures (insignificant changes in gradient) and smaller refer to strong boundaries. So textures would have bigger weights and hence they would appear smoother. We also propose to smooth image of gradient magnitudes with median filter to enhance visual quality of results. The method proposed in this paper is easy to implement and compute and it gives good results in comparison with other techniques like bilateral filter.

## Examples
![example2](/images/example2.jpg)
## Comparison
Here is the comparison with other smoothing algorithms.
a) - original image
b) - guided filter
c) - bilateral filter
d) - our filter
![comparison](/images/comparison.png)
## Edge detection
Here is the output of Canny edge detector that was applied on the image with and without preprocessing with our filter.
![edges](/images/edge_detection.png)

## How to use code
Libraries used:
Expand All @@ -30,8 +11,6 @@ Libraries used:
Here is the simple example of filter usage with opencv Mat images:

```cpp
//opencv included in Source.cpp if you need to change include path,
//you should change it there
#include "FilterBasedOnGradientAnalysis.cpp"

int main()
Expand All @@ -41,7 +20,7 @@ int main()
int kernelSize = 3; //set kernelSize = 3 for filtering with 3x3 kernel
int runsNumber = 2; //set number of runs: parameter n is 1 by default
Filter<float, uint8_t> filter; //create the instance of filter
cv::Mat output = filter(img, kernelSize, n=runsNumber); //smooth image
cv::Mat output = filter(img, kernelSize, runsNumber); //smooth image

cv::imwrite("your_output_file_name", output); //write the result
return 0;
Expand All @@ -58,3 +37,26 @@ runs_number = 2 # set number of runs
output = fga.smooth(img, kernel_size, n=runs_number) # smooth image
cv2.imwrite('your_output_file_name', output) # write the result
```

## General idea
Our algorithm uses filtering and to achieve edge-preserving smoothing it uses two components of gradient vectors: their magnitudes (or lengths) and directions. Our method discriminates between two types of boundaries in given neighborhood: regular and irregular ones.
![boundaries](/images/boundaries.png)
Regular boundaries have small deviations of gradient angles and the opposite for irregular ones. To measure closeness of angles cosine of doubled difference is used. As additional measure that helps to discriminate the types of boundaries inverted gradient values were used.
![gradients](/images/gradients.png)
When gradient magnitudes are inverted bigger values refer to textures (insignificant changes in gradient) and smaller refer to strong boundaries. So textures would have bigger weights and hence they would appear smoother. We also propose to smooth image of gradient magnitudes with median filter to enhance visual quality of results. The method proposed in this paper is easy to implement and compute and it gives good results in comparison with other techniques like bilateral filter.

## Citation

If you used the code or want to reference this method in your work, please cite:

```
@inproceedings{gudkov2020image,
title={Image smoothing algorithm based on gradient analysis},
author={Gudkov, Vladimir and Moiseev, Ilia},
booktitle={2020 Ural Symposium on Biomedical Engineering, Radioelectronics and Information Technology (USBEREIT)},
pages={403--406},
year={2020},
organization={IEEE}
}
```

17 changes: 17 additions & 0 deletions examples/example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//opencv included in Source.cpp if you need to change include path,
//you should change it there
#include "../src/FilterBasedOnGradientAnalysis.cpp"


int main()
{
cv::Mat img = cv::imread("your_input_file_name", cv::IMREAD_COLOR); //read image using opencv from file into Mat type

int kernelSize = 3; //set kernelSize = 3 for filtering with 3x3 kernel
int runsNumber = 2; //set number of runs: parameter n is 1 by default
Filter<float, uint8_t, uint32_t> filter; //create the instance of filter
cv::Mat output = filter(img, kernelSize, runsNumber); //smooth image

cv::imwrite("your_output_file_name", output); //write the result
return 0;
}
17 changes: 17 additions & 0 deletions examples/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import sys
import os
import cv2

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(SCRIPT_DIR))
from fga import smooth


os.makedirs('../output', exist_ok=True)

img = cv2.imread('../images/engel_sm.bmp', cv2.IMREAD_COLOR)
kernel_size = 7
runs_number = 1

output = smooth(img, kernel_size, n=runs_number)
cv2.imwrite(f'../output/engel_sm_{kernel_size}_{runs_number}.bmp', output)
1 change: 1 addition & 0 deletions fga/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .filter_based_on_gradient_analysis import smooth
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,11 @@
# Image smoothing Algorithm Based on Gradient Analysis

import numpy as np
from math import sqrt, atan2, cos, isnan


def _euclid_norm(vect):
return sqrt(vect[0]*vect[0] + vect[1]*vect[1])


def _angle_rad(vect):
return atan2(vect[1], vect[0])


def _grad(x, y, image):
gradx = image[y][x-1] - image[y][x+1]
grady = image[y+1][x] - image[y-1][x]
gradx = image[y][x - 1] - image[y][x + 1]
grady = image[y + 1][x] - image[y - 1][x]
return [gradx, grady]


Expand All @@ -28,35 +19,26 @@ def compute_grads_channel(image, grads):


def compute_grads(image, grads):
list(map(lambda i: compute_grads_channel(image[:, :, i], grads[:, :, :, i]), [i for i in range(3)]))
for i in range(3):
compute_grads_channel(image[:, :, i], grads[:, :, :, i])


def compute_modules_channel(image, modules, grads):
for y in range(image.shape[0]):
for x in range(image.shape[1]):
if 0 < x < image.shape[1] - 1 and 0 < y < image.shape[0] - 1:
modules[y][x] = _euclid_norm(grads[y][x])


def compute_modules(image, modules, grads):
list(map(lambda i: compute_modules_channel(image[:, :, i], modules[:, :, i], grads[:, :, :, i]),
[i for i in range(3)]))
def compute_modules(grads):
return np.linalg.norm(grads, axis=2)


def compute_angles_channel(image, angles, grads):
for y in range(image.shape[0]):
for x in range(image.shape[1]):
if 0 < x < image.shape[1] - 1 and 0 < y < image.shape[0] - 1:
angle = _angle_rad(grads[y, x])
if not isnan(angle):
angles[y, x] = angle
else:
angles[y, x] = 0
g = grads[y, x]
angle = np.arctan2(g[1], g[0])
angles[y, x] = angle if not np.isnan(angle) else 0


def compute_angles(image, angles, grads):
list(map(lambda i: compute_angles_channel(image[:, :, i], angles[:, :, i], grads[:, :, :, i]),
[i for i in range(3)]))
for i in range(3):
compute_angles_channel(image[:, :, i], angles[:, :, i], grads[:, :, :, i])


def smooth_channel(src, k_size, n=1, grads=None, modules=None, angles=None, dst=None):
Expand Down Expand Up @@ -93,8 +75,7 @@ def _smooth_channel(src, k_size, grads=None, modules=None, angles=None, dst=None
grads = np.zeros((src.shape[0], src.shape[1], 2))
compute_grads_channel(src.astype(np.float64), grads)
if modules is None:
modules = np.zeros((src.shape[0], src.shape[1]))
compute_modules_channel(src.astype(np.float64), modules, grads)
modules = compute_modules(grads)
if angles is None:
angles = np.zeros((src.shape[0], src.shape[1]))
compute_angles_channel(src.astype(np.float64), angles, grads)
Expand All @@ -119,7 +100,7 @@ def _smooth_channel(src, k_size, grads=None, modules=None, angles=None, dst=None
if s != i or t != j:
alpha = 1. / modules[s][t]
beta = 2. * (angles[i][j] - angles[s][t])
weight = (cos(beta) + 1) * alpha
weight = (np.cos(beta) + 1) * alpha
else:
# weight of central pixel
weight = 1.
Expand All @@ -139,18 +120,18 @@ def _smooth(src, dst, k_size, grads=None, modules=None, angles=None):
grads = np.zeros((src.shape[0], src.shape[1], 2, 3))
compute_grads(src.astype(np.float64), grads)
if modules is None:
modules = np.zeros((src.shape[0], src.shape[1], 3))
compute_modules(src.astype(np.float64), modules, grads)
modules = compute_modules(grads)
if angles is None:
angles = np.zeros((src.shape[0], src.shape[1], 3))
compute_angles(src.astype(np.float64), angles, grads)

list(map(lambda i: smooth_channel(src[:, :, i].astype(np.float64),
k_size,
grads=grads[:, :, :, i],
modules=modules[:, :, i],
angles=angles[:, :, i],
dst=dst[:, :, i]), [i for i in range(3)]))
for i in range(3):
smooth_channel(src[:, :, i].astype(np.float64),
k_size,
grads=grads[:, :, :, i],
modules=modules[:, :, i],
angles=angles[:, :, i],
dst=dst[:, :, i])
return dst


Expand All @@ -166,12 +147,16 @@ def smooth(src, k_size, n=1, grads=None, modules=None, angles=None):
:param angles: gradient angles for each pixel with shape (n, m, 3)
:return: smoothed image with same shape as src and type np.float64
"""
if k_size % 2 == 0:
raise ValueError(f'k_size should be odd, got {k_size} instead')

src_proxy = np.copy(src)
dst = np.zeros(src.shape, np.float64)
for i in range(n):
if i == 0:
_smooth(src_proxy, dst, k_size, grads=grads, modules=modules, angles=angles)
else:

if n == 1:
_smooth(src_proxy, dst, k_size, grads=grads, modules=modules, angles=angles)
else:
for i in range(n):
_smooth(src_proxy, dst, k_size)
src_proxy = dst
src_proxy = dst
return dst
Binary file removed images/comparison.png
Binary file not shown.
Binary file removed images/edge_detection.png
Binary file not shown.
Binary file added images/engel_sm.bmp
Binary file not shown.
Binary file added images/engel_sm_3_2.bmp
Binary file not shown.
Binary file added images/engel_sm_7_1.bmp
Binary file not shown.
Binary file added images/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed images/example1.jpg
Binary file not shown.
Binary file removed images/example2.jpg
Binary file not shown.
61 changes: 61 additions & 0 deletions test/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest import TestCase
import unittest
import cv2
import sys
import os
import numpy as np


class TestSmoothing(TestCase):
def test_smoothing_7_1(self):
"""
Test smoothing results
"""
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(SCRIPT_DIR))
from fga import smooth

img = cv2.imread('../images/engel_sm.bmp', cv2.IMREAD_COLOR)
kernel_size = 7
runs_number = 1

output = smooth(img, kernel_size, n=runs_number)
gt_output = cv2.imread('../images/engel_sm_7_1.bmp', cv2.IMREAD_COLOR)

self.assertTrue(np.mean(output - gt_output) < 1)
self.assertTrue((output == gt_output).all())

def test_smoothing_3_2(self):
"""
Test smoothing results
"""
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(SCRIPT_DIR))
from fga import smooth

img = cv2.imread('../images/engel_sm.bmp', cv2.IMREAD_COLOR)
kernel_size = 3
runs_number = 2

output = smooth(img, kernel_size, n=runs_number)
gt_output = cv2.imread('../images/engel_sm_3_2.bmp', cv2.IMREAD_COLOR)

self.assertTrue(np.mean(output - gt_output) < 1)
self.assertTrue((output == gt_output).all())

def test_smoothing_2_1(self):
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(SCRIPT_DIR))
from fga import smooth

img = cv2.imread('../images/engel_sm.bmp', cv2.IMREAD_COLOR)
kernel_size = 2
runs_number = 1

with self.assertRaises(ValueError):
smooth(img, kernel_size, n=runs_number)


if __name__ == '__main__':
tc = TestSmoothing()
unittest.main()