diff --git a/Project Proposal.md b/Project Proposal.md new file mode 100644 index 00000000..2d0f712c --- /dev/null +++ b/Project Proposal.md @@ -0,0 +1,21 @@ +# Drawing with Fingers +The main idea of our project is to have an interface where someone can use a webcam to trace their finger and use their finger to paint on the screen. We will do a lot of image processing and finger tracking work with OpenCV. + +## _Minimal Viable Product_: +* A display that can trace finger movement in black on a white drawing canvas +## _Stretch Goal_: +* Having a way to turn off the paint so it’s not continuous +* Having two paint brushes/brushes whose hue can be tuned in real time + +## Learning Goals for each person: +Tommy: I want to get better at programming with another person, and also with making sure that I use Git properly. +Josh: My goal is to get to use OpenCV more which I have evaded ever since the beginning of last semester. + +## Possible Libraries to Use: +The libraries that we will probably be using are OpenCV and Pygame. OpenCV will be how we can utilize the webcams on laptops to keep track of finger movements in the video frame. Pygame will be used to creative an interactive drawing canvas that will put down colors based on the finger movements. The saturation of the colors will stay constant. Different finger speed will then result in different values. We are hoping to incorporate the other hand/fingers in an interesting way so that we can adjust the hue of the brushes. + +## Goal before the mid-project check-in +By mid-project check in, we plan on having a skeleton of our code decided, and have a plan for how to implement tracking the finger. We also plan on knowing if pygame and OpenCV have all the resources we need for this project. + +## Risks??? +The biggest risks might include if we don’t plan out our implementation well and if we have a lot of difficulty with classes. diff --git a/ProjectReflection.md b/ProjectReflection.md new file mode 100644 index 00000000..53a539cd --- /dev/null +++ b/ProjectReflection.md @@ -0,0 +1,49 @@ +# Interactive Programming Project Reflection + +This is the base repo for the interactive programming project for Software Design, Spring 2018 at Olin College. + +You need to install OpenCV for this project. Run this line to install: `$ pip install opencv-python` + +#### To run the program +To run the program, please either download zip or type in `git clone https://github.com/Tweir195/InteractiveProgramming.git` in the command line. To run the program, please type `python main.py` in the command line. + +Our program allows for three different modes, which you can specify through the few additional commands in the command line: + +`-t`: The trailing mode where the line you control is of fixed length, so older points after a certain number of new points are drawn will disappear. This is the default mode in which the program will run. + +`-g`: The gaming mode in which you try to hit colored boxes and get scores and added length to your line upon hittng the boxes. + +`-l` or `--length`: An integer following this command will specify the starting length of the line in the trailing mode and the gaming mode. The default starting length is 3. + +`-d` or `--draw`: The drawing mode where the line you control will stay on the screen as long as you haven't quit. + +When the program is running, there are several keyboard interactions that you can use: Key `q` would allow you to quit the program; key `s` would allow you to save the current canvas; key `t` would allow you to toggle trailing ON or OFF. + +## Project Overview +In this interactive programming project, we created a program that uses OpenCV to track a red object and makes up a drawing app by drawing in a canvas where the red object has been. There are a few modes that you can choose from: a game, a drawing canvas, and a canvas with a disappearing line. + +## Results +The most basic function of our program is to draw a tailing line. Our program would first filter out the color red in the whole camera frame. After identifying the contour in the filtered red frame, the program records the coordinate of the center of contours as a point to draw lines with. In the end, we would have a list of such points. By drawing a line between every two consecutive points in the list, we would end up with the trajectory of the red object in the frame. The way we maintain the list is by appending new points to the list until the designated length has been reached and then just shifting out the first element in the list and appending the new point to the list. + +![alt text](https://github.com/Tweir195/InteractiveProgramming/blob/master/drawing.jpg) + +At the same time, there's the option to make all the points stay on the canvas. To implement this, the only slight variation is that we would keep appending the coordinates to the list of all the points instead of shifting out the first one every time. + +![alt text](https://github.com/Tweir195/InteractiveProgramming/blob/master/gaming.jpg) + +The gaming mode is just a step up from the simple trailing mode. When the gaming mode is enabled, the canvas class would place a randomly colored rectangle at a random location inside canvas. Upon a point falling inside the drawn rectangle, users will be given ten points, and the tail of the line will be made longer. The current rectangle disappears while a new one shows up at another randomly chosen location. At the start of the program, the current time of the program is also recorded. When a countdown from 30 finishes, the program will freeze gaming and display the scores that the user receives in this round.Whenever one second elapses, the countdown in the game mode will decrease by one. + +## Implementation: still needs a UML +Our program includes two classes: `canvas` and `fingerTrack`. The `canvas` class creates a canvas to be drawn on. The initialization of `canvas` involves three input parameters: the width of the canvas, the height, and the scaler to enlarge the canvas by. The width and the height are based off the size of the camera frame. The scaler is always 2 except in the gaming mode where a scaler of 1 is applied to allow for better accuracy when playing the game. The `fingerTrack` class performs color tracking and actual drawing on the canvas. Each location of the points to be drawn are also scaled with the scaler in the `canvas` class. It also takes the `canvas` class as an input parameter when drawing new points and lines. + +One of our most important data structures is a list. We use lists to store such data as the coordinates of the center of the contours and the colors we are using for the lines between two consecutive points. We chose lists because they are mutable and have built-in operations like `append`. As a result, instead of using a data type to store a large number of points that have existed in the past, we used lists so that every time a point comes in, we can simply shift out the first one and append the new element to the end of the list, thus making a big save on memory. + +In our beginning planning of the project, after we had decided on using OpenCV to make a drawing platform, we had a few options. We thought about trying to make a game similar to the snake game, but the snake was controlled by the web-cam on the computer. We also thought about just making a canvas on the screen that could be drawn on by the web-cam, and have the color be controlled by the speed of movement. We were unsure about how easily we would be able to make the snake game, and decided on making the drawing canvas. However, we finished this with plenty of time, and expanded into a game in addition to the drawing canvas. + +UML diagram: +![alt text](https://github.com/Tweir195/InteractiveProgramming/blob/master/UML_Project_4.jpg) + +## Reflection +One thing that would have helped us a lot was having a more appropriately scoped project that takes better advantage of classes. We ended up doing a cool project, but because we started in an easier project, we had to keep finding ways to expand. This made us experiment with the code, without realizing we should have made a separate class for each mode. We could improve it by optimizing it so that there is less lag time and also so that it is better at picking up only the item that we want (this will avoid the jagged lines that can happen when there is interference). + +One of the good things about the experience is that we used both pair programming and divide-and-conquer in the process. Our approach in dividing and conquer was based on the next tasks that need to get done instead of which files or classes specifically so that both us have a good understanding of the program as a whole. We were able to move along in the project quite steadily, not feeling stressed about not finishing our project. diff --git a/README.md b/README.md index 5f822327..7f67e55a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# InteractiveProgramming +### Our initial project proposal can be found [here](https://github.com/QingmuDeng/InteractiveProgramming/blob/master/Project%20Proposal.md). +### Our project reflection can be found [here](https://github.com/Tweir195/InteractiveProgramming/blob/master/ProjectReflection.md). + +You need to install OpenCV for this project. Run this line to install: `$ pip install opencv-python` + +# Interactive Programming + This is the base repo for the interactive programming project for Software Design, Spring 2018 at Olin College. + +You need to install OpenCV for this project. Run this line to install: `$ pip install opencv-python` + +#### To run the program +To run the program, please either download zip or type in `git clone https://github.com/Tweir195/InteractiveProgramming.git` in the command line. To run the program, please type `python main.py` in the command line. + +Our program allows for three different modes, which you can specify through the few additional commands in the command line: + +`-t`: The trailing mode where the line you control is of fixed length, so older points after a certain number of new points are drawn will disappear. This is the default mode in which the program will run. + +`-g`: The gaming mode in which you try to hit colored boxes and get scores and added length to your line upon hittng the boxes. + +`-l` or `--length`: An integer following this command will specify the starting length of the line in the trailing mode and the gaming mode. The default starting length is 3. + +`-d` or `--draw`: The drawing mode where the line you control will stay on the screen as long as you haven't quit. + +When the program is running, there are several keyboard interactions that you can use: Key `q` would allow you to quit the program; key `s` would allow you to save the current canvas; key `t` would allow you to toggle trailing ON or OFF. diff --git a/UML_Project_4.jpg b/UML_Project_4.jpg new file mode 100644 index 00000000..5d7adb12 Binary files /dev/null and b/UML_Project_4.jpg differ diff --git a/canvas.py b/canvas.py new file mode 100644 index 00000000..8706c837 --- /dev/null +++ b/canvas.py @@ -0,0 +1,98 @@ +import numpy as np +import cv2 +import random + + +class canvas(): + + def __init__(self, width, height, scaler): + """Initizes a canvas class with following attributes + + width: the width of the drawing canvas given in pixels + height: the height of the drawing canvas given in pixels + new_canvas: a numpy array of zeros with depth of 3 + randx: a range of x pixel value to choose from + randy: a range of y pixel value to choose from + colorlist: white,red, green, blue, yellow, purple, orange to randomly choose from + boxsize: the size of the box appearing in the gaming mode + points: the score a user has earned + value: the score given to a user upon hitting one rectangle + run: a boolean that makes sure boxes appear and disappear accordingly + """ + self.screen_scaler = scaler + self.width = int(width) * self.screen_scaler + self.height = int(height) * self.screen_scaler + self.new_canvas = np.zeros((self.height, self.width, 3), np.uint8) + self.randx = np.linspace(10,580*self.screen_scaler) + self.randy = np.linspace(10,380*self.screen_scaler) + self.colorlist = [(255,255,255), (0,0,255), (0,255,0), (255,0,0), (0,255,255), (255,0,188), (0,15,255)] + self.boxsize = 30 + self.points = 0 + self.value = 10 + self.run = False + + def set_color(self, B, G, R): + """Stores the BGR value + """ + self.color = (B, G, R) + + def set_bgColor(self): + """Applies the BGR value to the drawing canvas + """ + self.new_canvas[:, :] = self.color + + def show_canvas(self): + """Displays a canvas on the screen + """ + cv2.imshow('newCanvas', self.new_canvas) + + def save_drawing(self): + """This function allows users to save their drawing with a name of + their choice. + """ + file_name = input('Please name your drawing: ') + cv2.imwrite(file_name+'.jpg', self.new_canvas) + + def clear(self): + """This function clears the screen. + """ + canvas.new_canvas = np.zeros((self.height, self.width, 3), np.uint8) + + def make_rect(self): + self.xpos = int(random.choice(self.randx)) + self.ypos = int(random.choice(self.randy)) + self.color = random.choice(self.colorlist) + + def show_rect(self): + """Draws a rectangle in the gaming mode. + """ + cv2.rectangle(self.new_canvas, (self.xpos, self.ypos), (self.xpos+self.boxsize,self.ypos+self.boxsize), self.color, 3) + self.run = True + + def in_rect(self, pointx, pointy): + """Decides whether a point is within a rectangle + + pointx: x pixel position of the point + pointy: y pixel position of the point + """ + if self.xpos>> map(1, 0, 10, 0, 200) + 20 + >>> map(50, 0, 150, 0, 255) + 85 + """ + return int(((x - oldL)/(oldH-oldL))*(newH-newL)+newL) + + def brush_color(self, hue): + """This function takes in a uint8 value for hue and generate + a BGR color range """ + color = np.uint8([[[hue, 255, 255]]]) + return cv2.cvtColor(color, cv2.COLOR_HSV2BGR) + + def BGR2HSV(self, frame): + """This functions takes in a frame and converts it from BGR + to HSV values + """ + return cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) + + def red_mask(self, frame): + """This function generates a red mask based on the frame being + passed in + """ + mask1 = cv2.inRange(frame, self.red_maskL[0], self.red_maskH[0]) + mask2 = cv2.inRange(frame, self.red_maskL[1], self.red_maskH[1]) + return (mask1 | mask2) + + def shift(self, myList, myElement): + """Shift out the first element in this list and append the new element to the end + + >>> shift([1, 2, 3], 5) + [2, 3, 5] + """ + return myList[1:] + [myElement] + + def find_center(self, mask, target, canvas, disappr=True): + """This function takes in a cv2 mask, find the center of the + contours in the mask, and draw a green dot at the center location + on the target frame + """ + im2, contours, hierarchy = cv2.findContours(mask, 1, 2) + try: + if self.frame_num > self.refreshDelay or self.frame_num == 0: + cnt = contours[0] + M = cv2.moments(cnt) + #print(M['m10'] / M['m00']) + self.cx = int(M['m10'] / M['m00']) * canvas.screen_scaler + self.cy = int(M['m01'] / M['m00']) * canvas.screen_scaler + self.frame_num = 0 + if len(self.path) <= 1: + self.path.append((self.cx, self.cy)) + elif len(self.path) >= 2: + # Calculate the distance between the two newest point. + pair = self.path[-1] + diffx = abs(self.cx-pair[0]) + diffy = abs(self.cy-pair[1]) + distance = math.sqrt(diffx**2+diffy**2) + if distance < 150: + # print('Far enough') + if len(self.path) < self.pathlength: + self.path.append((self.cx, self.cy)) + else: + if disappr: + self.path = self.shift(self.path, (self.cx, self.cy)) + else: + self.path.append((self.cx, self.cy)) + dist2hue = self.map(distance, 0.0, 150.0, 0.0, 255.0) + paintColor = self.brush_color(dist2hue) + if len(self.colors) < self.pathlength: + self.colors.append((int(paintColor[0][0][0]), int(paintColor[0][0][1]), int(paintColor[0][0][2]))) + else: + if disappr: + self.colors = self.shift(self.colors, (int(paintColor[0][0][0]), int(paintColor[0][0][1]), int(paintColor[0][0][2]))) + else: + self.colors.append((int(paintColor[0][0][0]), int(paintColor[0][0][1]), int(paintColor[0][0][2]))) + # print(self.colors) + cv2.circle(target, (self.cx, self.cy), 2, (0, 255, 0), -1) + self.notFound = False + except IndexError: + """""" + self.notFound = True + + def draw(self, canvas): + """This function draws the lines on the canvas of the screen. + The default is that only the 20 newest points will be drawn on screen. + """ + canvas.new_canvas = np.zeros((canvas.height, canvas.width, 3), np.uint8) + for i in range(len(self.path)): + if len(self.path) <= 1: + break + + else: + if i < len(self.path)-2: + cv2.line(canvas.new_canvas, self.path[i], self.path[i+1], self.colors[i], 3) + + if len(self.path) > 4: + def det(p1, p2, p3, p4): + deltaA = p1[0] - p2[0] + deltaB = p1[1] - p2[1] + deltaC = p3[0] - p4[0] + deltaD = p3[1] - p4[1] + return deltaA * deltaD - deltaB * deltaC + div = det(self.path[-1], self.path[-2], self.path[-3], self.path[-4]) + if div != 0 and not self.notFound: + """""" + # print('the two line intersects!!!') diff --git a/gaming.jpg b/gaming.jpg new file mode 100644 index 00000000..093f6c60 Binary files /dev/null and b/gaming.jpg differ diff --git a/main.py b/main.py new file mode 100644 index 00000000..cdf00749 --- /dev/null +++ b/main.py @@ -0,0 +1,111 @@ +from fingerTrack import * +from canvas import * +import cv2 +from optparse import OptionParser +import time + +def init_opts(): + """ + This function runs the different modes of our program with input in the + command line + """ + parser = OptionParser() + parser.add_option("-d", action="store_false", + dest="disappr", default=True, + help="To run the program in the drawing mode") + parser.add_option("-t", action="store_true", + dest="disappr", default=True, + help="To run the program in the tailing mode where the lines disappear after a period of time") + parser.add_option("-g", action="store_true", + dest="game", default=False, + help="To run the program in the gaming mode where you try to hit boxes") + parser.add_option("-l", "--length", action="store", type='int', + dest="length", default=3, + help="The starting length of the line") + options, args = parser.parse_args() + return options, args + + +def main(): + """ This function puts together the methods from classes in other files to + run the program + """ + + font = cv2.FONT_HERSHEY_SIMPLEX + options, args = init_opts() + track = finger_track() + cap = cv2.VideoCapture(0) + # this makes the canvas larger for all modes except for the game because the + # game needs a smaller canvas to be less frustrating to play + scaler = 2 + if options.game: + scaler = 1 + newCanvas = canvas(cap.get(3), cap.get(4), scaler) + disappr = options.disappr + track.pathlength = options.length + game_time = 30 + current_time = 1 + start = time.time() + while True: + if time.time() - start > 1 and options.game: + current_time += 1 + start = time.time() + # Display the page that shows your score when the time is up until you + # press the q key + if current_time == game_time+1: + while True: + cv2.putText(newCanvas.new_canvas, 'Yay!!!', (int(newCanvas.width/2-100), int(newCanvas.height/2)), font, 3, (255, 0, 0), 2) + cv2.putText(newCanvas.new_canvas, 'Your final score is:', (int(newCanvas.width/2-300), int(newCanvas.height/2+50)), font, 2, (0, 0, 255), 2) + cv2.putText(newCanvas.new_canvas, str(newCanvas.points)+'!!!', (int(newCanvas.width/2-75), int(newCanvas.height/2+50)+75), font, 3, (0, 255, 0), 2) + newCanvas.show_canvas() + if cv2.waitKey(1) & 0xFF == ord('q'): + break + break + ret, frame = cap.read() + frame = cv2.flip(frame, 1) + + # This section tracks the color red that is in the frame + hsv = track.BGR2HSV(frame) + redMask = track.red_mask(hsv) + mask = cv2.bilateralFilter(redMask, 10, 40, 40) + mask = cv2.blur(mask, (5, 5)) + res = cv2.bitwise_and(frame, frame, mask=redMask) + cv2.imshow('original', res) + mask = cv2.blur(mask, (20, 20)) + track.find_center(mask, frame, newCanvas, disappr=disappr) + track.draw(newCanvas) + + # This section runs the game option if it was selected + if options.game: + if newCanvas.points == 0: + if newCanvas.run == False: + newCanvas.make_rect() + newCanvas.show_rect() + flag = newCanvas.in_rect(track.cx, track.cy) + if flag == True: + newCanvas.addpoints(track) + newCanvas.clear() + newCanvas.show_rect() + newCanvas.show_rect() + cv2.putText(newCanvas.new_canvas, 'Time left: '+str(game_time-current_time), (0, 15), font, .5, (255, 255, 255), 1) + newCanvas.show_canvas() + + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + if cv2.waitKey(1) & 0xFF == ord('s'): + newCanvas.save_drawing() + break + if cv2.waitKey(1) & 0xFF == ord('d'): + if disappr: + disappr = False + else: + disappr = True + if cv2.waitKey(1) & 0xFF == ord('c'): + newCanvas.clear() + + cap.release() + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() diff --git a/paint.py b/paint.py new file mode 100644 index 00000000..a936e7cc --- /dev/null +++ b/paint.py @@ -0,0 +1,134 @@ + +import cv2 +import numpy as np +import math + +"""capturing video from camera""" +cap = cv2.VideoCapture(0) +frame_num = 0 +cx = 0 +cy = 0 +path = [] +clearpath = [] +dist = [] +colors = [] + + +def map(x, oldL, oldH, newL, newH): + """This function maps a value from one range to a differnet range + + x: the value in the old range + oldL: the lower limit of the old range of values + oldH: the upper limit of the old range of values + newL: the lower limit of the new range of values + newH: the upper limit of the new range of values + """ + return int(((x - oldL)/(oldH-oldL))*(newH-newL)+newL) + +def brush_color(hue): + """This function takes in a uint8 value for hue and generate + a BGR color range """ + color = np.uint8([[[hue, 255, 255]]]) + return cv2.cvtColor(color, cv2.COLOR_HSV2BGR) + +while(True): + #capture frame by frame + ret, frame = cap.read() + frame = cv2.flip(frame,1) + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) + #find specific color + lower_white = np.array([0, 0, 230]) + upper_white = np.array([180, 25, 255]) + lower_color = np.array([0,80,50]) + upper_color = np.array([20,100,100]) + lower_red = np.array([150,150,50]) + upper_red = np.array([180,255,150]) + + + mask1 = cv2.inRange(hsv, np.array([0, 150, 100]), np.array([1, 255, 255])); + mask2 = cv2.inRange(hsv, np.array([178, 150, 100]), np.array([180, 255, 255])); + mask = mask1 | mask2 + + # Attempting Green + # mask = cv2.inRange(hsv, np.array([50,100,100]), np.array([65,255,255])) + + mask = cv2.bilateralFilter(mask, 10, 40, 40) + mask = cv2.blur(mask,(5,5)) + + res = cv2.bitwise_and(frame,frame,mask=mask) + mask = cv2.blur(mask,(20,20)) + # Getting a contour and the center of the contour + im2,contours,hierarchy = cv2.findContours(mask, 1, 2) + try: + if frame_num > 0 or frame_num == 0: + cnt = contours[0] + M = cv2.moments(cnt) + # print(M['m10']/M['m00']) + cx = int(M['m10']/M['m00']) + # print(cx) + cy = int(M['m01']/M['m00']) + # print(cy) + frame_num = 0 + path.append((cx, cy)) + # if diffx > 100 or diffy > 100: + # print('too far') + # else: + cv2.circle(res, (cx, cy), 2, (0, 255, 0), -1) + except IndexError: + """""" + + # TODO: Add if statements to make sure that any outliers + # would be removed from the list or ignored when drawing + # the linewidth + if len(path) == 1: + clearpath.append(path[0]) + elif len(path) > 2: + pair = path[-2] + # print(pair, pair[0], cx, pair[1], cy) + diffx = abs(cx-pair[0]) + diffy = abs(cy-pair[1]) + distance = math.sqrt(diffx**2+diffy**2) + if distance<10: + dist2hue = map(distance, 0.0, 10.0, 0.0, 255.0) + paintColor = brush_color(dist2hue) + print(paintColor[0][0][0]) + colors.append((int(paintColor[0][0][0]), int(paintColor[0][0][1]), int(paintColor[0][0][2]))) + print(colors) + clearpath.append(pair) + + for i in range(len(clearpath)): + if len(clearpath) < 1: + break + elif i<(len(clearpath)-1)<21: + cv2.line(res, clearpath[i], clearpath[i+1], colors[i], 3) + elif 20 < i < (len(clearpath)-1): + cv2.rectangle(res, (0,0), (600, 400), (0,0,0)) + for j in range(20): + cv2.line(res, clearpath[-(j+1)], clearpath[-(j+2)], colors[-(j+2)], 3) + + + # print(dist) + frame_num += 1 + # cnts = cv2.findContours(res, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) + #display the resulting frame + cv2.imshow('frame',frame) + #cv2.imshow('mask', mask) + cv2.imshow('res',res) + # cv2.imshow('counto', ) + if cv2.waitKey(1) & 0xFF == ord('q'): + break +#when finshed, release the capture +cap.release() +cv2.destroyAllWindows() +"""playing video from file +cap = cv2.VideoCapture('test.avi') + +while(cap.isOpened()): + ret, frame = cap.read() + cv2.waitKey(25) + cv2.imshow('frame',frame) + if cv2.waitKey(1) & 0xFF == ord('q'): + break +cap.release() +cv2.destroyAllWindows() +"""