Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

View file

@ -0,0 +1,4 @@
[fits]
description = FITS image reading via PyFITS
provides = imread, imread_collection

View file

@ -0,0 +1,146 @@
__all__ = ['imread', 'imread_collection']
import skimage.io as io
from warnings import warn
try:
from astropy.io import fits
except ImportError:
raise ImportError(
"Astropy could not be found. It is needed to read FITS files.\n"
"Please refer to https://www.astropy.org for installation\n"
"instructions.")
def imread(fname):
"""Load an image from a FITS file.
Parameters
----------
fname : string
Image file name, e.g. ``test.fits``.
Returns
-------
img_array : ndarray
Unlike plugins such as PIL, where different color bands/channels are
stored in the third dimension, FITS images are greyscale-only and can
be N-dimensional, so an array of the native FITS dimensionality is
returned, without color channels.
Currently if no image is found in the file, None will be returned
Notes
-----
Currently FITS ``imread()`` always returns the first image extension when
given a Multi-Extension FITS file; use ``imread_collection()`` (which does
lazy loading) to get all the extensions at once.
"""
hdulist = fits.open(fname)
# Iterate over FITS image extensions, ignoring any other extension types
# such as binary tables, and get the first image data array:
img_array = None
for hdu in hdulist:
if isinstance(hdu, fits.ImageHDU) or \
isinstance(hdu, fits.PrimaryHDU):
if hdu.data is not None:
img_array = hdu.data
break
hdulist.close()
return img_array
def imread_collection(load_pattern, conserve_memory=True):
"""Load a collection of images from one or more FITS files
Parameters
----------
load_pattern : str or list
List of extensions to load. Filename globbing is currently
unsupported.
converve_memory : bool
If True, never keep more than one in memory at a specific
time. Otherwise, images will be cached once they are loaded.
Returns
-------
ic : ImageCollection
Collection of images.
"""
intype = type(load_pattern)
if intype is not list and intype is not str:
raise TypeError("Input must be a filename or list of filenames")
# Ensure we have a list, otherwise we'll end up iterating over the string:
if intype is not list:
load_pattern = [load_pattern]
# Generate a list of filename/extension pairs by opening the list of
# files and finding the image extensions in each one:
ext_list = []
for filename in load_pattern:
hdulist = fits.open(filename)
for n, hdu in zip(range(len(hdulist)), hdulist):
if isinstance(hdu, fits.ImageHDU) or \
isinstance(hdu, fits.PrimaryHDU):
# Ignore (primary) header units with no data (use '.size'
# rather than '.data' to avoid actually loading the image):
try:
data_size = hdu.size # size is int in Astropy 3.1.2
except TypeError:
data_size = hdu.size()
if data_size > 0:
ext_list.append((filename, n))
hdulist.close()
return io.ImageCollection(ext_list, load_func=FITSFactory,
conserve_memory=conserve_memory)
def FITSFactory(image_ext):
"""Load an image extension from a FITS file and return a NumPy array
Parameters
----------
image_ext : tuple
FITS extension to load, in the format ``(filename, ext_num)``.
The FITS ``(extname, extver)`` format is unsupported, since this
function is not called directly by the user and
``imread_collection()`` does the work of figuring out which
extensions need loading.
"""
# Expect a length-2 tuple with a filename as the first element:
if not isinstance(image_ext, tuple):
raise TypeError("Expected a tuple")
if len(image_ext) != 2:
raise ValueError("Expected a tuple of length 2")
filename = image_ext[0]
extnum = image_ext[1]
if type(filename) is not str or type(extnum) is not int:
raise ValueError("Expected a (filename, extension) tuple")
hdulist = fits.open(filename)
data = hdulist[extnum].data
hdulist.close()
if data is None:
raise RuntimeError(
"Extension %d of %s has no data" % (extnum, filename))
return data

View file

@ -0,0 +1,4 @@
[gdal]
description = Image reading via the GDAL Library (www.gdal.org)
provides = imread

View file

@ -0,0 +1,19 @@
__all__ = ['imread']
from warnings import warn
try:
import osgeo.gdal as gdal
except ImportError:
raise ImportError("The GDAL Library could not be found. "
"Please refer to http://www.gdal.org/ "
"for further instructions.")
def imread(fname):
"""Load an image from file.
"""
ds = gdal.Open(fname)
return ds.ReadAsArray()

View file

@ -0,0 +1,4 @@
[gtk]
description = Fast image display using the GTK library
provides = imshow, _app_show

View file

@ -0,0 +1,54 @@
from .util import prepare_for_display, window_manager, GuiLockError
try:
# we try to acquire the gui lock first
# or else the gui import might trample another
# gui's pyos_inputhook.
window_manager.acquire('gtk')
except GuiLockError as gle:
print(gle)
else:
try:
import gtk
except ImportError:
print('pygtk libraries not installed.')
print('plugin not loaded.')
window_manager._release('gtk')
else:
class ImageWindow(gtk.Window):
def __init__(self, arr, mgr):
gtk.Window.__init__(self)
self.mgr = mgr
self.mgr.add_window(self)
self.connect("destroy", self.destroy)
width = arr.shape[1]
height = arr.shape[0]
rstride = arr.strides[0]
pb = gtk.gdk.pixbuf_new_from_data(arr.data,
gtk.gdk.COLORSPACE_RGB,
False, 8, width, height,
rstride)
self.img = gtk.Image()
self.img.set_from_pixbuf(pb)
self.add(self.img)
self.img.show()
def destroy(self, widget, data=None):
self.mgr.remove_window(self)
def imshow(arr):
arr = prepare_for_display(arr)
iw = ImageWindow(arr, window_manager)
iw.show()
def _app_show():
if window_manager.has_windows():
window_manager.register_callback(gtk.main_quit)
gtk.main()
else:
print('no images to display')

View file

@ -0,0 +1,3 @@
[imageio]
description = Image reading via the ImageIO Library
provides = imread, imsave

View file

@ -0,0 +1,10 @@
__all__ = ['imread', 'imsave']
from functools import wraps
import numpy as np
from imageio import imread as imageio_imread, imsave
@wraps(imageio_imread)
def imread(*args, **kwargs):
return np.asarray(imageio_imread(*args, **kwargs))

View file

@ -0,0 +1,3 @@
[imread]
description = Image reading and writing via imread
provides = imread, imsave

View file

@ -0,0 +1,44 @@
__all__ = ['imread', 'imsave']
from ...util.dtype import _convert
try:
import imread as _imread
except ImportError:
raise ImportError("Imread could not be found"
"Please refer to http://pypi.python.org/pypi/imread/ "
"for further instructions.")
def imread(fname, dtype=None):
"""Load an image from file.
Parameters
----------
fname : str
Name of input file
"""
im = _imread.imread(fname)
if dtype is not None:
im = _convert(im, dtype)
return im
def imsave(fname, arr, format_str=None):
"""Save an image to disk.
Parameters
----------
fname : str
Name of destination file.
arr : ndarray of uint8 or uint16
Array (image) to save.
format_str: str,optional
Format to save as.
Notes
-----
Currently, only 8-bit precision is supported.
"""
return _imread.imsave(fname, arr, formatstr=format_str)

View file

@ -0,0 +1,3 @@
[matplotlib]
description = Display or save images using Matplotlib
provides = imshow, imread, imshow_collection, _app_show

View file

@ -0,0 +1,208 @@
from collections import namedtuple
import numpy as np
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.image
from ...util import dtype as dtypes
from ...exposure import is_low_contrast
from ..._shared.utils import warn
from math import floor, ceil
_default_colormap = 'gray'
_nonstandard_colormap = 'viridis'
_diverging_colormap = 'RdBu'
ImageProperties = namedtuple('ImageProperties',
['signed', 'out_of_range_float',
'low_data_range', 'unsupported_dtype'])
def _get_image_properties(image):
"""Determine nonstandard properties of an input image.
Parameters
----------
image : array
The input image.
Returns
-------
ip : ImageProperties named tuple
The properties of the image:
- signed: whether the image has negative values.
- out_of_range_float: if the image has floating point data
outside of [-1, 1].
- low_data_range: if the image is in the standard image
range (e.g. [0, 1] for a floating point image) but its
data range would be too small to display with standard
image ranges.
- unsupported_dtype: if the image data type is not a
standard skimage type, e.g. ``numpy.uint64``.
"""
immin, immax = np.min(image), np.max(image)
imtype = image.dtype.type
try:
lo, hi = dtypes.dtype_range[imtype]
except KeyError:
lo, hi = immin, immax
signed = immin < 0
out_of_range_float = (np.issubdtype(image.dtype, np.floating) and
(immin < lo or immax > hi))
low_data_range = (immin != immax and
is_low_contrast(image))
unsupported_dtype = image.dtype not in dtypes._supported_types
return ImageProperties(signed, out_of_range_float,
low_data_range, unsupported_dtype)
def _raise_warnings(image_properties):
"""Raise the appropriate warning for each nonstandard image type.
Parameters
----------
image_properties : ImageProperties named tuple
The properties of the considered image.
"""
ip = image_properties
if ip.unsupported_dtype:
warn("Non-standard image type; displaying image with "
"stretched contrast.", stacklevel=3)
if ip.low_data_range:
warn("Low image data range; displaying image with "
"stretched contrast.", stacklevel=3)
if ip.out_of_range_float:
warn("Float image out of standard range; displaying "
"image with stretched contrast.", stacklevel=3)
def _get_display_range(image):
"""Return the display range for a given set of image properties.
Parameters
----------
image : array
The input image.
Returns
-------
lo, hi : same type as immin, immax
The display range to be used for the input image.
cmap : string
The name of the colormap to use.
"""
ip = _get_image_properties(image)
immin, immax = np.min(image), np.max(image)
if ip.signed:
magnitude = max(abs(immin), abs(immax))
lo, hi = -magnitude, magnitude
cmap = _diverging_colormap
elif any(ip):
_raise_warnings(ip)
lo, hi = immin, immax
cmap = _nonstandard_colormap
else:
lo = 0
imtype = image.dtype.type
hi = dtypes.dtype_range[imtype][1]
cmap = _default_colormap
return lo, hi, cmap
def imshow(image, ax=None, show_cbar=None, **kwargs):
"""Show the input image and return the current axes.
By default, the image is displayed in greyscale, rather than
the matplotlib default colormap.
Images are assumed to have standard range for their type. For
example, if a floating point image has values in [0, 0.5], the
most intense color will be gray50, not white.
If the image exceeds the standard range, or if the range is too
small to display, we fall back on displaying exactly the range of
the input image, along with a colorbar to clearly indicate that
this range transformation has occurred.
For signed images, we use a diverging colormap centered at 0.
Parameters
----------
image : array, shape (M, N[, 3])
The image to display.
ax: `matplotlib.axes.Axes`, optional
The axis to use for the image, defaults to plt.gca().
show_cbar: boolean, optional.
Whether to show the colorbar (used to override default behavior).
**kwargs : Keyword arguments
These are passed directly to `matplotlib.pyplot.imshow`.
Returns
-------
ax_im : `matplotlib.pyplot.AxesImage`
The `AxesImage` object returned by `plt.imshow`.
"""
import matplotlib.pyplot as plt
lo, hi, cmap = _get_display_range(image)
kwargs.setdefault('interpolation', 'nearest')
kwargs.setdefault('cmap', cmap)
kwargs.setdefault('vmin', lo)
kwargs.setdefault('vmax', hi)
ax = ax or plt.gca()
ax_im = ax.imshow(image, **kwargs)
if (cmap != _default_colormap and show_cbar is not False) or show_cbar:
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
plt.colorbar(ax_im, cax=cax)
ax.get_figure().tight_layout()
return ax_im
def imshow_collection(ic, *args, **kwargs):
"""Display all images in the collection.
Returns
-------
fig : `matplotlib.figure.Figure`
The `Figure` object returned by `plt.subplots`.
"""
import matplotlib.pyplot as plt
if len(ic) < 1:
raise ValueError('Number of images to plot must be greater than 0')
# The target is to plot images on a grid with aspect ratio 4:3
num_images = len(ic)
# Two pairs of `nrows, ncols` are possible
k = (num_images * 12)**0.5
r1 = max(1, floor(k / 4))
r2 = ceil(k / 4)
c1 = ceil(num_images / r1)
c2 = ceil(num_images / r2)
# Select the one which is closer to 4:3
if abs(r1 / c1 - 0.75) < abs(r2 / c2 - 0.75):
nrows, ncols = r1, c1
else:
nrows, ncols = r2, c2
fig, axes = plt.subplots(nrows=nrows, ncols=ncols)
ax = np.asarray(axes).ravel()
for n, image in enumerate(ic):
ax[n].imshow(image, *args, **kwargs)
kwargs['ax'] = axes
return fig
imread = matplotlib.image.imread
def _app_show():
from matplotlib.pyplot import show
show()

View file

@ -0,0 +1,3 @@
[pil]
description = Image reading via the Python Imaging Library
provides = imread, imsave

View file

@ -0,0 +1,267 @@
__all__ = ['imread', 'imsave']
from distutils.version import LooseVersion
import warnings
import numpy as np
from PIL import Image, __version__ as pil_version
from ...util import img_as_ubyte, img_as_uint
def imread(fname, dtype=None, img_num=None, **kwargs):
"""Load an image from file.
Parameters
----------
fname : str or file
File name or file-like-object.
dtype : numpy dtype object or string specifier
Specifies data type of array elements.
img_num : int, optional
Specifies which image to read in a file with multiple images
(zero-indexed).
kwargs : keyword pairs, optional
Addition keyword arguments to pass through.
Notes
-----
Files are read using the Python Imaging Library.
See PIL docs [1]_ for a list of supported formats.
References
----------
.. [1] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
"""
if isinstance(fname, str):
with open(fname, 'rb') as f:
im = Image.open(f)
return pil_to_ndarray(im, dtype=dtype, img_num=img_num)
else:
im = Image.open(fname)
if im.format == 'MPO' and LooseVersion(pil_version) < '6.0.0':
warnings.warn("You are trying to read a MPO image. "
"To ensure a good support of this format, "
"please upgrade pillow to 6.0.0 version or later.",
stacklevel=2)
return pil_to_ndarray(im, dtype=dtype, img_num=img_num)
def pil_to_ndarray(image, dtype=None, img_num=None):
"""Import a PIL Image object to an ndarray, in memory.
Parameters
----------
Refer to ``imread``.
"""
try:
# this will raise an IOError if the file is not readable
image.getdata()[0]
except IOError as e:
site = "http://pillow.readthedocs.org/en/latest/installation.html#external-libraries"
pillow_error_message = str(e)
error_message = ('Could not load "%s" \n'
'Reason: "%s"\n'
'Please see documentation at: %s'
% (image.filename, pillow_error_message, site))
raise ValueError(error_message)
frames = []
grayscale = None
i = 0
while 1:
try:
image.seek(i)
except EOFError:
break
frame = image
if img_num is not None and img_num != i:
image.getdata()[0]
i += 1
continue
if image.format == 'PNG' and image.mode == 'I' and dtype is None:
dtype = 'uint16'
if image.mode == 'P':
if grayscale is None:
grayscale = _palette_is_grayscale(image)
if grayscale:
frame = image.convert('L')
else:
if image.format == 'PNG' and 'transparency' in image.info:
frame = image.convert('RGBA')
else:
frame = image.convert('RGB')
elif image.mode == '1':
frame = image.convert('L')
elif 'A' in image.mode:
frame = image.convert('RGBA')
elif image.mode == 'CMYK':
frame = image.convert('RGB')
if image.mode.startswith('I;16'):
shape = image.size
dtype = '>u2' if image.mode.endswith('B') else '<u2'
if 'S' in image.mode:
dtype = dtype.replace('u', 'i')
frame = np.fromstring(frame.tobytes(), dtype)
frame.shape = shape[::-1]
else:
frame = np.array(frame, dtype=dtype)
frames.append(frame)
i += 1
if img_num is not None:
break
if hasattr(image, 'fp') and image.fp:
image.fp.close()
if img_num is None and len(frames) > 1:
return np.array(frames)
elif frames:
return frames[0]
elif img_num:
raise IndexError('Could not find image #%s' % img_num)
def _palette_is_grayscale(pil_image):
"""Return True if PIL image in palette mode is grayscale.
Parameters
----------
pil_image : PIL image
PIL Image that is in Palette mode.
Returns
-------
is_grayscale : bool
True if all colors in image palette are gray.
"""
if pil_image.mode != 'P':
raise ValueError('pil_image.mode must be equal to "P".')
# get palette as an array with R, G, B columns
palette = np.asarray(pil_image.getpalette()).reshape((256, 3))
# Not all palette colors are used; unused colors have junk values.
start, stop = pil_image.getextrema()
valid_palette = palette[start:stop + 1]
# Image is grayscale if channel differences (R - G and G - B)
# are all zero.
return np.allclose(np.diff(valid_palette), 0)
def ndarray_to_pil(arr, format_str=None):
"""Export an ndarray to a PIL object.
Parameters
----------
Refer to ``imsave``.
"""
if arr.ndim == 3:
arr = img_as_ubyte(arr)
mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]]
elif format_str in ['png', 'PNG']:
mode = 'I;16'
mode_base = 'I'
if arr.dtype.kind == 'f':
arr = img_as_uint(arr)
elif arr.max() < 256 and arr.min() >= 0:
arr = arr.astype(np.uint8)
mode = mode_base = 'L'
else:
arr = img_as_uint(arr)
else:
arr = img_as_ubyte(arr)
mode = 'L'
mode_base = 'L'
try:
array_buffer = arr.tobytes()
except AttributeError:
array_buffer = arr.tostring() # Numpy < 1.9
if arr.ndim == 2:
im = Image.new(mode_base, arr.T.shape)
try:
im.frombytes(array_buffer, 'raw', mode)
except AttributeError:
im.fromstring(array_buffer, 'raw', mode) # PIL 1.1.7
else:
image_shape = (arr.shape[1], arr.shape[0])
try:
im = Image.frombytes(mode, image_shape, array_buffer)
except AttributeError:
im = Image.fromstring(mode, image_shape, array_buffer) # PIL 1.1.7
return im
def imsave(fname, arr, format_str=None, **kwargs):
"""Save an image to disk.
Parameters
----------
fname : str or file-like object
Name of destination file.
arr : ndarray of uint8 or float
Array (image) to save. Arrays of data-type uint8 should have
values in [0, 255], whereas floating-point arrays must be
in [0, 1].
format_str: str
Format to save as, this is defaulted to PNG if using a file-like
object; this will be derived from the extension if fname is a string
kwargs: dict
Keyword arguments to the Pillow save function (or tifffile save
function, for Tiff files). These are format dependent. For example,
Pillow's JPEG save function supports an integer ``quality`` argument
with values in [1, 95], while TIFFFile supports a ``compress``
integer argument with values in [0, 9].
Notes
-----
Use the Python Imaging Library.
See PIL docs [1]_ for a list of other supported formats.
All images besides single channel PNGs are converted using `img_as_uint8`.
Single Channel PNGs have the following behavior:
- Integer values in [0, 255] and Boolean types -> img_as_uint8
- Floating point and other integers -> img_as_uint16
References
----------
.. [1] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
"""
# default to PNG if file-like object
if not isinstance(fname, str) and format_str is None:
format_str = "PNG"
# Check for png in filename
if (isinstance(fname, str)
and fname.lower().endswith(".png")):
format_str = "PNG"
arr = np.asanyarray(arr)
if arr.dtype.kind == 'b':
arr = arr.astype(np.uint8)
if arr.ndim not in (2, 3):
raise ValueError("Invalid shape for image array: %s" % (arr.shape, ))
if arr.ndim == 3:
if arr.shape[2] not in (3, 4):
raise ValueError("Invalid number of channels in image array.")
img = ndarray_to_pil(arr, format_str=format_str)
img.save(fname, format=format_str, **kwargs)

View file

@ -0,0 +1,342 @@
# the module for the qt color_mixer plugin
from qtpy import QtCore
from qtpy.QtWidgets import (QWidget, QStackedWidget, QSlider, QGridLayout,
QLabel, QFrame, QComboBox, QRadioButton,
QPushButton)
from .util import ColorMixer
class IntelligentSlider(QWidget):
''' A slider that adds a 'name' attribute and calls a callback
with 'name' as an argument to the registered callback.
This allows you to create large groups of sliders in a loop,
but still keep track of the individual events
It also prints a label below the slider.
The range of the slider is hardcoded from zero - 1000,
but it supports a conversion factor so you can scale the results'''
def __init__(self, name, a, b, callback):
QWidget.__init__(self)
self.name = name
self.callback = callback
self.a = a
self.b = b
self.manually_triggered = False
self.slider = QSlider()
self.slider.setRange(0, 1000)
self.slider.setValue(500)
self.slider.valueChanged.connect(self.slider_changed)
self.name_label = QLabel()
self.name_label.setText(self.name)
self.name_label.setAlignment(QtCore.Qt.AlignCenter)
self.value_label = QLabel()
self.value_label.setText('%2.2f' % (self.slider.value() * self.a
+ self.b))
self.value_label.setAlignment(QtCore.Qt.AlignCenter)
self.layout = QGridLayout(self)
self.layout.addWidget(self.name_label, 0, 0)
self.layout.addWidget(self.slider, 1, 0, QtCore.Qt.AlignHCenter)
self.layout.addWidget(self.value_label, 2, 0)
# bind this to the valueChanged signal of the slider
def slider_changed(self, val):
val = self.val()
self.value_label.setText(str(val)[:4])
if not self.manually_triggered:
self.callback(self.name, val)
def set_conv_fac(self, a, b):
self.a = a
self.b = b
def set_value(self, val):
self.manually_triggered = True
self.slider.setValue(int((val - self.b) / self.a))
self.value_label.setText('%2.2f' % val)
self.manually_triggered = False
def val(self):
return self.slider.value() * self.a + self.b
class MixerPanel(QFrame):
'''A color mixer to hook up to an image.
You pass the image you the panel to operate on
and it operates on that image in place. You also
pass a callback to be called to trigger a refresh.
This callback is called every time the mixer modifies
your image.'''
def __init__(self, img):
QFrame.__init__(self)
# self.setFrameStyle(QFrame.Box | QFrame.Sunken)
self.img = img
self.mixer = ColorMixer(self.img)
self.callback = None
#---------------------------------------------------------------
# ComboBox
#---------------------------------------------------------------
self.combo_box_entries = ['RGB Color', 'HSV Color',
'Brightness/Contrast',
'Gamma',
'Gamma (Sigmoidal)']
self.combo_box = QComboBox()
for entry in self.combo_box_entries:
self.combo_box.addItem(entry)
self.combo_box.currentIndexChanged.connect(self.combo_box_changed)
#---------------------------------------------------------------
# RGB color sliders
#---------------------------------------------------------------
# radio buttons
self.rgb_add = QRadioButton('Additive')
self.rgb_mul = QRadioButton('Multiplicative')
self.rgb_mul.toggled.connect(self.rgb_radio_changed)
self.rgb_add.toggled.connect(self.rgb_radio_changed)
# sliders
rs = IntelligentSlider('R', 0.51, -255, self.rgb_changed)
gs = IntelligentSlider('G', 0.51, -255, self.rgb_changed)
bs = IntelligentSlider('B', 0.51, -255, self.rgb_changed)
self.rs = rs
self.gs = gs
self.bs = bs
self.rgb_widget = QWidget()
self.rgb_widget.layout = QGridLayout(self.rgb_widget)
self.rgb_widget.layout.addWidget(self.rgb_add, 0, 0, 1, 3)
self.rgb_widget.layout.addWidget(self.rgb_mul, 1, 0, 1, 3)
self.rgb_widget.layout.addWidget(self.rs, 2, 0)
self.rgb_widget.layout.addWidget(self.gs, 2, 1)
self.rgb_widget.layout.addWidget(self.bs, 2, 2)
#---------------------------------------------------------------
# HSV sliders
#---------------------------------------------------------------
# radio buttons
self.hsv_add = QRadioButton('Additive')
self.hsv_mul = QRadioButton('Multiplicative')
self.hsv_mul.toggled.connect(self.hsv_radio_changed)
self.hsv_mul.toggled.connect(self.hsv_radio_changed)
# sliders
hs = IntelligentSlider('H', 0.36, -180, self.hsv_changed)
ss = IntelligentSlider('S', 0.002, 0, self.hsv_changed)
vs = IntelligentSlider('V', 0.002, 0, self.hsv_changed)
self.hs = hs
self.ss = ss
self.vs = vs
self.hsv_widget = QWidget()
self.hsv_widget.layout = QGridLayout(self.hsv_widget)
self.hsv_widget.layout.addWidget(self.hsv_add, 0, 0, 1, 3)
self.hsv_widget.layout.addWidget(self.hsv_mul, 1, 0, 1, 3)
self.hsv_widget.layout.addWidget(self.hs, 2, 0)
self.hsv_widget.layout.addWidget(self.ss, 2, 1)
self.hsv_widget.layout.addWidget(self.vs, 2, 2)
#---------------------------------------------------------------
# Brightness/Contrast sliders
#---------------------------------------------------------------
# sliders
cont = IntelligentSlider('x', 0.002, 0, self.bright_changed)
bright = IntelligentSlider('+', 0.51, -255, self.bright_changed)
self.cont = cont
self.bright = bright
# layout
self.bright_widget = QWidget()
self.bright_widget.layout = QGridLayout(self.bright_widget)
self.bright_widget.layout.addWidget(self.cont, 0, 0)
self.bright_widget.layout.addWidget(self.bright, 0, 1)
#----------------------------------------------------------------------
# Gamma Slider
#----------------------------------------------------------------------
gamma = IntelligentSlider('gamma', 0.005, 0, self.gamma_changed)
self.gamma = gamma
# layout
self.gamma_widget = QWidget()
self.gamma_widget.layout = QGridLayout(self.gamma_widget)
self.gamma_widget.layout.addWidget(self.gamma, 0, 0)
#---------------------------------------------------------------
# Sigmoid Gamma sliders
#---------------------------------------------------------------
# sliders
alpha = IntelligentSlider('alpha', 0.011, 1, self.sig_gamma_changed)
beta = IntelligentSlider('beta', 0.012, 0, self.sig_gamma_changed)
self.a_gamma = alpha
self.b_gamma = beta
# layout
self.sig_gamma_widget = QWidget()
self.sig_gamma_widget.layout = QGridLayout(self.sig_gamma_widget)
self.sig_gamma_widget.layout.addWidget(self.a_gamma, 0, 0)
self.sig_gamma_widget.layout.addWidget(self.b_gamma, 0, 1)
#---------------------------------------------------------------
# Buttons
#---------------------------------------------------------------
self.commit_button = QPushButton('Commit')
self.commit_button.clicked.connect(self.commit_changes)
self.revert_button = QPushButton('Revert')
self.revert_button.clicked.connect(self.revert_changes)
#---------------------------------------------------------------
# Mixer Layout
#---------------------------------------------------------------
self.sliders = QStackedWidget()
self.sliders.addWidget(self.rgb_widget)
self.sliders.addWidget(self.hsv_widget)
self.sliders.addWidget(self.bright_widget)
self.sliders.addWidget(self.gamma_widget)
self.sliders.addWidget(self.sig_gamma_widget)
self.layout = QGridLayout(self)
self.layout.addWidget(self.combo_box, 0, 0)
self.layout.addWidget(self.sliders, 1, 0)
self.layout.addWidget(self.commit_button, 2, 0)
self.layout.addWidget(self.revert_button, 3, 0)
#---------------------------------------------------------------
# State Initialization
#---------------------------------------------------------------
self.combo_box.setCurrentIndex(0)
self.rgb_mul.setChecked(True)
self.hsv_mul.setChecked(True)
def set_callback(self, callback):
self.callback = callback
def combo_box_changed(self, index):
self.sliders.setCurrentIndex(index)
self.reset()
def rgb_radio_changed(self):
self.reset()
def hsv_radio_changed(self):
self.reset()
def reset(self):
self.reset_sliders()
self.mixer.set_to_stateimg()
if self.callback:
self.callback()
def reset_sliders(self):
# handle changing the conversion factors necessary
if self.rgb_add.isChecked():
self.rs.set_conv_fac(0.51, -255)
self.rs.set_value(0)
self.gs.set_conv_fac(0.51, -255)
self.gs.set_value(0)
self.bs.set_conv_fac(0.51, -255)
self.bs.set_value(0)
else:
self.rs.set_conv_fac(0.002, 0)
self.rs.set_value(1.)
self.gs.set_conv_fac(0.002, 0)
self.gs.set_value(1.)
self.bs.set_conv_fac(0.002, 0)
self.bs.set_value(1.)
self.hs.set_value(0)
if self.hsv_add.isChecked():
self.ss.set_conv_fac(0.002, -1)
self.ss.set_value(0)
self.vs.set_conv_fac(0.002, -1)
self.vs.set_value(0)
else:
self.ss.set_conv_fac(0.002, 0)
self.ss.set_value(1.)
self.vs.set_conv_fac(0.002, 0)
self.vs.set_value(1.)
self.bright.set_value(0)
self.cont.set_value(1.)
self.gamma.set_value(1)
self.a_gamma.set_value(1)
self.b_gamma.set_value(0.5)
def rgb_changed(self, name, val):
if name == 'R':
channel = self.mixer.RED
elif name == 'G':
channel = self.mixer.GREEN
else:
channel = self.mixer.BLUE
if self.rgb_mul.isChecked():
self.mixer.multiply(channel, val)
elif self.rgb_add.isChecked():
self.mixer.add(channel, val)
else:
pass
if self.callback:
self.callback()
def hsv_changed(self, name, val):
h = self.hs.val()
s = self.ss.val()
v = self.vs.val()
if self.hsv_mul.isChecked():
self.mixer.hsv_multiply(h, s, v)
elif self.hsv_add.isChecked():
self.mixer.hsv_add(h, s, v)
else:
pass
if self.callback:
self.callback()
def bright_changed(self, name, val):
b = self.bright.val()
c = self.cont.val()
self.mixer.brightness(c, b)
if self.callback:
self.callback()
def gamma_changed(self, name, val):
self.mixer.gamma(val)
if self.callback:
self.callback()
def sig_gamma_changed(self, name, val):
ag = self.a_gamma.val()
bg = self.b_gamma.val()
self.mixer.sigmoid_gamma(ag, bg)
if self.callback:
self.callback()
def commit_changes(self):
self.mixer.commit_changes()
self.reset_sliders()
def revert_changes(self):
self.mixer.revert()
self.reset_sliders()
if self.callback:
self.callback()

View file

@ -0,0 +1,143 @@
import numpy as np
from qtpy.QtGui import QPainter, QColor
from qtpy.QtWidgets import QWidget, QGridLayout, QFrame
from .util import histograms
class ColorHistogram(QWidget):
'''A Class which draws a scaling histogram in
a widget.
Where counts are the bin values in the histogram
and colormap is a tuple of (R, G, B) tuples the same length
as counts. These are the colors to apply to the histogram bars.
Colormap can also contain a single tuple (R, G, B), in which case this is
the color applied to all bars of that histogram.
The histogram assumes the bins were evenly spaced.
'''
def __init__(self, counts, colormap):
QWidget.__init__(self)
self._validate_input(counts, colormap)
self.counts = counts
self.n = np.sum(self.counts)
self.colormap = colormap
self.setMinimumSize(100, 50)
def _validate_input(self, counts, colormap):
if len(counts) != len(colormap):
if len(colormap) != 3:
msg = '''Colormap must be a list of 3-tuples the same
length as counts or a 3-tuple'''
raise ValueError(msg)
def paintEvent(self, evt):
# get the widget dimensions
orig_width = self.width()
orig_height = self.height()
# fill perc % of the widget
perc = 1
width = int(orig_width * perc)
height = int(orig_height * perc)
# get the starting origin
x_orig = int((orig_width - width) / 2)
# we want to start at the bottom and draw up.
y_orig = orig_height - int((orig_height - height) / 2)
# a running x-position
running_pos = x_orig
# calculate to number of bars
nbars = len(self.counts)
# calculate the bar widths, this compilcation is
# necessary because integer trunction severely cripples
# the layout.
remainder = width % nbars
bar_width = [int(width / nbars)] * nbars
for i in range(remainder):
bar_width[i] += 1
paint = QPainter()
paint.begin(self)
# determine the scaling factor
max_val = np.max(self.counts)
scale = 1. * height / max_val
# determine if we have a colormap and drop into the appropriate
# loop.
if hasattr(self.colormap[0], '__iter__'):
# assume we have a colormap
for i in range(len(self.counts)):
bar_height = self.counts[i]
r, g, b = self.colormap[i]
paint.setPen(QColor(r, g, b))
paint.setBrush(QColor(r, g, b))
paint.drawRect(running_pos, y_orig, bar_width[i],
-bar_height)
running_pos += bar_width[i]
else:
# we have a tuple
r, g, b = self.colormap
paint.setPen(QColor(r, g, b))
paint.setBrush(QColor(r, g, b))
for i in range(len(self.counts)):
bar_height = self.counts[i] * scale
paint.drawRect(running_pos, y_orig, bar_width[i],
-bar_height)
running_pos += bar_width[i]
paint.end()
def update_hist(self, counts, cmap):
self._validate_input(counts, cmap)
self.counts = counts
self.colormap = cmap
self.repaint()
class QuadHistogram(QFrame):
'''A class which uses ColorHistogram to draw
the 4 histograms of an image. R, G, B, and Value.
The 4 histograms are layout out in a grid,
and can be specified horizontal or vertical,
and in which order ie. ['R', 'G', 'B', 'V']
'''
def __init__(self, img, layout='vertical', order=['R', 'G', 'B', 'V']):
QFrame.__init__(self)
r, g, b, v = histograms(img, 100)
self.r_hist = ColorHistogram(r, (255, 0, 0))
self.g_hist = ColorHistogram(g, (0, 255, 0))
self.b_hist = ColorHistogram(b, (0, 0, 255))
self.v_hist = ColorHistogram(v, (0, 0, 0))
self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
self.layout = QGridLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
order_map = {'R': self.r_hist, 'G': self.g_hist, 'B': self.b_hist,
'V': self.v_hist}
if layout == 'vertical':
for i in range(len(order)):
self.layout.addWidget(order_map[order[i]], i, 0)
elif layout == 'horizontal':
for i in range(len(order)):
self.layout.addWidget(order_map[order[i]], 0, i)
def update_hists(self, img):
r, g, b, v = histograms(img, 100)
self.r_hist.update_hist(r, (255, 0, 0))
self.g_hist.update_hist(g, (0, 255, 0))
self.b_hist.update_hist(b, (0, 0, 255))
self.v_hist.update_hist(v, (0, 0, 0))

View file

@ -0,0 +1,4 @@
[qt]
description = Fast image display using the Qt library
provides = imshow, _app_show, imsave, imread

View file

@ -0,0 +1,151 @@
import numpy as np
from .util import prepare_for_display, window_manager
from ..._shared.utils import warn
from qtpy.QtWidgets import (QApplication, QLabel, QMainWindow, QWidget,
QGridLayout)
from qtpy.QtGui import QImage, QPixmap
from qtpy import QtCore
# We try to acquire the gui lock first or else the gui import might
# trample another GUI's PyOS_InputHook.
window_manager.acquire('qt')
app = None
class ImageLabel(QLabel):
def __init__(self, parent, arr):
QLabel.__init__(self)
# we need to hold a reference to
# arr because QImage doesn't copy the data
# and the buffer must be alive as long
# as the image is alive.
self.arr = arr
# we also need to pass in the row-stride to
# the constructor, because we can't guarantee
# that every row of the numpy data is
# 4-byte aligned. Which Qt would require
# if we didn't pass the stride.
self.img = QImage(arr.data, arr.shape[1], arr.shape[0],
arr.strides[0], QImage.Format_RGB888)
self.pm = QPixmap.fromImage(self.img)
self.setPixmap(self.pm)
self.setAlignment(QtCore.Qt.AlignTop)
self.setMinimumSize(100, 100)
def resizeEvent(self, evt):
width = self.width()
pm = QPixmap.fromImage(self.img)
self.pm = pm.scaledToWidth(width)
self.setPixmap(self.pm)
class ImageWindow(QMainWindow):
def __init__(self, arr, mgr):
QMainWindow.__init__(self)
self.setWindowTitle('skimage')
self.mgr = mgr
self.main_widget = QWidget()
self.layout = QGridLayout(self.main_widget)
self.setCentralWidget(self.main_widget)
self.label = ImageLabel(self, arr)
self.layout.addWidget(self.label, 0, 0)
self.layout.addLayout
self.mgr.add_window(self)
self.main_widget.show()
def closeEvent(self, event):
# Allow window to be destroyed by removing any
# references to it
self.mgr.remove_window(self)
def imread(filename):
"""
Read an image using QT's QImage.load
"""
qtimg = QImage()
if not qtimg.load(filename):
# QImage.load() returns false on failure, so raise an exception
raise IOError('Unable to load file %s' % filename)
if qtimg.depth() == 1:
raise IOError('1-bit images currently not supported')
# TODO: Warn about other odd formats we don't currently handle properly,
# such as the odd 16-bit packed formats QT supports
arrayptr = qtimg.bits()
# QT may pad the image, so we need to use bytesPerLine, not width for
# the conversion to a numpy array
bytes_per_pixel = qtimg.depth() // 8
pixels_per_line = qtimg.bytesPerLine() // bytes_per_pixel
img_size = pixels_per_line * qtimg.height() * bytes_per_pixel
arrayptr.setsize(img_size)
img = np.array(arrayptr)
# Reshape and trim down to correct dimensions
if bytes_per_pixel > 1:
img = img.reshape((qtimg.height(), pixels_per_line, bytes_per_pixel))
img = img[:, :qtimg.width(), :]
else:
img = img.reshape((qtimg.height(), pixels_per_line))
img = img[:, :qtimg.width()]
# Strip qt's false alpha channel if needed
# and reorder color axes as required
if bytes_per_pixel == 4 and not qtimg.hasAlphaChannel():
img = img[:, :, 2::-1]
elif bytes_per_pixel == 4:
img[:, :, 0:3] = img[:, :, 2::-1]
return img
def imshow(arr, fancy=False):
global app
if not app:
app = QApplication([])
arr = prepare_for_display(arr)
if not fancy:
iw = ImageWindow(arr, window_manager)
else:
from .skivi import SkiviImageWindow
iw = SkiviImageWindow(arr, window_manager)
iw.show()
def _app_show():
global app
if app and window_manager.has_windows():
app.exec_()
else:
print('No images to show. See `imshow`.')
def imsave(filename, img, format_str=None):
# we can add support for other than 3D uint8 here...
img = prepare_for_display(img)
qimg = QImage(img.data, img.shape[1], img.shape[0],
img.strides[0], QImage.Format_RGB888)
if _is_filelike(filename):
byte_array = QtCore.QByteArray()
qbuffer = QtCore.QBuffer(byte_array)
qbuffer.open(QtCore.QIODevice.ReadWrite)
saved = qimg.save(qbuffer, format_str.upper())
qbuffer.seek(0)
filename.write(qbuffer.readAll().data())
qbuffer.close()
else:
saved = qimg.save(filename)
if not saved:
from textwrap import dedent
msg = dedent(
'''The image was not saved. Allowable file formats
for the QT imsave plugin are:
BMP, JPG, JPEG, PNG, PPM, TIFF, XBM, XPM''')
raise RuntimeError(msg)
def _is_filelike(possible_filelike):
return callable(getattr(possible_filelike, 'write', None))

View file

@ -0,0 +1,3 @@
[simpleitk]
description = Image reading and writing via SimpleITK
provides = imread, imsave

View file

@ -0,0 +1,21 @@
__all__ = ['imread', 'imsave']
try:
import SimpleITK as sitk
except ImportError:
raise ImportError("SimpleITK could not be found. "
"Please try "
" easy_install SimpleITK "
"or refer to "
" http://simpleitk.org/ "
"for further instructions.")
def imread(fname):
sitk_img = sitk.ReadImage(fname)
return sitk.GetArrayFromImage(sitk_img)
def imsave(fname, arr):
sitk_img = sitk.GetImageFromArray(arr, isVector=True)
sitk.WriteImage(sitk_img, fname)

View file

@ -0,0 +1,234 @@
'''
Skivi is written/maintained/developed by:
S. Chris Colbert - sccolbert@gmail.com
Skivi is free software and is part of the scikit-image project.
Skivi is governed by the licenses of the scikit-image project.
Please report any bugs to the author.
The skivi module is not meant to be used directly.
Use skimage.io.imshow(img, fancy=True)'''
from textwrap import dedent
from qtpy import QtCore, QtWidgets
from qtpy.QtWidgets import QMainWindow, QLabel, QWidget, QFrame, QGridLayout
from qtpy.QtGui import QImage, QPixmap
from .q_color_mixer import MixerPanel
from .q_histogram import QuadHistogram
class ImageLabel(QLabel):
def __init__(self, parent, arr):
QLabel.__init__(self)
self.parent = parent
# we need to hold a reference to
# arr because QImage doesn't copy the data
# and the buffer must be alive as long
# as the image is alive.
self.arr = arr
# we also need to pass in the row-stride to
# the constructor, because we can't guarantee
# that every row of the numpy data is
# 4-byte aligned. Which Qt would require
# if we didn't pass the stride.
self.img = QImage(arr.data, arr.shape[1], arr.shape[0],
arr.strides[0], QImage.Format_RGB888)
self.pm = QPixmap.fromImage(self.img)
self.setPixmap(self.pm)
self.setAlignment(QtCore.Qt.AlignTop)
self.setMinimumSize(100, 100)
self.setMouseTracking(True)
def mouseMoveEvent(self, evt):
self.parent.label_mouseMoveEvent(evt)
def resizeEvent(self, evt):
width = self.width()
pm = QPixmap.fromImage(self.img)
self.pm = pm.scaledToWidth(width)
self.setPixmap(self.pm)
def update_image(self):
width = self.width()
pm = QPixmap.fromImage(self.img)
pm = pm.scaledToWidth(width)
self.setPixmap(pm)
class RGBHSVDisplay(QFrame):
def __init__(self):
QFrame.__init__(self)
self.setFrameStyle(QFrame.Box | QFrame.Sunken)
self.posx_label = QLabel('X-pos:')
self.posx_value = QLabel()
self.posy_label = QLabel('Y-pos:')
self.posy_value = QLabel()
self.r_label = QLabel('R:')
self.r_value = QLabel()
self.g_label = QLabel('G:')
self.g_value = QLabel()
self.b_label = QLabel('B:')
self.b_value = QLabel()
self.h_label = QLabel('H:')
self.h_value = QLabel()
self.s_label = QLabel('S:')
self.s_value = QLabel()
self.v_label = QLabel('V:')
self.v_value = QLabel()
self.layout = QGridLayout(self)
self.layout.addWidget(self.posx_label, 0, 0)
self.layout.addWidget(self.posx_value, 0, 1)
self.layout.addWidget(self.posy_label, 1, 0)
self.layout.addWidget(self.posy_value, 1, 1)
self.layout.addWidget(self.r_label, 0, 2)
self.layout.addWidget(self.r_value, 0, 3)
self.layout.addWidget(self.g_label, 1, 2)
self.layout.addWidget(self.g_value, 1, 3)
self.layout.addWidget(self.b_label, 2, 2)
self.layout.addWidget(self.b_value, 2, 3)
self.layout.addWidget(self.h_label, 0, 4)
self.layout.addWidget(self.h_value, 0, 5)
self.layout.addWidget(self.s_label, 1, 4)
self.layout.addWidget(self.s_value, 1, 5)
self.layout.addWidget(self.v_label, 2, 4)
self.layout.addWidget(self.v_value, 2, 5)
def update_vals(self, data):
xpos, ypos, r, g, b, h, s, v = data
self.posx_value.setText(str(xpos)[:5])
self.posy_value.setText(str(ypos)[:5])
self.r_value.setText(str(r)[:5])
self.g_value.setText(str(g)[:5])
self.b_value.setText(str(b)[:5])
self.h_value.setText(str(h)[:5])
self.s_value.setText(str(s)[:5])
self.v_value.setText(str(v)[:5])
class SkiviImageWindow(QMainWindow):
def __init__(self, arr, mgr):
QMainWindow.__init__(self)
self.arr = arr
self.mgr = mgr
self.main_widget = QWidget()
self.layout = QGridLayout(self.main_widget)
self.setCentralWidget(self.main_widget)
self.label = ImageLabel(self, arr)
self.label_container = QFrame()
self.label_container.setFrameShape(QFrame.StyledPanel | QFrame.Sunken)
self.label_container.setLineWidth(1)
self.label_container.layout = QGridLayout(self.label_container)
self.label_container.layout.setContentsMargins(0, 0, 0, 0)
self.label_container.layout.addWidget(self.label, 0, 0)
self.layout.addWidget(self.label_container, 0, 0)
self.mgr.add_window(self)
self.main_widget.show()
self.setWindowTitle('Skivi - The skimage viewer.')
self.mixer_panel = MixerPanel(self.arr)
self.layout.addWidget(self.mixer_panel, 0, 2)
self.mixer_panel.show()
self.mixer_panel.set_callback(self.refresh_image)
self.rgbv_hist = QuadHistogram(self.arr)
self.layout.addWidget(self.rgbv_hist, 0, 1)
self.rgbv_hist.show()
self.rgb_hsv_disp = RGBHSVDisplay()
self.layout.addWidget(self.rgb_hsv_disp, 1, 0)
self.rgb_hsv_disp.show()
self.layout.setColumnStretch(0, 1)
self.layout.setRowStretch(0, 1)
self.save_file = QtWidgets.QPushButton('Save to File')
self.save_file.clicked.connect(self.save_to_file)
self.save_stack = QtWidgets.QPushButton('Save to Stack')
self.save_stack.clicked.connect(self.save_to_stack)
self.save_file.show()
self.save_stack.show()
self.layout.addWidget(self.save_stack, 1, 1)
self.layout.addWidget(self.save_file, 1, 2)
def closeEvent(self, event):
# Allow window to be destroyed by removing any
# references to it
self.mgr.remove_window(self)
def update_histograms(self):
self.rgbv_hist.update_hists(self.arr)
def refresh_image(self):
self.label.update_image()
self.update_histograms()
def scale_mouse_pos(self, x, y):
width = self.label.pm.width()
height = self.label.pm.height()
x_frac = 1. * x / width
y_frac = 1. * y / height
width = self.arr.shape[1]
height = self.arr.shape[0]
new_x = int(width * x_frac)
new_y = int(height * y_frac)
return(new_x, new_y)
def label_mouseMoveEvent(self, evt):
x = evt.x()
y = evt.y()
x, y = self.scale_mouse_pos(x, y)
# handle tracking out of array bounds
maxw = self.arr.shape[1]
maxh = self.arr.shape[0]
if x >= maxw or y >= maxh or x < 0 or y < 0:
r = g = b = h = s = v = ''
else:
r = self.arr[y, x, 0]
g = self.arr[y, x, 1]
b = self.arr[y, x, 2]
h, s, v = self.mixer_panel.mixer.rgb_2_hsv_pixel(r, g, b)
self.rgb_hsv_disp.update_vals((x, y, r, g, b, h, s, v))
def save_to_stack(self):
from ... import io
img = self.arr.copy()
io.push(img)
msg = dedent('''
The image has been pushed to the io stack.
Use io.pop() to retrieve the most recently
pushed image.''')
msglabel = QLabel(msg)
dialog = QtWidgets.QDialog()
ok = QtWidgets.QPushButton('OK', dialog)
ok.clicked.connect(dialog.accept)
ok.setDefault(True)
dialog.layout = QGridLayout(dialog)
dialog.layout.addWidget(msglabel, 0, 0, 1, 3)
dialog.layout.addWidget(ok, 1, 1)
dialog.exec_()
def save_to_file(self):
from ... import io
filename = str(QtWidgets.QFileDialog.getSaveFileName())
if len(filename) == 0:
return
io.imsave(filename, self.arr)

View file

@ -0,0 +1,3 @@
[tifffile]
description = Load and save TIFF and TIFF-based images using tifffile.py
provides = imread, imsave

View file

@ -0,0 +1,37 @@
from warnings import warn
from tifffile import TiffFile, imsave, parse_kwargs
def imread(fname, **kwargs):
"""Load a tiff image from file.
Parameters
----------
fname : str or file
File name or file-like-object.
kwargs : keyword pairs, optional
Additional keyword arguments to pass through (see ``tifffile``'s
``imread`` function).
Notes
-----
Provided by Christophe Golhke's tifffile.py [1]_, and supports many
advanced image types including multi-page and floating point.
References
----------
.. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py
"""
if 'img_num' in kwargs:
kwargs['key'] = kwargs.pop('img_num')
# parse_kwargs will extract keyword arguments intended for the TiffFile
# class and remove them from the kwargs dictionary in-place
tiff_keys = ['multifile', 'multifile_close', 'fastij', 'is_ome']
kwargs_tiff = parse_kwargs(kwargs, *tiff_keys)
# read and return tiff as numpy array
with TiffFile(fname, **kwargs_tiff) as tif:
return tif.asarray(**kwargs)

View file

@ -0,0 +1,435 @@
import numpy as np
from . import _colormixer
from . import _histograms
import threading
from ...util import img_as_ubyte
# utilities to make life easier for plugin writers.
import multiprocessing
CPU_COUNT = multiprocessing.cpu_count()
class GuiLockError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class WindowManager(object):
''' A class to keep track of spawned windows,
and make any needed callback once all the windows,
are closed.'''
def __init__(self):
self._windows = []
self._callback = None
self._callback_args = ()
self._callback_kwargs = {}
self._gui_lock = False
self._guikit = ''
def _check_locked(self):
if not self._gui_lock:
raise GuiLockError(\
'Must first acquire the gui lock before using this image manager')
def _exec_callback(self):
if self._callback:
self._callback(*self._callback_args, **self._callback_kwargs)
def acquire(self, kit):
if self._gui_lock:
raise GuiLockError(\
'The gui lock can only be acquired by one toolkit per session. \
The lock is already acquired by %s' % self._guikit)
else:
self._gui_lock = True
self._guikit = str(kit)
def _release(self, kit):
# releaseing the lock will lose all references to currently
# tracked images and the callback.
# this function is private for reason!
self._check_locked()
if str(kit) == self._guikit:
self._windows = []
self._callback = None
self._callback_args = ()
self._callback_kwargs = {}
self._gui_lock = False
self._guikit = ''
else:
raise RuntimeError('Only the toolkit that owns the lock may '
'release it')
def add_window(self, win):
self._check_locked()
self._windows.append(win)
def remove_window(self, win):
self._check_locked()
try:
self._windows.remove(win)
except ValueError:
print('Unable to find referenced window in tracked windows.')
print('Ignoring...')
else:
if len(self._windows) == 0:
self._exec_callback()
def register_callback(self, cb, *cbargs, **cbkwargs):
self._check_locked()
self._callback = cb
self._callback_args = cbargs
self._callback_kwargs = cbkwargs
def has_windows(self):
if len(self._windows) > 0:
return True
else:
return False
window_manager = WindowManager()
def prepare_for_display(npy_img):
'''Convert a 2D or 3D numpy array of any dtype into a
3D numpy array with dtype uint8. This array will
be suitable for use in passing to gui toolkits for
image display purposes.
Parameters
----------
npy_img : ndarray, 2D or 3D
The image to convert for display
Returns
-------
out : ndarray, 3D dtype=np.uint8
The converted image. This is guaranteed to be a contiguous array.
Notes
-----
If the input image is floating point, it is assumed that the data
is in the range of 0.0 - 1.0. No check is made to assert this
condition. The image is then scaled to be in the range 0 - 255
and then cast to np.uint8
For all other dtypes, the array is simply cast to np.uint8
If a 2D array is passed, the single channel is replicated
to the 2nd and 3rd channels.
If the array contains an alpha channel, this channel is
ignored.
'''
if npy_img.ndim < 2:
raise ValueError('Image must be 2D or 3D array')
height = npy_img.shape[0]
width = npy_img.shape[1]
out = np.empty((height, width, 3), dtype=np.uint8)
npy_img = img_as_ubyte(npy_img)
if npy_img.ndim == 2 or \
(npy_img.ndim == 3 and npy_img.shape[2] == 1):
npy_plane = npy_img.reshape((height, width))
out[:, :, 0] = npy_plane
out[:, :, 1] = npy_plane
out[:, :, 2] = npy_plane
elif npy_img.ndim == 3:
if npy_img.shape[2] == 3 or npy_img.shape[2] == 4:
out[:, :, :3] = npy_img[:, :, :3]
else:
raise ValueError('Image must have 1, 3, or 4 channels')
else:
raise ValueError('Image must have 2 or 3 dimensions')
return out
def histograms(image, nbins):
'''Calculate the channel histograms of the current image.
Parameters
----------
image : ndarray, ndim=3, dtype=np.uint8
Input image.
nbins : int
The number of bins.
Returns
-------
out : (rcounts, gcounts, bcounts, vcounts)
The binned histograms of the RGB channels and intensity values.
This is a NAIVE histogram routine, meant primarily for fast display.
'''
return _histograms.histograms(image, nbins)
class ImgThread(threading.Thread):
def __init__(self, func, *args):
super(ImgThread, self).__init__()
self.func = func
self.args = args
def run(self):
self.func(*self.args)
class ThreadDispatch(object):
def __init__(self, img, stateimg, func, *args):
height = img.shape[0]
self.cores = CPU_COUNT
self.threads = []
self.chunks = []
if self.cores == 1:
self.chunks.append((img, stateimg))
elif self.cores >= 4:
self.chunks.append((img[:(height // 4), :, :],
stateimg[:(height // 4), :, :]))
self.chunks.append((img[(height // 4):(height // 2), :, :],
stateimg[(height // 4):(height // 2), :, :]))
self.chunks.append((img[(height // 2):(3 * height // 4), :, :],
stateimg[(height // 2):(3 * height // 4), :, :]
))
self.chunks.append((img[(3 * height // 4):, :, :],
stateimg[(3 * height // 4):, :, :]))
# if they don't have 1, or 4 or more, 2 is good.
else:
self.chunks.append((img[:(height // 2), :, :],
stateimg[:(height // 2), :, :]))
self.chunks.append((img[(height // 2):, :, :],
stateimg[(height // 2):, :, :]))
for i in range(len(self.chunks)):
self.threads.append(ImgThread(func, self.chunks[i][0],
self.chunks[i][1], *args))
def run(self):
for t in self.threads:
t.start()
for t in self.threads:
t.join()
class ColorMixer(object):
''' a class to manage mixing colors in an image.
The input array must be an RGB uint8 image.
The mixer maintains an original copy of the image,
and uses this copy to query the pixel data for operations.
It also makes a copy for sharing state across operations.
That is, if you add to a channel, and multiply to same channel,
the two operations are carried separately and the results
averaged together.
it modifies your array in place. This ensures that if you
bust over a threshold, you can always come back down.
The passed values to a function are always considered
absolute. Thus to threshold a channel completely you
can do mixer.add(RED, 255). Or to double the intensity
of the blue channel: mixer.multiply(BLUE, 2.)
To reverse these operations, respectively:
mixer.add(RED, 0), mixer.multiply(BLUE, 1.)
The majority of the backend is implemented in Cython,
so it should be quite quick.
'''
RED = 0
GREEN = 1
BLUE = 2
valid_channels = [RED, GREEN, BLUE]
def __init__(self, img):
if type(img) != np.ndarray:
raise ValueError('Image must be a numpy array')
if img.dtype != np.uint8:
raise ValueError('Image must have dtype uint8')
if img.ndim != 3 or img.shape[2] != 3:
raise ValueError('Image must be 3 channel MxNx3')
self.img = img
self.origimg = img.copy()
self.stateimg = img.copy()
def get_stateimage(self):
return self.stateimg
def commit_changes(self):
self.stateimg[:] = self.img[:]
def revert(self):
self.stateimg[:] = self.origimg[:]
self.img[:] = self.stateimg[:]
def set_to_stateimg(self):
self.img[:] = self.stateimg[:]
def add(self, channel, ammount):
'''Add the specified ammount to the specified channel.
Parameters
----------
channel : flag
the color channel to operate on
RED, GREED, or BLUE
ammount : integer
the ammount of color to add to the channel,
can be positive or negative.
'''
if channel not in self.valid_channels:
raise ValueError('assert_channel is not a valid channel.')
pool = ThreadDispatch(self.img, self.stateimg,
_colormixer.add, channel, ammount)
pool.run()
def multiply(self, channel, ammount):
'''Mutliply the indicated channel by the specified value.
Parameters
----------
channel : flag
the color channel to operate on
RED, GREED, or BLUE
ammount : integer
the ammount of color to add to the channel,
can be positive or negative.
'''
if channel not in self.valid_channels:
raise ValueError('assert_channel is not a valid channel.')
pool = ThreadDispatch(self.img, self.stateimg,
_colormixer.multiply, channel, ammount)
pool.run()
def brightness(self, factor, offset):
'''Adjust the brightness off an image with an offset and factor.
Parameters
----------
offset : integer
The ammount to add to each channel.
factor : float
The factor to multiply each channel by.
result = clip((pixel + offset)*factor)
'''
pool = ThreadDispatch(self.img, self.stateimg,
_colormixer.brightness, factor, offset)
pool.run()
def sigmoid_gamma(self, alpha, beta):
pool = ThreadDispatch(self.img, self.stateimg,
_colormixer.sigmoid_gamma, alpha, beta)
pool.run()
def gamma(self, gamma):
pool = ThreadDispatch(self.img, self.stateimg,
_colormixer.gamma, gamma)
pool.run()
def hsv_add(self, h_amt, s_amt, v_amt):
'''Adjust the H, S, V channels of an image by a constant ammount.
This is similar to the add() mixer function, but operates over the
entire image at once. Thus all three additive values, H, S, V, must
be supplied simultaneously.
Parameters
----------
h_amt : float
The ammount to add to the hue (-180..180)
s_amt : float
The ammount to add to the saturation (-1..1)
v_amt : float
The ammount to add to the value (-1..1)
'''
pool = ThreadDispatch(self.img, self.stateimg,
_colormixer.hsv_add, h_amt, s_amt, v_amt)
pool.run()
def hsv_multiply(self, h_amt, s_amt, v_amt):
'''Adjust the H, S, V channels of an image by a constant ammount.
This is similar to the add() mixer function, but operates over the
entire image at once. Thus all three additive values, H, S, V, must
be supplied simultaneously.
Note that since hue is in degrees, it makes no sense to multiply
that channel, thus an add operation is performed on the hue. And the
values given for h_amt, should be the same as for hsv_add
Parameters
----------
h_amt : float
The ammount to to add to the hue (-180..180)
s_amt : float
The ammount to multiply to the saturation (0..1)
v_amt : float
The ammount to multiply to the value (0..1)
'''
pool = ThreadDispatch(self.img, self.stateimg,
_colormixer.hsv_multiply, h_amt, s_amt, v_amt)
pool.run()
def rgb_2_hsv_pixel(self, R, G, B):
'''Convert an RGB value to HSV
Parameters
----------
R : int
Red value
G : int
Green value
B : int
Blue value
Returns
-------
out : (H, S, V) Floats
The HSV values
'''
H, S, V = _colormixer.py_rgb_2_hsv(R, G, B)
return (H, S, V)
def hsv_2_rgb_pixel(self, H, S, V):
'''Convert an HSV value to RGB
Parameters
----------
H : float
Hue value
S : float
Saturation value
V : float
Intensity value
Returns
-------
out : (R, G, B) ints
The RGB values
'''
R, G, B = _colormixer.py_hsv_2_rgb(H, S, V)
return (R, G, B)