CofeehousePy/deps/scikit-image/skimage/io/_plugins/util.py

434 lines
13 KiB
Python

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)