Finished the refactor of the mixer and the histogram.

Performance is now much better, a bunch of bugs are gone,
and it shouldnt throw anymore exceptions.
This commit is contained in:
sccolbert
2009-11-06 20:40:43 +01:00
parent cf19f11bbb
commit ca051a3e98
7 changed files with 271 additions and 214 deletions
-61
View File
@@ -529,68 +529,7 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img,
img[i, j, 1] = <np.uint8_t>RGB[1]
img[i, j, 2] = <np.uint8_t>RGB[2]
@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 np.uint8_t R, G, B, V
for i in range(height):
for j in range(width):
R = img[i, j, 0]
G = img[i, j, 1]
B = img[i, j, 2]
V = <np.uint8_t> (0.3 * R + 0.59 * G + 0.11 * B)
rbin = <int>(R / bin_width)
gbin = <int>(G / bin_width)
bbin = <int>(B / bin_width)
vbin = <int>(V / bin_width)
if rbin == nbins:
rbin -= 1
if gbin == nbins:
gbin -= 1
if bbin == nbins:
gbin -= 1
if vbin == nbins:
vbin -= 1
r[rbin] += 1
g[gbin] += 1
b[bbin] += 1
v[vbin] += 1
return (r, g, b, v)
+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)
+14 -5
View File
@@ -6,8 +6,6 @@ from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QVBoxLayout,
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.
@@ -26,6 +24,7 @@ class IntelligentSlider(QWidget):
self.callback = callback
self.a = a
self.b = b
self.manually_triggered = False
self.slider = QSlider()
self.slider.setRange(0, 1000)
@@ -49,15 +48,19 @@ class IntelligentSlider(QWidget):
def slider_changed(self, val):
val = self.val()
self.value_label.setText(str(val)[:4])
self.callback(self.name, val)
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(str(val)[:4])
self.manually_triggered = False
def val(self):
return self.slider.value() * self.a + self.b
@@ -227,13 +230,19 @@ class MixerPanel(QWidget):
def combo_box_changed(self, index):
self.sliders.setCurrentIndex(index)
self.reset_sliders()
self.reset()
def rgb_radio_changed(self):
self.reset_sliders()
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
+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))
+7 -129
View File
@@ -18,6 +18,7 @@ else:
QPainter, QColor, QFrame)
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.'
@@ -124,121 +125,6 @@ else:
self.v_value.setText(str(v)[:5])
class Histogram(QWidget):
'''A Class which draws a scaling histogram in
a widget.
The argument to the constructor 'vals' is a list of tuples
of the following form:
vals = [(counts, colormap)]
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, in which case this is
the color applied to all bars of that histogram.
Each histogram is drawn in order from left to right in its own
box and the values are scaled so that max(count) = height.
This is a linear scaling.
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) != 1:
msg = 'Colormap must be same length as count or 1'
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.0
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)
if len(self.colormap) == 1:
self.colormap = self.colormap * len(self.counts)
# determine the scaling factor
max_val = np.max(self.counts)
scale = 1. * height / max_val
# draw the bars for this graph
for i in range(len(self.counts)):
bar_height = self.counts[i] * scale
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]
paint.end()
def update_hist(self, counts, cmap):
self._validate_input(counts, cmap)
self.counts = counts
self.colormap = cmap
self.repaint()
class MultiHist(QFrame):
def __init__(self, vals):
QFrame.__init__(self)
self.hists = []
for counts, cmap in vals:
self.hists.append(Histogram(counts, cmap))
self.layout = QtGui.QGridLayout(self)
for i in range(len(self.hists))[::-1]:
self.layout.addWidget(self.hists[i], i, 0)
def update_hists(self, vals):
for i in range(len(vals)):
counts, cmap = vals[i]
self.hists[i].update_hist(counts, cmap)
class FancyImageWindow(ImageWindow):
def __init__(self, arr, mgr):
ImageWindow.__init__(self, arr, mgr)
@@ -253,9 +139,9 @@ else:
self.mixer_panel.show()
self.mixer_panel.set_callback(self.refresh_image)
self.rgb_hist = MultiHist(self.calc_hist())
self.layout.addWidget(self.rgb_hist, 0, 1)
self.rgb_hist.show()
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)
@@ -273,21 +159,13 @@ else:
self.layout.addWidget(self.save_file, 1, 2)
def update_histogram(self):
self.rgb_hist.update_hists(self.calc_hist())
def calc_hist(self):
rvals, gvals, bvals, grays = \
self.mixer_panel.mixer.histograms(100)
vals = ((rvals, ((255,0,0),)),(gvals, ((0,255,0),)),
(bvals, ((0,0,255),)), (grays, ((0, 0, 0),)))
return vals
def update_histograms(self):
self.rgbv_hist.update_hists(self.arr)
def refresh_image(self):
pm = QPixmap.fromImage(self.label.img)
self.label.setPixmap(pm)
self.update_histogram()
self.update_histograms()
def scale_mouse_pos(self, x, y):
width = self.label.width()
+21 -18
View File
@@ -1,5 +1,6 @@
import numpy as np
import _colormixer
import _histograms
# utilities to make life easier for plugin writers.
@@ -151,6 +152,26 @@ def prepare_for_display(npy_img):
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.
@@ -347,21 +368,3 @@ class ColorMixer(object):
R, G, B = _colormixer.py_hsv_2_rgb(H, S, V)
return (R, G, B)
def histograms(self, nbins):
'''Calculate the channel histograms of the current image.
Parameters
----------
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.
'''
return _colormixer.histograms(self.img, nbins)
+6 -1
View File
@@ -15,12 +15,17 @@ def configuration(parent_package='', top_path=None):
# 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'], working_path=base_path)
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__':