# Pyctools - a picture processing algorithm development kit.
# http://github.com/jim-easterbrook/pyctools
# Copyright (C) 2014-20 Pyctools contributors
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
# <http://www.gnu.org/licenses/>.
__all__ = ['Resize', 'FilterResponse']
__docformat__ = 'restructuredtext en'
import sys
if 'sphinx' in sys.modules:
__all__.append('resize_frame')
import numpy
from pyctools.core.config import ConfigInt
from pyctools.core.base import Transformer
from .resizecore import resize_frame
[docs]
class Resize(Transformer):
"""Filter an image and/or resize with interpolation.
Resize (or just filter) an image using user supplied filter(s). The
filters are supplied in a :py:class:`~pyctools.core.frame.Frame`
object sent to the :py:meth:`filter` input. If the frame data's 3rd
dimension is unity then the same filter is applied to each component
of the input. Alternatively the frame data's 3rd dimension should
match the input's, allowing a different filter to be applied to each
colour.
Images can be resized by almost any amount. The resizing is
controlled by integer "up" and "down" factors and is not constrained
to simple ratios such as 2:1 or 5:4.
To filter images without resizing leave the "up" and "down" factors
at their default value of 1.
The core method :py:meth:`resize_frame` is written in Cython,
allowing real-time image resizing on a typical computer.
The ``filter`` output forwards the filter frame whenever it changes.
It can be connected to a :py:class:`FilterResponse` component to
compute the (new) frequency response.
Config:
========= === ====
``xup`` int Horizontal up-conversion factor.
``xdown`` int Horizontal down-conversion factor.
``yup`` int Vertical up-conversion factor.
``ydown`` int Vertical down-conversion factor.
========= === ====
"""
inputs = ['input', 'filter'] #:
outputs = ['output', 'filter'] #:
def initialise(self):
self.config['xup'] = ConfigInt(min_value=1)
self.config['xdown'] = ConfigInt(min_value=1)
self.config['yup'] = ConfigInt(min_value=1)
self.config['ydown'] = ConfigInt(min_value=1)
self.filter_frame = None
def get_filter(self):
new_filter = self.input_buffer['filter'].peek()
if not new_filter:
return False
if new_filter == self.filter_frame:
return True
self.send('filter', new_filter)
filter_coefs = new_filter.as_numpy(dtype=numpy.float32)
if filter_coefs.ndim != 3:
self.logger.warning('Filter input must be 3 dimensional')
return False
ylen, xlen = filter_coefs.shape[:2]
if (xlen % 2) != 1 or (ylen % 2) != 1:
self.logger.warning('Filter input must have odd width & height')
return False
self.filter_frame = new_filter
self.filter_coefs = filter_coefs
self.fil_count = None
return True
def transform(self, in_frame, out_frame):
if not self.get_filter():
return False
self.update_config()
x_up = self.config['xup']
x_down = self.config['xdown']
y_up = self.config['yup']
y_down = self.config['ydown']
in_data = in_frame.as_numpy(dtype=numpy.float32)
if self.fil_count != self.filter_coefs.shape[2]:
self.fil_count = self.filter_coefs.shape[2]
if self.fil_count != 1 and self.fil_count != in_data.shape[2]:
self.logger.warning('Mismatch between %d filters and %d images',
self.fil_count, in_data.shape[2])
norm_filter = self.filter_coefs * numpy.float32(x_up * y_up)
out_frame.data = resize_frame(
in_data, norm_filter, x_up, x_down, y_up, y_down)
audit = 'data = filter(data)\n'
if x_up != 1 or x_down != 1:
audit = 'data = resize(data)\n'
audit += ' x_up: %d, x_down: %d\n' % (x_up, x_down)
if y_up != 1 or y_down != 1:
audit = 'data = resize(data)\n'
audit += ' y_up: %d, y_down: %d\n' % (y_up, y_down)
audit += ' filter = {\n '
audit += '\n '.join(
self.filter_frame.metadata.get('audit').splitlines())
audit += '\n }\n'
out_frame.set_audit(self, audit)
return True
[docs]
class FilterResponse(Transformer):
"""Compute frequency response of a 1-D filter.
The filter is padded to a power of 2 (e.g. 1024) before computing
the Fourier transform. The magnitude of the positive frequency half
is output in a form suitable for the
:py:class:`~pyctools.components.io.plotdata.PlotData` component.
"""
inputs = ['filter'] #:
outputs = ['response'] #:
def transform(self, in_frame, out_frame):
filter_coefs = in_frame.as_numpy(dtype=numpy.float32)
if filter_coefs.ndim != 3:
self.logger.warning('Filter frame must be 3 dimensional')
return False
ylen, xlen, comps = filter_coefs.shape
if xlen > 1 and ylen > 1:
return False
responses = []
pad_len = 1024
if xlen > 1:
while pad_len < xlen:
pad_len *= 2
padded = numpy.zeros(pad_len)
for c in range(comps):
padded[0:xlen] = filter_coefs[0, :, c]
responses.append(numpy.absolute(numpy.fft.rfft(padded)))
elif ylen > 1:
while pad_len < ylen:
pad_len *= 2
padded = numpy.zeros(pad_len)
for c in range(comps):
padded[0:ylen] = filter_coefs[:, 0, c]
responses.append(numpy.absolute(numpy.fft.rfft(padded)))
responses.insert(0, numpy.linspace(0.0, 0.5, responses[0].shape[0]))
# generate output frame
out_frame.data = numpy.stack(responses)
out_frame.type = 'resp'
labels = ['normalised frequency']
if comps > 1:
for c in range(comps):
labels.append('component {}'.format(c))
out_frame.metadata.set('labels', repr(labels))
audit = out_frame.metadata.get('audit')
audit += 'data = FilterResponse(data)\n'
out_frame.metadata.set('audit', audit)
return True