From 663738ab791d5da43037b58a36c78e6f3ffb317b Mon Sep 17 00:00:00 2001 From: sccolbert Date: Wed, 4 Nov 2009 02:28:57 +0100 Subject: [PATCH] added the beginnings of the color mixes, and improved fancy imshow for qt --- scikits/image/io/_plugins/_colormixer.pyx | 51 ++++++++++++ scikits/image/io/_plugins/qt_plugin.py | 95 ++++++++++++++++++++--- scikits/image/io/_plugins/util.py | 87 ++++++++++++++++++++- 3 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 scikits/image/io/_plugins/_colormixer.pyx diff --git a/scikits/image/io/_plugins/_colormixer.pyx b/scikits/image/io/_plugins/_colormixer.pyx new file mode 100644 index 00000000..35d7bd6b --- /dev/null +++ b/scikits/image/io/_plugins/_colormixer.pyx @@ -0,0 +1,51 @@ +# ColorMixer function implementations +import numpy as np +cimport numpy as np + +import cython + +@cython.boundscheck(False) +def add(np.ndarray[np.uint8_t, ndim=3] img, + np.ndarray[np.uint8_t, ndim=3] stateimg, + int channel, int ammount): + + cdef int height = img.shape[0] + cdef int width = img.shape[1] + cdef int k = channel + cdef int n = ammount + + cdef np.int16_t op_result + + cdef int i, j + for i in range(height): + for j in range(width): + op_result = (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] = 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 ammount): + + cdef int height = img.shape[0] + cdef int width = img.shape[1] + cdef int k = channel + cdef float n = ammount + + cdef float op_result + + cdef int i, j + for i in range(height): + for j in range(width): + op_result = (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] = op_result \ 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 839b7d71..62b94526 100644 --- a/scikits/image/io/_plugins/qt_plugin.py +++ b/scikits/image/io/_plugins/qt_plugin.py @@ -1,4 +1,4 @@ -from util import prepare_for_display, window_manager, GuiLockError +from util import prepare_for_display, window_manager, GuiLockError, ColorMixer import numpy as np import sys @@ -14,7 +14,8 @@ except GuiLockError, gle: else: try: from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap, - QLabel, QWidget, QVBoxLayout) + QLabel, QWidget, QVBoxLayout, QSlider) + from PyQt4 import QtCore, QtGui except ImportError: print 'PyQT4 libraries not installed. Plugin not loaded.' @@ -53,9 +54,14 @@ else: def __init__(self, arr, mgr): QMainWindow.__init__(self) self.mgr = mgr + self.main_widget = QWidget() + self.layout = QtGui.QHBoxLayout(self.main_widget) + self.setCentralWidget(self.main_widget) + self.label = LabelImage(self, arr) - self.setCentralWidget(self.label) + self.layout.addWidget(self.label) self.mgr.add_window(self) + self.main_widget.show() def closeEvent(self, event): # Allow window to be destroyed by removing any @@ -66,28 +72,93 @@ else: pass + class SliderBlock(QWidget): + def __init__(self, srange, callback): + QWidget.__init__(self) + + self.callback = callback + + low = srange[0] + high = srange[1] + init = srange[2] + + self.rslider = QSlider() + self.rslider.setRange(low, high) + self.rslider.setValue(init) + + self.gslider = QSlider() + self.gslider.setRange(low, high) + self.gslider.setValue(init) + + self.bslider = QSlider() + self.bslider.setRange(low, high) + self.bslider.setValue(init) + + self.rslider.valueChanged.connect(self.rslider_changed) + self.gslider.valueChanged.connect(self.gslider_changed) + self.bslider.valueChanged.connect(self.bslider_changed) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.rslider) + self.layout.addWidget(self.gslider) + self.layout.addWidget(self.bslider) + + def rslider_changed(self, val): + self.callback('RED', val) + + def gslider_changed(self, val): + self.callback('GREEN', val) + + def bslider_changed(self, val): + self.callback('BLUE', val) + + class FancyImageWindow(ImageWindow): def __init__(self, arr, mgr): ImageWindow.__init__(self, arr, mgr) self.arr = arr - # for image manipulation - self.arrfloat = np.asarray(arr, dtype=np.float64) - self.arruint8 = arr.copy() - self.statusBar().showMessage('X: Y: ') self.label.setScaledContents(True) self.label.setMouseTracking(True) + self.mixer = ColorMixer(self.arr) - def keyPressEvent(self, evt): - self.arrfloat[:,:,0] *= 1.1 - np.clip(self.arrfloat, 0, 255, self.arrfloat) - self.arr[:] = self.arrfloat[:] + self.sliders = SliderBlock((-255, 255, 0), self.svalueChanged) + self.msliders = SliderBlock((0, 1000, 500), self.mvalueChanged) + + self.layout.addWidget(self.sliders) + self.layout.addWidget(self.msliders) + + self.sliders.show() + self.msliders.show() + + def svalueChanged(self, who, val): + if who == 'RED': + self.mixer.add(self.mixer.RED, val) + elif who == 'GREEN': + self.mixer.add(self.mixer.GREEN, val) + elif who == 'BLUE': + self.mixer.add(self.mixer.BLUE, val) + else: + return + + pm = QPixmap.fromImage(self.label.img) + self.label.setPixmap(pm) + + def mvalueChanged(self, who, val): + val = val / 500. + if who == 'RED': + self.mixer.multiply(self.mixer.RED, val) + elif who == 'GREEN': + self.mixer.multiply(self.mixer.GREEN, val) + elif who == 'BLUE': + self.mixer.multiply(self.mixer.BLUE, val) + else: + return pm = QPixmap.fromImage(self.label.img) self.label.setPixmap(pm) - print 'heard' 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 6dd4f8e7..96ae939c 100644 --- a/scikits/image/io/_plugins/util.py +++ b/scikits/image/io/_plugins/util.py @@ -1,5 +1,5 @@ import numpy as np - +import _colormixer # utilities to make life easier for plugin writers. @@ -149,3 +149,88 @@ def prepare_for_display(npy_img): raise ValueError('Image must have 2 or 3 dimensions') return out + + + +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.stateimg = img.copy() + + def get_stateimage(self): + return self.stateimg + + def commit_changes(self): + self.stateimg[:] = self.img[:] + + def add(self, channel, ammount): + '''Add the specified ammount to the specified channel. + + Parameters + ---------- + channel : flag + the color channel to operate on + RED, GREED, or BLUE + ammount : integer + the ammount of color to add to the channel, + can be positive or negative. + + ''' + assert channel in self.valid_channels + + _colormixer.add(self.img, self.stateimg, channel, ammount) + + def multiply(self, channel, ammount): + '''Mutliply the indicated channel by the specified value. + + Parameters + ---------- + channel : flag + the color channel to operate on + RED, GREED, or BLUE + ammount : integer + the ammount of color to add to the channel, + can be positive or negative. + + ''' + assert channel in self.valid_channels + + _colormixer.multiply(self.img, self.stateimg, channel, ammount)