Merge branch 'io' of git://github.com/sccolbert/scikits.image into io

Conflicts:
	scikits/image/io/_plugins/qt_plugin.py
This commit is contained in:
Stefan van der Walt
2009-11-07 17:52:21 +02:00
10 changed files with 2993 additions and 1905 deletions
File diff suppressed because it is too large Load Diff
+538
View File
@@ -0,0 +1,538 @@
# -*- 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)
float pow(float, float)
@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
for i in range(height):
for j in range(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
for i in range(height):
for j in range(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
for i in range(height):
for j in range(width):
for k in range(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)
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
for i in range(height):
for j in range(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
gamma = 1./gamma
for i in range(height):
for j in range(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)
cdef void rgb_2_hsv(float* RGB, float* HSV):
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
cdef void hsv_2_rgb(float* HSV, float* RGB):
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
for i in range(height):
for j in range(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
for i in range(height):
for j in range(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)
+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))
+171 -19
View File
@@ -14,8 +14,11 @@ except GuiLockError, gle:
else:
try:
from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap,
QLabel, QWidget, QVBoxLayout)
from PyQt4.QtCore import Qt
QLabel, QWidget, QVBoxLayout, QSlider,
QPainter, QColor, QFrame, QLayoutItem)
from PyQt4 import QtCore, QtGui
from q_color_mixer import MixerPanel
from q_histogram import QuadHistogram
except ImportError:
print 'PyQT4 libraries not installed. Plugin not loaded.'
@@ -28,44 +31,186 @@ else:
class LabelImage(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.setAlignment(Qt.AlignTop)
self.setPixmap(self.pm)
self.setAlignment(QtCore.Qt.AlignTop)
self.setMinimumSize(100, 100)
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 ImageWindow(QMainWindow):
def __init__(self, arr, mgr):
QMainWindow.__init__(self)
self.mgr = mgr
self.main_widget = QWidget()
self.layout = QtGui.QGridLayout(self.main_widget)
self.setCentralWidget(self.main_widget)
self.label = LabelImage(self, arr)
self.setCentralWidget(self.label)
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 label_mouseMoveEvent(self, evt):
pass
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 FancyImageWindow(ImageWindow):
def __init__(self, arr, mgr):
ImageWindow.__init__(self, arr, mgr)
# we need to hold a reference to arr,
# if we want to access the data later,
# because QImage does not copy the data.
self.arr = arr
self.statusBar().showMessage('X: Y: ')
self.label.setScaledContents(True)
self.label.setMouseTracking(True)
self.label.mouseMoveEvent = self.label_mouseMoveEvent
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_variable = QtGui.QPushButton('Save to Variable')
self.save_variable.clicked.connect(self.save_to_variable)
self.save_file.show()
self.save_variable.show()
self.layout.addWidget(self.save_variable, 1, 1)
self.layout.addWidget(self.save_file, 1, 2)
def update_histograms(self):
self.rgbv_hist.update_hists(self.arr)
def save_to_variable(self):
from scikits.image import io
from textwrap import dedent
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)
def refresh_image(self):
self.label.update_image()
self.update_histograms()
def scale_mouse_pos(self, x, y):
width = self.label.width()
height = self.label.height()
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]
@@ -78,17 +223,23 @@ else:
x = evt.x()
y = evt.y()
x, y = self.scale_mouse_pos(x, y)
msg = 'X: %d, Y: %d ' % (x, y)
R = self.arr[y,x,0]
G = self.arr[y,x,1]
B = self.arr[y,x,2]
msg += 'R: %s, G:, %s, B: %s' % (R, G, B)
self.statusBar().showMessage(msg)
# 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 imshow(arr, fancy=False):
global app
if not app:
app = QApplication([])
@@ -101,6 +252,7 @@ else:
iw.show()
def _app_show():
global app
if app and window_manager.has_windows():
+220 -1
View File
@@ -1,5 +1,6 @@
import numpy as np
import _colormixer
import _histograms
# utilities to make life easier for plugin writers.
@@ -149,3 +150,221 @@ 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 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
_colormixer.add(self.img, self.stateimg, channel, ammount)
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
_colormixer.multiply(self.img, self.stateimg, channel, ammount)
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)
'''
_colormixer.brightness(self.img, self.stateimg, factor, offset)
def sigmoid_gamma(self, alpha, beta):
_colormixer.sigmoid_gamma(self.img, self.stateimg, alpha, beta)
def gamma(self, gamma):
_colormixer.gamma(self.img, self.stateimg, gamma)
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)
'''
_colormixer.hsv_add(self.img, self.stateimg, h_amt, s_amt, v_amt)
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)
'''
_colormixer.hsv_multiply(self.img, self.stateimg, h_amt, s_amt, v_amt)
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)
+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())
)
+49
View File
@@ -0,0 +1,49 @@
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
if __name__ == "__main__":
run_module_suite()
+1
View File
@@ -7,6 +7,7 @@ def configuration(parent_package='', top_path=None):
config.add_subpackage('opencv')
config.add_subpackage('analysis')
config.add_subpackage('io')
def add_test_directories(arg, dirname, fnames):
if dirname.split(os.path.sep)[-1] == 'tests':