├── .gitignore ├── README.rst ├── __init__.py ├── example.png ├── features ├── __init__.py ├── blur.py ├── composition.py ├── contrast.py ├── faces.py └── noise.py ├── grid.py ├── histogram.py ├── image.py ├── logger.py ├── main.py ├── papers.py ├── test.py ├── training_data.py └── windows.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Image Quality Analysis Final Project 2 | ==================================== 3 | 4 | This is my final project code for Computational Vision. 5 | **I really wish I could improve the quality of code at some point.** 6 | This was created in approximately two weeks, which includes figuring 7 | out how to compile and learn OpenCV. 8 | 9 | .. image:: example.png 10 | 11 | Prerequisites 12 | ------------- 13 | 14 | You'll need OpenCV and Numpy to run this. 15 | 16 | Configuration 17 | -------------- 18 | 19 | The main entry point is main.py. 20 | 21 | Not all the options are available for configuration on the command line, 22 | so you'll have to manually edit the main.py code (which is messy, I know). 23 | 24 | Near the top of main.py are the features we're using:: 25 | 26 | measurements = ( 27 | contrast, 28 | noise, 29 | blur, 30 | composition, 31 | #faces, # => under composition 32 | ) 33 | 34 | Uncomment or comment the features you wish the analyzer to use. Blur is 35 | the longest to compute on average and composition is incomplete. 36 | 37 | If you want to change/add some training data...:: 38 | 39 | california_night = TrainingData('good/Another Beautiful California Night.jpg', 40 | measures=measurements, kind='good', 41 | ) 42 | 43 | These (and other lines like it) list some test images. Currently, they are 44 | not included with the project yet:: 45 | 46 | 47 | li_dir = load_images_from('li') 48 | chow_dir = load_images_from('chow') 49 | china_dir = load_images_from('/Users/jeff/Desktop/china-day6/full') 50 | 51 | This contains a collection of images to use. You can create your own here. 52 | But you'll need to modify the switch case below it:: 53 | 54 | if options.imageset: 55 | if options.imageset == 'li': 56 | process(li_dir) 57 | return 0 58 | elif options.imageset == 'chow': 59 | process(chow_dir) 60 | return 0 61 | elif options.imageset == 'china': 62 | process(china_dir) 63 | return 0 64 | elif options.imageset == 'custom': 65 | process() 66 | return 0 67 | 68 | Inside the process() function, there is a max_size argument:: 69 | 70 | max_size = None#(800, 600) 71 | 72 | Setting it to a 2-tuple restricts the width or height to the given value 73 | if the image is greater than the given size. This only applies to image sets. 74 | 75 | Similarly, there's an identical one for single images in the main():: 76 | 77 | size = None #(320,240,'crop') # (0.5, 0.5, 'resize-p') 78 | if size is None: 79 | im = tdata.load() 80 | elif size[-1] == 'crop': 81 | im = image.random_cropped_region(tdata.load(), size[:2]) 82 | elif size[-1] == 'resize': 83 | im = tdata.load(size[:2]) 84 | elif size[-1] == 'resize-p': 85 | im = image.resize(tdata.load(), by_percent=size[:2]) 86 | else: 87 | raise TypeError, "Invalid image sizing type." 88 | 89 | The size argument takes a 3-tuple, the first two are sizes, and the last 90 | is the method to reduce the size. 'resize-p' resizes by percent (in 91 | decimal form), while 'crop' crops a random region in the image. 'resize' is 92 | identical to max_size argument. 93 | 94 | Usage 95 | --------- 96 | 97 | To use, run main.py on an image set:: 98 | 99 | python main.py --imageset=custom 100 | 101 | The results are placed in output.html. If you want to run on only one image, 102 | use the file argument:: 103 | 104 | python main.py --file=/path/to/file.jpg 105 | 106 | You can also pass the debug flag to see intermediary image processing 107 | steps:: 108 | 109 | python main.py --file=/path/to/file.jpg -d # Don't run this! 110 | 111 | But **this creates a ALOT of debugging windows**. You'll want to see the 112 | debugging information for one feature, use the --type argument:: 113 | 114 | python main.py --file="/path/to/file.jpg" -d --type=blur 115 | 116 | And that is all for now. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffh/CV-Image-Quality-Analysis/323e9bf4ad1ba25c28853bc85691eadcbae35464/__init__.py -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffh/CV-Image-Quality-Analysis/323e9bf4ad1ba25c28853bc85691eadcbae35464/example.png -------------------------------------------------------------------------------- /features/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffh/CV-Image-Quality-Analysis/323e9bf4ad1ba25c28853bc85691eadcbae35464/features/__init__.py -------------------------------------------------------------------------------- /features/blur.py: -------------------------------------------------------------------------------- 1 | import image 2 | import cv 3 | import random 4 | import noise 5 | import math 6 | from numpy import array 7 | 8 | # saturation is all ZERO when image is grayscale! 9 | def get_focus_points(im,debug=False): 10 | edges = image.dilate(image.auto_edges(im)) 11 | #d = 1 12 | #sobel = image.sobel(im, xorder=d, yorder=d) 13 | sobel = image.laplace(im) 14 | hsv = image.rgb2hsv(im) 15 | 16 | focused = image.And(sobel, edges) 17 | if im.nChannels == 3: 18 | hue, saturation, value = image.split(hsv) 19 | saturation = image.dilate(saturation) 20 | focused = image.And(focused, saturation) 21 | focused = image.threshold(image.dilate(focused), threshold=32) 22 | 23 | if debug: 24 | image.show(edges, "1. Edges") 25 | image.show(sobel, "2. Sobel") 26 | if im.nChannels == 3: 27 | image.show(saturation, "3. Saturation") 28 | return focused 29 | 30 | def convert_to_points(im): 31 | points = [] 32 | size = cv.GetSize(im) 33 | for x in xrange(size[0]): 34 | for y in xrange(size[1]): 35 | if im[y, x] > 0: 36 | points.append((x, y)) 37 | return points 38 | 39 | """ 40 | # too slow, too inefficient 41 | from hcluster import pdist, linkage, to_tree, dendrogram, centroid 42 | def form_groups(points, threshold=5, min_size=0.025): 43 | "Clusters points based on a given threshold. If the cluster's dist > threshold, it is split." 44 | print "Threshold =", threshold 45 | points = array(points) 46 | #Y = pdist(points) 47 | print "centroids" 48 | Z = centroid(points) 49 | print "dendrogram" 50 | dendrogram(Z) 51 | print "to_tree" 52 | R = to_tree(Z) 53 | print "get_count" 54 | total = R.get_count() 55 | clusters = [R] 56 | seen = set() 57 | min_size = max(int(total * min_size), 4) 58 | 59 | while 1: 60 | if clusters[0] in seen: 61 | break 62 | node = clusters.pop(0) 63 | diff = abs(node.get_left().get_count() - node.get_right().get_count()) 64 | should_split = not node.is_leaf() and node.dist > threshold 65 | should_split = should_split and total > min_size and \ 66 | node.get_left().get_count() > min_size and \ 67 | node.get_right().get_count() > min_size 68 | if not should_split: 69 | clusters.append(node) 70 | seen = seen.union([node]) 71 | else: 72 | clusters.extend((node.get_left(), node.get_right())) 73 | print 'clusters', len(clusters) 74 | def get_ids(c, accum=None): 75 | if c.is_leaf(): 76 | return [c.id] 77 | l = [] 78 | l.extend(get_ids(c.get_left())) 79 | l.extend(get_ids(c.get_right())) 80 | return l 81 | return [get_ids(c) for c in clusters] 82 | """ 83 | from scipy.cluster.vq import whiten, kmeans2, vq 84 | def form_groups(points, estimated_size=10, iter=1): 85 | if len(points) < 1: 86 | return [] 87 | points = array(points) 88 | centroids, variance = kmeans2(points, estimated_size, iter=iter, minit='points') 89 | group_indicies, dist = vq(points, centroids) 90 | group = {} 91 | for i,index in enumerate(group_indicies): 92 | if index not in group: 93 | group[index] = [] 94 | group[index].append(points[i]) 95 | return group.values() 96 | 97 | def draw_groups(groups, im=None): 98 | 99 | hulls = [] 100 | for group in groups: 101 | ch = ConvexHull([(g[0], g[1]) for g in group])#map(lambda x: (x[0], x[1]), group)) 102 | #ch = ConvexHull(map(lambda x: points[x], group)) 103 | hulls.append(ch) 104 | 105 | ppp = ch.points_per_pixel() 106 | #if ch.area() >= min_area: # not a 3x3 region 107 | # densities.append(ppp) 108 | a = int(ppp * 255) 109 | if im: 110 | ch.draw_filled_hull(im, rgb=(a,a,a))#r(255),r(255),r(255))) 111 | 112 | 113 | #if debug and ch.area() < min_area: 114 | # ch.draw_centroid(focused_regions, rgb=(255,0,0)) 115 | 116 | #ch.draw_hull(focused_regions, rgb=(r(255),r(255),r(255))) 117 | 118 | #ch.draw_points(focused_regions, rgb=(r(255),r(255),r(255))) 119 | return hulls 120 | 121 | class Contours(object): 122 | def __init__(self, im): 123 | self.im = im 124 | self.storage = cv.CreateMemStorage(0) 125 | self.contours = cv.FindContours(im, self.storage, cv.CV_RETR_TREE, 126 | cv.CV_CHAIN_APPROX_SIMPLE) 127 | 128 | def approx_poly(self): 129 | self.contours = cv.ApproxPoly(self.contours, self.storage, 130 | cv.CV_POLY_APPROX_DP, 3, 1) 131 | return self 132 | 133 | def draw(self, im, levels=3, thickness=3, external_rgb=(255,0,0), internal_rgb=(0,255,0)): 134 | # draw contours in red and green 135 | cv.DrawContours (im, self.contours, 136 | cv.RGB(*external_rgb), cv.RGB(*internal_rgb), 137 | levels, thickness, cv.CV_AA) 138 | 139 | def __iter__(self): 140 | start = contour = self.contours 141 | while contour: 142 | yield contour 143 | contour = contour.h_next() 144 | self.contours = start 145 | 146 | class ConvexHull(object): 147 | def __init__(self, points): 148 | # compute the convex hull 149 | self.boundary_points = points[:] 150 | self.points = points 151 | self.storage = cv.CreateMemStorage(0) 152 | self.hull = cv.ConvexHull2(self.points, self.storage, cv.CV_CLOCKWISE, 1) 153 | self._centroid = None 154 | 155 | def __contains__(self, point): 156 | return cv.PointPolygonTest(self.boundary_points, point, 0) >= 0 157 | 158 | def draw_points(self, im, rgb=(255,0,0)): 159 | for pt in self.points: 160 | cv.Circle (im, pt, 2, cv.RGB(*rgb), 161 | cv.CV_FILLED, cv.CV_AA, 0) 162 | 163 | def draw_filled_hull(self, im, rgb=(255,255,255)): 164 | cv.FillPoly(im, [self.hull], cv.RGB(*rgb), cv.CV_AA) 165 | 166 | def draw_hull(self, im, rgb=(0,255,0)): 167 | cv.PolyLine(im, [self.hull], 1, cv.RGB(*rgb), 1, cv.CV_AA) 168 | 169 | def draw_centroid(self, im, rgb=(0,0,255)): 170 | cv.Circle (im, self.centroid(), 2, cv.RGB(*rgb), 171 | cv.CV_FILLED, cv.CV_AA, 0) 172 | 173 | def area(self): 174 | return max(abs(cv.ContourArea(self.hull)), 0.00001) 175 | 176 | def points_per_pixel(self): 177 | return len(self.points) / self.area() 178 | 179 | def centroid(self): 180 | if self._centroid is None: 181 | sum = [0, 0] 182 | for p in self.points: 183 | sum[0] += p[0] 184 | sum[1] += p[1] 185 | s = float(len(self.points)) 186 | self._centroid = int(sum[0] / s), int(sum[1] / s) 187 | return self._centroid 188 | 189 | def pixel_remove(im): 190 | p = im[1,1] 191 | if p == 0: 192 | return 0 193 | c = 0 194 | if im[0,0] > 0 and im[2,2] > 0: 195 | c += 1 196 | if im[1,0] > 0 and im[1,2] > 0: 197 | c += 1 198 | if im[2,0] > 0 and im[0,2] > 0: 199 | c += 1 200 | if im[0,1] > 0 and im[2,1] > 0: 201 | c += 1 202 | if c > 2: 203 | return 0 204 | return c > 0 205 | 206 | def remove_useless_points(im): 207 | return image.neighbor_map(im, pixel_remove) 208 | 209 | # ri_threshold = 0.2 is too weak for large images 210 | def boolean(score): 211 | # the focus regions should cover 15%+ of the screen 212 | # the density of the regions should be > 20% point/pixel 213 | region_image_ratio, density, density_std, saturation_score, light_score = score 214 | if density > 0.7: 215 | return False 216 | if light_score > 0.5: 217 | return True 218 | return (region_image_ratio <= 0.1 and \ 219 | (density - density_std < 0.2)) 220 | # TODO: saturation_score needs more tweaking 221 | #or saturation_score > 0.8 222 | #return score < d_threshold 223 | 224 | requires_result_from = [] # noise perhaps??? 225 | # was using Agglomerative Clustering, but too slow for points > ~5000 226 | def measure(im, debug=False): 227 | size = cv.GetSize(im) 228 | npixels = size[0] * size[1] 229 | #print 'np', npixels 230 | 231 | 232 | focused = get_focus_points(im, debug) 233 | points = convert_to_points(focused) 234 | 235 | if debug: 236 | print "\t"+str(len(points)), '/', npixels, '=', len(points) / float(npixels) 237 | print "\tlen(points) =", len(points) 238 | image.show(focused, "4. Focused Points") 239 | 240 | saturation_score = 0 241 | if not image.is_grayscale(im): 242 | edges = image.auto_edges(im) 243 | _, saturation, _ = image.split(image.rgb2hsv(im)) 244 | if debug: 245 | image.show(saturation, "5. Saturation") 246 | #saturation = image.laplace(image.gaussian(saturation, 3)) 247 | saturation = image.invert(saturation) 248 | mask = image.invert(image.threshold(im, threshold=16)) 249 | if debug: 250 | image.show(saturation, "5.3. Laplace of Saturation") 251 | cv.Set(saturation, 0, mask) 252 | cv.Set(saturation, 0, focused) 253 | if debug: 254 | image.show(mask, "5.6. Mask(focused AND invert(threshold(im, 16)))") 255 | image.show(saturation, "6. Set(<5.3>, 0, <5.6>)") 256 | 257 | saturation_score = cv.Sum(saturation)[0] / float(npixels * 255) 258 | print "\tSaturation Score:", saturation_score 259 | 260 | # light exposure 261 | h,s,v = image.split(image.rgb2hsv(im)) 262 | if debug: 263 | image.show(h, "7. Hue") 264 | image.show(s, "7. Saturation") 265 | image.show(v, "7. Value") 266 | diff = cv.CloneImage(v) 267 | cv.Set(diff, 0, image.threshold(s, threshold=16)) 268 | diff = image.dilate(diff, iterations=10) 269 | if debug: 270 | thres_s = image.threshold(s, threshold=16) 271 | image.show(thres_s, "8.3. Mask(threshold(<7.Saturation>, 16))") 272 | image.show(diff, "8.6. Dilate(Set(<7.Value>, 0, <8.3>), 10)") 273 | 274 | cdiff = cv.CountNonZero(diff) 275 | if cdiff > 0 and cdiff / float(npixels) > 0.01: 276 | test = cv.CloneImage(v) 277 | cv.Set(test, 0, image.invert(diff)) 278 | s = cv.Sum(test)[0] / float(cdiff * 255) 279 | if debug: 280 | print '\tLight Exposure Score:', s 281 | else: 282 | s = 0 283 | 284 | if image.is_grayscale(im): 285 | return focused, (1, 1, 1, saturation_score, s) 286 | 287 | # we want to short circuit ASAP to avoid doing KMeans 50% of the image's pixels 288 | if len(points) > npixels/2: 289 | return focused, (1, 1, 1, saturation_score, s) 290 | 291 | # we're so blurry we don't have any points! 292 | if len(points) < 1: 293 | return focused, (0, 0, 0, saturation_score, s) 294 | 295 | if debug: 296 | im2 = cv.CloneImage(im) 297 | focused_regions = image.new_from(im) 298 | cv.Set(focused_regions, 0) 299 | 300 | r = lambda x: random.randrange(1, x) 301 | groups = form_groups(points, 302 | estimated_size=min(max(int(len(points) / 1000), 2), 15)) 303 | #groups = form_groups(points, threshold=max(cv.GetSize(im))/16) 304 | #print 'groups', len(groups) 305 | hulls = draw_groups(groups, focused_regions) 306 | focused_regions = image.threshold(focused_regions, threshold=32, type=cv.CV_THRESH_TOZERO) 307 | min_area = npixels * 0.0005 308 | densities = [h.points_per_pixel() for h in hulls if h.area() >= min_area] 309 | 310 | if debug: 311 | #image.show(focused, "Focused Points") 312 | image.show(focused_regions, "9. Focused Regions from <4>") 313 | cv.Sub(im2, image.gray2rgb(image.invert(image.threshold(focused_regions, threshold=1))), im2) 314 | image.show(im2, "10. threshold(<9>)") 315 | 316 | 317 | focused_regions = image.rgb2gray(focused_regions) 318 | 319 | densities = array(densities) 320 | c = cv.CountNonZero(focused_regions) 321 | c /= float(npixels) 322 | 323 | score = (c, densities.mean(), densities.std(), saturation_score, s) 324 | 325 | return focused, score -------------------------------------------------------------------------------- /features/composition.py: -------------------------------------------------------------------------------- 1 | import image 2 | import cv 3 | from features.blur import form_groups, convert_to_points, ConvexHull, draw_groups, Contours 4 | from histogram import GrayscaleHist 5 | from numpy import array 6 | from contrast import average 7 | from features import faces 8 | from grid import Grid 9 | 10 | def measure_focused_roi(im, roi, area, focus_points, debug=False): 11 | g = Grid(cv.GetSize(im)) 12 | canvas = image.new_from(im) 13 | cv.Set(canvas, 0) 14 | focus_in_roi = image.And(focus_points, roi) 15 | if debug: 16 | image.show(focus_in_roi, "ROI + Focused Points") 17 | 18 | densities = [] 19 | points = convert_to_points(focus_in_roi) 20 | groups = form_groups(points, estimated_size=24, iter=5) 21 | for group in groups: 22 | ch = ConvexHull(map(lambda x: (x[0], x[1]), group)) 23 | 24 | ppp = ch.points_per_pixel() 25 | a = int(ppp * 255) 26 | ch.draw_filled_hull(canvas, rgb=(a,a,a)) 27 | if debug: 28 | image.show(canvas, "Focused Regions in ROI") 29 | 30 | quadrants = g.split_in_four(canvas) 31 | sums = [] 32 | for i,quad in enumerate(quadrants): 33 | sums.append(cv.Sum(quad)[0] / float(area/4)) 34 | arr = array(sums) 35 | print arr.mean(), arr.std() 36 | diff = max(sums) - min(sums) 37 | 38 | return diff, arr.std() 39 | 40 | def error_from_uniform(mean, stddev): 41 | return abs(mean - 128)/128.0, abs(stddev - 64)/64.0 42 | 43 | def measure_color_roi(im, roi, area, focused_regions, debug=False): 44 | im = cv.CloneImage(im) 45 | g = Grid(cv.GetSize(im)) 46 | 47 | 48 | """ 49 | contours = Contours(image.threshold(focused_regions, threshold=1)).approx_poly() 50 | if debug: 51 | test = image.new_from(im) 52 | cv.Set(test, 0) 53 | for c in contours: 54 | i = 1 55 | while c: 56 | cv.FillPoly(test, [[c[x] for x in range(len(c))]], cv.RGB(0,64*i,0)) 57 | c = c.v_next() 58 | i += 1 59 | #contours.draw(test, levels=9) 60 | image.show(test, "Test") 61 | """ 62 | #mask = image.And(image.threshold(focused_regions, threshold=1), roi) 63 | # 64 | #canvas = image.new_from(im, nChannels=1) 65 | #cv.Set(canvas, 0) 66 | #if cv.CountNonZero(mask) <= 1: 67 | # return 0, 0 68 | #contours = Contours(image.dilate(mask)).approx_poly() 69 | #for c in contours: 70 | # i = 1 71 | # while c: 72 | # cv.FillPoly(canvas, [[c[x] for x in range(len(c))]], 255) 73 | # c = c.v_next() 74 | # i += 1 75 | #mask = image.Or(mask, canvas) 76 | #if debug: 77 | # image.show(mask, "MASK") 78 | # 79 | #cv.Set(im, 0, image.invert(mask)) 80 | cv.Set(im, 0, image.invert(roi)) 81 | 82 | #area = cv.CountNonZero(image.threshold(im, threshold=1)) 83 | 84 | if debug: 85 | image.show(g.draw(im,thickness=2), "Image + ROI + Focus point mask") 86 | 87 | scores = [] 88 | im = image.rgb2gray(im) 89 | #canvas = image.And(plane, roi) 90 | quadrants = g.split_in_four(im) 91 | hist = [] 92 | for q,quad in enumerate(quadrants): 93 | #scores.append(cv.Sum(quad)[0] / float(area/4)) 94 | h = GrayscaleHist(value_range=(1,255)).use_image(quad) 95 | #image.show(h.to_img(), ['gray', 'red','green','blue'][i] + ' in ' + str(q)) 96 | hist.append(h.to_array()) 97 | scores = [] 98 | excluded_points = set([(2, 1), (3, 0)]) 99 | for i,h1 in enumerate(hist): 100 | for j,h2 in enumerate(hist): 101 | if i <= j or (i,j) in excluded_points: 102 | continue 103 | h = abs(h2-h1) 104 | ht = GrayscaleHist(value_range=(0,255)).use_array_as_hist(h) 105 | scores.append((h[5:].mean(), h[5:].std())) 106 | means = max([x[0] for x in scores]) 107 | stddevs = max([x[1] for x in scores]) 108 | return means/255.0, stddevs/255.0 109 | 110 | def measure_saturation(im, debug=False): 111 | _, sat, _ = image.split(image.rgb2hsv(im)) 112 | arr = image.cv2array(sat) 113 | return arr.mean(), arr.std() 114 | 115 | def measure_contrast(im, debug=False): 116 | h = GrayscaleHist(bins=64).use_image(image.rgb2gray(im)) 117 | return h.stddev() 118 | 119 | def boolean((focused, contrast, saturation, face_score)): 120 | # focus = maximum different between one of four quads interms of # of focus pixels 121 | #focus, (means, stddev), face_score = score 122 | #return focus < 0.1 or (means > 0.05 or stddev > 0.025) #or faces.boolean(face_score) 123 | focused_mean_diff, focused_std = focused 124 | sat_mean, sat_std = saturation 125 | if not faces.boolean(face_score): 126 | return False 127 | if contrast > 48: 128 | return False 129 | return focused_mean_diff - focused_std * 2 < 0.2 or sat_mean + sat_std <= 96 130 | 131 | requires_result_from = ['blur'] 132 | def measure(im, blur, noise=None, debug=False): 133 | focus_points = blur[0] 134 | #is_noisy = noise[2] 135 | 136 | size = cv.GetSize(im) 137 | npixels = size[0] * size[1] 138 | 139 | #if focused_regions is None: 140 | # focused_regions = image.new_from(im) 141 | # cv.Set(focused_regions, 0) 142 | # groups = form_groups(focus_points, 143 | # estimated_size=min(max(int(len(npixels) / 1000), 2), 15)) 144 | # #groups = form_groups(points, threshold=max(cv.GetSize(im))/16) 145 | # #print 'groups', len(groups) 146 | # draw_groups(groups, focused_regions) 147 | 148 | im2 = cv.CloneImage(im) 149 | g = Grid(cv.GetSize(im2)) 150 | if debug: 151 | image.show(g.draw(im2), "Image with Grid + ROI") 152 | 153 | roi = image.new_from(im, nChannels=1) 154 | cv.Set(roi, 0) 155 | #g.draw_lines(roi, thickness=int(max(min((size[0] + size[1]) * 1/100.0, 255), 1))) 156 | g.draw_regions(roi) 157 | area = cv.Sum(roi)[0] 158 | 159 | (_, face_rects), face_score = faces.measure(im) 160 | face_block = image.new_from(im, nChannels=1) 161 | cv.Set(face_block, 0) 162 | for r in face_rects: 163 | r.draw(face_block, color=cv.RGB(255,255,255), thickness=cv.CV_FILLED) 164 | 165 | if debug: 166 | face_roi = cv.CloneImage(im) 167 | cv.Set(face_roi, 0, image.invert(roi)) 168 | cv.Set(face_roi, 0, image.invert(image.threshold(face_block, threshold=1))) 169 | 170 | image.show(face_block, "Faces in Binary") 171 | image.show(g.draw(face_roi), "Face + ROI") 172 | 173 | return (im, ( 174 | measure_focused_roi(im, roi, area, focus_points, debug), 175 | #measure_color_roi(im, roi, area, focus_points, debug), 176 | measure_contrast(im, debug), 177 | measure_saturation(im, debug), 178 | faces.measure(im, debug)[1], 179 | )) -------------------------------------------------------------------------------- /features/contrast.py: -------------------------------------------------------------------------------- 1 | import image 2 | import cv 3 | from windows import GrayRGBWindow 4 | from grid import Grid 5 | 6 | from histogram import GrayscaleHist 7 | 8 | def average(seq): 9 | if len(seq) == 0: 10 | return 0 11 | s = sum(seq) 12 | return s / float(len(seq)) 13 | 14 | def boolean(score, threshold=32): # restricted to the life, universe, and everything 15 | (mean, stddev) = score 16 | return (mean < threshold) and stddev < 64 17 | #return mean - stddev/2 <= 0 or mean < 32 18 | 19 | def score_hist(hist): 20 | #a1, a2 = average(hist.count(slice(None, 32))), average(hist.count(slice(32, None))) 21 | #return min(a1, a2) / max(a1, a2) 22 | return hist.mean(), hist.stddev() 23 | 24 | requires_result_from = [] 25 | def measure(im, debug=False): 26 | gray = image.rgb2gray(im) 27 | _,s,v = image.split(image.rgb2hsv(im)) 28 | h = GrayscaleHist(bins=64).use_image(v) 29 | s = GrayscaleHist(bins=64).use_image(s) 30 | scores = [score_hist(h)] 31 | 32 | if debug: 33 | image.show(v, "1. Value") 34 | image.show(h.to_img(), "2. Value Histogram") 35 | image.show(s.to_img(), "2. Saturation Histogram") 36 | print score_hist(s) 37 | 38 | return ( 39 | None, #h.to_img(), 40 | scores[0], 41 | ) -------------------------------------------------------------------------------- /features/faces.py: -------------------------------------------------------------------------------- 1 | import image 2 | import cv 3 | import os 4 | 5 | DIR = os.path.join(*('data/haarcascades'.split('/'))) 6 | face_cascades = [os.path.join(DIR,x) for x in ( 7 | 'haarcascade_frontalface_alt.xml', 8 | 'haarcascade_frontalface_alt2.xml', 9 | 'haarcascade_frontalface_alt_tree.xml', 10 | 'haarcascade_frontalface_default.xml', 11 | 'haarcascade_profileface.xml', 12 | )] 13 | 14 | eye_cascades = [os.path.join(DIR,x) for x in ( 15 | 'haarcascade_eye.xml', 16 | 'haarcascade_eye_tree_eyeglasses.xml', 17 | 'haarcascade_mcs_eyepair_big.xml', 18 | 'haarcascade_mcs_eyepair_small.xml', 19 | 'haarcascade_mcs_lefteye.xml', 20 | 'haarcascade_lefteye_2splits.xml', 21 | 'haarcascade_mcs_righteye.xml', 22 | 'haarcascade_righteye_2splits.xml', 23 | )] 24 | 25 | mouth_cascades = [os.path.join(DIR,x) for x in ( 26 | 'haarcascade_mcs_mouth.xml', 27 | )] 28 | 29 | body_cascades = [os.path.join(DIR,x) for x in ( 30 | 'haarcascade_fullbody.xml', 31 | 'haarcascade_upperbody.xml', 32 | 'haarcascade_lowerbody.xml', 33 | 'haarcascade_mcs_upperbody.xml', 34 | )] 35 | 36 | def detect_skin(im, debug=False): 37 | hsv = image.rgb2hsv(im) 38 | 39 | if debug: 40 | image.show(hsv, 'hsv') 41 | h,s,v = image.split(hsv) 42 | 43 | if cv.CountNonZero(h) == cv.CountNonZero(s) == 0: 44 | white = image.new_from(im) 45 | cv.Set(white, 255) 46 | return white 47 | 48 | if debug: 49 | image.show(h, "Hue") 50 | image.show(s,"sat1") 51 | 52 | h_rng = 0, 46 53 | s_rng = 48, 178 54 | 55 | h = image.threshold(image.gaussian(h, 5), threshold=h_rng[1], type=cv.CV_THRESH_TOZERO_INV) 56 | h = image.threshold(h, threshold=h_rng[0], type=cv.CV_THRESH_TOZERO) 57 | h = image.threshold(h, threshold=1) 58 | 59 | s = image.threshold(image.gaussian(s, 5), threshold=s_rng[1], type=cv.CV_THRESH_TOZERO_INV) 60 | s = image.threshold(s, threshold=s_rng[0], type=cv.CV_THRESH_TOZERO) 61 | if debug: 62 | image.show(s,"sat2") 63 | s = image.threshold(s, threshold=1) 64 | 65 | v = image.dilate(image.erode(image.And(s, h))) 66 | 67 | 68 | #im = image.hsv2rgb(image.merge(h,s,v)) 69 | if debug: 70 | image.show(v, "Human") 71 | return image.threshold(v, threshold=1) 72 | 73 | im_scale = 0.5 74 | 75 | def detect(im, cascade, haar_scale=1.2, min_neighbors=2, min_size=(20,20)): 76 | haar_flags = 0 77 | 78 | gray = image.rgb2gray(im) 79 | small = image.resize(gray, by_percent=im_scale) 80 | cv.EqualizeHist(small, small) 81 | objs = cv.HaarDetectObjects(small, cascade, cv.CreateMemStorage(0), 82 | haar_scale, min_neighbors, cv.CV_HAAR_DO_CANNY_PRUNING, min_size) 83 | return [Rect(r,n,cv.GetSize(im)) for r,n in objs] 84 | 85 | def draw_objects(im, objs, color=cv.RGB(255,0,0), thickness=1): 86 | for ((x, y, w, h), n) in objs: 87 | #print n 88 | if n < 4: continue 89 | pt1 = tuple(map(int, (x / im_scale, y / im_scale))) 90 | pt2 = tuple(map(int,((x+w) / im_scale, (y+h) / im_scale))) 91 | cv.Rectangle(im, pt1, pt2, color, thickness=thickness) 92 | return im 93 | 94 | class Rect(object): 95 | def __init__(self, (x, y, w, h), n=None, im_size=None): 96 | self.x, self.y, self.w, self.h = x, y, w, h 97 | self.n = n 98 | self.im_size = im_size 99 | 100 | def __contains__(self, rect): 101 | x1, x2 = self.x, self.x+self.w 102 | y1, y2 = self.y, self.y+self.h 103 | return x1 <= rect.x <= x2 and y1 <= rect.y <= y2 and \ 104 | x1 <= rect.x + rect.w <= x2 and y1 <= rect.y + rect.h <= y2 105 | 106 | def draw(self, im, color=(255,255,255), thickness=4): 107 | im_size = cv.GetSize(im) 108 | im2_size = self.im_size 109 | size_ratio = ( 110 | (im_size[0] / float(im2_size[0])) / im_scale, 111 | (im_size[1] / float(im2_size[1])) / im_scale, 112 | ) 113 | 114 | pt1 = int(self.x * size_ratio[0]), int(self.y * size_ratio[1]) 115 | pt2 = int((self.x+self.w) * size_ratio[0]), int((self.y+self.h) * size_ratio[1]) 116 | cv.Rectangle(im, pt1, pt2, color, thickness=thickness) 117 | return im 118 | 119 | def intersects_with(self, rect): 120 | return self.x <= rect.x <= self.x+self.w or self.y <= rect.y <= self.y+self.h or \ 121 | rect.x <= self.x <= rect.x+rect.w or rect.y <= self.y <= rect.y+rect.h 122 | 123 | def area(self): 124 | return self.w * self.h 125 | 126 | def __cmp__(self, rect): 127 | return cmp(self.area(), rect.area()) 128 | 129 | def __eq__(self, rect): 130 | return (self.x, self.y, self.w, self.h, self.n) == \ 131 | (rect.x, rect.y, rect.w, rect.h, rect.n) 132 | 133 | def __hash__(self): 134 | return hash((self.x, self.y, self.w, self.h)) 135 | 136 | def __repr__(self): 137 | return "Rect((%d, %d, %d, %d), %d)" % ( 138 | self.x, self.y, self.w, self.h, self.n, 139 | ) 140 | 141 | def intersected_area(self, rect): 142 | if not self.intersects_with(rect): 143 | return 0 144 | width = min(self.x + self.w, rect.x + rect.w) - max(self.x, rect.x) 145 | height = min(self.y + self.h, rect.y + rect.h) - max(self.y, rect.y) 146 | return width * height 147 | 148 | def to_tuple(self): 149 | return ((self.x, self.y, self.w, self.h), self.n) 150 | 151 | def __iter__(self): 152 | return iter(((self.x, self.y, self.w, self.h), self.n)) 153 | 154 | def filter_overlap(rects, overlap_ratio=0.75): 155 | "Filters overlapping objects as one. (Picks a random overlapped object as it)." 156 | to_remove = [] 157 | for i,r1 in enumerate(rects): 158 | for j,r2 in enumerate(rects): 159 | if i <= j: continue 160 | a = min(r1.area(), r2.area()) 161 | if r1.intersected_area(r2) > a * overlap_ratio: 162 | to_remove.append(max(r1, r2)) 163 | to_remove = set(to_remove) 164 | filtered_rects = [r for r in rects if r not in to_remove] 165 | counts = {} 166 | for r in filtered_rects: 167 | for r2 in rects: 168 | if r == r2: continue 169 | a = min(r1.area(), r2.area()) 170 | if r1.intersected_area(r2) > a * overlap_ratio: 171 | counts[r] = counts.get(r, 0) + 1 172 | return counts 173 | 174 | def boolean(faces): 175 | return faces < 1 176 | 177 | #cascade = cv.Load(face_cascades[-2]) 178 | 179 | requires_result_from = [] 180 | def measure(im, debug=False): 181 | im2 = image.max_size(im, (800, 600)) 182 | 183 | b,g,r = image.split(im2) 184 | #cv.EqualizeHist(r,r) 185 | ##cv.EqualizeHist(g,g) 186 | ##cv.EqualizeHist(b,b) 187 | im2 = image.merge(b,g,r) 188 | 189 | eyes = 0 190 | #objs = [] 191 | #for cascade in eye_cascades: 192 | # print cascade 193 | # cascade = cv.Load(cascade) 194 | # objs = filter_overlap(detect(im, cascade)) 195 | # draw_objects(im, objs, color=cv.RGB(0,255,0)) 196 | # eyes += len(objs) 197 | #faces = 0 198 | if debug: 199 | im3 = cv.CloneImage(im2) 200 | faces = [] 201 | for cascade in face_cascades:#(face_cascades[0],face_cascades[-1]): 202 | cascade = cv.Load(cascade) 203 | detected_faces = detect(im2, cascade) 204 | faces += detected_faces 205 | if debug: 206 | for i,rect in enumerate(faces): 207 | rect.draw(im3, color=cv.RGB(255,16*i,16*i)) 208 | if debug: 209 | image.show(im3, "Faces + Repeats") 210 | faces = filter_overlap(faces) 211 | #print (objs[1], objs[6]) 212 | #draw_objects(im2, map(tuple, faces.keys())) 213 | for rect, count in faces.iteritems(): 214 | rect.draw(im2, color=cv.RGB(255,0,0)) 215 | #print (objs[3],objs[13]) 216 | #draw_objects(im, filter_overlap((objs[3],objs[13]))) 217 | 218 | #objs = [] 219 | #for cascade in body_cascades: 220 | # print cascade 221 | # cascade = cv.Load(cascade) 222 | # objs += detect(im, cascade) 223 | #draw_objects(im, filter_overlap(objs), color=cv.RGB(0,0,255)) 224 | 225 | #objs = [] 226 | #for cascade in mouth_cascades: 227 | # print cascade 228 | # cascade = cv.Load(cascade) 229 | # objs += detect(im, cascade) 230 | #draw_objects(im, filter_overlap(objs), color=cv.RGB(255,0,255)) 231 | 232 | score = 0 233 | for face_rect, count in faces.iteritems(): 234 | score += count * 0.25 + 0.15 235 | print faces 236 | 237 | if debug: 238 | image.show(im2, "Faces") 239 | return (im2, faces), score -------------------------------------------------------------------------------- /features/noise.py: -------------------------------------------------------------------------------- 1 | import image 2 | import cv 3 | from numpy import array 4 | from grid import Grid 5 | 6 | class Noise(object): 7 | def __init__(self, mean, stddev): 8 | self.mean = mean 9 | self.stddev = stddev 10 | 11 | def __call__(self, im): 12 | size = cv.GetSize(im) 13 | #print size 14 | pixel = im[int(size[0]/2), int(size[1]/2)] 15 | mean, stddev = [x[0] for x in cv.AvgSdv(im)] 16 | #if (value < self.mean-self.stddev or value >= self.mean+self.stddev): 17 | #if stddev > self.stddev: 18 | if stddev > self.stddev: 19 | return 255 20 | return 0 21 | 22 | def count_neighbors(im): 23 | size = cv.GetSize(im) 24 | mx, my = int(size[0]/2), int(size[1]/2) 25 | pixel = im[mx, my] 26 | if pixel == 0: 27 | return 0 28 | c = cv.CountNonZero(im) 29 | return c 30 | 31 | def noise_per_grid(images): 32 | size = cv.GetSize(images[0]) 33 | total = float(size[0] * size[1]) 34 | scores = [] 35 | for im in images: 36 | scores.append(cv.CountNonZero(im) / total) 37 | return scores 38 | 39 | def boolean(score, threshold=0.13, diff_threshold=0.05): 40 | diff = score[1]-score[0] 41 | diff_mean, diff_std = score[2], score[3] 42 | uv_score, avg_std = score[4] 43 | 44 | #return uv_score > 0.90 or uv_score < 0.1 45 | 46 | #return (diff_std > 25) 47 | if (score[0] > threshold and score[1] > threshold and \ 48 | (abs(diff) > diff_threshold or diff < 0)): 49 | return uv_score > 0.9 and avg_std > 60 50 | return False 51 | 52 | requires_result_from = [] 53 | """ 54 | Too "clever", too slow 55 | 56 | def measure(im, debug=False): 57 | #im = image.random_cropped_region(im, (640, 480)) 58 | #l = image.laplace(im) 59 | l = image.sobel(im, xorder=2, yorder=2) 60 | #l = image.dilate(l) 61 | size = cv.GetSize(l) 62 | mean, stddev = map(lambda x: x[0], cv.AvgSdv(l)) 63 | n = Noise(mean, stddev) 64 | l = image.neighbor_map(l, n, nbr_size=3) 65 | edges = image.threshold(image.auto_edges(im, percentage=0.1), threshold=1) 66 | cv.Set(l, 0, edges) 67 | 68 | l = image.neighbor_map(l, count_neighbors, nbr_size=3) 69 | score = cv.Sum(l)[0] / float((size[0]-2) * (size[1]-2) * 9) 70 | 71 | if debug: 72 | image.show(edges, "Edges") 73 | image.show(image.threshold(l, threshold=1), "Final") 74 | image.show(image.sub(im, image.gray2rgb(l)), "Final Sub") 75 | 76 | return image.threshold(l, threshold=1), score 77 | """ 78 | 79 | 80 | def measure(im, debug=False): 81 | gray = image.rgb2gray(im) 82 | size = cv.GetSize(im) 83 | total = float(size[0] * size[1]) 84 | l = image.sub(gray, image.gaussian(gray, 5)) 85 | l2 = image.sub(gray, image.gaussian(gray, 9)) 86 | edges = image.dilate(image.auto_edges(im, percentage=0.2)) 87 | if debug: 88 | image.show(image.threshold(l, threshold=1), "Before Edge Removal (kernel=5)") 89 | image.show(image.threshold(l2, threshold=1), "Before Edge Removal (kernel=9)") 90 | cv.Set(l, 0, image.threshold(edges, threshold=1)) 91 | cv.Set(l2, 0, image.threshold(edges, threshold=1)) 92 | 93 | l = image.threshold(l, threshold=1) 94 | l2 = image.threshold(l2, threshold=1) 95 | 96 | 97 | 98 | if debug: 99 | image.show(image.threshold(edges, threshold=1), "Edges") 100 | image.show(l, "After Edge Removal (kernel=5)") 101 | image.show(l2, "After Edge Removal (kernel=9)") 102 | 103 | noise2 = image.new_from(gray) 104 | cv.EqualizeHist(gray, noise2) 105 | cv.AbsDiff(noise2, gray, noise2) 106 | cv.Set(noise2, 0, image.threshold(image.sobel(im, xorder=2, yorder=2), threshold=4)) 107 | diff = image.cv2array(noise2) 108 | if debug: 109 | image.show(noise2, "DIFF") 110 | print "M", diff.mean(), "S", diff.std() 111 | diff_stat = (diff.mean(), diff.std()) 112 | percent_noise = cv.CountNonZero(noise2) / total 113 | if debug: 114 | image.show(noise2, "NOISE2") 115 | 116 | 117 | 118 | # magical, I don't understand how this works 119 | _, sat, _ = image.split(image.rgb2hsv(im)) 120 | edges = image.auto_edges(im) 121 | l,u,v = tuple(map(image.equalize_hist, image.split(image.rgb2luv(im)))) 122 | u,v = tuple(map(image.gaussian, (u,v))) 123 | if debug: 124 | image.show(l, "1. L") 125 | image.show(u, "1. U") 126 | image.show(v, "1. V") 127 | la,ua,va,uva = tuple(map(image.cv2array, (l,u,v, image.And(l,u,v)))) 128 | test = image.new_from(gray) 129 | test2 = image.new_from(gray) 130 | cv.Xor(u,v,test) 131 | if debug: 132 | image.show(test, "2. U Xor V") 133 | cv.Set(test, 0, image.dilate(edges)) 134 | #cv.Set(test, 0, image.invert(image.threshold(sat, threshold=8))) 135 | uv_score = cv.CountNonZero(test) / total 136 | if debug: 137 | image.show(test, "3. U Xor V - dilate(Edges) - invert(threshold(Saturation))") 138 | 139 | g = Grid(size) 140 | images = map(image.cv2array, g.split_into(test, 6)) 141 | arr = image.cv2array(test) 142 | avg_mean, avg_std = arr.mean(), arr.std() 143 | 144 | 145 | #ms = [(a.mean(), a.std()) for a in images] 146 | #min_mean = min_std = 255 147 | #max_mean = max_std = 0 148 | #for m,s in ms: 149 | # min_mean = min(min_mean, m) 150 | # min_std = min(min_std, s) 151 | # max_mean = max(max_mean, m) 152 | # max_std = max(max_std, s) 153 | #if debug: 154 | # print min_mean, min_std 155 | # print avg_mean, avg_std 156 | # print max_mean, max_std 157 | # 158 | #score = uv_score, min_mean, avg_mean, avg_std, max_mean 159 | uv_score = uv_score, avg_std 160 | 161 | score = cv.CountNonZero(l) / total, cv.CountNonZero(l2) / total, \ 162 | diff_stat[0], diff_stat[1], uv_score 163 | 164 | return l, score 165 | 166 | 167 | def boolean((uv_score, avg_std)): 168 | return (uv_score > 0.9 or uv_score < 0.1) and avg_std >= 60 169 | 170 | 171 | def measure(im, debug=False): 172 | 173 | gray = image.rgb2gray(im) 174 | size = cv.GetSize(im) 175 | total = float(size[0] * size[1]) 176 | edges = image.auto_edges(im) 177 | 178 | _, sat, val = image.split(image.rgb2hsv(im)) 179 | edges = image.auto_edges(im) 180 | l,u,v = tuple(map(image.equalize_hist, image.split(image.rgb2luv(im)))) 181 | u,v = tuple(map(image.gaussian, (u,v))) 182 | if debug: 183 | image.show(l, "1. L") 184 | image.show(u, "1. U") 185 | image.show(v, "1. V") 186 | la,ua,va,uva = tuple(map(image.cv2array, (l,u,v, image.And(l,u,v)))) 187 | test = image.new_from(gray) 188 | test2 = image.new_from(gray) 189 | cv.Xor(u,v,test) 190 | #cv.AbsDiff(u,v, test2) 191 | if debug: 192 | #cv.Threshold(test, test, 32, 255, cv.CV_THRESH_BINARY) 193 | image.show(test, "2. U Xor V") 194 | #image.show(test2, "TEST 2") 195 | #test = image.dilate(test) 196 | cv.Set(test, 0, image.dilate(edges)) 197 | #cv.Set(test, 0, image.invert(image.threshold(sat, threshold=8))) 198 | uv_score = cv.CountNonZero(test) / total 199 | if debug: 200 | image.show(test, "3. U Xor V - dilate(Edges) - invert(threshold(Saturation))") 201 | 202 | arr = image.cv2array(test) 203 | avg_mean, avg_std = arr.mean(), arr.std() 204 | 205 | score = uv_score, avg_std 206 | 207 | return test, score 208 | 209 | ### NEW METHOD 210 | 211 | def boolean((mean, std, over)): 212 | # over <= 3 213 | n = 16 # grid size 214 | return over <= 0.05*(n*n) and mean > 70 215 | 216 | def measure(im, debug=False): 217 | gray = image.rgb2gray(im) 218 | size = cv.GetSize(im) 219 | total = float(size[0] * size[1]) 220 | edges = image.auto_edges(im) 221 | 222 | hue, sat, val = tuple(map(image.equalize_hist, image.split(image.rgb2hsv(im)) )) 223 | l,u,v = tuple(map(image.equalize_hist, image.split(image.rgb2luv(im)))) 224 | 225 | values = [] 226 | if debug: 227 | image.show(l, "L") 228 | image.show(val, "Value") 229 | sat = image.threshold(val,255-32)#image.And(val, sat) 230 | if debug: 231 | image.show(sat, "Thresh") 232 | #cv.And(val, l, val) 233 | cv.Sub(l, sat, l) 234 | cv.Set(l, 0, image.dilate(edges, iterations=3)) 235 | if debug: 236 | image.show(l, "L - Value") 237 | val = l 238 | g = Grid(cv.GetSize(val)) 239 | images = g.split_into(val, 16) 240 | arr = image.cv2array(val) 241 | avgmean, avgstd = arr.mean(), arr.std() 242 | for i in images: 243 | a = image.cv2array(i) 244 | mean, std = abs(a.mean() - avgmean), max(a.std(), 0) 245 | values.append((mean+std)) 246 | 247 | if debug: 248 | print values 249 | print "AVG", avgmean, avgstd 250 | image.show(val, "Result") 251 | 252 | return val, (avgmean, avgstd, len([v for v in values if v > avgstd*2])) -------------------------------------------------------------------------------- /grid.py: -------------------------------------------------------------------------------- 1 | import cv 2 | import image 3 | 4 | class Grid(object): 5 | def __init__(self, size): 6 | super(Grid, self).__init__() 7 | self.size = size 8 | 9 | def split_into(self, im, count): 10 | if type(count) not in (list, tuple): 11 | count = (count, count) 12 | box_w, box_h = self.size[0]/count[0], self.size[1]/count[1] 13 | images = [] 14 | for x in range(count[0]): 15 | for y in range(count[1]): 16 | box = (box_w * x, box_h * y, box_w-1, box_h-1) 17 | images.append(image.crop(im, box)) 18 | return images 19 | 20 | def draw_regions(self, im, color=cv.RGB(255,255,255), thickness=cv.CV_FILLED): 21 | t1, t2 = (self.size[0]/3, self.size[1]/3) 22 | b1, b2 = t1/3, t2/3 23 | 24 | cv.Rectangle(im, (t1-b1, t2-b2), (t1+b1, t2+b2), color, thickness) 25 | cv.Rectangle(im, (t1*2-b1, t2-b2), (t1*2+b1, t2+b2), color, thickness) 26 | cv.Rectangle(im, (t1-b1, t2*2-b2), (t1+b1, t2*2+b2), color, thickness) 27 | cv.Rectangle(im, (t1*2-b1, t2*2-b2), (t1*2+b1, t2*2+b2), color, thickness) 28 | return im 29 | 30 | def draw_lines(self, im, color=cv.RGB(255,255,255), thickness=5): 31 | t1, t2 = (self.size[0]/3, self.size[1]/3) 32 | b1, b2 = t1/3, t2/3 33 | cv.Line(im, (t1, 0), (t1, self.size[1]), color, thickness=thickness) 34 | cv.Line(im, (t1*2, 0), (t1*2, self.size[1]), color, thickness=thickness) 35 | cv.Line(im, (0, t2), (self.size[0], t2), color, thickness=thickness) 36 | cv.Line(im, (0, t2*2), (self.size[0], t2*2), color, thickness=thickness) 37 | 38 | return im 39 | 40 | def draw(self, im, color=cv.RGB(255,0,0), thickness=10): 41 | self.draw_lines(im, color, abs(thickness)) 42 | self.draw_regions(im, color, thickness) 43 | 44 | return im 45 | 46 | def split_in_four(self, im): 47 | half = self.size[0]/2, self.size[1]/2 48 | top_left = image.crop(im, (0, 0, half[0], half[1])) 49 | top_right = image.crop(im, (half[0], 0, half[0], half[1])) 50 | bottom_left = image.crop(im, (0, half[1], half[0], half[1])) 51 | bottom_right = image.crop(im, (half[0], half[1], half[0], half[1])) 52 | return top_left, top_right, bottom_left, bottom_right -------------------------------------------------------------------------------- /histogram.py: -------------------------------------------------------------------------------- 1 | import image 2 | import cv 3 | import numpy 4 | 5 | class GrayscaleHist(object): 6 | def __init__(self, bins=32, value_range=(0,255), scale=10): 7 | self.bins = bins 8 | self.value_range = value_range 9 | self.scale = scale 10 | 11 | self.kind = 'opencv' 12 | 13 | def use_image(self, im): 14 | self.kind = 'opencv' 15 | self.hist = cv.CreateHist([self.bins], cv.CV_HIST_ARRAY, [self.value_range], 1) 16 | cv.CalcHist([im], self.hist) 17 | self.min_value, self.max_value, _, _ = cv.GetMinMaxHistValue(self.hist) 18 | return self 19 | 20 | def use_array(self, arr): 21 | self.kind = 'numpy' 22 | if not isinstance(arr, numpy.ndarray): 23 | arr = numpy.array(list(arr)) 24 | self.hist, _ = numpy.histogram(arr, self.bins, self.value_range) 25 | self.min_value, self.max_value = min(self.hist), max(self.hist) 26 | return self 27 | 28 | def use_array_as_hist(self, arr): 29 | self.kind = 'numpy' 30 | if not isinstance(arr, numpy.ndarray): 31 | arr = numpy.array(list(arr)) 32 | self.hist = arr 33 | self.bin = len(self.hist) 34 | self.min_value, self.max_value = min(self.hist), max(self.hist) 35 | return self 36 | 37 | def mean(self): 38 | if self.kind == 'numpy': 39 | return self.hist.mean() 40 | return numpy.array(list(self)).mean() 41 | 42 | def stddev(self): 43 | if self.kind == 'numpy': 44 | return self.hist.std() 45 | return numpy.array(list(self)).std() 46 | 47 | def count(self, value): 48 | if isinstance(value, slice): 49 | return [self.count(i) for i in range(*value.indices(len(self)))] 50 | if self.kind == 'numpy': 51 | return self.hist[value] 52 | return cv.QueryHistValue_1D(self.hist, value) 53 | 54 | def intensity_at_bin(self, value): 55 | if isinstance(value, slice): 56 | return [self.intensity_at_bin(i) for i in range(*value.indices(len(self)))] 57 | 58 | if self.value_range: 59 | max_value = float(self.value_range[1]) 60 | else: 61 | max_value = float(self.max_value) 62 | 63 | return round(self.count(value) * max_value / float(max(self.max_value, 0.0001))) 64 | 65 | def index(self, intensity): 66 | l = list(self) 67 | return l.index(intensity) 68 | 69 | def __getitem__(self, bin_num): 70 | if isinstance(bin_num, slice): 71 | return [self[i] for i in range(*bin_num.indices(len(self)))] 72 | return self.intensity_at_bin(bin_num) 73 | 74 | def __len__(self): 75 | return self.bins 76 | 77 | def __iter__(self): 78 | for bin_num in range(self.bins): 79 | yield self.intensity_at_bin(bin_num) 80 | 81 | def to_array(self): 82 | return numpy.array(list(self)) 83 | 84 | def draw(self, im, color=cv.RGB(0,0,0), offset=(0,0)): 85 | height = cv.GetSize(im)[1] 86 | 87 | pad = 2 88 | 89 | if self.value_range: 90 | max_value = float(self.value_range[1]) 91 | else: 92 | max_value = float(self.max_value) 93 | 94 | for b in range(self.bins): 95 | value = self.intensity_at_bin(b) 96 | 97 | cv.Rectangle(im, 98 | (offset[0] + b*self.scale + pad, 99 | offset[1] + height-int(value/max_value * height)), 100 | (offset[0] + int((b+1)*self.scale - pad), 101 | offset[1] + height), 102 | color, cv.CV_FILLED) 103 | return im 104 | 105 | def to_img(self, height=200, color=cv.RGB(0,0,0), offset=(0,0), bg=(255,255,255)): 106 | im = cv.CreateImage((self.bins * self.scale, height), 8, 3) 107 | cv.Set(im, bg) 108 | return self.draw(im, color, offset=(0,0)) -------------------------------------------------------------------------------- /image.py: -------------------------------------------------------------------------------- 1 | import cv 2 | import os 3 | import math 4 | import random 5 | import numpy 6 | 7 | def rgba(r,g,b,a): 8 | v = cv.RGB(r,g,b) 9 | return v[:3] + (a,) 10 | 11 | def is_grayscale(im): 12 | if im.nChannels == 1: 13 | return True 14 | h,s,v = split(rgb2hsv(im)) 15 | if cv.CountNonZero(h) == 0 == cv.CountNonZero(s): 16 | return True 17 | return False 18 | 19 | def new_from(im, depth=None, nChannels=None, size=None): 20 | if depth is None: 21 | depth = im.depth 22 | if nChannels is None: 23 | nChannels = im.nChannels 24 | if size is None: 25 | size = cv.GetSize(im) 26 | 27 | return cv.CreateImage(size, depth, nChannels) 28 | 29 | def crop(im, rect): 30 | cropped = new_from(im, size=rect[2:]) 31 | cv.SetImageROI(im, tuple(rect)) 32 | cv.Copy(im, cropped) 33 | cv.ResetImageROI(im) 34 | return cropped 35 | 36 | def random_cropped_region(im, size): 37 | isize = cv.GetSize(im) 38 | s = (min(size[0], isize[0]), min(size[1], isize[1])) 39 | if s == isize: 40 | return im 41 | mx, my = isize[0] - s[0], isize[1] - s[1] 42 | x, y = random.randrange(mx), random.randrange(my) 43 | print x,y, isize, size 44 | print (x,y,x+size[0],y+size[1]) 45 | return crop(im, (x,y,size[0],size[1])) 46 | 47 | def op(cvFunc, im1, im2, *images): 48 | new_im = new_from(im1) 49 | cvFunc(im1, im2, new_im) 50 | for im in images: 51 | cvFunc(new_im, im, new_im) 52 | return new_im 53 | 54 | def add(im1, im2, *images): return op(cv.Add, im1, im2, *images) 55 | def sub(im1, im2, *images):return op(cv.Sub, im1, im2, *images) 56 | def multiply(im1, im2, *images): return op(cv.Mul, im1, im2, *images) 57 | def And(im1, im2, *images): return op(cv.And, im1, im2, *images) 58 | def Or(im1, im2, *images): return op(cv.Or, im1, im2, *images) 59 | def Xor(im1, im2, *images): return op(cv.Xor, im1, im2, *images) 60 | def absDiff(im1, im2, *images):return op(cv.AbsDiff, im1, im2, *images) 61 | 62 | def blend(im1, im2, alpha=0.5): 63 | new_im = new_from(im1) 64 | cv.AddWeighted(im1, alpha, im2, 1-alpha, 0.0, new_im) 65 | return new_im 66 | 67 | def gaussian(im, size=9): 68 | if type(size) in (int, float, long): 69 | size = (size, size) 70 | new_im = new_from(im) 71 | cv.Smooth(im, new_im, cv.CV_GAUSSIAN, size[0], size[1]) 72 | return new_im 73 | 74 | def erode(im, element=None, iterations=1): 75 | new_im = new_from(im) 76 | cv.Erode(im, new_im, element, iterations) 77 | return new_im 78 | 79 | def dilate(im, element=None, iterations=1): 80 | new_im = new_from(im) 81 | cv.Dilate(im, new_im, element, iterations) 82 | return new_im 83 | 84 | def resize(im, size=None, by_percent=None, method=cv.CV_INTER_LINEAR): 85 | assert size != None or by_percent != None 86 | if size is not None: 87 | im_size = cv.GetSize(im) 88 | size = list(size) 89 | if size[0] is None: 90 | size[0] = size[1] / float(im_size[1]) * im_size[0] 91 | if size[1] is None: 92 | size[1] = size[0] / float(im_size[0]) * im_size[1] 93 | 94 | if by_percent is not None: 95 | size = list(cv.GetSize(im)) 96 | if type(by_percent) in (list, tuple): 97 | size[0] *= by_percent[0] 98 | size[1] *= by_percent[1] 99 | else: 100 | size[0] *= by_percent 101 | size[1] *= by_percent 102 | size = (int(size[0]), int(size[1])) 103 | resized_im = new_from(im, size=size) 104 | cv.Resize(im, resized_im, method) 105 | return resized_im 106 | 107 | def max_size(im, size, method=cv.CV_INTER_LINEAR): 108 | im_size = cv.GetSize(im) 109 | if im_size[0] > size[0]: 110 | return resize(im, (size[0],None), method=method) 111 | if im_size[1] > size[1]: 112 | return resize(im, (None, size[1]), method=method) 113 | return im 114 | 115 | def load(path, grayscale=False, max_size=None): 116 | path = os.path.join('data', path) 117 | if not os.path.exists(path): 118 | raise TypeError, "File does not exist: "+repr(os.path.abspath(path)) 119 | 120 | if grayscale: 121 | return cv.LoadImage(path, cv.CV_LOAD_IMAGE_GRAYSCALE) 122 | return cv.LoadImage(path, cv.CV_LOAD_IMAGE_COLOR) 123 | 124 | def invert(im): 125 | inv = new_from(im) 126 | cv.SubRS(im, cv.ScalarAll(int('1' * im.depth, 2)), inv) 127 | return inv 128 | 129 | def threshold(im, threshold=128, type=cv.CV_THRESH_BINARY, max_value=255): 130 | if not is_grayscale(im): 131 | im = rgb2gray(im) 132 | new_im = new_from(im) 133 | cv.Threshold(im, new_im, threshold, max_value, type) 134 | return new_im 135 | 136 | def dft(im, flags,): 137 | new_im = new_from(im) 138 | 139 | def split(im): 140 | if im.nChannels == 3: 141 | imr = new_from(im, nChannels=1) 142 | img = new_from(im, nChannels=1) 143 | imb = new_from(im, nChannels=1) 144 | cv.Split(im, imr, img, imb, None) 145 | return (imr, img, imb) 146 | else: 147 | return (im,) 148 | 149 | def merge(im1, im2, im3, im4=None): 150 | assert 1 == im1.nChannels == im2.nChannels == im3.nChannels 151 | new_im = new_from(im1, nChannels=3) 152 | cv.Merge(im1, im2, im3, im4, new_im) 153 | return new_im 154 | 155 | def equalize_hist(im): 156 | new_im = new_from(im) 157 | cv.EqualizeHist(im, new_im) 158 | return new_im 159 | 160 | def rgb2hsv(im): 161 | hsv = new_from(im) 162 | cv.CvtColor(im, hsv, cv.CV_BGR2HSV) 163 | return hsv 164 | 165 | def hsv2rgb(im): 166 | rgb = new_from(im) 167 | cv.CvtColor(im, rgb, cv.CV_HSV2BGR) 168 | return rgb 169 | 170 | def rgb2luv(im): 171 | luv = new_from(im) 172 | cv.CvtColor(im, luv, cv.CV_BGR2Luv) 173 | return luv 174 | 175 | def luv2rgb(im): 176 | rgb = new_from(im) 177 | cv.CvtColor(im, rgb, cv.CV_Luv2BGR) 178 | return rgb 179 | 180 | def rgb2gray(im): 181 | if im.nChannels == 1: 182 | return im 183 | gray = new_from(im, cv.IPL_DEPTH_8U, nChannels=1) 184 | cv.CvtColor(im, gray, cv.CV_RGB2GRAY) 185 | return gray 186 | 187 | def gray2rgb(im): 188 | if im.nChannels==3: 189 | return im 190 | color = new_from(im, cv.IPL_DEPTH_8U, nChannels=3) 191 | cv.CvtColor(im, color, cv.CV_GRAY2RGB) 192 | return color 193 | 194 | def laplace(im): 195 | im = rgb2gray(im) 196 | new_im = new_from(im, depth=cv.IPL_DEPTH_16S) 197 | cv.Laplace(im, new_im) 198 | cv.ConvertScaleAbs(new_im, im) 199 | return im 200 | 201 | def edges(im, threshold1=50, threshold2=150, aperture_size=3): 202 | edges = new_from(im, cv.IPL_DEPTH_8U, 1) 203 | gray = rgb2gray(im) 204 | cv.Canny(gray, edges, threshold1, threshold2, aperture_size) 205 | return edges 206 | 207 | def auto_edges(im, starting_threshold=50, percentage=0.2): 208 | size = cv.GetSize(im) 209 | total = size[0] * size[1] * percentage 210 | e = edges(im, starting_threshold, starting_threshold*3) 211 | while cv.CountNonZero(e) > total: 212 | starting_threshold += 10 213 | e = edges(im, starting_threshold, starting_threshold*3) 214 | return e 215 | 216 | def corners(im, max_corners=100, quality=0.1, min_dist=5, block_size=3, use_harris=False, 217 | mask=None, k=0.04): 218 | eig = new_from(im, depth=cv.IPL_DEPTH_32F, nChannels=1) 219 | tmp = new_from(im, depth=cv.IPL_DEPTH_32F, nChannels=1) 220 | gray = rgb2gray(im) 221 | corners = cv.GoodFeaturesToTrack(gray, eig, tmp, max_corners, quality, min_dist, 222 | mask, block_size, use_harris, k) 223 | #cv.Scale(eig, eig, 100, 0.00) 224 | return corners 225 | 226 | size = [10, 20] 227 | 228 | def show(im, title="Image"): 229 | global size # I know, bad practice... 230 | cv.NamedWindow(title) 231 | im2 = max_size(im, (640, 480)) 232 | cv.ShowImage(title, im2) 233 | cv.MoveWindow(title, size[0], size[1]) 234 | size[0] += 20 235 | size[1] += 20 236 | 237 | def sobel(im, xorder=1, yorder=1): 238 | recommended_size = { 239 | 1: 3, 2: 3, 3: 5, 240 | 4: 5, 5:7, 6: 7, 7:9, 8:9 241 | } 242 | im = rgb2gray(im) 243 | new_im = cv.CreateImage(cv.GetSize(im), cv.IPL_DEPTH_16S, 1) 244 | cv.Sobel(im, new_im, xorder, yorder, 245 | apertureSize=recommended_size[max(xorder, yorder)]) 246 | cv.ConvertScaleAbs(new_im, im) 247 | return im 248 | 249 | def save(im, path): 250 | #root = os.path.join('output') 251 | root = os.path.dirname(path) 252 | try: 253 | os.makedirs(root) 254 | except OSError: 255 | pass 256 | path = os.path.join(root, path) 257 | cv.SaveImage(path, im) 258 | 259 | def set_pixel(im, x, y, rgb): 260 | if type(rgb) in (tuple, list) and len(rgb) == 3: 261 | rgb = (rgb[2], rgb[1], rgb[0]) 262 | cv.Set2D(im, y, x, rgb) 263 | 264 | def neighbor_map(im, func, nbr_size=3): 265 | oim = new_from(im) 266 | size = cv.GetSize(oim) 267 | nbr_size = int(nbr_size / 2) 268 | size = tuple((x - nbr_size for x in size)) 269 | for x in xrange(nbr_size, size[0]): 270 | for y in xrange(nbr_size, size[1]): 271 | value = func(im[y-nbr_size:y+nbr_size+1, x-nbr_size:x+nbr_size+1]) 272 | set_pixel(oim, x, y, value) 273 | return oim 274 | 275 | def draw_points(im, points, radius=2, color=cv.RGB(255,0,0), thickness=1): 276 | for pt in points: 277 | cv.Circle(im, tuple(map(int, pt)), radius, color, thickness) 278 | return im 279 | 280 | 281 | def cv2array(im): 282 | depth2dtype = { 283 | cv.IPL_DEPTH_8U: 'uint8', 284 | cv.IPL_DEPTH_8S: 'int8', 285 | cv.IPL_DEPTH_16U: 'uint16', 286 | cv.IPL_DEPTH_16S: 'int16', 287 | cv.IPL_DEPTH_32S: 'int32', 288 | cv.IPL_DEPTH_32F: 'float32', 289 | cv.IPL_DEPTH_64F: 'float64', 290 | } 291 | 292 | arrdtype=im.depth 293 | a = numpy.fromstring( 294 | im.tostring(), 295 | dtype=depth2dtype[im.depth], 296 | count=im.width*im.height*im.nChannels) 297 | a.shape = (im.height,im.width,im.nChannels) 298 | return a 299 | 300 | 301 | def array2cv(a): 302 | dtype2depth = { 303 | 'uint8': cv.IPL_DEPTH_8U, 304 | 'int8': cv.IPL_DEPTH_8S, 305 | 'uint16': cv.IPL_DEPTH_16U, 306 | 'int16': cv.IPL_DEPTH_16S, 307 | 'int32': cv.IPL_DEPTH_32S, 308 | 'float32': cv.IPL_DEPTH_32F, 309 | 'float64': cv.IPL_DEPTH_64F, 310 | } 311 | try: 312 | nChannels = a.shape[2] 313 | except: 314 | nChannels = 1 315 | cv_im = cv.CreateImageHeader((a.shape[1],a.shape[0]), 316 | dtype2depth[str(a.dtype)], 317 | nChannels) 318 | cv.SetData(cv_im, a.tostring(), 319 | a.dtype.itemsize*nChannels*a.shape[1]) 320 | return cv_im -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import image 3 | 4 | class FileLogger(object): 5 | def __init__(self, handle=None): 6 | if handle is None: 7 | handle = sys.stdout 8 | self.handle = handle 9 | 10 | def write(self, *message, **kwargs): 11 | self.handle.write(' '.join(map(str, message)) % kwargs) 12 | 13 | def writeln(self, *message, **kwargs): 14 | self.write(*message, **kwargs) 15 | self.handle.write("\n") 16 | 17 | def result(self, image_path, results, expected, compare=True): 18 | self.writeln(image_path + ":") 19 | for name,result in results.iteritems(): 20 | self.writeln("\t%(e)s%(name)s => %(score)r %(b)s (%(sec)s secs)", 21 | name=name, score=result[1], 22 | b="Yes" if result[2] else "No", 23 | e="* " if compare and name in expected else "", 24 | sec=result[3]) 25 | 26 | def close(self): 27 | self.handle.close() 28 | 29 | class HtmlLogger(FileLogger): 30 | def __init__(self, handle=None, resized_dir=None): 31 | super(HtmlLogger, self).__init__(handle) 32 | self.resized_dir = resized_dir 33 | self.write("""Log 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | """) 51 | 52 | def result(self, image_path, results, expected, compare=True): 53 | fullpath = path = os.path.abspath(os.path.join('data', image_path)) 54 | if self.resized_dir is not None: 55 | im = image.max_size(image.load(path), (320, 240)) 56 | path = os.path.join(self.resized_dir, os.path.basename(image_path)) 57 | print os.path.dirname(image_path) 58 | image.save(im, path) 59 | del im 60 | self.write('') 61 | self.write('', 62 | fullpath=fullpath, path=path, imgpath=image_path, len=len(results)) 63 | for i,pair in enumerate(results.iteritems()): 64 | name,result = pair 65 | if i != 0: 66 | self.write("") 67 | self.write('', 68 | name=name, score=result[1]) 69 | self.write('', 70 | cls="bad" if compare and result[2] != (name in expected) else "good", 71 | confirmed="Yes" if result[2] else "", 72 | expected="Yes" if name in expected else "") 73 | self.write('', 74 | cls="bad" if compare and result[3] > 10 else "", 75 | timing=result[3]) 76 | self.write("") 77 | 78 | def close(self): 79 | self.write("
ImageMetricScoreResult?Expected?Compute Time

%(imgpath)s
%(name)s%(score)r%(confirmed)s%(expected)s%(timing).2f second(s)
") 80 | super(HtmlLogger, self).close() 81 | 82 | class MultiLogger(object): 83 | def __init__(self, *loggers): 84 | self.loggers = loggers 85 | 86 | def result(self, image_path, results, expected, compare=True): 87 | for l in self.loggers: 88 | l.result(image_path, results, expected, compare) 89 | 90 | def close(self): 91 | for l in self.loggers: 92 | l.close() -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.6 2 | import sys 3 | import image 4 | import cv 5 | import os 6 | from windows import (EdgeThresholdTweaker, DerivativeTweaker, \ 7 | ColorHistograms, HistogramWindow) 8 | #from constants import edge_threshold 9 | import papers 10 | from features import noise, blur, contrast, composition, faces 11 | from training_data import TrainingData 12 | from logger import FileLogger, HtmlLogger, MultiLogger 13 | from optparse import OptionParser 14 | 15 | #import matplotlib.pyplot as plt 16 | measurements = ( 17 | contrast, 18 | noise, 19 | blur, 20 | composition, 21 | #faces, # => under composition 22 | ) 23 | max_size = (640, 480) # set to None to use original image sizes (can take awhile!) 24 | 25 | # good - bad_qualities = possible bad qualities that might be marked 26 | california_night = TrainingData('good/Another Beautiful California Night.jpg', 27 | measures=measurements, kind='good', 28 | ) 29 | bnw_horse = TrainingData('good/AP McCoy Black & White Horse Racing Photo.jpg', 30 | measures=measurements, kind='good', #bad_qualities=['grayscale'], 31 | ) 32 | dreamer = TrainingData('good/Beautiful Dreamer, Awake Unto Me.jpg', 33 | measures=measurements, kind='good',# bad_qualities=['composition'] 34 | ) 35 | tiger = TrainingData('good/Beautiful Face.jpg', 36 | measures=measurements, kind='good', 37 | ) 38 | bricks = TrainingData('good/Climbing the Bricks Factory.jpg', 39 | measures=measurements, kind='good', 40 | ) 41 | grass = TrainingData('good/It is time to make each moment beautiful.jpg', 42 | measures=measurements, kind='good',#bad_qualities=['blur'] 43 | ) 44 | rule_of_thirds = TrainingData('good/Rule of thirds.jpg', 45 | measures=measurements, kind='good',#bad_qualities=['blur'] 46 | ) 47 | china_family = TrainingData('good/china_family.jpg', 48 | measures=measurements, kind='good', 49 | ) 50 | fair = TrainingData('good/Long_exposure_at_the_fair.jpg', 51 | measures=measurements, kind='good',#bad_qualities=['blur'] 52 | ) 53 | cat = TrainingData('good/portrait of tracy II.jpg', 54 | measures=measurements, kind='good',#bad_qualities=['blur'] 55 | ) 56 | dof_ground = TrainingData('good/dof_ground.jpg', 57 | measures=measurements, kind='good',#bad_qualities=['blur'] 58 | ) 59 | cloth = TrainingData('good/cloth.jpg', 60 | measures=measurements, kind='good', 61 | ) 62 | 63 | # poor 64 | blurry = TrainingData('poor/Blurry_men_climbing_stairs.jpeg', measures=measurements, 65 | bad_qualities=['noise', 'blur'] 66 | ) 67 | ferris_wheel = TrainingData('poor/Ferris wheel at night.jpg', measures=measurements, 68 | bad_qualities=['noise', 'composition', 'blur'] # i'm on the fence on noise 69 | ) 70 | room = TrainingData('poor/mobys house.jpg', measures=measurements, 71 | bad_qualities=['noise', 'blur'] 72 | ) 73 | indiana = TrainingData('poor/Northwestern Indiana.jpg', measures=measurements, 74 | bad_qualities=['noise', 'contrast', 'composition', 'blur'] 75 | ) 76 | gates = TrainingData('poor/overexposed gates.jpg', measures=measurements, 77 | bad_qualities=['contrast'] 78 | ) 79 | seaside = TrainingData('poor/overexposed seaside.jpg', measures=measurements, 80 | bad_qualities=['contrast'] 81 | ) 82 | subway = TrainingData('poor/Overexposed U-bahn.jpg', measures=measurements, 83 | bad_qualities=['contrast', 'blur'] 84 | ) 85 | corner = TrainingData('poor/That Corner.jpg', measures=measurements, 86 | bad_qualities=['contrast'] 87 | ) 88 | transformer = TrainingData('poor/transformer blurry 1869.jpeg', measures=measurements, 89 | bad_qualities=['noise', 'blur', 'contrast'] 90 | ) 91 | street = TrainingData('poor/underexposed.jpg', measures=measurements, 92 | bad_qualities=['noise', 'contrast'] 93 | ) 94 | china_hotel = TrainingData('poor/china_hotel.jpg', measures=measurements, 95 | bad_qualities=['blur', 'noise'] 96 | ) 97 | china_blurry = TrainingData('poor/china_blurry.jpg', measures=measurements, 98 | bad_qualities=['blur', 'noise'] 99 | ) 100 | china_noise = TrainingData('poor/china_noise.jpg', measures=measurements, 101 | bad_qualities=['contrast', 'noise', 'blur'] 102 | ) 103 | 104 | #li = TrainingData('li/poor/Syracuse U. Visit 028.jpg', measures=measurements, 105 | # bad_qualities=['blur'] 106 | #) 107 | # 108 | #li2 = TrainingData('li/good/DSC00144.JPG', measures=measurements,) 109 | # 110 | #li3 = TrainingData('li/poor/ZXCVBNM 028.jpg', measures=measurements, 111 | # bad_qualities=['blur', 'noise'] 112 | #) 113 | # 114 | #li4 = TrainingData('li/poor/IMG_0795.JPG', measures=measurements, 115 | #) 116 | # 117 | #li5 = TrainingData('li/good/Syracuse U. Visit 005.jpg', measures=measurements, 118 | # bad_qualities=['contrast'] 119 | #) 120 | 121 | #chow = TrainingData('chow/good/DSC_0291.JPG', measures=measurements) 122 | 123 | def load_image(image_path): 124 | root = os.path.basename(image_path) 125 | return TrainingData(image_path, 126 | measures=measurements, kind="good" if root.find('good') != -1 else "bad", 127 | compare=False) 128 | 129 | def load_images_from(dirname): 130 | objs = [] 131 | for root,dirs,files in os.walk(os.path.join('data', dirname)): 132 | for f in files: 133 | name = f.lower() 134 | if not (name.endswith('.jpeg') or name.endswith('.jpg')): 135 | continue 136 | objs.append(load_image(os.path.join(root, f).replace('data/', ''))) 137 | return objs 138 | 139 | li_dir = load_images_from('li') 140 | chow_dir = load_images_from('chow') 141 | china_dir = load_images_from('/Users/jeff/Desktop/china-day6/full') 142 | 143 | 144 | def process(process_dir=None): 145 | if process_dir is None: 146 | process_dir = [v for v in globals().values() if isinstance(v, TrainingData)] 147 | 148 | logger = MultiLogger( 149 | FileLogger(), 150 | HtmlLogger(open('output.html', 'w+'), os.path.abspath('thumb')) 151 | ) 152 | max_size = None#(800, 600) 153 | for obj in process_dir: 154 | if obj.kind == 'good': 155 | obj.process(max_size=max_size, logger=logger) 156 | for obj in process_dir: 157 | if obj.kind == 'bad': 158 | obj.process(max_size=max_size, logger=logger) 159 | 160 | 161 | from windows import CornerTweaker 162 | def main(progname, *args): 163 | 164 | parser = OptionParser() 165 | parser.add_option("-f", "--file", dest="filename", default=None, 166 | help="analyze a given FILE ending in .jpg or .jpeg", metavar="FILE") 167 | parser.add_option("-i", "--imageset", dest="imageset", default=None, 168 | help="Runs on a predefined set of algorithms (li,chow,china,custom)") 169 | parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False, 170 | help="Enable visual debugging.") 171 | parser.add_option("-t", "--type", dest="type", default="all", 172 | help="Specifies the type of feature to debug. Defaults to all.") 173 | 174 | (options, args) = parser.parse_args(list(args)) 175 | 176 | if options.imageset: 177 | if options.imageset == 'li': 178 | process(li_dir) 179 | return 0 180 | elif options.imageset == 'chow': 181 | process(chow_dir) 182 | return 0 183 | elif options.imageset == 'china': 184 | process(china_dir) 185 | return 0 186 | elif options.imageset == 'custom': 187 | process() 188 | return 0 189 | 190 | if not options.filename: 191 | print "Please specify a file (--file) or image set (--imageset)." 192 | return 1 193 | 194 | if not options.debug: 195 | process([load_image(options.filename)]) 196 | return 0 197 | 198 | if options.filename.startswith('data/'): 199 | options.filename = options.filename[len('data/'):] 200 | 201 | tdata = load_image(options.filename) 202 | kind = options.type.lower() 203 | 204 | size = None #(320,240,'crop') # (0.5, 0.5, 'resize-p') 205 | if size is None: 206 | im = tdata.load() 207 | elif size[-1] == 'crop': 208 | im = image.random_cropped_region(tdata.load(), size[:2]) 209 | elif size[-1] == 'resize': 210 | im = tdata.load(size[:2]) 211 | elif size[-1] == 'resize-p': 212 | im = image.resize(tdata.load(), by_percent=size[:2]) 213 | else: 214 | raise TypeError, "Invalid image sizing type." 215 | 216 | image.show(im, "Image") 217 | #l,u,v = image.split(image.rgb2luv(im)) 218 | ##cv.Set(l, 128) 219 | ##cv.EqualizeHist(l, l) 220 | ##cv.EqualizeHist(u, u) 221 | ##image.show(image.luv2rgb(image.merge(l,u,v)), "test") 222 | #s = cv.GetSize(im) 223 | #t = image.absDiff(u,v) 224 | #image.show(t, "test") 225 | #print "Test Score:", cv.CountNonZero(t) / float(s[0] * s[1]) 226 | ##image.show(image.threshold(image.And(u,v), threshold=1), "LUV") 227 | 228 | # noise 229 | if kind in ('all','noise'): 230 | noise_img, score = noise.measure(im, debug=True) 231 | #image.show(noise_img, "Noise Result") 232 | print 'Noise Score:', score, noise.boolean(score) 233 | 234 | # contrast 235 | if kind in ('all','contrast'): 236 | contrast_img, score = contrast.measure(im, debug=True) 237 | #image.show(contrast_img, "Contrast Result") 238 | print 'Contrast Score:', score, contrast.boolean(score) 239 | 240 | # blur 241 | if kind in ('all','blur','composition'): 242 | focused, score = blur.measure(im, debug=kind in ('all','blur')) 243 | #image.show(focused, "Blur Result") 244 | print 'Blur Score:', score, blur.boolean(score) 245 | 246 | # composition 247 | if kind in ('all','composition'): 248 | composition_img, score = composition.measure(im, 249 | (focused,score, blur.boolean(score)), debug=True) 250 | print 'Composition Score:', score, composition.boolean(score) 251 | 252 | if kind in ('faces',): 253 | result, score = faces.measure(im,debug=True) 254 | print "Face Score:", score, faces.boolean(faces) 255 | 256 | #win = CornerTweaker(im) 257 | #win.show() 258 | 259 | #_, sat, _ = image.split(image.rgb2hsv(im)) 260 | #arr = image.cv2array(sat) 261 | #print arr.mean(), arr.std() 262 | 263 | # faces 264 | #im, score = faces.measure(im, debug=True) 265 | #print score, faces.boolean(score) 266 | 267 | # composition 268 | #noise_img, score = noise.measure(im, debug=False) 269 | ##n = (noise_img, score, noise.boolean(score)) 270 | #hulls, score = blur.measure(im, debug=False) 271 | #b = (hulls, score, blur.boolean(score)) 272 | #cimg, score = composition.measure(im, b, debug=True) 273 | #print score, composition.boolean(score) 274 | 275 | # BLUR 276 | #from time import time 277 | #start = time() 278 | ##im2 = image.threshold(image.laplace(im), threshold=75, type=cv.CV_THRESH_TOZERO) 279 | #hulls, score = blur.measure(im, debug=True) 280 | ##blur_img, score = blur.measure(im, debug=True) 281 | #end = time() 282 | #print "Time:", (end - start), "seconds" 283 | #image.show(im, "image") 284 | ##image.show(noise_img, "Noise Image") 285 | #print score, blur.boolean(score) 286 | 287 | 288 | #CONTRAST 289 | 290 | #_, score = contrast.measure(im, debug=True) 291 | #image.show(im, "Image") 292 | #print score, contrast.boolean(score) 293 | 294 | """ 295 | 296 | #BLUR 297 | 298 | #im2 = image.threshold(image.laplace(im), threshold=75, type=cv.CV_THRESH_TOZERO) 299 | im3, score = blur.measure(im, debug=True) 300 | image.show(im, "image") 301 | image.show(im3, "Focus Mask") 302 | print score, blur.boolean(score) 303 | #plt.show() 304 | """ 305 | 306 | 307 | #NOISE 308 | 309 | #noise_img, score = noise.measure(im, debug=True) 310 | #image.show(noise_img, "Noise") 311 | #print score, noise.boolean(score) 312 | 313 | 314 | """ 315 | #hwin = ColorHistograms(im) 316 | #hwin.show() 317 | hwin = HistogramWindow(image.rgb2gray(im)) 318 | hwin.show() 319 | 320 | print cv.GetSize(im), cv.GetSize(im2) 321 | print 'blur', papers.blurry_histogram(im) 322 | #print papers.blurry_histogram(im2) 323 | 324 | wind = DerivativeTweaker(im, title="image derivative") 325 | wind.show() 326 | 327 | win = EdgeThresholdTweaker(im, title="image edges") 328 | win.show(50)#edge_threshold(im)) 329 | 330 | #win2 = EdgeThresholdTweaker(im2, title="image resized edges") 331 | #win2.show(edge_threshold(im2)) 332 | """ 333 | cv.WaitKey() 334 | cv.DestroyAllWindows() 335 | return 0 336 | 337 | if __name__ == '__main__': 338 | sys.exit(main(*sys.argv)) -------------------------------------------------------------------------------- /papers.py: -------------------------------------------------------------------------------- 1 | import cv 2 | import image 3 | import random 4 | 5 | def blurry_histogram(im, num_samples=300, offset=1): 6 | im = image.rgb2gray(im) 7 | size = cv.GetSize(im) 8 | used = set([]) 9 | i = 0 10 | diffs = {} 11 | while i < num_samples: 12 | # we can't use the first row of pixels 13 | x = random.randrange(0, size[0]) 14 | y = random.randrange(offset, size[1]) 15 | if (x,y) not in used: 16 | pixel1 = cv.Get2D(im, y, x) 17 | pixel2 = cv.Get2D(im, y-offset, x) 18 | diff = tuple(map(lambda a,b: a-b, pixel1, pixel2)) 19 | if diff not in diffs: 20 | diffs[diff] = 0 21 | diffs[diff] += 1 22 | used = used.union([(x,y)]) 23 | i += 1 24 | max_i = max_v = 0 25 | second_max_i = second_max_v = 0 26 | for key,val in diffs.iteritems(): 27 | if max_v < val: 28 | second_max_i, second_max_v = max_i, max_v 29 | max_v, max_i = val, key 30 | return (max_v - second_max_v) / abs(max_i[0] - second_max_i[0]) -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import image 2 | import cv 3 | 4 | l = cv.CreateImage((256,256), cv.IPL_DEPTH_8U, 1) 5 | cv.Set(l, 255) 6 | u = image.new_from(l) 7 | v = image.new_from(l) 8 | cv.Set(u, 0) 9 | cv.Set(v, 0) 10 | 11 | size = cv.GetSize(l) 12 | print size 13 | 14 | for x in range(256): 15 | for y in range(size[1]): 16 | cv.Set2D(u, y, x, x) 17 | cv.Set2D(v, 255-x, min(y, 255), x) 18 | 19 | image.show(u, "U") 20 | image.show(v, "V") 21 | 22 | rgb = image.luv2rgb(image.merge(l,u,v)) 23 | r,g,b = image.split(rgb) 24 | #xor = image.threshold(image.Xor(u,v), 0, cv.CV_THRESH_BINARY) 25 | xor = image.Xor(u,v) 26 | cv.Threshold(xor, xor, 16, 255, cv.CV_THRESH_TOZERO) 27 | image.show(rgb, "RGB") 28 | image.show(xor, "Xor") 29 | 30 | #cv.Sub(rgb, image.gray2rgb(image.invert(xor)), rgb) 31 | _, sat, _ = image.split(image.rgb2hsv(rgb)) 32 | image.show(sat, 'Saturation') 33 | #cv.Set(xor, 0, image.invert(image.threshold(sat, threshold=4))) 34 | 35 | cv.Sub(rgb, image.invert(image.gray2rgb(xor)), rgb) 36 | 37 | image.show(rgb, "Rgb - Xor") 38 | arr = image.cv2array(xor) 39 | avg_mean, avg_std = arr.mean(), arr.std() 40 | print cv.CountNonZero(xor) / float(size[0] * size[1]), avg_mean, avg_std 41 | 42 | cv.WaitKey() 43 | cv.DestroyAllWindows() -------------------------------------------------------------------------------- /training_data.py: -------------------------------------------------------------------------------- 1 | import image 2 | from time import time 3 | 4 | from logger import FileLogger 5 | 6 | class TrainingData(object): 7 | """Represents a training data image, for batch processing.""" 8 | def __init__(self, imgpath, measures, bad_qualities=None, kind='bad', compare=True): 9 | self.imgpath, self.measures = imgpath, measures 10 | self.compare = compare 11 | self.kind = kind 12 | if bad_qualities is None: 13 | self.bad_qualities = set() 14 | else: 15 | self.bad_qualities = set(bad_qualities) 16 | 17 | def load(self, max_size=None): 18 | try: 19 | im = image.load(self.imgpath) 20 | except IOError: 21 | return None 22 | if max_size is not None: 23 | im = image.max_size(im, max_size) 24 | return im 25 | 26 | def _get_name(self, mod): 27 | return mod.__name__.split('.')[-1] 28 | 29 | def execute(self, im, mod, kwargs): 30 | name = self._get_name(mod) 31 | start = time() 32 | mimg, score = mod.measure(im, **kwargs) 33 | end = time() 34 | return mimg, score, mod.boolean(score), end - start 35 | 36 | def process(self, max_size=None, logger=None): 37 | if logger is None: 38 | logger = FileLogger() 39 | im = self.load(max_size) 40 | if im is None: 41 | logger.result(self.imgpath, {'Error': "Failed to load"}, self.bad_qualities, self.compare) 42 | return None 43 | remaining = self.measures[:] 44 | results = {} 45 | # rudimentary runner that only executes ones that have the given requirements 46 | while len(remaining) > 0: 47 | still_remaining = [] 48 | for mod in remaining: 49 | # verify prereqs 50 | abort = False 51 | for prereq in mod.requires_result_from: 52 | if prereq not in results: 53 | still_remaining.append(mod) 54 | abort = True 55 | break 56 | if abort: 57 | print "Failed to meet req for:", self._get_name(mod) 58 | continue 59 | 60 | kwargs = {} 61 | for name in mod.requires_result_from: 62 | kwargs[name] = results[name] 63 | results[self._get_name(mod)] = self.execute(im, mod, kwargs) 64 | 65 | msg = 'Unresolve dependencies. I have %s to satisfy %s.' % ( 66 | ','.join(results.keys()), 67 | ','.join(map( 68 | lambda x: self._get_name(x) + "("+','.join(x.requires_result_from)+")", 69 | remaining)) 70 | ) 71 | 72 | assert len(remaining) != len(still_remaining), msg 73 | remaining = still_remaining 74 | 75 | logger.result(self.imgpath, results, self.bad_qualities, self.compare) 76 | 77 | -------------------------------------------------------------------------------- /windows.py: -------------------------------------------------------------------------------- 1 | import cv 2 | import image 3 | 4 | class ArgTweakerWindow(object): 5 | """Handles the display of images and tweaking its values. 6 | - im: the original image to process 7 | - render: the function to process im and render_kwargs. Should return a 8 | new image. 9 | - render_kwargs: (optional) a dict of function kwargs to pass to 10 | the render function. 11 | - title: (optional) the title of the window. One will be uniquely created 12 | if not specified. 13 | """ 14 | def __init__(self, im, render, render_kwargs={}, title=None): 15 | self.title, self.im, self._render = title, im, render 16 | self.render_args = render_kwargs 17 | self._tb = [] 18 | self.reset_args() 19 | 20 | def rename_arg(self, name): 21 | """Renames an arg that's more like python function parameter names. 22 | 'Threshold Value' => 'threshold_value' 23 | """ 24 | return name.lower().replace(' ', '_') 25 | 26 | def num_arg_steps(self, name): 27 | """Returns number of steps for a trackbar for a particular argument.""" 28 | min, max, step = self.render_args[name][:3] 29 | return int((max - min) / step) 30 | 31 | def mid_arg_value(self, name): 32 | """Returns the median value of the ranges of values the trackbar 33 | can take. 34 | """ 35 | return self.arg_value(name, int(self.num_arg_steps(name) / 2)) 36 | 37 | def arg_value(self, name, trackbar_value): 38 | """Returns the value at a given trackbar location.""" 39 | min, max, step = self.render_args[name][:3] 40 | return min + step * trackbar_value 41 | 42 | def reset_args(self): 43 | """Resets all the known values set by the trackbars to their default 44 | values. Does not update the trackbar. 45 | """ 46 | self.args = {} 47 | for name,tupl in self.render_args.iteritems(): 48 | if len(tupl) > 3: 49 | print tupl 50 | self.args[self.rename_arg(name)] = tupl[-1] 51 | else: 52 | self.args[self.rename_arg(name)] = self.mid_arg_value(name) 53 | 54 | def show(self): 55 | """Creates a new window and displays it, along with trackbars to 56 | tweak any given arguments. 57 | """ 58 | if self.title is None: 59 | self.title = "Image - " + str(id(self)) 60 | cv.NamedWindow(self.title, flags=0) 61 | self.reset_args() 62 | for name,arg_range in self.render_args.iteritems(): 63 | def execute(name, total): 64 | min = self.render_args[name][0] 65 | steps = self.render_args[name][2] 66 | self._tb.append(cv.CreateTrackbar( 67 | name, self.title, 68 | (self.args[name] - (min - 1)) / steps - 1, 69 | total, 70 | lambda x: self.update_arg(name, x))) 71 | execute(name, self.num_arg_steps(name)) 72 | self.update() 73 | 74 | def render(self): 75 | """Shorthand to calling the function passed into the constructor. 76 | Returns the new image to display 77 | """ 78 | return self._render(self.im, **self.args) 79 | 80 | def update(self): 81 | """Updates the window with the rendered image.""" 82 | im = self.render() 83 | size = cv.GetSize(im) 84 | cv.ShowImage(self.title, im) 85 | cv.ResizeWindow(self.title, size[0], size[1] + len(self.args) * 35) 86 | 87 | def update_arg(self, name, x): 88 | """This is invoked by each trackbar. 89 | """ 90 | new_value = self.arg_value(name, x) 91 | if new_value != self.args[self.rename_arg(name)]: 92 | print 'set', repr(name), new_value 93 | self.args[self.rename_arg(name)] = new_value 94 | self.update() 95 | return self.args 96 | 97 | def destroy(self): 98 | """Destroys the window associated with this instance.""" 99 | cv.DestroyWindow(self.title) 100 | 101 | class EdgeThresholdTweaker(ArgTweakerWindow): 102 | """An edge threshold tweaker window.""" 103 | def __init__(self, im, title=None): 104 | super(EdgeThresholdTweaker, self).__init__(im, self.draw, { 105 | 'threshold': [1, 300, 1, 50], 106 | }, title) 107 | 108 | def show(self, default_threshold=50): 109 | self.render_args['threshold'][3] = default_threshold 110 | super(EdgeThresholdTweaker, self).show() 111 | 112 | def draw(self, im, threshold): 113 | new_im = image.new_from(im) 114 | edges = image.edges(im, threshold, threshold*3, 3) 115 | cv.SetZero(new_im) 116 | cv.Copy(im, new_im, edges) 117 | size = cv.GetSize(im) 118 | print cv.CountNonZero(image.threshold(edges)) / float(size[0] * size[1]) 119 | #cv.Dilate(new_im, new_im) 120 | #cv.Erode(new_im, new_im) 121 | return new_im 122 | 123 | class DerivativeTweaker(ArgTweakerWindow): 124 | """An image derivative tweaker window.""" 125 | def __init__(self, im, title=None): 126 | super(DerivativeTweaker, self).__init__(im, self.draw, { 127 | 'order': [1, 8, 1, 1], 128 | }) 129 | self.im = im 130 | self.title = title 131 | 132 | def draw(self, im, order): 133 | return image.sobel(cv.CloneImage(im), xorder=order, yorder=order) 134 | 135 | 136 | class CornerTweaker(ArgTweakerWindow): 137 | """An edge threshold tweaker window.""" 138 | def __init__(self, im, title=None): 139 | #corners(im, max_corners=100, quality=0.1, min_dist=5, block_size=3, use_harris=False, 140 | # mask=None, k=0.04): 141 | super(CornerTweaker, self).__init__(im, self.draw, { 142 | 'max_corners': [100, 10000, 10, 100], 143 | 'quality': [0.05, 1.00, 0.05, 0.1], 144 | 'min_dist': [1, 100, 1, 5], 145 | 'block_size': [1, 100, 1, 3], 146 | 'use_harris': [0, 1, 1, 0], 147 | }, title) 148 | 149 | def draw(self, im, **kwargs): 150 | new_im = cv.CloneImage(im) 151 | corners = image.corners(im, **kwargs) 152 | image.draw_points(new_im, corners) 153 | return new_im 154 | 155 | class HistogramWindow(object): 156 | """Displays a histogram for a given image.""" 157 | def __init__(self, im, title=None, hist_size=256, color=cv.ScalarAll(0)): 158 | ranges = [ [0, hist_size] ] 159 | self.hist_size = hist_size 160 | self.color = color 161 | self.im = im 162 | self.hist_image = cv.CreateImage((320, 200), 8, 3) 163 | self.hist = cv.CreateHist([hist_size], cv.CV_HIST_ARRAY, ranges, 1) 164 | self.title = title 165 | 166 | def update(self, im=None): 167 | if im is not None: 168 | self.im = im 169 | 170 | cv.CalcArrHist([self.im], self.hist) 171 | (min_value, max_value, _, _) = cv.GetMinMaxHistValue(self.hist) 172 | cv.Scale(self.hist.bins, self.hist.bins, float(self.hist_image.height) / max_value, 0) 173 | 174 | cv.Set(self.hist_image, cv.ScalarAll(255)) 175 | bin_w = round(float(self.hist_image.width) / self.hist_size) 176 | 177 | for i in range(self.hist_size): 178 | cv.Rectangle(self.hist_image, (int(i * bin_w), self.hist_image.height), 179 | (int((i + 1) * bin_w), self.hist_image.height - cv.Round(self.hist.bins[i])), 180 | self.color, -1, 8, 0) 181 | 182 | cv.ShowImage(self.title, self.hist_image) 183 | 184 | def show(self): 185 | if self.title is None: 186 | self.title = "Histogram-"+str(id(self)) 187 | cv.NamedWindow(self.title, 0) 188 | self.update() 189 | 190 | class ColorHistograms(object): 191 | def __init__(self, im, title=None): 192 | assert im.nChannels == 3 193 | self.title = title 194 | ims = image.split(im) 195 | if self.title is None: 196 | self.title = "Histogram-" + str(id(im))+ "-" 197 | self.histograms = ( 198 | HistogramWindow(ims[0], self.title+"red", color=cv.Scalar(255,0,0)), 199 | HistogramWindow(ims[1], self.title+"green", color=cv.Scalar(0,255,0)), 200 | HistogramWindow(ims[2], self.title+"blue", color=cv.Scalar(0,0,255)), 201 | ) 202 | 203 | def show(self): 204 | for h in self.histograms: 205 | h.show() 206 | 207 | class GrayRGBWindow(ArgTweakerWindow): 208 | def __init__(self, images, title=None): 209 | assert len(images) == 4, "Requries (grayscale, r, g, b)" 210 | super(GrayRGBWindow, self).__init__(images, self.draw, { 211 | 'layers': [0, len(images), 1, 0], 212 | }, title) 213 | 214 | def show(self, layer=0): 215 | self.render_args['layers'][3] = layer 216 | super(GrayRGBWindow, self).show() 217 | 218 | def draw(self, images, layers): 219 | if layers == 0: 220 | gray, r,g,b = images 221 | im = cv.CloneImage(r) 222 | for i in (g,b): 223 | im = image.add(im, i) 224 | white = image.new_from(gray) 225 | cv.Set(white, (0,0,0)) 226 | cv.Set(white, (255,255,255), image.invert(image.threshold(im, threshold=1))) 227 | im = image.Or(image.blend(im, gray), white) 228 | else: 229 | im = cv.CloneImage(images[layers-1]) 230 | if layers != 1: 231 | white = image.new_from(images[0]) 232 | cv.Set(white, (0,0,0)) 233 | cv.Set(white, (255,255,255), image.invert(image.threshold(im, threshold=1))) 234 | im = image.Or(im, white) 235 | return im 236 | --------------------------------------------------------------------------------