# This file is part of Sympathy for Data.
# Copyright (c) 2018, Combine Control Systems AB
#
# SYMPATHY FOR DATA COMMERCIAL LICENSE
# You should have received a link to the License with Sympathy for Data.
from sympathy.api import node
from sympathy.api.nodeconfig import Ports, Tag, Tags
import numpy as np
import skimage.restoration as rest
import skimage.util as util
import skimage
from sylib.imageprocessing.image import ImagePort
from sylib.imageprocessing.algorithm_selector import ImageFiltering_abstract
from sylib.imageprocessing.generic_filtering import GenericImageFiltering
from packaging import version
skimage_version = version.Version(skimage.__version__)
# Test for deprecation of criterion arguments
check_19 = skimage_version >= version.Version('0.19.0')
check_21 = skimage_version >= version.Version('0.21.0')
def _random_nose_kwargs(par):
seed = par['seed'].value
if seed == 0:
seed = None
seed_key = 'rng' if check_21 else 'seed'
return {seed_key: seed}
def alg_gaussian(im, par):
return util.random_noise(
im, mode='gaussian',
clip=par['clip'].value,
mean=par['mean'].value, var=par['variance'].value,
**_random_nose_kwargs(par)
)
def alg_speckle(im, par):
return util.random_noise(
im, mode='speckle',
clip=par['clip'].value,
mean=par['mean'].value, var=par['variance'].value,
**_random_nose_kwargs(par)
)
def alg_poisson(im, par):
return util.random_noise(
im, mode='poisson',
clip=par['clip'].value,
**_random_nose_kwargs(par)
)
def alg_salt(im, par):
return util.random_noise(
im, mode='salt',
clip=par['clip'].value,
amount=par['amount'].value,
**_random_nose_kwargs(par)
)
def alg_pepper(im, par):
return util.random_noise(
im, mode='pepper',
clip=par['clip'].value,
amount=par['amount'].value,
**_random_nose_kwargs(par)
)
def alg_salt_pepper(im, par):
return util.random_noise(
im, mode='s&p',
clip=par['clip'].value,
amount=par['amount'].value,
salt_vs_pepper=par['salt vs pepper'].value,
**_random_nose_kwargs(par)
)
NOISE_ALGS = {
'Gaussian': {
'description': (
'Gaussian distributed white noise'),
'multi_chromatic': True,
'algorithm': alg_gaussian,
'seed': 'Random seed, if zero then a fresh random seed is generated',
'mean': 'Mean value of gaussian noise',
'variance': 'Variance of gaussian noise',
'clip': 'Clips the output to range 0..1',
},
'Speckle': {
'description': (
'Multiplies incoming image with a uniform noise with '
'given mean and variance'),
'multi_chromatic': True,
'algorithm': alg_speckle,
'seed': 'Random seed, if zero then a fresh random seed is generated',
'mean': 'Mean value of speckle noise',
'variance': 'Variance of speckle noise',
'clip': 'Clips the output to range 0..1',
},
'Poisson': {
'description': (
'Poisson distributed noise generated from the data'),
'multi_chromatic': True,
'algorithm': alg_poisson,
'seed': 'Random seed, if zero then a fresh random seed is generated',
'clip': 'Clips the output to range 0..1',
},
'Salt': {
'description': (
'Replaces random pixels with a 1'),
'multi_chromatic': True,
'algorithm': alg_salt,
'seed': 'Random seed, if zero then a fresh random seed is generated',
'amount': 'Fraction of pixels that will be replaced',
},
'Pepper': {
'description': (
'Replaces random pixels with a 0'),
'multi_chromatic': True,
'algorithm': alg_pepper,
'seed': 'Random seed, if zero then a fresh random seed is generated',
'amount': 'Fraction of pixels that will be replaced',
},
'Salt and pepper': {
'description': (
'Replaces random pixels with a 1 or a 0'),
'multi_chromatic': True,
'algorithm': alg_salt_pepper,
'seed': 'Random seed, if zero then a fresh random seed is generated',
'amount': 'Fraction of pixels that will be replaced',
'salt vs pepper': (
'Fraction of replaced pixels that will be replaced '
'with a 1 (salt)')
},
}
NOISE_PARAMETERS = [
'mean', 'variance', 'amount', 'salt vs pepper',
'seed', 'clip',
]
NOISE_TYPES = {
'mean': float,
'variance': float,
'amount': float,
'salt vs pepper': float,
'seed': int,
'clip': bool
}
NOISE_DEFAULTS = {
'mean': 0.0,
'variance': 0.01,
'amount': 0.05,
'salt vs pepper': 0.5,
'clip': True,
'seed': 0,
}
def alg_tv_bregman(im, par):
if not check_19:
kwargs = {"max_iter": par['iter'].value,
"isotropic": par['isotropic'].value}
else:
kwargs = {"max_num_iter": par['iter'].value,
"isotropic": par['isotropic'].value}
return rest.denoise_tv_bregman(
np.copy(im), par['weight'].value,
**kwargs)
def alg_tv_chambolle(im, par):
return rest.denoise_tv_chambolle(
im, weight=par['weight'].value,
n_iter_max=par['iter'].value)
def _denoise_kwargs(par):
return {} if check_19 else {'multichannel': par['multichannel'].value}
def alg_bilateral(im, par):
sigma_color = par['sigma color'].value
if sigma_color == 0.0:
sigma_color = None
return rest.denoise_bilateral(
np.copy(im),
win_size=par['window size'].value,
sigma_color=sigma_color,
sigma_spatial=par['sigma spatial'].value,
bins=par['bins'].value,
mode=par['edge mode'].value,
cval=par['cval'].value,
**_denoise_kwargs(par))
def alg_nl_means(im, par):
return rest.denoise_nl_means(
im,
patch_size=par['patch size'].value,
patch_distance=par['patch distance'].value,
h=par['h'].value,
sigma=par['sigma'].value,
fast_mode=par['fast'].value,
**_denoise_kwargs(par))
def alg_wavelet(im, par):
kwargs = {}
sigma = par['sigma'].value
if sigma != 0.0:
kwargs['sigma'] = sigma
wavelet_levels = par['wavelet levels'].value
if wavelet_levels > 0:
kwargs['wavelet_levels'] = wavelet_levels
return rest.denoise_wavelet(
im,
wavelet=par['wavelet'].value,
mode=par['mode'].value,
convert2ycbcr=par['convert to YCbCr'].value,
method=par['method'].value,
**_denoise_kwargs(par))
DENOISE_ALGS = {
'Total variation Bregman': {
'description': (
'Total variation denoising using split-Bregman optimization'),
'multi_chromatic': True,
'algorithm': alg_tv_bregman,
'weight': (
'Controls amount of denoising performed. Smaller values give '
'higher denoising'),
'iter': 'Maximum number of iterations of algoritm',
'isotropic': 'If false use anisotropic filtering instead',
},
'Total variation Chambolle': {
'description': (
'Total variation denoising using split-Bregman optimization'),
'multi_chromatic': False,
'algorithm': alg_tv_chambolle,
'weight': (
'Controls amount of denoising performed. Greater values give '
'higher denoising'),
'iter': 'Maximum number of iterations of algoritm',
},
'Bilateral': {
'description': (
'Edge-preserving denoising filter'),
'multi_chromatic': True,
'algorithm': alg_bilateral,
'window size': 'Window size for filtering',
'sigma color': (
'Standard deviation for grey/color distance. Larger values results'
'in averaging of pixels with larger grey/color differences. If '
'0.0 then compute and use the standard deviation in the image.'),
'sigma spatial': (
'Standard deviation for range distances. A larger value results '
'in averaging pixels that are further apart.'),
'bins': ('Number of discrete values for gaussian weights of '
'color filtering'),
'edge mode': 'How to handle values outside the image borders',
'cval': 'Value used when mode is constant',
'multichannel': (
'Wheter to treat the channels as colors or a separate '
'spatial dimension')
},
'Non-local means': {
'description': (
'A de-noising technique suited for images with specific textures. '
'It finds other pixels in a neighbourhood that have a similar'
'texture within the given patch size, and computes the average of'
'these pixels.'),
'multi_chromatic': True,
'algorithm': alg_nl_means,
'patch size': 'Size of the texture area that must match',
'patch distance': 'Maximum distance to search for similar pixels',
'h': (
'Cut-off distance (grey levels). Higher values accept more '
'patches.'),
'fast': 'Uses an alternative faster algorithm',
'sigma': (
'Optional: standard deviation of a presumed white gaussian noise'),
'multichannel': (
'Process multiple channels at the same time')
},
'Wavelet': {
'description': 'Wavelet denoising',
'multi_chromatic': True,
'algorithm': alg_wavelet,
'sigma': (
'Estimated standard deviation of noise, if 0.0 then an '
'automatic estimate is calculated'),
'wavelet': 'Type of wavelet to calculate',
'mode': (
'Type of denoising to perform, soft is best for additive noise'),
'wavelet levels': (
'Number of wavelet decomposition levels to use, if 0 then use 3\n'
'less levels than the maximum given the image size'),
'multichannel': (
'Applies denoising for each channel separately vs. together'),
'convert to YCbCr': (
'If true and if multichannel is true, then convert RGB to the '
'YCbCr colorspace.'),
'method': 'Thresholding method to use',
},
}
DENOISE_PARAMETERS = [
'weight', 'iter', 'isotropic', 'sigma', 'wavelet levels', 'wavelet',
'mode', 'convert to YCbCr', 'method', 'window size',
'sigma color', 'sigma spatial', 'bins', 'edge mode', 'cval',
'patch size', 'patch distance', 'h', 'fast', 'multichannel',
]
DENOISE_TYPES = {
'weight': float,
'iter': int,
'isotropic': bool,
'sigma': float,
'wavelet levels': int,
'wavelet': ['db1', 'db2', 'haar', 'sym9'],
'mode': ['soft', 'hard'],
'multichannel': bool,
'convert to YCbCr': bool,
'method': ['BayesShrink', 'VisuShrink'],
'window size': int,
'sigma color': float,
'sigma spatial': float,
'bins': int,
'edge mode': ['constant', 'edge', 'symmetric', 'reflect', 'wrap'],
'cval': float,
'patch size': int,
'patch distance': int,
'h': float,
'fast': bool,
}
DENOISE_DEFAULTS = {
'weight': 1.0,
'iter': 500,
'isotropic': True,
'wavelet levels': 0,
'sigma': 0.0,
'wavelet': 'db1',
'mode': 'soft',
'multichannel': False,
'convert to YCbCr': False,
'method': 'VisuShrink',
'window size': 13,
'sigma color': 0,
'sigma spatial': 1,
'bins': 10000,
'edge mode': 'constant',
'cval': 0,
'patch size': 7,
'patch distance': 11,
'h': 0.1,
'fast': True,
}
[docs]
class ImageNoise(ImageFiltering_abstract, GenericImageFiltering, node.Node):
author = 'Mathias Broxvall'
icon = 'image_noise.svg'
description = 'Adds noise to an image'
name = 'Random noise image'
tags = Tags(Tag.ImageProcessing.ImageManipulation)
nodeid = 'com.sympathyfordata.imageanalysis.random_noise'
algorithms = NOISE_ALGS
options_list = NOISE_PARAMETERS
options_types = NOISE_TYPES
options_default = NOISE_DEFAULTS
parameters = node.parameters()
parameters.set_string('algorithm', value=next(iter(algorithms)),
description='', label='Algorithm')
ImageFiltering_abstract.generate_parameters(parameters, options_types,
options_default)
inputs = Ports([
ImagePort('source image to add noise to', name='source'),
])
outputs = Ports([
ImagePort('result after adding noise', name='result'),
])
__doc__ = ImageFiltering_abstract.generate_docstring(
description, algorithms, options_list, inputs, outputs)
[docs]
class ImageDenoise(ImageFiltering_abstract, GenericImageFiltering, node.Node):
author = 'Mathias Broxvall'
icon = 'image_denoise.svg'
description = 'Perform image denoising'
name = 'Denoise image'
tags = Tags(Tag.ImageProcessing.ImageManipulation)
nodeid = 'com.sympathyfordata.imageanalysis.image_denoise'
algorithms = DENOISE_ALGS
options_list = DENOISE_PARAMETERS
options_types = DENOISE_TYPES
options_default = DENOISE_DEFAULTS
parameters = node.parameters()
parameters.set_string('algorithm', value=next(iter(algorithms)),
description='', label='Algorithm')
ImageFiltering_abstract.generate_parameters(parameters, options_types,
options_default)
inputs = Ports([
ImagePort('source image to denoise', name='source'),
])
outputs = Ports([
ImagePort('result after denoising', name='result'),
])
__doc__ = ImageFiltering_abstract.generate_docstring(
description, algorithms, options_list, inputs, outputs)