diff --git a/README.md b/README.md index 5f822327..4c43e7e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ # InteractiveProgramming -This is the base repo for the interactive programming project for Software Design, Spring 2018 at Olin College. +This is forked from the base repo for the interactive programming project for Software Design, Spring 2018 at Olin College. + +## Lost Vegas +Our project allows a user to play blackjack interactively with a computer. Using a real deck of cards, a player can deal cards, have them recognized by the computer, and play a hand of blackjack against the computer in real-time. The card detection mechanism works via OpenCV while the Blackjack game uses a class-based implementation in Python + +## Requirements +Software requirements can be found in the `requirements.txt` file and installed via `pip install -r requirements.txt`. + +This project does have a hardware component though and may be difficult to demo as a result. Steps to demo the project using the hardware: +1. Grab an external usb webcam and plug into your computer (we used a Microsoft Lifecam). +2. Grab a deck of cards +3. A black surface is extremely helpful (but not required) for card matching. +4. When identifying cards, hold the external webcam roughly a cubit directly above the card. + +## Resources and Documentation +See our [wiki](https://github.com/isaacvandor/InteractiveProgramming/wiki) for complete documentation and resources + +### Project Reflection & Write-up +See the [wiki page](https://github.com/isaacvandor/InteractiveProgramming/wiki/MP4-Project-Write-up-&-Reflection) diff --git a/blackjack.py b/blackjack.py new file mode 100644 index 00000000..71f9bf0c --- /dev/null +++ b/blackjack.py @@ -0,0 +1,307 @@ +import random +from unicards import unicard +import card_setup +import card_detect +import time + +class Card: + """Represents a standard playing card. + + Attributes: + suit: integer 0-3 + rank: integer 1-13 + """ + + suit_names = ["Clubs", "Diamonds", "Hearts", "Spades"] + rank_names = [None, "Ace", "2", "3", "4", "5", "6", "7", + "8", "9", "10", "Jack", "Queen", "King"] + + def __init__(self, suit=0, rank=2): + """Initializes a Card with a suit and rank.""" + self.suit = suit + self.rank = rank + + def __str__(self): + """Returns a unicard representation of the Card.""" + suit = self.suit + rank = self.rank + return convert_to_unicard(suit,rank) + + + def __eq__(self, other): + """Checks whether self and other have the same rank and suit. + + returns: boolean + """ + return self.suit == other.suit and self.rank == other.rank + + def __lt__(self, other): + """Compares this card to other, first by suit, then rank. + + returns: boolean + """ + t1 = self.suit, self.rank + t2 = other.suit, other.rank + return t1 < t2 + + + + +class Deck: + """Represents a deck of cards. + + Attributes: + cards: list of Card objects. + """ + + def __init__(self): + """Initializes the Deck with 52 cards.""" + self.cards = [] + for suit in range(4): + for rank in range(1, 14): + card = Card(suit, rank) + self.cards.append(card) + + def __str__(self): + """Returns a string representation of the deck. + """ + res = [] + for card in self.cards: + res.append(str(card)) + return ' '.join(res) + + def add_card(self, card): + """Adds a card to the deck. + + card: Card + """ + self.cards.append(card) + + def remove_card(self, card): + """Removes a card from the deck or raises exception if it is not there. + + card: Card + """ + self.cards.remove(card) + + def pop_card(self, i=-1): + """Removes and returns a card from the deck. + + i: index of the card to pop; by default, pops the last card. + """ + return self.cards.pop(i) + + def shuffle(self): + """Shuffles the cards in this deck.""" + random.shuffle(self.cards) + + def sort(self): + """Sorts the cards in ascending order.""" + self.cards.sort() + + def move_cards(self, hand, num): + """Moves the given number of cards from the deck into the Hand. + + hand: destination Hand object + num: integer number of cards to move + """ + for i in range(num): + hand.add_card(self.pop_card()) + + +class Hand(Deck): + """Represents a hand of playing cards.""" + + def __init__(self, label=''): + self.cards = [] + self.label = label + self.total = 0 + + +class Game(): + """Represents a game of blackjack + + player: Hand object containing real cards + dealer: Hand object of the computer's cards + """ + + def __init__(self): + """Starts a new round of blackjack""" + self.deck=Deck() + self.deck.shuffle() + self.player=Hand('Player') + #self.player=card_detect.cards + self.dealer=Hand('Dealer') + + def deal(self): + """Deals 2 cards to the player and one to the dealer""" + self.deck.move_cards(self.dealer,1) + print("Dealer" + str(self.dealer) + " []") + #self.deck.move_cards(self.player,2) + #print("Player" + str(self.player)) + + # Call mainloop function from card_detect.py and print resulting rank and suit + print("Please show your first card to the camera") + rank_name, suit_name = card_detect.mainloop() + rank_name1 = rank_name + suit_name1 = suit_name + print("Player has:" + str(rank_name1),'of', str(suit_name1)) + suitInt1, rankInt1 = convert_card_to_int(suit_name1, rank_name1) + print("Please show your second card to the camera (program will wait momentarily)") + time.sleep(5) + + # Call mainloop function from card_detect.py and print resulting rank and suit + rank_name, suit_name = card_detect.mainloop() + rank_name2 = rank_name + suit_name2 = suit_name + print("Player has:" + str(rank_name2),'of', str(suit_name2)) + suitInt2, rankInt2 = convert_card_to_int(suit_name2, rank_name2) + + # Remove cards from deck and deal them to player + p1 = Card(suitInt1, rankInt1) + p2 = Card(suitInt2, rankInt2) + self.deck.remove_card(p1) + self.deck.remove_card(p2) + self.player.add_card(p1) + self.player.add_card(p2) + print("Player" + str(self.player)) + + + + def play(self): + """Simulates the player's turn in a game of blackjack""" + ans = input("Would you like to hit or stay?\n") + if (ans == "hit"): + self.deck.move_cards(self.player,1) + print("Player" + str(self.player)) + self.play() + elif (ans == "stay"): + total = (self.get_player_total()) + if (total == 'BUST'): + print("BUST\nDealer Wins") + else: + print(self.house()) + else: + print("Error: End of Game") + + def house(self): + """Simulates the dealer's turn in a game of blackjack + + returns: String containing the results of the game""" + self.deck.move_cards(self.dealer,1) + print("Dealer" + str(self.dealer)) + self.get_dealer_total() + + while(self.dealer.total<=17): + (self.get_dealer_total()) + if(self.dealer.total>=self.player.total): + return ("Dealer Wins") + else: + self.deck.move_cards(self.dealer,1) + print("Dealer" + str(self.dealer)) + self.get_dealer_total() + + if(self.dealer.total>21): + return ("Dealer Bust\n Player Wins") + elif(self.dealer.total>=self.player.total): + return ("Dealer Wins") + return ("Player Wins") + + def get_dealer_total(self): + """Calculates the current total points of the dealer. + + If there is an ace it will determine whether to be worth 11 or 1 point + based on if the current total is less than or equal to 10""" + values = [None, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10] + self.dealer.cards.sort(reverse = True) + self.dealer.total = 0 + for x in self.dealer.cards: + if x.rank == 1: + if self.dealer.total<=10: + self.dealer.total += 11 + else: + self.dealer.total += 1 + else: + self.dealer.total += values[x.rank] + + + def get_player_total(self): + """Calculates the current total points of the player and gives the + player the option to choose if an ace counts for 1 point or 11. + + returns: BUST if it is over 21 or it will return the total + if it is less than 21""" + values = [None, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10] + self.player.cards.sort(reverse = True) + for x in self.player.cards: + if x.rank == 1: + ace = int(input("11 or 1 \n")) + while (ace != 1 and ace != 11): + ace = int(input("11 or 1\n")) + self.player.total += ace + else: + self.player.total += values[x.rank] + if self.player.total > 21: + return "BUST" + else: + return self.player.total + + +def convert_to_unicard(suit=0, rank=2): + unicard_suit = ['s','h','d', 'c'] + unicard_rank = [None, 'A','2','3','4','5','6','7','8','9','T','J','Q','K'] + if (suit == -1 or rank == -1): + return "NaN" + card = str(unicard_rank[rank])+str(unicard_suit[suit]) + return unicard(card) + +def convert_card_to_int(suit,rank): + """Converts the OpenCV card detected into integers so it is readable by + the program""" + + if(suit == "Spades"): + suitInt = 0 + elif(suit == "Hearts"): + suitInt = 1 + elif(suit == "Diamonds"): + suitInt = 2 + elif(suit == "Clubs"): + suitInt = 3 + else: + suitInt = -1 + + if(rank == "Ace"): + rankInt = 1 + elif(rank == "Two"): + rankInt = 2 + elif(rank == "Three"): + rankInt = 3 + elif(rank == "Four"): + rankInt = 4 + elif(rank == "Five"): + rankInt = 5 + elif(rank == "Six"): + rankInt = 6 + elif(rank == "Seven"): + rankInt = 7 + elif(rank == "Eight"): + rankInt = 8 + elif(rank == "Nine"): + rankInt = 9 + elif(rank == "Ten"): + rankInt = 10 + elif(rank == "Jack"): + rankInt = 11 + elif(rank == "Queen"): + rankInt = 12 + elif(rank == "King"): + rankInt = 13 + else: + rankInt = -1 + + return(suitInt,rankInt) + +if __name__ == '__main__': + round1 = Game() + round1.deal() + round1.play() diff --git a/card_detect.py b/card_detect.py new file mode 100644 index 00000000..5c7284fb --- /dev/null +++ b/card_detect.py @@ -0,0 +1,98 @@ +""" Experiment with card detection and filtering using OpenCV """ +''' SoftDes MP4: Interactive Programming - Isaac Vandor & Raquel Dunoff ''' +''' Open camera and show detected card''' + +# Import necessary packages +import cv2 +import numpy as np +import time +import os +import card_setup + +# Define constants and initialize variables +def mainloop(): + # Camera settings + IM_WIDTH = 1280 + IM_HEIGHT = 720 + FRAME_RATE = 10 + #suit_name = "Hearts" + #rank_name = "Queen" + + ## Define font to use + font = cv2.FONT_HERSHEY_DUPLEX + + # Initialize camera object and video feed from the camera. Change integer to reflect internal webcam versus usb webcam. + video_stream = cv2.VideoCapture(1) + time.sleep(1) # Give the camera time to warm up + + # Load the train rank and suit images + path = os.path.dirname(os.path.abspath(__file__)) + train_ranks = card_setup.load_ranks( path + '/card_imgs/') + train_suits = card_setup.load_suits( path + '/card_imgs/') + + ''' + Grab frames from the video stream + and process them to find and identify playing cards. + ''' + + cam_quit = 0 # Loop control variable + + # Begin capturing frames + while cam_quit == 0: + start = time.time() + # Grab frame from video stream + #image = videostream.read() + ret, image = video_stream.read() + + # Pre-process camera image (gray, blur, and threshold it) + pre_proc = card_setup.preprocess_image(image) + + # Find and sort the contours of all cards in the image (query cards) + contours_sort, contour_is_card = card_setup.find_cards(pre_proc) + + # If there are no contours, do nothing + if len(contours_sort) != 0: + + # Initialize a new "cards" list to assign the card objects w/ k as index. + cards = [] + k = 0 + + # For each contour detected: + for i in range(len(contours_sort)): + if (contour_is_card[i] == 1): + + # Append card object and run preprocess_card func. to generate + # flattened image and isolate card suit and rank + cards.append(card_setup.preprocess_card(contours_sort[i],image)) + + # Find the best rank and suit match for the card. + cards[k].best_rank_match,cards[k].best_suit_match,cards[k].rank_diff,cards[k].suit_diff = card_setup.match_card(cards[k],train_ranks,train_suits) + + # Draw center point and match result on the image. + image, rank_name, suit_name = card_setup.draw_results(image, cards[k]) + #print(str(rank_name)) + #print(str(suit_name)) + k = k + 1 + # Draw card contours on image + if (len(cards) != 0): + temp_contours = [] + for i in range(len(cards)): + temp_contours.append(cards[i].contour) + cv2.drawContours(image,temp_contours, -1, (0,0,0), 3) + cv2.drawContours(image,temp_contours, -1, (255,150,20), 2) + + # Finally, display the image + cv2.imshow("Playing Card Detector",image) + + while (time.time() - start < 5): + cam_quit=1 + + # Poll the keyboard. If 'q' is pressed, exit the program. + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + cam_quit = 1 + + + # Close all windows and close the video feed. + cv2.destroyAllWindows() + return (rank_name, suit_name) diff --git a/card_detect_standalone.py b/card_detect_standalone.py new file mode 100644 index 00000000..b41296cf --- /dev/null +++ b/card_detect_standalone.py @@ -0,0 +1,93 @@ +""" Experiment with card detection and filtering using OpenCV """ +''' SoftDes MP4: Interactive Programming - Isaac Vandor & Raquel Dunoff ''' +''' Open camera and show detected card''' + +# Import necessary packages +import cv2 +import numpy as np +import time +import os +import card_setup + +# Define constants and initialize variables + +# Camera settings +IM_WIDTH = 1280 +IM_HEIGHT = 720 +FRAME_RATE = 10 + +## Define font to use +font = cv2.FONT_HERSHEY_DUPLEX + +# Initialize camera object and video feed from the camera. Change integer to reflect internal webcam versus usb webcam. +video_stream = cv2.VideoCapture(1) +time.sleep(1) # Give the camera time to warm up + +# Load the train rank and suit images +path = os.path.dirname(os.path.abspath(__file__)) +train_ranks = card_setup.load_ranks( path + '/card_imgs/') +train_suits = card_setup.load_suits( path + '/card_imgs/') + + +''' +Grab frames from the video stream +and process them to find and identify playing cards. +''' + +cam_quit = 0 # Loop control variable + +# Begin capturing frames +while cam_quit == 0: + + # Grab frame from video stream + #image = videostream.read() + ret, image = video_stream.read() + + # Pre-process camera image (gray, blur, and threshold it) + pre_proc = card_setup.preprocess_image(image) + + # Find and sort the contours of all cards in the image (query cards) + contours_sort, contour_is_card = card_setup.find_cards(pre_proc) + + # If there are no contours, do nothing + if len(contours_sort) != 0: + + # Initialize a new "cards" list to assign the card objects w/ k as index. + cards = [] + k = 0 + + # For each contour detected: + for i in range(len(contours_sort)): + if (contour_is_card[i] == 1): + + # Append card object and run preprocess_card func. to generate + # flattened image and isolate card suit and rank + cards.append(card_setup.preprocess_card(contours_sort[i],image)) + + # Find the best rank and suit match for the card. + cards[k].best_rank_match,cards[k].best_suit_match,cards[k].rank_diff,cards[k].suit_diff = card_setup.match_card(cards[k],train_ranks,train_suits) + + # Draw center point and match result on the image. + image, rank_name, suit_name = card_setup.draw_results(image, cards[k]) + k = k + 1 + + # Draw card contours on image + if (len(cards) != 0): + temp_contours = [] + for i in range(len(cards)): + temp_contours.append(cards[i].contour) + cv2.drawContours(image,temp_contours, -1, (0,0,0), 3) + cv2.drawContours(image,temp_contours, -1, (255,150,20), 2) + + # Finally, display the image + cv2.imshow("Playing Card Detector",image) + + # Poll the keyboard. If 'q' is pressed, exit the program. + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + cam_quit = 1 + + +# Close all windows and close the video feed. +cv2.destroyAllWindows() +cap.release() diff --git a/card_imgs/Ace.jpg b/card_imgs/Ace.jpg new file mode 100644 index 00000000..10a63f8a Binary files /dev/null and b/card_imgs/Ace.jpg differ diff --git a/card_imgs/Clubs.jpg b/card_imgs/Clubs.jpg new file mode 100644 index 00000000..343a9e61 Binary files /dev/null and b/card_imgs/Clubs.jpg differ diff --git a/card_imgs/Diamonds.jpg b/card_imgs/Diamonds.jpg new file mode 100644 index 00000000..fef1104e Binary files /dev/null and b/card_imgs/Diamonds.jpg differ diff --git a/card_imgs/Eight.jpg b/card_imgs/Eight.jpg new file mode 100644 index 00000000..791b52a2 Binary files /dev/null and b/card_imgs/Eight.jpg differ diff --git a/card_imgs/Five.jpg b/card_imgs/Five.jpg new file mode 100644 index 00000000..cdffd1cc Binary files /dev/null and b/card_imgs/Five.jpg differ diff --git a/card_imgs/Four.jpg b/card_imgs/Four.jpg new file mode 100644 index 00000000..06847e72 Binary files /dev/null and b/card_imgs/Four.jpg differ diff --git a/card_imgs/Hearts.jpg b/card_imgs/Hearts.jpg new file mode 100644 index 00000000..2c9e386c Binary files /dev/null and b/card_imgs/Hearts.jpg differ diff --git a/card_imgs/Jack.jpg b/card_imgs/Jack.jpg new file mode 100644 index 00000000..c1a808c7 Binary files /dev/null and b/card_imgs/Jack.jpg differ diff --git a/card_imgs/King.jpg b/card_imgs/King.jpg new file mode 100644 index 00000000..b3ddbb6c Binary files /dev/null and b/card_imgs/King.jpg differ diff --git a/card_imgs/Nine.jpg b/card_imgs/Nine.jpg new file mode 100644 index 00000000..fa1b0fac Binary files /dev/null and b/card_imgs/Nine.jpg differ diff --git a/card_imgs/Queen.jpg b/card_imgs/Queen.jpg new file mode 100644 index 00000000..ec19e68a Binary files /dev/null and b/card_imgs/Queen.jpg differ diff --git a/card_imgs/Seven.jpg b/card_imgs/Seven.jpg new file mode 100644 index 00000000..ff1b8592 Binary files /dev/null and b/card_imgs/Seven.jpg differ diff --git a/card_imgs/Six.jpg b/card_imgs/Six.jpg new file mode 100644 index 00000000..7b86fc6e Binary files /dev/null and b/card_imgs/Six.jpg differ diff --git a/card_imgs/Spades.jpg b/card_imgs/Spades.jpg new file mode 100644 index 00000000..c6968867 Binary files /dev/null and b/card_imgs/Spades.jpg differ diff --git a/card_imgs/Ten.jpg b/card_imgs/Ten.jpg new file mode 100644 index 00000000..58046d12 Binary files /dev/null and b/card_imgs/Ten.jpg differ diff --git a/card_imgs/Three.jpg b/card_imgs/Three.jpg new file mode 100644 index 00000000..8eca7aa0 Binary files /dev/null and b/card_imgs/Three.jpg differ diff --git a/card_imgs/Two.jpg b/card_imgs/Two.jpg new file mode 100644 index 00000000..2893a78e Binary files /dev/null and b/card_imgs/Two.jpg differ diff --git a/card_isolater.py b/card_isolater.py new file mode 100644 index 00000000..6e7780ed --- /dev/null +++ b/card_isolater.py @@ -0,0 +1,120 @@ +""" Experiment with card detection and filtering using OpenCV """ +''' SoftDes MP4: Interactive Programming - Isaac Vandor & Raquel Dunoff ''' +''' Takes a card picture and creates a flattened image of it. + Isolates the suit and rank and saves the isolated images. + Runs through A - K ranks and then the 4 suits. ''' + + +# Import necessary packages +import cv2 +import numpy as np +import time +import cards +import os + +img_path = os.path.dirname(os.path.abspath(__file__)) + '/card_imgs/' + +IM_WIDTH = 1280 +IM_HEIGHT = 720 + +RANK_WIDTH = 70 +RANK_HEIGHT = 125 + +SUIT_WIDTH = 70 +SUIT_HEIGHT = 100 + +# Initialize USB camera +cap = cv2.VideoCapture(1) + +# Use counter variable to switch from isolating Rank to isolating Suit +i = 1 + +for name in ['Ace','Two','Three','Four','Five','Six','Seven','Eight', + 'Nine','Ten','Jack','Queen','King','Spades','Diamonds', + 'Clubs','Hearts']: + + filename = name + '.jpg' + + print('Press "p" to take a picture of ' + filename) + +# Press 'p' to take a picture + while(True): + + ret, frame = cap.read() + cv2.imshow("Card",frame) + key = cv2.waitKey(1) & 0xFF + if key == ord("p"): + image = frame + break + if key == ord("q"): + cam_quit = 1 + cap.release + break + + # Pre-process image + gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) + blur = cv2.GaussianBlur(gray,(5,25),0) + retval, threshold = cv2.threshold(blur,100,255,cv2.THRESH_BINARY) + + # Find contours and sort them by size + dummy,cnts,hier = cv2.findContours(threshold,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + cnts = sorted(cnts, key=cv2.contourArea,reverse=True) + + # Assume largest contour is the card. If there are no contours, print an error + flag = 0 + image2 = image.copy() + + if len(cnts) == 0: + print('No contours found!') + quit() + + card = cnts[0] + + # Approximate the corner points of the card + peri = cv2.arcLength(card,True) + approx = cv2.approxPolyDP(card,0.01*peri,True) + pts = np.float32(approx) + + x,y,w,h = cv2.boundingRect(card) + + # Flatten the card and convert it to 200x300 + warp = cards.flattener(image,pts,w,h) + + # Grab corner of card image, zoom, and threshold + corner = warp[0:84, 0:32] + #corner_gray = cv2.cvtColor(corner,cv2.COLOR_BGR2GRAY) + corner_zoom = cv2.resize(corner, (0,0), fx=4, fy=4) + corner_blur = cv2.GaussianBlur(corner_zoom,(5,5),0) + retval, corner_thresh = cv2.threshold(corner_blur, 155, 255, cv2. THRESH_BINARY_INV) + + # Isolate suit or rank + if i <= 13: # Isolate rank + rank = corner_thresh[20:185, 0:128] # Grabs portion of image that shows rank + dummy, rank_cnts, hier = cv2.findContours(rank, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + rank_cnts = sorted(rank_cnts, key=cv2.contourArea,reverse=True) + x,y,w,h = cv2.boundingRect(rank_cnts[0]) + rank_roi = rank[y:y+h, x:x+w] + rank_sized = cv2.resize(rank_roi, (RANK_WIDTH, RANK_HEIGHT), 0, 0) + final_img = rank_sized + + if i > 13: # Isolate suit + suit = corner_thresh[186:336, 0:128] # Grabs portion of image that shows suit + dummy, suit_cnts, hier = cv2.findContours(suit, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + suit_cnts = sorted(suit_cnts, key=cv2.contourArea,reverse=True) + x,y,w,h = cv2.boundingRect(suit_cnts[0]) + suit_roi = suit[y:y+h, x:x+w] + suit_sized = cv2.resize(suit_roi, (SUIT_WIDTH, SUIT_HEIGHT), 0, 0) + final_img = suit_sized + + cv2.imshow("Image",final_img) + + # Save image + print('Press "s" to continue.') + key = cv2.waitKey(0) & 0xFF + if key == ord('s'): + cv2.imwrite(img_path+filename,final_img) + + i = i + 1 + +cv2.destroyAllWindows() +cap.release() diff --git a/card_setup.py b/card_setup.py new file mode 100644 index 00000000..21fe776f --- /dev/null +++ b/card_setup.py @@ -0,0 +1,400 @@ +""" Experiment with card detection and filtering using OpenCV """ +''' SoftDes MP4: Interactive Programming - Isaac Vandor & Raquel Dunoff ''' +''' Card rank/suit training and classification''' + + +# Import necessary packages +import numpy as np +import cv2 +import time + +''' +Variables for dimensions, thresholding, and calculating difference between +unknown and training cards +''' + +# Light threshold levels +background_threshold = 60 +card_threshold = 30 + +# Width and height of card corner +corner_width = 32 +corner_height = 84 + +# Dimensions of rank training images +rank_width = 70 +rank_height = 125 + +# Dimensions of suit training images +suit_width = 70 +suit_height = 100 + +# Maximum differences between cards for rank & suit +max_rank_difference = 2000 +max_suit_difference = 700 + +# Maximum/Minimum card area dimensions +max_card_area = 120000 +min_card_area = 25000 + +# Fonts....cuz apparently opencv loves the Hershey font +font = cv2.FONT_HERSHEY_DUPLEX + +''' +Create clases to store unknown card information and training data +''' + +class Unknown_card: + '''Structure to store information about unknown cards in the image frame.''' + + def __init__(self): + self.contour = [] # Contour of card + self.width, self.height = 0, 0 # Width and height of card + self.corner_pts = [] # Corner points of card + self.center = [] # Center point of card + self.warp = [] # 200x300, flattened, grayed, blurred image + self.rank_img = [] # Thresholded, sized image of card's rank + self.suit_img = [] # Thresholded, sized image of card's suit + self.best_rank_match = "Unknown" # Best matched rank + self.best_suit_match = "Unknown" # Best matched suit + self.rank_difference = 0 # Difference between rank image and best matched train rank image + self.suit_difference = 0 # Difference between suit image and best matched train suit image + +class Rank_training: + """Structure to store information about card rank images.""" + + def __init__(self): + self.img = [] # Thresholded, sized rank image + self.name = "Placeholder" + +class Suit_training: + """Structure to store information about card suit images.""" + + def __init__(self): + self.img = [] # Thresholded, sized suit image + self.name = "Placeholder" + +'''Functions to handle rank and suit sorting''' + +def load_ranks(filepath): + '''Load rank images from directory and store + them in a list of rank_training objects.''' + + rank_training = [] + i = 0 + + for Rank in ['Ace','Two','Three','Four','Five','Six','Seven', + 'Eight','Nine','Ten','Jack','Queen','King']: + rank_training.append(Rank_training()) + rank_training[i].name = Rank + filename = Rank + '.jpg' + rank_training[i].img = cv2.imread(filepath+filename, cv2.IMREAD_GRAYSCALE) + i = i + 1 + + return rank_training + +def load_suits(filepath): + '''Load suit images from directory and store + them in a list of suit_training objects.''' + + suit_training = [] + i = 0 + + for Suit in ['Spades','Diamonds','Clubs','Hearts']: + suit_training.append(Suit_training()) + suit_training[i].name = Suit + filename = Suit + '.jpg' + suit_training[i].img = cv2.imread(filepath+filename, cv2.IMREAD_GRAYSCALE) + i = i + 1 + + return suit_training + +def preprocess_image(image): + '''Returns a grayed, blurred, and thresholded camera image.''' + + gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) + blur = cv2.GaussianBlur(gray,(5,5),0) + + ''' + Tries to adaptively change threshold based on lighting conditions + A background pixel in the center top of the image is sampled to determine + its intensity. + ''' + img_width, img_height = np.shape(image)[:2] + background_level = gray[int(img_height/100)][int(img_width/2)] + threshold_level = background_threshold + background_level + + retval, threshold = cv2.threshold(blur,threshold_level,255,cv2.THRESH_BINARY) + + return threshold + +def find_cards(threshold_image): + '''Finds all card-sized contours in a thresholded camera image. + Returns the number of cards and a list of card contours sorted + from largest to smallest.''' + + # Find contours and sort their indices by contour size + dummy,contours,hierarchy = cv2.findContours(threshold_image,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + index_sort = sorted(range(len(contours)), key=lambda i : cv2.contourArea(contours[i]),reverse=True) + + # If there are no contours, do nothing + if len(contours) == 0: + return [], [] + + # Otherwise, initialize empty sorted contour and hierarchy lists + contours_sort = [] + hierarchy_sort = [] + contour_is_card = np.zeros(len(contours),dtype=int) + + # Fill empty lists with sorted contour and sorted hierarchy. Now, + # the indices of the contour list still correspond with those of + # the hierarchy list. The hierarchy array can be used to check if + # the contours have parents or not. + for i in index_sort: + contours_sort.append(contours[i]) + hierarchy_sort.append(hierarchy[0][i]) + + ''' + Determine which of the contours are cards by applying the + following criteria: + 1) smaller area than the maximum card size + 2) bigger area than the minimum card size + 3) have no parents + 4) have four corners + ''' + for i in range(len(contours_sort)): + size = cv2.contourArea(contours_sort[i]) + perimeter = cv2.arcLength(contours_sort[i],True) + approx = cv2.approxPolyDP(contours_sort[i],0.01*perimeter,True) + + if ((size < max_card_area) and (size > min_card_area) + and (hierarchy_sort[i][3] == -1) and (len(approx) == 4)): + contour_is_card[i] = 1 + + return contours_sort, contour_is_card + +def preprocess_card(contour, image): + '''Uses contour to find information about the unknown card. Isolates rank + and suit images from the card.''' + + # Initialize new Unknown_card object + unknownCard = Unknown_card() + + unknownCard.contour = contour + + # Find perimeter of card and use it to approximate corner points + perimeter = cv2.arcLength(contour,True) + approx = cv2.approxPolyDP(contour,0.01*perimeter,True) + pts = np.float32(approx) + unknownCard.corner_pts = pts + + # Find width and height of card's bounding rectangle + x,y,width,height = cv2.boundingRect(contour) + unknownCard.width, unknownCard.height = width, height + + # Find center point of card by taking x and y average of the four corners. + average = np.sum(pts, axis=0)/len(pts) + cent_x = int(average[0][0]) + cent_y = int(average[0][1]) + unknownCard.center = [cent_x, cent_y] + + # Warp card into 200x300 flattened image using perspective transform + unknownCard.warp = flattener(image, pts, width, height) + + # Grab corner of warped card image and do a 4x zoom + unknown_corner = unknownCard.warp[0:corner_height, 0:corner_width] + unknown_corner_zoom = cv2.resize(unknown_corner, (0,0), fx=4, fy=4) + + # Sample known white pixel intensity to determine good threshold level + white_level = unknown_corner_zoom[15,int((corner_width*4)/2)] + threshold_level = white_level - card_threshold + if (threshold_level <= 0): + threshold_level = 1 + retval, unknown_threshold = cv2.threshold(unknown_corner_zoom, threshold_level, 255, cv2. THRESH_BINARY_INV) + + # Split in to top and bottom half (top shows rank, bottom shows suit) + unknown_rank = unknown_threshold[20:185, 0:128] + unknown_suit = unknown_threshold[186:336, 0:128] + + # Find rank contour and bounding rectangle, isolate and find largest contour + dummy, unknown_rank_contours, hierarchy = cv2.findContours(unknown_rank, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + unknown_rank_contours = sorted(unknown_rank_contours, key=cv2.contourArea,reverse=True) + + # Find bounding rectangle for largest contour, use it to resize unknown rank + # image to match dimensions of the rank_training image + if len(unknown_rank_contours) != 0: + x1,y1,w1,h1 = cv2.boundingRect(unknown_rank_contours[0]) + unknown_rank_roi = unknown_rank[y1:y1+h1, x1:x1+w1] + unknown_rank_sized = cv2.resize(unknown_rank_roi, (rank_width,rank_height), 0, 0) + unknownCard.rank_img = unknown_rank_sized + + # Find suit contour and bounding rectangle, isolate and find largest contour + dummy, unknown_suit_contours, hierarchy = cv2.findContours(unknown_suit, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + unknown_suit_contours = sorted(unknown_suit_contours, key=cv2.contourArea,reverse=True) + + # Find bounding rectangle for largest contour, use it to resize unknown suit + # image to match dimensions of the suit_training image + if len(unknown_suit_contours) != 0: + x2,y2,w2,h2 = cv2.boundingRect(unknown_suit_contours[0]) + unknown_suit_roi = unknown_suit[y2:y2+h2, x2:x2+w2] + unknown_suit_sized = cv2.resize(unknown_suit_roi, (suit_width, suit_height), 0, 0) + unknownCard.suit_img = unknown_suit_sized + + return unknownCard + +def match_card(unknownCard, rank_training, suit_training): + '''Finds best rank and suit matches for the unknown card. Differences + the unknown card rank and suit images with the train rank and suit images. + The best match is the rank or suit image that has the least difference.''' + + best_rank_match_difference = 10000 + best_suit_match_difference = 10000 + best_rank_match_name = "Unknown" + best_suit_match_name = "Unknown" + i = 0 + + ''' + If no contours were found in unknown card in preprocess_card function, + the img size is zero, so skip the difference process and leave card as unknown + ''' + if (len(unknownCard.rank_img) != 0) and (len(unknownCard.suit_img) != 0): + ''' + Find the difference the unknown card rank image from each of the train rank images, + and store the result with the least difference + ''' + for unknown_rank in rank_training: + + image_difference = cv2.absdiff(unknownCard.rank_img, unknown_rank.img) + rank_difference = int(np.sum(image_difference)/255) + + if rank_difference < best_rank_match_difference: + best_rank_difference_img = image_difference + best_rank_match_difference = rank_difference + best_rank_name = unknown_rank.name + + # Find the difference for suit images and store result + for unknown_suit in suit_training: + + image_difference = cv2.absdiff(unknownCard.suit_img, unknown_suit.img) + suit_difference = int(np.sum(image_difference)/255) + + if suit_difference < best_suit_match_difference: + best_suit_difference_img = image_difference + best_suit_match_difference = suit_difference + best_suit_name = unknown_suit.name + + ''' + Combine best rank and best suit match to get unknown card's identity. + If the best matches have too high of a difference value, card identity + is still unknown + ''' + if (best_rank_match_difference < max_rank_difference): + best_rank_match_name = best_rank_name + + if (best_suit_match_difference < max_suit_difference): + best_suit_match_name = best_suit_name + + # Return the identiy of the card and the quality of the suit and rank match + return best_rank_match_name, best_suit_match_name, best_rank_match_difference, best_suit_match_difference + + +def draw_results(image, unknownCard): + '''Draw the card name, center point, and contour on the image frame.''' + #card_results = [] + + x = unknownCard.center[0] + y = unknownCard.center[1] + cv2.circle(image,(x,y),5,(255,0,0),-1) + + rank_name = unknownCard.best_rank_match + suit_name = unknownCard.best_suit_match + + #card_results = [rank_name, suit_name] + #print(card_results) + + # Draw card name twice, so letters have black outline + cv2.putText(image,(rank_name+' of'),(x-60,y-10),font,1,(0,0,0),3,cv2.LINE_AA) + cv2.putText(image,(rank_name+' of'),(x-60,y-10),font,1,(200,200,200),2,cv2.LINE_AA) + + cv2.putText(image,suit_name,(x-60,y+25),font,1,(0,0,0),3,cv2.LINE_AA) + cv2.putText(image,suit_name,(x-60,y+25),font,1,(200,200,200),2,cv2.LINE_AA) + #print(rank_name+' of', suit_name) + ''' + Can draw difference value for troubleshooting purposes + rank_difference = str(unknownCard.rank_difference) + suit_difference = str(unknownCard.suit_difference) + cv2.putText(image,rank_difference,(x+20,y+30),font,0.5,(0,0,255),1,cv2.LINE_AA) + cv2.putText(image,suit_difference,(x+20,y+50),font,0.5,(0,0,255),1,cv2.LINE_AA) + ''' + return image, rank_name, suit_name + +def flattener(image, pts, w, h): + ''' + Flattens an image of a card into a top-down 200x300 perspective. + Returns the flattened, re-sized, grayed image. + See www.pyimagesearch.com/2014/08/25/4-point-opencv-getperspective-transform-example/ + ''' + temp_rect = np.zeros((4,2), dtype = "float32") + + s = np.sum(pts, axis = 2) + + tl = pts[np.argmin(s)] + br = pts[np.argmax(s)] + + diff = np.diff(pts, axis = -1) + tr = pts[np.argmin(diff)] + bl = pts[np.argmax(diff)] + + # Need to create an array listing points in order of + # [top left, top right, bottom right, bottom left] + # before doing the perspective transform + + if w <= 0.8*h: # If card is vertically oriented + temp_rect[0] = tl + temp_rect[1] = tr + temp_rect[2] = br + temp_rect[3] = bl + + if w >= 1.2*h: # If card is horizontally oriented + temp_rect[0] = bl + temp_rect[1] = tl + temp_rect[2] = tr + temp_rect[3] = br + + # If the card is 'diamond' oriented, a different algorithm + # has to be used to identify which point is top left, top right + # bottom left, and bottom right. + + if w > 0.8*h and w < 1.2*h: #If card is diamond oriented + # If furthest left point is higher than furthest right point, + # card is tilted to the left. + if pts[1][0][1] <= pts[3][0][1]: + # If card is titled to the left, approxPolyDP returns points + # in this order: top right, top left, bottom left, bottom right + temp_rect[0] = pts[1][0] # Top left + temp_rect[1] = pts[0][0] # Top right + temp_rect[2] = pts[3][0] # Bottom right + temp_rect[3] = pts[2][0] # Bottom left + + # If furthest left point is lower than furthest right point, + # card is tilted to the right + if pts[1][0][1] > pts[3][0][1]: + # If card is titled to the right, approxPolyDP returns points + # in this order: top left, bottom left, bottom right, top right + temp_rect[0] = pts[0][0] # Top left + temp_rect[1] = pts[3][0] # Top right + temp_rect[2] = pts[2][0] # Bottom right + temp_rect[3] = pts[1][0] # Bottom left + + + max_width = 200 + max_height = 300 + + # Create destination array, calculate perspective transform matrix, + # and warp card image + dst = np.array([[0,0],[max_width-1,0],[max_width-1,max_height-1],[0, max_height-1]], np.float32) + M = cv2.getPerspectiveTransform(temp_rect,dst) + warp = cv2.warpPerspective(image, M, (max_width, max_height)) + warp = cv2.cvtColor(warp,cv2.COLOR_BGR2GRAY) + + return warp diff --git a/project_imgs/MP4 UML Diagram.pdf b/project_imgs/MP4 UML Diagram.pdf new file mode 100644 index 00000000..b4a920b9 Binary files /dev/null and b/project_imgs/MP4 UML Diagram.pdf differ diff --git a/project_imgs/UMLCropped.png b/project_imgs/UMLCropped.png new file mode 100644 index 00000000..d0e8da7a Binary files /dev/null and b/project_imgs/UMLCropped.png differ diff --git a/project_imgs/housealwayswins.png b/project_imgs/housealwayswins.png new file mode 100644 index 00000000..cf5ed939 Binary files /dev/null and b/project_imgs/housealwayswins.png differ diff --git a/project_imgs/playingcarddetector.png b/project_imgs/playingcarddetector.png new file mode 100644 index 00000000..506e55b7 Binary files /dev/null and b/project_imgs/playingcarddetector.png differ diff --git a/project_imgs/postdealing.png b/project_imgs/postdealing.png new file mode 100644 index 00000000..132baf31 Binary files /dev/null and b/project_imgs/postdealing.png differ diff --git a/project_imgs/sometimestheplayerwins.png b/project_imgs/sometimestheplayerwins.png new file mode 100644 index 00000000..abe7fcfb Binary files /dev/null and b/project_imgs/sometimestheplayerwins.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..107cad51 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +opencv-python==3.4.0.12 +numpy==1.14.0 +unicards==0.6