mirror of
https://github.com/wassname/scikit-image.git
synced 2026-07-07 15:45:04 +08:00
Merge branch 'io'
Conflicts: scikits/image/analysis/spath.c scikits/image/setup.py
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
[pil]
|
||||
description = Image reading via the Python Imaging Library
|
||||
provides = imread
|
||||
provides = imread, imsave
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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,4 +1,4 @@
|
||||
[qt]
|
||||
description = Fast image display using the Qt library
|
||||
provides = imshow, _app_show
|
||||
provides = imshow, _app_show, imsave
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
if __name__ == "__main__":
|
||||
from scikits.image.scripts import scivi
|
||||
scivi.main()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user