summaryrefslogtreecommitdiff
path: root/silx/opencl/medfilt.py
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
commitf7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch)
tree9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/opencl/medfilt.py
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/opencl/medfilt.py')
-rw-r--r--silx/opencl/medfilt.py269
1 files changed, 269 insertions, 0 deletions
diff --git a/silx/opencl/medfilt.py b/silx/opencl/medfilt.py
new file mode 100644
index 0000000..90cd49a
--- /dev/null
+++ b/silx/opencl/medfilt.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+#
+# Project: Azimuthal integration
+# https://github.com/silx-kit/pyFAI
+#
+# Copyright (C) 2012-2017 European Synchrotron Radiation Facility, Grenoble, France
+#
+# Principal author: Jérôme Kieffer (Jerome.Kieffer@ESRF.eu)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+# .
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+# .
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""A module for performing the 1d, 2d and 3d median filter ...
+
+The target is to mimic the signature of scipy.signal.medfilt and scipy.medfilt2
+
+The first implementation targets 2D implementation where this operation is costly (~10s/2kx2k image)
+"""
+from __future__ import absolute_import, print_function, with_statement, division
+
+
+__author__ = "Jerome Kieffer"
+__license__ = "MIT"
+__date__ = "15/03/2017"
+__copyright__ = "2012-2017, ESRF, Grenoble"
+__contact__ = "jerome.kieffer@esrf.fr"
+
+import logging
+import numpy
+from collections import OrderedDict
+
+from .common import pyopencl, kernel_workgroup_size
+from .processing import EventDescription, OpenclProcessing, BufferDescription
+
+if pyopencl:
+ mf = pyopencl.mem_flags
+else:
+ raise ImportError("pyopencl is not installed")
+logger = logging.getLogger("silx.opencl.medfilt")
+
+
+class MedianFilter2D(OpenclProcessing):
+ """A class for doing median filtering using OpenCL"""
+ buffers = [
+ BufferDescription("result", 1, numpy.float32, mf.WRITE_ONLY),
+ BufferDescription("image_raw", 1, numpy.float32, mf.READ_ONLY),
+ BufferDescription("image", 1, numpy.float32, mf.READ_WRITE),
+ ]
+ kernel_files = ["preprocess.cl", "bitonic.cl", "medfilt.cl"]
+ mapping = {numpy.int8: "s8_to_float",
+ numpy.uint8: "u8_to_float",
+ numpy.int16: "s16_to_float",
+ numpy.uint16: "u16_to_float",
+ numpy.uint32: "u32_to_float",
+ numpy.int32: "s32_to_float"}
+
+ def __init__(self, shape, kernel_size=(3, 3),
+ ctx=None, devicetype="all", platformid=None, deviceid=None,
+ block_size=None, profile=False
+ ):
+ """Constructor of the OpenCL 2D median filtering class
+
+ :param shape: shape of the images to treat
+ :param kernel size: 2-tuple of odd values
+ :param ctx: actual working context, left to None for automatic
+ initialization from device type or platformid/deviceid
+ :param devicetype: type of device, can be "CPU", "GPU", "ACC" or "ALL"
+ :param platformid: integer with the platform_identifier, as given by clinfo
+ :param deviceid: Integer with the device identifier, as given by clinfo
+ :param block_size: preferred workgroup size, may vary depending on the outpcome of the compilation
+ :param profile: switch on profiling to be able to profile at the kernel level,
+ store profiling elements (makes code slightly slower)
+ """
+ OpenclProcessing.__init__(self, ctx=ctx, devicetype=devicetype,
+ platformid=platformid, deviceid=deviceid,
+ block_size=block_size, profile=profile)
+ self.shape = shape
+ self.size = self.shape[0] * self.shape[1]
+ self.kernel_size = self.calc_kernel_size(kernel_size)
+ self.workgroup_size = (self.calc_wg(self.kernel_size), 1) # 3D kernel
+ self.buffers = [BufferDescription(i.name, i.size * self.size, i.dtype, i.flags)
+ for i in self.__class__.buffers]
+
+ self.allocate_buffers()
+ self.local_mem = self._get_local_mem(self.workgroup_size[0])
+ OpenclProcessing.compile_kernels(self, self.kernel_files, "-D NIMAGE=%i" % self.size)
+ self.set_kernel_arguments()
+
+ def set_kernel_arguments(self):
+ """Parametrize all kernel arguments
+ """
+ for val in self.mapping.values():
+ self.cl_kernel_args[val] = OrderedDict(((i, self.cl_mem[i]) for i in ("image_raw", "image")))
+ self.cl_kernel_args["medfilt2d"] = OrderedDict((("image", self.cl_mem["image"]),
+ ("result", self.cl_mem["result"]),
+ ("local", self.local_mem),
+ ("khs1", numpy.int32(self.kernel_size[0] // 2)), # Kernel half-size along dim1 (lines)
+ ("khs2", numpy.int32(self.kernel_size[1] // 2)), # Kernel half-size along dim2 (columns)
+ ("height", numpy.int32(self.shape[0])), # Image size along dim1 (lines)
+ ("width", numpy.int32(self.shape[1]))))
+# ('debug', self.cl_mem["debug"]))) # Image size along dim2 (columns))
+
+ def _get_local_mem(self, wg):
+ return pyopencl.LocalMemory(wg * 32) # 4byte per float, 8 element per thread
+
+ def send_buffer(self, data, dest):
+ """Send a numpy array to the device, including the cast on the device if possible
+
+ :param data: numpy array with data
+ :param dest: name of the buffer as registered in the class
+ """
+
+ dest_type = numpy.dtype([i.dtype for i in self.buffers if i.name == dest][0])
+ events = []
+ if (data.dtype == dest_type) or (data.dtype.itemsize > dest_type.itemsize):
+ copy_image = pyopencl.enqueue_copy(self.queue, self.cl_mem[dest], numpy.ascontiguousarray(data, dest_type))
+ events.append(EventDescription("copy H->D %s" % dest, copy_image))
+ else:
+ copy_image = pyopencl.enqueue_copy(self.queue, self.cl_mem["image_raw"], numpy.ascontiguousarray(data))
+ kernel = getattr(self.program, self.mapping[data.dtype.type])
+ cast_to_float = kernel(self.queue, (self.size,), None, self.cl_mem["image_raw"], self.cl_mem[dest])
+ events += [EventDescription("copy H->D %s" % dest, copy_image), EventDescription("cast to float", cast_to_float)]
+ if self.profile:
+ self.events += events
+
+ def calc_wg(self, kernel_size):
+ """calculate and return the optimal workgroup size for the first dimension, taking into account
+ the 8-height band
+
+ :param kernel_size: 2-tuple of int, shape of the median window
+ :return: optimal workgroup size
+ """
+ needed_threads = ((kernel_size[0] + 7) // 8) * kernel_size[1]
+ if needed_threads < 8:
+ wg = 8
+ elif needed_threads < 32:
+ wg = 32
+ else:
+ wg = 1 << (int(needed_threads).bit_length())
+ return wg
+
+ def medfilt2d(self, image, kernel_size=None):
+ """Actually apply the median filtering on the image
+
+ :param image: numpy array with the image
+ :param kernel_size: 2-tuple if
+ :return: median-filtered 2D image
+
+
+ Nota: for window size 1x1 -> 7x7 up to 49 / 64 elements in 8 threads, 8elt/th
+ 9x9 -> 15x15 up to 225 / 256 elements in 32 threads, 8elt/th
+ 17x17 -> 21x21 up to 441 / 512 elements in 64 threads, 8elt/th
+
+ TODO: change window size on the fly,
+
+
+ """
+ events = []
+ if kernel_size is None:
+ kernel_size = self.kernel_size
+ else:
+ kernel_size = self.calc_kernel_size(kernel_size)
+ kernel_half_size = kernel_size // numpy.int32(2)
+ # this is the workgroup size
+ wg = self.calc_wg(kernel_size)
+
+ # check for valid work group size:
+ amws = kernel_workgroup_size(self.program, "medfilt2d")
+ logger.warning("max actual workgroup size: %s, expected: %s", amws, wg)
+ if wg > amws:
+ raise RuntimeError("Workgroup size is too big for medfilt2d: %s>%s" % (wg, amws))
+
+ localmem = self._get_local_mem(wg)
+
+ assert image.ndim == 2, "Treat only 2D images"
+ assert image.shape[0] <= self.shape[0], "height is OK"
+ assert image.shape[1] <= self.shape[1], "width is OK"
+
+ with self.sem:
+ self.send_buffer(image, "image")
+
+ kwargs = self.cl_kernel_args["medfilt2d"]
+ kwargs["local"] = localmem
+ kwargs["khs1"] = kernel_half_size[0]
+ kwargs["khs2"] = kernel_half_size[1]
+ kwargs["height"] = numpy.int32(image.shape[0])
+ kwargs["width"] = numpy.int32(image.shape[1])
+# for k, v in kwargs.items():
+# print("%s: %s (%s)" % (k, v, type(v)))
+ mf2d = self.program.medfilt2d(self.queue,
+ (wg, image.shape[1]),
+ (wg, 1), *list(kwargs.values()))
+ events.append(EventDescription("median filter 2d", mf2d))
+
+ result = numpy.empty(image.shape, numpy.float32)
+ ev = pyopencl.enqueue_copy(self.queue, result, self.cl_mem["result"])
+ events.append(EventDescription("copy D->H result", ev))
+ ev.wait()
+ if self.profile:
+ self.events += events
+ return result
+ __call__ = medfilt2d
+
+ @staticmethod
+ def calc_kernel_size(kernel_size):
+ """format the kernel size to be a 2-length numpy array of int32
+ """
+ kernel_size = numpy.asarray(kernel_size, dtype=numpy.int32)
+ if kernel_size.shape == ():
+ kernel_size = numpy.repeat(kernel_size.item(), 2).astype(numpy.int32)
+ for size in kernel_size:
+ if (size % 2) != 1:
+ raise ValueError("Each element of kernel_size should be odd.")
+ return kernel_size
+
+
+class _MedFilt2d(object):
+ median_filter = None
+
+ @classmethod
+ def medfilt2d(cls, ary, kernel_size=3):
+ """Median filter a 2-dimensional array.
+
+ Apply a median filter to the `input` array using a local window-size
+ given by `kernel_size` (must be odd).
+
+ :param ary: A 2-dimensional input array.
+ :param kernel_size: A scalar or a list of length 2, giving the size of the
+ median filter window in each dimension. Elements of
+ `kernel_size` should be odd. If `kernel_size` is a scalar,
+ then this scalar is used as the size in each dimension.
+ Default is a kernel of size (3, 3).
+ :return: An array the same size as input containing the median filtered
+ result. always work on float32 values
+
+ About the padding:
+
+ * The filling mode in scipy.signal.medfilt2d is zero-padding
+ * This implementation is equivalent to:
+ scipy.ndimage.filters.median_filter(ary, kernel_size, mode="nearest")
+
+ """
+ image = numpy.atleast_2d(ary)
+ shape = numpy.array(image.shape)
+ if cls.median_filter is None:
+ cls.median_filter = MedianFilter2D(image.shape, kernel_size)
+ elif (numpy.array(cls.median_filter.shape) < shape).any():
+ # enlarger the buffer size
+ new_shape = numpy.maximum(numpy.array(cls.median_filter.shape), shape)
+ ctx = cls.median_filter.ctx
+ cls.median_filter = MedianFilter2D(new_shape, kernel_size, ctx=ctx)
+ return cls.median_filter.medfilt2d(image)
+
+medfilt2d = _MedFilt2d.medfilt2d