From ca051a3e98baf7144fa56393597b9dd7b36540cc Mon Sep 17 00:00:00 2001 From: sccolbert Date: Fri, 6 Nov 2009 20:40:43 +0100 Subject: [PATCH] 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. --- scikits/image/io/_plugins/_colormixer.pyx | 61 --------- scikits/image/io/_plugins/_histograms.pyx | 83 ++++++++++++ scikits/image/io/_plugins/q_color_mixer.py | 19 ++- scikits/image/io/_plugins/q_histogram.py | 140 +++++++++++++++++++++ scikits/image/io/_plugins/qt_plugin.py | 136 ++------------------ scikits/image/io/_plugins/util.py | 39 +++--- scikits/image/io/setup.py | 7 +- 7 files changed, 271 insertions(+), 214 deletions(-) create mode 100644 scikits/image/io/_plugins/_histograms.pyx create mode 100644 scikits/image/io/_plugins/q_histogram.py diff --git a/scikits/image/io/_plugins/_colormixer.pyx b/scikits/image/io/_plugins/_colormixer.pyx index ec3d698c..6e0fa1f4 100644 --- a/scikits/image/io/_plugins/_colormixer.pyx +++ b/scikits/image/io/_plugins/_colormixer.pyx @@ -529,68 +529,7 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, img[i, j, 1] = RGB[1] img[i, j, 2] = 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 = (0.3 * R + 0.59 * G + 0.11 * B) - - rbin = (R / bin_width) - gbin = (G / bin_width) - bbin = (B / bin_width) - vbin = (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) diff --git a/scikits/image/io/_plugins/_histograms.pyx b/scikits/image/io/_plugins/_histograms.pyx new file mode 100644 index 00000000..41ec79a1 --- /dev/null +++ b/scikits/image/io/_plugins/_histograms.pyx @@ -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 = img[i, j, 0] + G = img[i, j, 1] + B = img[i, j, 2] + V = tri_max(R, G, B) + + rbin = (R / bin_width) + gbin = (G / bin_width) + bbin = (B / bin_width) + vbin = (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) diff --git a/scikits/image/io/_plugins/q_color_mixer.py b/scikits/image/io/_plugins/q_color_mixer.py index d041fb26..95cb74b7 100644 --- a/scikits/image/io/_plugins/q_color_mixer.py +++ b/scikits/image/io/_plugins/q_color_mixer.py @@ -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 diff --git a/scikits/image/io/_plugins/q_histogram.py b/scikits/image/io/_plugins/q_histogram.py new file mode 100644 index 00000000..cfdc889f --- /dev/null +++ b/scikits/image/io/_plugins/q_histogram.py @@ -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)) \ No newline at end of file diff --git a/scikits/image/io/_plugins/qt_plugin.py b/scikits/image/io/_plugins/qt_plugin.py index 499a4cbd..f718890c 100644 --- a/scikits/image/io/_plugins/qt_plugin.py +++ b/scikits/image/io/_plugins/qt_plugin.py @@ -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() diff --git a/scikits/image/io/_plugins/util.py b/scikits/image/io/_plugins/util.py index db8ed0eb..951ec16f 100644 --- a/scikits/image/io/_plugins/util.py +++ b/scikits/image/io/_plugins/util.py @@ -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) \ No newline at end of file diff --git a/scikits/image/io/setup.py b/scikits/image/io/setup.py index 2565d724..9c9cf023 100644 --- a/scikits/image/io/setup.py +++ b/scikits/image/io/setup.py @@ -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__':