From 52a36784e7efd239e9437f557756aa5743b3b8df Mon Sep 17 00:00:00 2001 From: "Jesse T. Gonzalez" Date: Thu, 5 Nov 2015 15:36:19 -0500 Subject: [PATCH 1/4] fixes texture and color detection --- set_solver.py | 57 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/set_solver.py b/set_solver.py index d692197..60f9f03 100644 --- a/set_solver.py +++ b/set_solver.py @@ -416,19 +416,11 @@ def get_color_from_hue(hue): if hist[3]+hist[4] > 1200: return sc.PROP_COLOR_GREEN - elif hist[8]+hist[9] > 250: + elif hist[8]+hist[9] > 30: return sc.PROP_COLOR_PURPLE else: return sc.PROP_COLOR_RED - - # if sum(hist[130:181]) > 750: - # return sc.PROP_COLOR_PURPLE - # elif sum(hist[40:81]) > 750: - # return sc.PROP_COLOR_GREEN - # else: - # return sc.PROP_COLOR_RED - def get_texture_from_hue(hue, contour_box): # for convenience card_w = sc.SIZE_CARD_W @@ -439,12 +431,14 @@ def get_texture_from_hue(hue, contour_box): # get a 20x20 square from each of the corners of the card, average values hue_bg = np.mean([ - hue[0:20, 0:20], hue[card_h-20:card_h, card_w-20:card_w], - hue[0:20, card_w-20:card_w], hue[card_h-20:card_h, 0:20]]) + hue[6:26, 6:26], hue[card_h-26:card_h-6, card_w-26:card_w-6], + hue[6:26, card_w-26:card_w-6], hue[card_h-26:card_h-6, 6:26]]) # get a 20x20 square from the center of the shape, average values hue_center = np.mean(hue[y+h/2-10:y+h/2+10, x+w/2-10:x+w/2+10]) + util.show(hue[y+h/2-10:y+h/2+10, x+w/2-10:x+w/2+10]) + # guess texture based on ratio of inside to outside hues hue_ratio = max(hue_bg, hue_center) / min(hue_bg, hue_center) if hue_ratio < 1.3: @@ -454,6 +448,45 @@ def get_texture_from_hue(hue, contour_box): else: return sc.PROP_TEXTURE_SOLID +def get_texture_from_hue_val(hue, val, contour_box): + # for convenience + card_w = sc.SIZE_CARD_W + card_h = sc.SIZE_CARD_H + + # uppack bounding box of contour + x, y, w, h = contour_box + + val_bg_box = np.mean([ + val[6:26, 6:26], val[card_h-26:card_h-6, card_w-26:card_w-6], + val[6:26, card_w-26:card_w-6], val[card_h-26:card_h-6, 6:26]], + axis=0).astype('uint8') + + val_center_box = val[y+h/2-10:y+h/2+10, x+w/2-10:x+w/2+10] + + val_diff = cv2.absdiff(val_bg_box, val_center_box) + val_mean = np.mean(val_diff) + val_std = np.std(val_diff) + + # get a 20x20 hue square from each of the corners of the card, average values + hue_bg = np.mean([ + hue[6:26, 6:26], hue[card_h-26:card_h-6, card_w-26:card_w-6], + hue[6:26, card_w-26:card_w-6], hue[card_h-26:card_h-6, 6:26]]) + + # get a 12x12 square from the center of the shape, average values + hue_center = np.mean(hue[y+h/2-10:y+h/2+10, x+w/2-10:x+w/2+10]) + + # guess texture based on ratio of inside to outside hues + hue_ratio = max(hue_bg, hue_center) / min(hue_bg, hue_center) + + if val_mean > 35 or hue_ratio > 5: + return sc.PROP_TEXTURE_SOLID + elif val_mean > 10: + return sc.PROP_TEXTURE_STRIPED + elif hue_ratio > 2: + return sc.PROP_TEXTURE_STRIPED + else: + return sc.PROP_TEXTURE_EMPTY + def get_shape_from_contour(contour, contour_box): # uppack bounding box of contour x, y, w, h = contour_box @@ -500,7 +533,7 @@ def get_card_properties_v2(card, debug=False): prop_num = get_dropoff([b[2]*b[3] for b in contour_boxes], maxratio=1.2) prop_col = get_color_from_hue(hue_crop) prop_shp = get_shape_from_contour(contours[0], contour_boxes[0]) - prop_tex = get_texture_from_hue(hue, contour_boxes[0]) + prop_tex = get_texture_from_hue_val(hue, val, contour_boxes[0]) if debug: pretty_print_properties([(prop_num, prop_col, prop_shp, prop_tex)]) From 01f6af43db7d56b8f13cce25ef015fa90cb4d23f Mon Sep 17 00:00:00 2001 From: Miriam Shiffman Date: Tue, 10 Nov 2015 15:36:43 -0500 Subject: [PATCH 2/4] update texture and number detection --- set_solver.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/set_solver.py b/set_solver.py index 60f9f03..a3536f5 100644 --- a/set_solver.py +++ b/set_solver.py @@ -1,10 +1,7 @@ import cv2, sys, os import cv2.cv as cv -import sys import numpy as np import util as util -import os -import code import set_constants as sc reload(util) @@ -12,7 +9,7 @@ def resize_image(img, new_width=600): """Given cv2 image object and maximum dimension, returns resized image such that height or width (whichever is larger) == max dimension""" h, w, _ = img.shape - + if h > w: img = np.rot90(img) new_height = int((1.0*h/w)*new_width) @@ -31,7 +28,7 @@ def get_card_properties(cards, training_set=None): texture = get_card_texture(img) p = (num, color, shape, texture) if None not in p: - properties.append(p) + properties.append(p) return properties def pretty_print_properties(properties): @@ -97,7 +94,7 @@ def transform_card(card, image): approximated_poly = get_approx_poly(card, do_rectify=True) if approximated_poly is None: - # could not find card poly + # could not find card poly return None dest = np.array(card_shape, np.float32) @@ -414,6 +411,8 @@ def get_color_from_hue(hue): # get histogram of hue values hist,_ = np.histogram(hue, 15, (0, 255)) + print hist + if hist[3]+hist[4] > 1200: return sc.PROP_COLOR_GREEN elif hist[8]+hist[9] > 30: @@ -474,14 +473,18 @@ def get_texture_from_hue_val(hue, val, contour_box): # get a 12x12 square from the center of the shape, average values hue_center = np.mean(hue[y+h/2-10:y+h/2+10, x+w/2-10:x+w/2+10]) - + # guess texture based on ratio of inside to outside hues hue_ratio = max(hue_bg, hue_center) / min(hue_bg, hue_center) + print "{}\t{}\t{}".format(val_std, hue_ratio, val_mean) + if val_mean > 35 or hue_ratio > 5: return sc.PROP_TEXTURE_SOLID elif val_mean > 10: return sc.PROP_TEXTURE_STRIPED + elif val_mean < 3: + return sc.PROP_TEXTURE_EMPTY elif hue_ratio > 2: return sc.PROP_TEXTURE_STRIPED else: @@ -529,8 +532,9 @@ def get_card_properties_v2(card, debug=False): x, y, w, h = contour_boxes[0] hue_crop = hue[y:y+h, x:x+w] - #prop_num = get_number_from_contours(contour_areas, contour_centers) - prop_num = get_dropoff([b[2]*b[3] for b in contour_boxes], maxratio=1.2) + # no more than 3 shapes per card + prop_num_init = get_dropoff([b[2]*b[3] for b in contour_boxes], maxratio=1.2) + prop_num = ( prop_num_init if prop_num_init < 3 else 3 ) prop_col = get_color_from_hue(hue_crop) prop_shp = get_shape_from_contour(contours[0], contour_boxes[0]) prop_tex = get_texture_from_hue_val(hue, val, contour_boxes[0]) @@ -549,6 +553,6 @@ def get_cards_properties(cards): for card in cards: p = get_card_properties_v2(card) if None not in p: - properties.append(p) + properties.append(p) - return properties \ No newline at end of file + return properties From c44ed109936728b5646e29d03e192183341ff4fd Mon Sep 17 00:00:00 2001 From: Miriam Shiffman Date: Tue, 10 Nov 2015 15:39:37 -0500 Subject: [PATCH 3/4] ignore capitalization of twitter handle --- bot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index 27fc925..0fb2b27 100644 --- a/bot.py +++ b/bot.py @@ -7,15 +7,13 @@ class listener_tweeter(StreamListener): - def _onconnect(self): - print 'Connected! Listening...' # override on_status to pass data from on_data method of tweepy's StreamListener def on_status(self, status): print '{} said: "{}"'.format(status.user.screen_name, status.text) # ignore retweets or tweets from self - if status.retweeted or ( status.user.screen_name == 'ProfessorSet' ): + if status.retweeted or ( status.user.screen_name.lower() == 'professorset' ): return img_str = None @@ -26,8 +24,8 @@ def on_status(self, status): try: tweet_url = re.search(r'https:.*\b', status.text).group(0) with_sets_outlined = (False if re.search(r'sets or not', status.text) else True) - text, img_str = solve_tweeted_set(tweet_url, with_sets_outlined=with_sets_outlined) + #print text #ignore tweets with no image except AttributeError: @@ -36,7 +34,7 @@ def on_status(self, status): text += '!'*n response_text = '.@{} {}'.format(status.user.screen_name, text) - print response_text + #print response_text try: response = api.retweet(id=status.id) @@ -100,12 +98,15 @@ def solve_tweeted_set(tweet_url, with_sets_outlined=True): # scrape tweet HTML string for image url soup = BeautifulSoup(tweet_content, 'lxml') img_url = soup.find('meta', attrs={'property': 'og:image'})['content'] + #print img_url # find Sets kwargs = {'path_is_url': True, 'pop_open': False} kwargs['draw_contours'] = (True if with_sets_outlined else False) kwargs['sets_or_no'] = (False if with_sets_outlined else True) + #print "about to play" num_sets, initial_img_str = t.play_game(img_url, **kwargs) + #print num_sets # send string with media_data (rather than media) tag because it is base64 encoded img_str = 'media_data={}'.format(initial_img_str) From 7eefc95eb238f47590bc05b2953554d0c4e788e0 Mon Sep 17 00:00:00 2001 From: Christopher Scanlin Date: Sun, 17 Jan 2016 23:56:15 -0800 Subject: [PATCH 4/4] add quick and dirty flask app. small changes to requirements and PIL Image import --- .gitignore | 3 ++ __init__.py | 0 cv.py | 1 + images/.DS_Store | Bin 6148 -> 6148 bytes images/user_images/input/.keep | 0 images/user_images/output/.keep | 0 requirements.txt | 2 -- solver_app.py | 54 ++++++++++++++++++++++++++++++++ tests.py | 16 ++++++---- 9 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 __init__.py create mode 120000 cv.py create mode 100644 images/user_images/input/.keep create mode 100644 images/user_images/output/.keep create mode 100644 solver_app.py diff --git a/.gitignore b/.gitignore index 89848a6..3f61c46 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ target/ .venv images/cards .DS_Store + +**/images/user_images/*/*.* +!**/images/user_images/*/*.keep diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cv.py b/cv.py new file mode 120000 index 0000000..d651bf1 --- /dev/null +++ b/cv.py @@ -0,0 +1 @@ +/usr/local/Cellar/opencv/2.4.10/lib/python2.7/site-packages/cv.py \ No newline at end of file diff --git a/images/.DS_Store b/images/.DS_Store index 8a9bec4d81d3cbd057e83d6a6ae2dd726f71eee1..7a9e9bfe126b11443e3707c2686acf72eee4e5ab 100644 GIT binary patch delta 64 zcmZoMXfc=|#>CJ*u~2NHo+2aT!~knX#>qTPo}0^B)qu~2NHo+2aD!~pA!4;mOJ8;Gz>?02hYWk_a7WGG@tVJJ>2FD^*R z$xmWnVECC-kds+lVqkESk%^gwm5rT)lZ%_1n~PgOkSjJgBfmVjB(bEl*eS6n8qCW~ zNlk*X0}@LzVC<0m{2VwtF)1uFwLD%x#5q5&Br!8DwFs!SzC0MBHzqtYFD1X+DZex? zr8ovE7@nC@k`XT;8c>v42Gkpnn3o!sS)7@anUh&k$-&9V$-x;fAW>bdYi6jUU}R}g ztD{hDZf>BXU}9`uTPv`PgF{-=)X_Jxptft#;w4L$Enji?DA0F6z{m)pg&2e(3^;jg nw-@#1&p$^0UQ0wCoLKn$e8IyVQ1Y+(if$Wde( diff --git a/images/user_images/input/.keep b/images/user_images/input/.keep new file mode 100644 index 0000000..e69de29 diff --git a/images/user_images/output/.keep b/images/user_images/output/.keep new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index 5cc3dea..8bdf233 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,6 @@ altgraph==0.10.2 bdist-mpkg==0.5.0 -bonjour-py==0.3 macholib==1.5.1 -matplotlib==1.3.1 modulegraph==0.10.4 numpy==1.8.0rc1 py2app==0.7.3 diff --git a/solver_app.py b/solver_app.py new file mode 100644 index 0000000..7379572 --- /dev/null +++ b/solver_app.py @@ -0,0 +1,54 @@ +import tests + +import os +from flask import Flask, request, redirect, url_for, send_from_directory +from werkzeug import secure_filename + +ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + +app = Flask(__name__) +app.config['UPLOAD_FOLDER'] = os.path.join(THIS_DIR, 'images', 'user_images', 'input') +app.config['OUTPUT_FOLDER'] = os.path.join(THIS_DIR, 'images', 'user_images', 'output') + +def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS + +@app.route('/', methods=['GET', 'POST']) +def upload_file(): + if request.method == 'POST': + file = request.files['file'] + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + final_image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(final_image_path) + + solve_uploaded_set(final_image_path) + + return redirect(url_for('show_results', filename=filename)) + return ''' + + Upload new File +

Upload new File

+
+

+ +

+ ''' + +@app.route('/') +def show_results(filename): + return send_from_directory(app.config['OUTPUT_FOLDER'], 'OUTPUT_{0}'.format(filename)) + +def solve_uploaded_set(image_url): + kwargs = { + 'pop_open': False, + 'save_image': True, + 'output_dir': app.config['OUTPUT_FOLDER'], + } + + return tests.play_game(image_url, **kwargs) + +if __name__ == "__main__": + app.run(host='192.168.1.101') diff --git a/tests.py b/tests.py index 8f31349..0cc4b80 100644 --- a/tests.py +++ b/tests.py @@ -5,8 +5,11 @@ from set_test import game from collections import Counter import numpy as np -import base64, Image, StringIO +import base64, StringIO +from PIL import Image +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) +DEFAULT_IMAGE_OUT_DIR = os.path.join(THIS_DIR, 'images', 'user_images', 'output') def test(): # 3 cards on flat table @@ -96,7 +99,8 @@ def main(): def play_game(path_in, path_is_url=False, printall=False, \ draw_contours=True, resize_contours=True, \ draw_rects=False, sets_or_no=False, \ - pop_open=True): + pop_open=True, save_image=False, \ + output_dir=DEFAULT_IMAGE_OUT_DIR): """Takes in an image path (to local file or onlin), finds all sets, and pretty prints them to screen. if printall - prints the identities of all cards in the image if draw_contours - outlines the cards belonging to each set @@ -197,8 +201,10 @@ def play_game(path_in, path_is_url=False, printall=False, \ mystr = output.getvalue() output.close() - # don't write image to file, dude....because we are badass Tweepy hackers - #cv2.imwrite('tmp.jpeg', final_img) + if save_image: + out_file_name = 'OUTPUT_{0}'.format(os.path.basename(path_in)) + final_out_path = os.path.join(output_dir, out_file_name) + cv2.imwrite(final_out_path, final_img) # encode image string to base64 and safe-encode it for Twitter upload request final = requests.utils.quote(base64.b64encode(mystr), safe='') @@ -211,5 +217,3 @@ def play_game(path_in, path_is_url=False, printall=False, \ # size is 89867.1532847 bytes return (num_sets, final) - -