From e9bdac2f1594de03271b81d74ca48bda3e84cf9c Mon Sep 17 00:00:00 2001 From: Julien Lengrand-Lambert Date: Mon, 19 Dec 2011 14:49:35 +0100 Subject: [PATCH] Adds histogram support to 3 channels images Finishes support for 3 channels images. Adds a function allowing to extract only one desired channel Updates the example to extract all channels. Support for 16 and 32 bits images should be tested. Function for 3c to 1c to be added quickly! Reported-by: Julien Lengrand-Lambert Signed-off-by: Julien Lengrand-Lambert --- tippy/statistics.py | 201 +++++++++++++++++++++++++++++++++ tippy/tests/test_statistics.py | 122 ++++++++++++++++++++ 2 files changed, 323 insertions(+) diff --git a/tippy/statistics.py b/tippy/statistics.py index 41a2555..2daccc1 100644 --- a/tippy/statistics.py +++ b/tippy/statistics.py @@ -6,5 +6,206 @@ Created on Dec 11, 2011 @author: Julien Lengrand-Lambert @email: julien@lengrand.fr ''' +import cv +import sys +class Histogram(): + """ + This class is designed to contain an openCV class plus some more information + to ease further operations. + + NOTE : Supports only 8 bits images for now. + More should be added soon. + """ + def __init__(self, img, bin_fact=cv.IPL_DEPTH_8U, min_range=0): + self.cvhist = None + self.min_range = 0 + self.depth = cv.IPL_DEPTH_8U + self.nb_bins = 0 + self.channels = 0 + self.compute_histogram(img, bin_fact, min_range) + + def compute_histogram(self, img, bin_fact=cv.IPL_DEPTH_8U, min_range=0): + """ + Creates the histogram of the input image + + Returns a Tippy histogram, with one item by channel of input image. + The number of bins is 2^bin_fact (256 by default) + min_range is the value of the lowest bin + max_range is automatically calculated using the input image depth. + max_range = (2^img_depth) + + NOTE : Supports only 8 bits images for now. + More should be added soon. + """ + # TESTS + try: + dims = cv.GetSize(img) + except TypeError: + raise TypeError("(%s) img : IplImage expected!" + % (sys._getframe().f_code.co_name)) + + # img test + if not(img.depth == cv.IPL_DEPTH_8U): + raise TypeError("(%s) 8U image expected!" + % (sys._getframe().f_code.co_name)) + if img.nChannels not in [1, 3]: + raise ValueError("(%s) 1 or 3 channels image expected!" + % (sys._getframe().f_code.co_name)) + # bin_fact test + if (bin_fact <= 0 or ((bin_fact % 2) != 0)): + raise ValueError("(%s) Positive odd integer expected!" + % (sys._getframe().f_code.co_name)) + elif type(bin_fact) not in [int, long]: + raise TypeError("(%s) Positive odd integer expected!" + % (sys._getframe().f_code.co_name)) + + # min_range test + if type(min_range) != int: + raise TypeError("(%s) Positive odd integer expected!" + % (sys._getframe().f_code.co_name)) + + self.channels = img.nChannels + self.nb_bins = int(pow(2, bin_fact)) + self.depth = int(img.depth) + max_range = pow(2, self.depth) + self.ranges = [min_range, max_range] + + img_list = [] + # preparing images depending on nChannels + if self.channels == 1: + img_list = [img] + elif self.channels == 3: + # TODO: change this into function + img_1 = cv.CreateImage(dims, cv.IPL_DEPTH_8U, 1) + img_2 = cv.CreateImage(dims, cv.IPL_DEPTH_8U, 1) + img_3 = cv.CreateImage(dims, cv.IPL_DEPTH_8U, 1) + + cv.Split(img, img_1, img_2, img_3, None) + img_list = [img_1, img_2, img_3] + + self.cvhist = self._compute_1ch_histogram(img_list) + + def _compute_1ch_histogram(self, img_list): + """ +<<<<<<< HEAD + DESIGNED FOR INTERNAL USE ONLY. + BE CAREFUL, NO VERIFICATIONS PERFORMED +======= + DESIGNED FOR INTERNAL USE ONLY + CAREFUL, NO VERIFICATIONS PERFORMED +>>>>>>> 335108ecf6cb601da09717b94407e54e677b9cee + """ + dims = [self.nb_bins] + all_ranges = [self.ranges] + + hist_list = [] + for img in img_list: + hist = cv.CreateHist( dims, + cv.CV_HIST_ARRAY, + all_ranges, + uniform=1) + cv.CalcHist([img], hist) + + hist_list.append(hist) + + return hist_list + + def to_tables(self): + """ + Transforms an histogram into a list of values. + This way, it is easier to process for calculations. + + Returns a list of nChannels items. + + NOTE : Supports only 8 bits images for now. + More should be added soon. + """ + if self.nb_bins < 1: + raise TypeError("(%s) Histogram expected!" + % (sys._getframe().f_code.co_name)) + + histable = [] + for ii in range(self.channels): + hist_list = [] + for jj in range(self.nb_bins): + hist_list.append(self.cvhist[ii].bins[jj]) + histable.append(hist_list) + return histable + + def to_images(self, scale_x=3, scale_y=3, y_range=64): + """ + Creates a graphical representation of an histogram. + Outputs a list of nChannels iplimages, one for each channel of the + histogram. + The images should have the same depth as the image used to create the + histogram. + """ + if ((type(scale_x) != int) or (scale_x <= 0)): + raise TypeError("(%s) Positive integer expected!" + % (sys._getframe().f_code.co_name)) + + if ((type(scale_y) != int) or (scale_y <= 0)): + raise TypeError("(%s) Positive integer expected!" + % (sys._getframe().f_code.co_name)) + + if ((type(y_range) != int) or (y_range <= 0)): + raise TypeError("(%s) Positive integer expected!" + % (sys._getframe().f_code.co_name)) + + images = [] + for jj in range(self.channels): + images.append(self.channel_to_image(jj + 1, + scale_x, + scale_y, + y_range)) + + return images + + def channel_to_image(self, chan, scale_x=3, scale_y=3, y_range=64): + """ + Creates an iplimage displaying histogram results after its computation + """ + if ((type(chan) != int) or (chan <= 0)): + raise TypeError("(%s) Positive integer expected!" + % (sys._getframe().f_code.co_name)) + elif chan > self.channels: + raise ValueError("(%s) Incoherent channel selected!" + % (sys._getframe().f_code.co_name)) + + if ((type(scale_x) != int) or (scale_x <= 0)): + raise TypeError("(%s) Positive integer expected!" + % (sys._getframe().f_code.co_name)) + + if ((type(scale_y) != int) or (scale_y <= 0)): + raise TypeError("(%s) Positive integer expected!" + % (sys._getframe().f_code.co_name)) + + if ((type(y_range) != int) or (y_range <= 0)): + raise TypeError("(%s) Positive integer expected!" + % (sys._getframe().f_code.co_name)) + + max_range = self.ranges[1] - 1 + (_, hist_max, _, _ ) = cv.GetMinMaxHistValue(self.cvhist[chan - 1]) + hist_img = cv.CreateImage( (max_range*scale_x, y_range*scale_y), + self.depth, + 1) + cv.Zero(hist_img) # resets image values + + for i in range(max_range): # 0 to max_range + hist_value = cv.QueryHistValue_1D(self.cvhist[chan - 1], i) + next_value = cv.QueryHistValue_1D(self.cvhist[chan - 1], i+1) + pt1 = ( int(i*scale_x), + int(y_range*scale_y)) + pt2 = ( int(i*scale_x+ scale_x), + int(y_range*scale_y)) + pt3 = ( int(i*scale_x+scale_x), + int((y_range-(next_value*y_range/hist_max))*scale_y)) + pt4 = ( int(i*scale_x), + int((y_range-(hist_value*y_range/hist_max))*scale_y)) + pts = (pt1, pt2, pt3, pt4) + cv.FillConvexPoly(hist_img, pts, 255, lineType=8, shift=0) + + return hist_img + diff --git a/tippy/tests/test_statistics.py b/tippy/tests/test_statistics.py index ba269b9..56a3d59 100644 --- a/tippy/tests/test_statistics.py +++ b/tippy/tests/test_statistics.py @@ -4,4 +4,126 @@ Created on Dec 11, 2011 @author: Julien Lengrand-Lambert @email: julien@lengrand.fr ''' +import unittest +import cv +import tippy.statistics as st + +class Test(unittest.TestCase): + + + def setUp(self): + """ + This method is called before each test + """ + self.img_1c = cv.LoadImage("data/tippy.jpg", cv.CV_LOAD_IMAGE_GRAYSCALE) # 1 channel image + self.img_3c = cv.LoadImage("data/tippy.jpg", cv.CV_LOAD_IMAGE_COLOR) # 3 channel image + self.img_2c = cv.CreateImage((10, 10), cv.IPL_DEPTH_8U, 2) # fake 2 channels image + self.img_16s = cv.CreateImage((15, 15), cv.IPL_DEPTH_16S, 1) # Non 8 bits image + + self.val = 10 # non Image + self.neg_val = -10 # non Image + self.string = "A" # non Image + + def tearDown(self): + """ + This method is called after each test + """ + pass + + #--- + ## TESTS + def test_compute_histogram(self): + # testing input image + self.assertRaises(TypeError, lambda:st.Histogram(self.val) ) + self.assertRaises(TypeError, lambda:st.Histogram(self.img_16s) ) + self.assertRaises(ValueError, lambda:st.Histogram(self.img_2c) ) + # testing bin_fact + self.assertRaises(ValueError, + lambda: st.Histogram(self.img_1c, self.neg_val)) + self.assertRaises(TypeError, + lambda: st.Histogram(self.img_1c, self.string)) + + # testing min_range. It can be either negative or even. + self.assertRaises(TypeError, + lambda: st.Histogram(self.img_1c, self.val, self.string)) +<<<<<<< HEAD + + # testing output + hist_1c = st.Histogram(self.img_1c) + self.assertEquals(self.img_1c.nChannels, hist_1c.channels) + hist_3c = st.Histogram(self.img_3c) + self.assertEquals(self.img_3c.nChannels, hist_3c.channels) + + def test_to_tables(self): + # testing output size + hist = st.Histogram(self.img_3c) + histable = hist.to_tables() + self.assertEqual(3, len(histable)) + self.assertEqual(256, len(histable[0])) + + def test_channel_to_image(self): + hist = st.Histogram(self.img_3c) + # testing channel value + self.assertRaises(ValueError, + lambda: hist.channel_to_image(4)) + self.assertRaises(TypeError, + lambda: hist.channel_to_image(self.string)) + self.assertRaises(TypeError, + lambda: hist.channel_to_image(self.neg_val)) +======= + + # testing output + hist_1c = st.Histogram(self.img_1c) + self.assertEquals(self.img_1c.nChannels, hist_1c.channels) + hist_3c = st.Histogram(self.img_3c) + self.assertEquals(self.img_3c.nChannels, hist_3c.channels) + + def test_hist2table(self): + # testing output size + hist = st.Histogram(self.img_3c) + histable = hist.hist2table() + self.assertEqual(3, len(histable)) + self.assertEqual(256, len(histable[0])) + + def test_hist2image(self): + hist = st.Histogram(self.img_1c) +>>>>>>> 335108ecf6cb601da09717b94407e54e677b9cee + # testing scale inputs + self.assertRaises(TypeError, + lambda: hist.channel_to_image(1, self.string)) + self.assertRaises(TypeError, + lambda: hist.channel_to_image(1, self.neg_val)) + self.assertRaises(TypeError, + lambda: hist.channel_to_image(1, self.val, self.string)) + self.assertRaises(TypeError, + lambda: hist.channel_to_image(1, self.val, self.neg_val)) + # testing range inputs + self.assertRaises(TypeError, + lambda: hist.channel_to_image(1, self.val, self.val, self.string)) + self.assertRaises(TypeError, + lambda: hist.channel_to_image(1, self.val, self.val, self.neg_val)) + + def test_to_images(self): + hist = st.Histogram(self.img_3c) + # testing scale inputs + self.assertRaises(TypeError, + lambda: hist.to_images(self.string)) + self.assertRaises(TypeError, + lambda: hist.to_images(self.neg_val)) + self.assertRaises(TypeError, + lambda: hist.to_images(self.val, self.string)) + self.assertRaises(TypeError, + lambda: hist.to_images(self.val, self.neg_val)) + # testing range inputs + self.assertRaises(TypeError, + lambda: hist.to_images(self.val, self.val, self.string)) + self.assertRaises(TypeError, + lambda: hist.to_images(self.val, self.val, self.neg_val)) + + # testing outputs + self.assertEqual(hist.channels, len(hist.to_images())) + +#if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + #unittest.main()