Merge branch 'io'

Conflicts:
	scikits/image/analysis/spath.c
	scikits/image/setup.py
This commit is contained in:
Stefan van der Walt
2009-11-09 00:02:30 +02:00
24 changed files with 2144 additions and 100 deletions
+2 -2
View File
@@ -10,8 +10,8 @@
- Mahipal Raythattha
Documentation infrastructure
- Chris Colbert
OpenCV wrappers
- S. Chris Colbert
OpenCV wrappers, Scivi, Qt and Gtk gui bits.
- Holger Rapp
OpenCV functions and better OSX library loader
+550
View File
@@ -0,0 +1,550 @@
# -*- python -*-
"""Colour Mixer
NumPy does not do overflow checking when adding or multiplying
integers, so currently the only way to clip results efficiently
(without making copies of the data) is with an extension such as this
one.
"""
import numpy as np
cimport numpy as np
import cython
cdef extern from "math.h":
float exp(float) nogil
float pow(float, float) nogil
@cython.boundscheck(False)
def add(np.ndarray[np.uint8_t, ndim=3] img,
np.ndarray[np.uint8_t, ndim=3] stateimg,
int channel, int amount):
"""Add a given amount to a colour channel of `stateimg`, and
store the result in `img`. Overflow is clipped.
Parameters
----------
img : (M, N, 3) ndarray of uint8
Output image.
stateimg : (M, N, 3) ndarray of uint8
Input image.
channel : int
Channel (0 for "red", 1 for "green", 2 for "blue").
amount : int
Value to add.
"""
cdef int height = img.shape[0]
cdef int width = img.shape[1]
cdef int k = channel
cdef int n = amount
cdef np.int16_t op_result
cdef int i, j
with nogil:
for i from 0 <= i < height:
for j from 0 <= j < width:
op_result = <np.int16_t>(stateimg[i,j,k] + n)
if op_result > 255:
img[i, j, k] = 255
elif op_result < 0:
img[i, j, k] = 0
else:
img[i, j, k] = <np.uint8_t>op_result
@cython.boundscheck(False)
def multiply(np.ndarray[np.uint8_t, ndim=3] img,
np.ndarray[np.uint8_t, ndim=3] stateimg,
int channel, float amount):
"""Multiply a colour channel of `stateimg` by a certain amount, and
store the result in `img`. Overflow is clipped.
Parameters
----------
img : (M, N, 3) ndarray of uint8
Output image.
stateimg : (M, N, 3) ndarray of uint8
Input image.
channel : int
Channel (0 for "red", 1 for "green", 2 for "blue").
amount : float
Multiplication factor.
"""
cdef int height = img.shape[0]
cdef int width = img.shape[1]
cdef int k = channel
cdef float n = amount
cdef float op_result
cdef int i, j
with nogil:
for i from 0 <= i < height:
for j from 0 <= j < width:
op_result = <float>(stateimg[i,j,k] * n)
if op_result > 255:
img[i, j, k] = 255
elif op_result < 0:
img[i, j, k] = 0
else:
img[i, j, k] = <np.uint8_t>op_result
@cython.boundscheck(False)
def brightness(np.ndarray[np.uint8_t, ndim=3] img,
np.ndarray[np.uint8_t, ndim=3] stateimg,
float factor, int offset):
"""Modify the brightness of an image.
'factor' is multiplied to all channels, which are
then added by 'amount'. Overflow is clipped.
Parameters
----------
img : (M, N, 3) ndarray of uint8
Output image.
stateimg : (M, N, 3) ndarray of uint8
Input image.
factor : float
Multiplication factor.
offset : int
Ammount to add to each channel.
"""
cdef int height = img.shape[0]
cdef int width = img.shape[1]
cdef float op_result
cdef int i, j, k
with nogil:
for i from 0 <= i < height:
for j from 0 <= j < width:
for k from 0 <= k < 3:
op_result = <float>((stateimg[i,j,k] * factor + offset))
if op_result > 255:
img[i, j, k] = 255
elif op_result < 0:
img[i, j, k] = 0
else:
img[i, j, k] = <np.uint8_t>op_result
@cython.boundscheck(False)
@cython.cdivision(True)
def sigmoid_gamma(np.ndarray[np.uint8_t, ndim=3] img,
np.ndarray[np.uint8_t, ndim=3] stateimg,
float alpha, float beta):
cdef int height = img.shape[0]
cdef int width = img.shape[1]
cdef float c1, c2, r, g, b
cdef int i, j, k
with nogil:
for i from 0 <= i < height:
for j from 0 <= j < width:
r = <float>stateimg[i,j,0] / 255.
g = <float>stateimg[i,j,1] / 255.
b = <float>stateimg[i,j,2] / 255.
c1 = 1 / (1 + exp(beta))
c2 = 1 / (1 + exp(beta - alpha)) - c1
r = 1 / (1 + exp(beta - r * alpha))
r = (r - c1) / c2
g = 1 / (1 + exp(beta - g * alpha))
g = (g - c1) / c2
b = 1 / (1 + exp(beta - b * alpha))
b = (b - c1) / c2
img[i,j,0] = <np.uint8_t>(r * 255)
img[i,j,1] = <np.uint8_t>(g * 255)
img[i,j,2] = <np.uint8_t>(b * 255)
@cython.boundscheck(False)
def gamma(np.ndarray[np.uint8_t, ndim=3] img,
np.ndarray[np.uint8_t, ndim=3] stateimg,
float gamma):
cdef int height = img.shape[0]
cdef int width = img.shape[1]
cdef float r, g, b
cdef int i, j
if gamma == 0:
gamma = 0.00000000000000000001
gamma = 1./gamma
with nogil:
for i from 0 <= i < height:
for j from 0 <= j < width:
r = <float>stateimg[i,j,0] / 255.
g = <float>stateimg[i,j,1] / 255.
b = <float>stateimg[i,j,2] / 255.
img[i,j,0] = <np.uint8_t>(pow(r, gamma) * 255)
img[i,j,1] = <np.uint8_t>(pow(g, gamma) * 255)
img[i,j,2] = <np.uint8_t>(pow(b, gamma) * 255)
@cython.cdivision(True)
cdef void rgb_2_hsv(float* RGB, float* HSV) nogil:
cdef float R, G, B, H, S, V, MAX, MIN
R = RGB[0]
G = RGB[1]
B = RGB[2]
if R > 255:
R = 255
elif R < 0:
R = 0
else:
pass
if G > 255:
G = 255
elif G < 0:
G = 0
else:
pass
if B > 255:
B = 255
elif B < 0:
B = 0
else:
pass
if R < G:
MIN = R
MAX = G
else:
MIN = G
MAX = R
if B < MIN:
MIN = B
elif B > MAX:
MAX = B
else:
pass
V = MAX / 255.
if MAX == MIN:
H = 0.
elif MAX == R:
H = (60 * (G - B) / (MAX - MIN) + 360) % 360
elif MAX == G:
H = 60 * (B - R) / (MAX - MIN) + 120
else:
H = 60 * (R - G) / (MAX - MIN) + 240
if MAX == 0:
S = 0
else:
S = 1 - MIN / MAX
HSV[0] = H
HSV[1] = S
HSV[2] = V
@cython.cdivision(True)
cdef void hsv_2_rgb(float* HSV, float* RGB) nogil:
cdef float H, S, V
cdef float f, p, q, t, r, g, b
cdef int hi
H = HSV[0]
S = HSV[1]
V = HSV[2]
if H > 360:
H = H % 360
elif H < 0:
H = 360 - ((-1 * H) % 360)
else:
pass
if S > 1:
S = 1
elif S < 0:
S = 0
else:
pass
if V > 1:
V = 1
elif V < 0:
V = 0
else:
pass
hi = (<int>(H / 60.)) % 6
f = (H / 60.) - (<int>(H / 60.))
p = V * (1 - S)
q = V * (1 - f * S)
t = V * (1 - (1 -f) * S)
if hi == 0:
r = V
g = t
b = p
elif hi == 1:
r = q
g = V
b = p
elif hi == 2:
r = p
g = V
b = t
elif hi == 3:
r = p
g = q
b = V
elif hi == 4:
r = t
g = p
b = V
else:
r = V
g = p
b = q
RGB[0] = r
RGB[1] = g
RGB[2] = b
def py_hsv_2_rgb(H, S, V):
'''Convert an HSV value to RGB.
Automatic clipping.
Parameters
----------
H : float
From 0. - 360.
S : float
From 0. - 1.
V : float
From 0. - 1.
Returns
-------
out : (R, G, B) ints
Each from 0 - 255
conversion convention from here:
http://en.wikipedia.org/wiki/HSL_and_HSV
'''
cdef float HSV[3]
cdef float RGB[3]
HSV[0] = H
HSV[1] = S
HSV[2] = V
hsv_2_rgb(HSV, RGB)
R = int(RGB[0] * 255)
G = int(RGB[1] * 255)
B = int(RGB[2] * 255)
return (R, G, B)
def py_rgb_2_hsv(R, G, B):
'''Convert an HSV value to RGB.
Automatic clipping.
Parameters
----------
R : int
From 0. - 255.
G : int
From 0. - 255.
B : int
From 0. - 255.
Returns
-------
out : (H, S, V) floats
Ranges (0...360), (0...1), (0...1)
conversion convention from here:
http://en.wikipedia.org/wiki/HSL_and_HSV
'''
cdef float HSV[3]
cdef float RGB[3]
RGB[0] = R
RGB[1] = G
RGB[2] = B
rgb_2_hsv(RGB, HSV)
H = HSV[0]
S = HSV[1]
V = HSV[2]
return (H, S, V)
@cython.boundscheck(False)
def hsv_add(np.ndarray[np.uint8_t, ndim=3] img,
np.ndarray[np.uint8_t, ndim=3] stateimg,
float h_amt, float s_amt, float v_amt):
"""Modify the image color by specifying additive HSV Values.
Since the underlying images are RGB, all three values HSV
must be specified at the same time.
The RGB triplet in the image is converted to HSV, the operation
is applied, and then the HSV triplet is converted back to RGB
HSV values are scaled to H(0. - 360.), S(0. - 1.), V(0. - 1.)
then the operation is performed and any overflow is clipped, then the
reverse transform is performed. Those are the ranges to keep in mind,
when passing in values.
Parameters
----------
img : (M, N, 3) ndarray of uint8
Output image.
stateimg : (M, N, 3) ndarray of uint8
Input image.
h_amt : float
Ammount to add to H channel.
s_amt : float
Ammount to add to S channel.
v_amt : float
Ammount to add to V channel.
"""
cdef int height = img.shape[0]
cdef int width = img.shape[1]
cdef float HSV[3]
cdef float RGB[3]
cdef int i, j
with nogil:
for i from 0 <= i < height:
for j from 0 <= j < width:
RGB[0] = stateimg[i, j, 0]
RGB[1] = stateimg[i, j, 1]
RGB[2] = stateimg[i, j, 2]
rgb_2_hsv(RGB, HSV)
# Add operation
HSV[0] += h_amt
HSV[1] += s_amt
HSV[2] += v_amt
hsv_2_rgb(HSV, RGB)
RGB[0] *= 255
RGB[1] *= 255
RGB[2] *= 255
img[i, j, 0] = <np.uint8_t>RGB[0]
img[i, j, 1] = <np.uint8_t>RGB[1]
img[i, j, 2] = <np.uint8_t>RGB[2]
@cython.boundscheck(False)
def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img,
np.ndarray[np.uint8_t, ndim=3] stateimg,
float h_amt, float s_amt, float v_amt):
"""Modify the image color by specifying multiplicative HSV Values.
Since the underlying images are RGB, all three values HSV
must be specified at the same time.
The RGB triplet in the image is converted to HSV, the operation
is applied, and then the HSV triplet is converted back to RGB
HSV values are scaled to H(0. - 360.), S(0. - 1.), V(0. - 1.)
then the operation is performed and any overflow is clipped, then the
reverse transform is performed. Those are the ranges to keep in mind,
when passing in values.
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
----------
img : (M, N, 3) ndarray of uint8
Output image.
stateimg : (M, N, 3) ndarray of uint8
Input image.
h_amt : float
Ammount to add to H channel.
s_amt : float
Ammount by which to multiply S channel.
v_amt : float
Ammount by which to multiply V channel.
"""
cdef int height = img.shape[0]
cdef int width = img.shape[1]
cdef float HSV[3]
cdef float RGB[3]
cdef int i, j
with nogil:
for i from 0 <= i < height:
for j from 0 <= j < width:
RGB[0] = stateimg[i, j, 0]
RGB[1] = stateimg[i, j, 1]
RGB[2] = stateimg[i, j, 2]
rgb_2_hsv(RGB, HSV)
# Multiply operation
HSV[0] += h_amt
HSV[1] *= s_amt
HSV[2] *= v_amt
hsv_2_rgb(HSV, RGB)
RGB[0] *= 255
RGB[1] *= 255
RGB[2] *= 255
img[i, j, 0] = <np.uint8_t>RGB[0]
img[i, j, 1] = <np.uint8_t>RGB[1]
img[i, j, 2] = <np.uint8_t>RGB[2]
+83
View File
@@ -0,0 +1,83 @@
import numpy as np
cimport numpy as np
import cython
cdef inline float tri_max(float a, float b, float c):
cdef float MAX
if a > b:
MAX = a
else:
MAX = b
if MAX > c:
return MAX
else:
return c
@cython.boundscheck(False)
def histograms(np.ndarray[np.uint8_t, ndim=3] img, int nbins):
'''Calculate the channel histograms of the current image.
Parameters
----------
img : ndarray, uint8, ndim=3
The image to calculate the histogram.
nbins : int
The number of bins.
Returns
-------
out : (rcounts, gcounts, bcounts, vcounts)
The binned histograms of the RGB channels and grayscale intensity.
This is a NAIVE histogram routine, meant primarily for fast display.
'''
cdef int width = img.shape[1]
cdef int height = img.shape[0]
cdef np.ndarray[np.int32_t, ndim=1] r
cdef np.ndarray[np.int32_t, ndim=1] g
cdef np.ndarray[np.int32_t, ndim=1] b
cdef np.ndarray[np.int32_t, ndim=1] v
r = np.zeros((nbins,), dtype=np.int32)
g = np.zeros((nbins,), dtype=np.int32)
b = np.zeros((nbins,), dtype=np.int32)
v = np.zeros((nbins,), dtype=np.int32)
cdef int i, j, k, rbin, gbin, bbin, vbin
cdef float bin_width = 255./ nbins
cdef float R, G, B, V
for i in range(height):
for j in range(width):
R = <float>img[i, j, 0]
G = <float>img[i, j, 1]
B = <float>img[i, j, 2]
V = tri_max(R, G, B)
rbin = <int>(R / bin_width)
gbin = <int>(G / bin_width)
bbin = <int>(B / bin_width)
vbin = <int>(V / bin_width)
# fully open last bin
if R == 255:
rbin -= 1
if G == 255:
gbin -= 1
if B == 255:
bbin -= 1
if V == 255:
vbin -= 1
r[rbin] += 1
g[gbin] += 1
b[bbin] += 1
v[vbin] += 1
return (r, g, b, v)
@@ -1,4 +1,4 @@
[matplotlib]
description = Display or save images using Matplotlib
provides = imshow, imsave
provides = imshow, _app_show
@@ -1,4 +1,5 @@
try:
from matplotlib.pyplot import imshow, imsave
except ImportError:
print "Could not import Matplotlib."
from matplotlib.pyplot import imshow, show
def _app_show():
show()
+1 -1
View File
@@ -1,4 +1,4 @@
[pil]
description = Image reading via the Python Imaging Library
provides = imread
provides = imread, imsave
+81 -35
View File
@@ -6,44 +6,90 @@ import numpy as np
try:
from PIL import Image
except ImportError:
print 'Could not load Python Imaging Library'
else:
def imread(fname, as_grey=False, dtype=None):
"""Load an image from file.
raise ImportError("The Python Image Library could not be found. "
"Please refer to http://pypi.python.org/pypi/PIL/ "
"for further instructions.")
"""
im = Image.open(fname)
if im.mode == 'P':
if palette_is_grayscale(im):
im = im.convert('L')
else:
im = im.convert('RGB')
def imread(fname, as_grey=False, dtype=None):
"""Load an image from file.
if as_grey and not \
im.mode in ('1', 'L', 'I', 'F', 'I;16', 'I;16L', 'I;16B'):
im = im.convert('F')
"""
im = Image.open(fname)
if im.mode == 'P':
if _palette_is_grayscale(im):
im = im.convert('L')
else:
im = im.convert('RGB')
return np.array(im, dtype=dtype)
if as_grey and not \
im.mode in ('1', 'L', 'I', 'F', 'I;16', 'I;16L', 'I;16B'):
im = im.convert('F')
def palette_is_grayscale(pil_image):
"""Return True if PIL image in palette mode is grayscale.
return np.array(im, dtype=dtype)
Parameters
----------
pil_image : PIL image
PIL Image that is in Palette mode.
def _palette_is_grayscale(pil_image):
"""Return True if PIL image in palette mode is grayscale.
Returns
-------
is_grayscale : bool
True if all colors in image palette are gray.
"""
assert pil_image.mode == '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]
# Image is grayscale if channel differences (R - G and G - B)
# are all zero.
return np.allclose(np.diff(valid_palette), 0)
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.
"""
assert pil_image.mode == '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]
# Image is grayscale if channel differences (R - G and G - B)
# are all zero.
return np.allclose(np.diff(valid_palette), 0)
def imsave(fname, arr):
"""Save an image to disk.
Parameters
----------
fname : str
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].
Notes
-----
Currently, only 8-bit precision is supported.
"""
arr = np.asarray(arr).squeeze()
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.")
# Image is floating point, assume in [0, 1]
if np.issubdtype(arr.dtype, float):
arr = arr * 255
arr = arr.astype(np.uint8)
if arr.ndim == 2:
mode = 'L'
elif arr.shape[2] in (3, 4):
mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]]
# Force all integers to bytes
arr = arr.astype(np.uint8)
img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring())
img.save(fname)
+348
View File
@@ -0,0 +1,348 @@
# the module for the qt color_mixer plugin
from PyQt4 import QtGui, QtCore
from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QVBoxLayout,
QGridLayout, QLabel)
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 registerd 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(QWidget):
'''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):
QWidget.__init__(self)
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 = QtGui.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 = QtGui.QRadioButton('Additive')
self.rgb_mul = QtGui.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 = QtGui.QRadioButton('Additive')
self.hsv_mul = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.QPushButton('Commit')
self.commit_button.clicked.connect(self.commit_changes)
self.revert_button = QtGui.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 = QtGui.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()
+140
View File
@@ -0,0 +1,140 @@
import numpy as np
from PyQt4.QtGui import QWidget, QPainter, QGridLayout, QColor
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 severly 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 appopriate
# 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(QWidget):
'''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']):
QWidget.__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.layout = QGridLayout(self)
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))
+1 -1
View File
@@ -1,4 +1,4 @@
[qt]
description = Fast image display using the Qt library
provides = imshow, _app_show
provides = imshow, _app_show, imsave
+101 -49
View File
@@ -1,64 +1,116 @@
from util import prepare_for_display, window_manager, GuiLockError
import numpy as np
import sys
# We try to aquire the gui lock first or else the gui import might
# trample another GUI's PyOS_InputHook.
window_manager.acquire('qt')
try:
# We try to aquire the gui lock first or else the gui import might
# trample another GUI's PyOS_InputHook.
window_manager.acquire('qt')
from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap,
QLabel, QWidget)
from PyQt4 import QtCore, QtGui
except GuiLockError, gle:
print gle
except ImportError:
window_manager._release('qt')
else:
try:
from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap,
QLabel)
raise ImportError("""\
PyQt4 libraries not installed. Please refer to
except ImportError:
print 'PyQT4 libraries not installed. Plugin not loaded.'
window_manager._release('qt')
http://www.riverbankcomputing.co.uk/software/pyqt/intro
for more information. PyQt4 is GPL licensed. For an
LGPL equivalent, see
http://www.pyside.org
""")
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 didnt 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('scikits.image')
self.mgr = mgr
self.main_widget = QWidget()
self.layout = QtGui.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 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 scivi import SciviImageWindow
iw = SciviImageWindow(arr, window_manager)
app = None
iw.show()
class ImageWindow(QMainWindow):
def __init__(self, arr, mgr):
QMainWindow.__init__(self)
self.mgr = mgr
img = QImage(arr.data, arr.shape[1], arr.shape[0],
QImage.Format_RGB888)
pm = QPixmap.fromImage(img)
label = QLabel()
label.setPixmap(pm)
label.show()
def _app_show():
global app
if app and window_manager.has_windows():
app.exec_()
else:
print 'No images to show. See `imshow`.'
self.label = label
self.setCentralWidget(self.label)
self.mgr.add_window(self)
def closeEvent(self, event):
# Allow window to be destroyed by removing any
# references to it
self.mgr.remove_window(self)
def imshow(arr):
global app
if not app:
app = QApplication([])
arr = prepare_for_display(arr)
iw = ImageWindow(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):
# 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)
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)
+233
View File
@@ -0,0 +1,233 @@
'''
Scivi is written/maintained/developed by:
S. Chris Colbert - sccolbert@gmail.com
Scivi is free software and is part of the scikits.image project.
Scivi is governed by the licenses of the scikits.image project.
Please report any bugs to the author.
The scivi module is not meant to be used directly.
Use scikits.image.io.imshow(img, fancy=True)'''
from textwrap import dedent
import numpy as np
import sys
from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap,
QLabel, QWidget, QVBoxLayout, QSlider,
QPainter, QColor, QFrame, QLayoutItem)
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 didnt 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(QWidget):
def __init__(self):
QWidget.__init__(self)
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 = QtGui.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 SciviImageWindow(QMainWindow):
def __init__(self, arr, mgr):
QMainWindow.__init__(self)
self.arr = arr
self.mgr = mgr
self.main_widget = QWidget()
self.layout = QtGui.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()
self.setWindowTitle('Scivi - The scikits.image 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 = QtGui.QPushButton('Save to File')
self.save_file.clicked.connect(self.save_to_file)
self.save_stack = QtGui.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 scikits.image 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 = QtGui.QDialog()
ok = QtGui.QPushButton('OK', dialog)
ok.clicked.connect(dialog.accept)
ok.setDefault(True)
dialog.layout = QtGui.QGridLayout(dialog)
dialog.layout.addWidget(msglabel, 0, 0, 1, 3)
dialog.layout.addWidget(ok, 1, 1)
dialog.exec_()
def save_to_file(self):
from scikits.image import io
filename = str(QtGui.QFileDialog.getSaveFileName())
if len(filename) == 0:
return
io.imsave(filename, self.arr)
+292
View File
@@ -1,7 +1,15 @@
import numpy as np
import _colormixer
import _histograms
import threading
# utilities to make life easier for plugin writers.
try:
import multiprocessing
CPU_COUNT = multiprocessing.cpu_count()
except ImportError:
CPU_COUNT = 2
class GuiLockError(Exception):
def __init__(self, msg):
@@ -149,3 +157,287 @@ def prepare_for_display(npy_img):
raise ValueError('Image must have 2 or 3 dimensions')
return out
def histograms(img, nbins):
'''Calculate the channel histograms of the current image.
Parameters
----------
img : ndarray, ndim=3, dtype=np.uint8
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(img, 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):
width = img.shape[1]
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 dont 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(self.cores):
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.
'''
assert channel in self.valid_channels
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.
'''
assert channel in self.valid_channels
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)
+30 -1
View File
@@ -1,6 +1,35 @@
__all__ = ['imread', 'imsave', 'imshow', 'show']
__all__ = ['imread', 'imsave', 'imshow', 'show', 'push', 'pop']
from scikits.image.io._plugins import call as call_plugin
import numpy as np
# Shared image queue
_image_stack = []
def push(img):
"""Push an image onto the shared image stack.
Parameters
----------
img : ndarray
Image to push.
"""
if not isinstance(img, np.ndarray):
raise ValueError("Can only push ndarrays to the image stack.")
_image_stack.append(img)
def pop():
"""Pop and image from the shared image stack.
Returns
-------
img : ndarray
Image popped from the stack.
"""
return _image_stack.pop()
def imread(fname, as_grey=False, dtype=None, plugin=None, flatten=None,
**plugin_args):
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python
from scikits.image._build import cython
import os.path
base_path = os.path.abspath(os.path.dirname(__file__))
def configuration(parent_package='', top_path=None):
from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs
config = Configuration('io', parent_package, top_path)
config.add_data_dir('tests')
config.add_data_files('_plugins/*.ini')
# This function tries to create C files from the given .pyx files. If
# it fails, we build the checked-in .c files.
cython(['_plugins/_colormixer.pyx', '_plugins/_histograms.pyx'],
working_path=base_path)
config.add_extension('_plugins._colormixer',
sources=['_plugins/_colormixer.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_plugins._histograms',
sources=['_plugins/_histograms.c'],
include_dirs=[get_numpy_include_dirs()])
return config
if __name__ == '__main__':
from numpy.distutils.core import setup
setup(maintainer = 'scikits.image Developers',
maintainer_email = 'scikits-image@googlegroups.com',
description = 'Image I/O Routines',
url = 'http://stefanv.github.com/scikits.image/',
license = 'Modified BSD',
**(configuration(top_path='').todict())
)
+140
View File
@@ -0,0 +1,140 @@
from numpy.testing import *
import numpy as np
import scikits.image.io._plugins._colormixer as cm
class ColorMixerTest(object):
def setup(self):
self.state = np.ones((18, 33, 3), dtype=np.uint8) * 200
self.img = np.zeros_like(self.state)
def test_basic(self):
self.op(self.img, self.state, 0, self.positive)
assert_array_equal(self.img[..., 0],
self.py_op(self.state[..., 0], self.positive))
def test_clip(self):
self.op(self.img, self.state, 0, self.positive_clip)
assert_array_equal(self.img[..., 0],
np.ones_like(self.img[..., 0]) * 255)
def test_negative(self):
self.op(self.img, self.state, 0, self.negative)
assert_array_equal(self.img[..., 0],
self.py_op(self.state[..., 0], self.negative))
def test_negative_clip(self):
self.op(self.img, self.state, 0, self.negative_clip)
assert_array_equal(self.img[..., 0],
np.zeros_like(self.img[..., 0]))
class TestColorMixerAdd(ColorMixerTest):
op = cm.add
py_op = np.add
positive = 50
positive_clip = 56
negative = -50
negative_clip = -220
class TestColorMixerMul(ColorMixerTest):
op = cm.multiply
py_op = np.multiply
positive = 1.2
positive_clip = 2
negative = 0.5
negative_clip = -0.5
class TestColorMixerBright(object):
def setup(self):
self.state = np.ones((18, 33, 3), dtype=np.uint8) * 200
self.img = np.zeros_like(self.state)
def test_brightness_pos(self):
cm.brightness(self.img, self.state, 1.25, 1)
assert_array_equal(self.img, np.ones_like(self.img) * 251)
def test_brightness_neg(self):
cm.brightness(self.img, self.state, 0.5, -50)
assert_array_equal(self.img, np.ones_like(self.img) * 50)
def test_brightness_pos_clip(self):
cm.brightness(self.img, self.state, 2, 0)
assert_array_equal(self.img, np.ones_like(self.img) * 255)
def test_brightness_neg_clip(self):
cm.brightness(self.img, self.state, 0, 0)
assert_array_equal(self.img, np.zeros_like(self.img))
class TestColorMixer(object):
def setup(self):
self.state = np.ones((18, 33, 3), dtype=np.uint8) * 50
self.img = np.zeros_like(self.state)
def test_sigmoid(self):
import math
alpha = 1.5
beta = 1.5
c1 = 1 / (1 + math.exp(beta))
c2 = 1 / (1 + math.exp(beta - alpha)) - c1
state = self.state / 255.
cm.sigmoid_gamma(self.img, self.state, alpha, beta)
img = 1 / (1 + np.exp(beta - state * alpha))
img = np.asarray((img - c1) / c2 * 255, dtype='uint8')
assert_almost_equal(img, self.img)
def test_gamma(self):
gamma = 1.5
cm.gamma(self.img, self.state, gamma)
img = np.asarray(((self.state/255.)**(1/gamma))*255, dtype='uint8')
assert_array_almost_equal(img, self.img)
def test_rgb_2_hsv(self):
r = 255
g = 0
b = 0
h, s, v = cm.py_rgb_2_hsv(r, g, b)
assert_almost_equal(np.array([h]), np.array([0]))
assert_almost_equal(np.array([s]), np.array([1]))
assert_almost_equal(np.array([v]), np.array([1]))
def test_hsv_2_rgb(self):
h = 0
s = 1
v = 1
r, g, b = cm.py_hsv_2_rgb(h, s, v)
assert_almost_equal(np.array([r]), np.array([255]))
assert_almost_equal(np.array([g]), np.array([0]))
assert_almost_equal(np.array([b]), np.array([0]))
def test_hsv_add(self):
cm.hsv_add(self.img, self.state, 360, 0, 0)
assert_almost_equal(self.img, self.state)
def test_hsv_add_clip_neg(self):
cm.hsv_add(self.img, self.state, 0, 0, -1)
assert_equal(self.img, np.zeros_like(self.state))
def test_hsv_add_clip_pos(self):
cm.hsv_add(self.img, self.state, 0, 0, 1)
assert_equal(self.img, np.ones_like(self.state)*255)
def test_hsv_mul(self):
cm.hsv_multiply(self.img, self.state, 360, 1, 1)
assert_almost_equal(self.img, self.state)
def test_hsv_mul_clip_neg(self):
cm.hsv_multiply(self.img, self.state, 0, 0, 0)
assert_equal(self.img, np.zeros_like(self.state))
if __name__ == "__main__":
run_module_suite()
+28
View File
@@ -0,0 +1,28 @@
from numpy.testing import *
import numpy as np
import scikits.image.io._plugins._colormixer as cm
from scikits.image.io._plugins._histograms import histograms
class TestHistogram:
def test_basic(self):
img = np.ones((50, 50, 3), dtype=np.uint8)
r, g, b, v = histograms(img, 255)
for band in (r, g, b, v):
yield assert_equal, band.sum(), 50*50
def test_counts(self):
channel = np.arange(255).reshape(51, 5)
img = np.empty((51, 5, 3), dtype='uint8')
img[:,:,0] = channel
img[:,:,1] = channel
img[:,:,2] = channel
r, g, b, v = histograms(img, 255)
assert_array_equal(r, g)
assert_array_equal(r, b)
assert_array_equal(r, v)
assert_array_equal(r, np.ones(255))
if __name__ == "__main__":
run_module_suite()
+17
View File
@@ -0,0 +1,17 @@
from numpy.testing import *
import numpy as np
import scikits.image.io as io
def test_stack_basic():
x = np.arange(12).reshape(3, 4)
io.push(x)
assert_array_equal(io.pop(), x)
@raises(ValueError)
def test_stack_non_array():
io.push([[1, 2, 3]])
if __name__ == "__main__":
run_module_suite()
@@ -1,9 +1,12 @@
import os.path
import numpy as np
from numpy.testing import *
from tempfile import NamedTemporaryFile
from scikits.image import data_dir
from scikits.image.io import imread
from scikits.image.io._plugins.pil_plugin import palette_is_grayscale
from scikits.image.io import imread, imsave
from scikits.image.io._plugins.pil_plugin import _palette_is_grayscale
def test_imread_flatten():
# a color image is flattened and returned as float32
@@ -26,6 +29,26 @@ def test_imread_palette():
def test_palette_is_gray():
from PIL import Image
gray = Image.open(os.path.join(data_dir, 'palette_gray.png'))
assert palette_is_grayscale(gray)
assert _palette_is_grayscale(gray)
color = Image.open(os.path.join(data_dir, 'palette_color.png'))
assert not palette_is_grayscale(color)
assert not _palette_is_grayscale(color)
class TestSave:
def roundtrip(self, dtype, x, scaling=1):
f = NamedTemporaryFile(suffix='.png')
imsave(f.name, x)
f.seek(0)
y = imread(f.name)
assert_array_almost_equal((x * scaling).astype(np.int32), y)
def test_imsave_roundtrip(self):
for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]:
for dtype in (np.uint8, np.uint16, np.float32, np.float64):
x = np.ones(shape, dtype=dtype) * np.random.random(shape)
if np.issubdtype(dtype, float):
yield self.roundtrip, dtype, x, 255
else:
x = (x * 255).astype(dtype)
yield self.roundtrip, dtype, x
View File
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env python
if __name__ == "__main__":
from scikits.image.scripts import scivi
scivi.main()
+13
View File
@@ -0,0 +1,13 @@
"""scikits.image viewer"""
def main():
import scikits.image.io as io
import sys
if len(sys.argv) != 2:
print "Usage: scivi <image-file>"
sys.exit(-1)
io.use_plugin('qt')
io.imshow(io.imread(sys.argv[1]), fancy=True)
io.show()
+1 -2
View File
@@ -7,13 +7,12 @@ def configuration(parent_package='', top_path=None):
config.add_subpackage('opencv')
config.add_subpackage('graph')
config.add_subpackage('io')
def add_test_directories(arg, dirname, fnames):
if dirname.split(os.path.sep)[-1] == 'tests':
config.add_data_dir(dirname)
config.add_data_files('io/_plugins/*.ini')
# Add test directories
from os.path import isdir, dirname, join, abspath
rel_isdir = lambda d: isdir(join(curpath, d))
+5
View File
@@ -78,4 +78,9 @@ if __name__ == "__main__":
packages=setuptools.find_packages(),
include_package_data=True,
zip_safe=False, # the package can run out of an .egg file
entry_points={
'console_scripts': [
'scivi = scikits.image.scripts.scivi:main']
},
)