diff --git a/.gitignore b/.gitignore index 70e0ffb..7a21adc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Images and videos +*.jpg +*.jpeg +*.png +*.mp4 + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 419b92f..0581966 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# opencv-draw-tools +# cv2_tools Library to help the drawing process with OpenCV. Thought to add labels to the images. Classification of images, etc. ![image](https://user-images.githubusercontent.com/18369529/53686731-3dba0500-3d2b-11e9-95e5-e4517c013d14.png) @@ -24,21 +24,70 @@ Finally you can install the library with: When you install `opencv-draw-tools`, it will automatically download `numpy` but not opencv becouse in some cases you will need another version. -## Usage - -### Test +## Test ``` -import opencv_draw_tools as cv2_tools +import cv2_tools print('Name: {}\nVersion:{}\nHelp:{}'.format(cv2_tools.name,cv2_tools.__version__,cv2_tools.help)) -cv2_tools.webcam_test() +webcam_test() +``` + +## Ussage and Important classes + +### ManagerCV2 + +``` +from cv2_tools.Management import ManagerCV2 +``` + +If you want to work with video or stream, this class will help you mantain your code cleaner while you get more features. + +For example: + - Open a a stream (your webcam). + - Reproduce it on real time with max FPS equals to 24. + - Press `esc` to finish the program. + - At the end print average FPS. + +``` +from cv2_tools.Managment import ManagerCV2 +import cv2 + +# keystroke=27 is the button `esc` +manager_cv2 = ManagerCV2(cv2.VideoCapture(0), is_stream=True, keystroke=27, wait_key=1, fps_limit=60) + + # This for will manage file descriptor for you + for frame in manager_cv2: + cv2.imshow('Example easy manager', frame) + cv2.destroyAllWindows() + print(manager_cv2.get_fps()) +``` + +If you want to use another button and you don't know the ID, you can check easily using the following code: + +``` +from cv2_tools.Managment import ManagerCV2 +import cv2 + +# keystroke=27 is the button `esc` +manager_cv2 = ManagerCV2(cv2.VideoCapture(0), is_stream=True, keystroke=27, wait_key=1, fps_limit=60) + + # This for will manage file descriptor for you + for frame in manager_cv2: + # Each time you press a button, you will get its id in your terminal + last_keystroke = manager_cv2.get_last_keystroke() + if last_keystroke != -1: + print(last_keystroke) + cv2.imshow('Easy button checker', frame) + cv2.destroyAllWindows() ``` -### Oriented Object Programming method + +### SelectorCV2 Firstly create a SelectorCV2 object. You can pass it optional parameters to configure the output. ``` -selector = cv2_tools.SelectorCV2(color=(200,90,0), filled=True) +from cv2_tools.Selection import SelectorCV2 +selector = SelectorCV2(color=(200,90,0), filled=True) ``` Also you can configure it later using the method (all optional parameters): @@ -80,70 +129,4 @@ selector.set_range_valid_rectangles( origin, destination) set_valid_rectangles(indexes) ``` -If you want, you can see the example [detect_faces.py](opencv_draw_tools/detect_faces.py), it also use an open source library called `face_recognition`. - - -### Manual method - -``` -import opencv_draw_tools as cv2_tools - - -""" - Draw better rectangles to select zones. - Keyword arguments: - frame -- opencv frame object where you want to draw - position -- touple with 4 elements (x1, y1, x2, y2) - This elements must be between 0 and 1 in case it is normalized - or between 0 and frame height/width. - tags -- list of strings/tags you want to associate to the selected zone (default []) - tag_position -- position where you want to add the tags, relatively to the selected zone (default None) - If None provided it will auto select the zone where it fits better: - - First try to put the text on the Bottom Rigth corner - - If it doesn't fit, try to put the text on the Bottom Left corner - - If it doesn't fit, try to put the text Inside the rectangle - - Finally if it doesn't fit, try to put the text On top of the rectangle - alpha -- transparency of the selected zone on the image (default 0.9) - 1 means totally visible and 0 totally invisible - color -- color of the selected zone, touple with 3 elements BGR (default (110,70,45) -> dark blue) - BGR = Blue - Green - Red - normalized -- boolean parameter, if True, position provided normalized (between 0 and 1) else you should provide concrete values (default False) - thickness -- thickness of the drawing in pixels (default 2) - filled -- boolean parameter, if True, will draw a filled rectangle with one-third opacity compared to the rectangle (default False) - peephole -- boolean parameter, if True, also draw additional effect, so it looks like a peephole -""" -frame = cv2_tools.select_zone(frame, position, tags=[]) -``` - -### Example with Webcam - -``` -import opencv_draw_tools as cv2_tools -cv2_tools.webcam_test() -``` - -See `webcam_test()` code: - -``` -def webcam_test(): - """Reproduce Webcam in real time with a selected zone.""" - print('Launching webcam test') - cap = cv2.VideoCapture(0) - f_width = cap.get(3) - f_height = cap.get(4) - window_name = 'opencv_draw_tools' - while True: - ret, frame = cap.read() - frame = cv2.flip(frame, 1) - if ret: - keystroke = cv2.waitKey(1) - position = (0.33,0.2,0.66,0.8) - tags = ['MIT License', '(C) Copyright\n Fernando\n Perez\n Gutierrez'] - frame = select_zone(frame, position, tags=tags, color=(130,58,14), thickness=2, filled=True, normalized=True) - cv2.imshow(window_name, frame) - # True if escape 'esc' is pressed - if keystroke == 27: - break - cv2.destroyAllWindows() - cv2.VideoCapture(0).release() -``` +If you want, you can see the example [detect_faces.py](examples/detect_faces.py), it also use an open source library called `face_recognition`. diff --git a/cv2_tools/LICENSE b/cv2_tools/LICENSE new file mode 100644 index 0000000..ea5b606 --- /dev/null +++ b/cv2_tools/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/cv2_tools/Management.py b/cv2_tools/Management.py new file mode 100644 index 0000000..3d7ec22 --- /dev/null +++ b/cv2_tools/Management.py @@ -0,0 +1,75 @@ +# MIT License +# Copyright (c) 2019 Fernando Perez +import time +import cv2 + +# TODO: Document ManagerCV2 +class ManagerCV2(): + + _tries_reconnect_stream = 10 + + def __init__(self, video, is_stream=False, keystroke=-1, wait_key=-1, fps_limit=0): + self.video = video + self.is_stream = is_stream + self.stream = video + self.fps_limit = fps_limit + + self.keystroke = keystroke + self.wait_key = wait_key + + self.last_keystroke = -1 + self.initial_time = None + self.final_time = None + self.count_frames = 0 + + + def __iter__(self): + self.initial_time = time.time() + self.last_frame_time = self.initial_time + self.count_frames = 0 + self.last_keystroke = -1 + return self + + + def __next__(self): + ret, frame = self.video.read() + self.final_time = time.time() + + if self.is_stream: + for i in range(ManagerCV2._tries_reconnect_stream): + ret, frame = self.video.read() + if ret: + break + if i+1 == ManagerCV2._tries_reconnect_stream: + self.end_iteration() + elif not ret: + self.end_iteration() + + if self.wait_key != -1: + self.last_keystroke = cv2.waitKey(self.wait_key) + if self.last_keystroke == self.keystroke: + self.end_iteration() + + self.count_frames += 1 + + # Here we limit the speed (if we want constant frames) + if self.fps_limit: + time_to_sleep = (1 / self.fps_limit) - (time.time() - self.last_frame_time) + if time_to_sleep > 0: + time.sleep(time_to_sleep) + + self.last_frame_time = time.time() + return frame + + + def get_last_keystroke(self): + return self.last_keystroke + + + def end_iteration(self): + self.video.release() + raise StopIteration + + + def get_fps(self): + return round(self.count_frames / (self.final_time - self.initial_time),3) diff --git a/cv2_tools/README.md b/cv2_tools/README.md new file mode 100644 index 0000000..32d46ee --- /dev/null +++ b/cv2_tools/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/cv2_tools/Selection.py b/cv2_tools/Selection.py new file mode 100644 index 0000000..391d9ae --- /dev/null +++ b/cv2_tools/Selection.py @@ -0,0 +1,123 @@ +# MIT License +# Copyright (c) 2019 Fernando Perez +import cv2 + +from cv2_tools.utils import * + + +# TODO: Document SelectorCV2 +class SelectorCV2(): + + + def __init__(self, alpha=0.9, color=(110,70,45), normalized=False, thickness=2, + filled=False, peephole=True, margin=5, closed_polygon=False): + self.zones = [] + self.polygon_zones = [] + self.all_tags = [] + # Visual parameters + self.alpha = alpha + self.color = color + self.normalized = normalized + self.thickness = thickness + self.filled = filled + self.peephole = peephole + self.margin = margin + # Polygon + self.closed_polygon = closed_polygon + + + def set_properties(self, alpha=None, color=None, normalized=None, + thickness=None, filled=None, peephole=None, + margin=None): + if alpha is not None: + self.alpha = alpha + if color is not None: + self.color = color + if normalized is not None: + self.normalized = normalized + if thickness is not None: + self.thickness = thickness + if filled is not None: + self.filled = filled + if peephole is not None: + self.peephole = peephole + if margin is not None: + self.margin = margin + + + def add_zone(self, zone, tags=None): + self.zones.append(zone) + if tags and type(tags) is not list: + tags = [tags] + elif not tags: + tags = [] + self.all_tags.append(tags) + + + def add_polygon(self, polygon, surrounding_box=False, tags=None): + if not polygon: + return + + self.polygon_zones.append(polygon) + + if surrounding_box: + min_x, min_y, max_x, max_y = polygon[0][0], polygon[0][1], 0, 0 + for position in polygon: + if position[0] < min_x: + min_x = position[0] + if position[0] > max_x: + max_x = position[0] + if position[1] < min_y: + min_y = position[1] + if position[1] > max_y: + max_y = position[1] + + self.zones.append((min_x, min_y, max_x, max_y)) + + if tags and type(tags) is not list: + tags = [tags] + elif not tags: + tags = [] + self.all_tags.append(tags) + + + def set_range_valid_rectangles(self, origin, destination): + self.zones = self.zones[origin:destination] + self.all_tags = self.all_tags[origin:destination] + + + def set_valid_rectangles(self, indexes): + # This if is just for efficiency + if not indexes: + self.zones = [] + self.all_tags = [] + return + + for i in range(len(self.zones)): + if i not in indexes: + self.zones.pop(i) + self.all_tags.pop(i) + + + def draw(self, frame, fx=1, fy=1, interpolation=cv2.INTER_LINEAR): + next_frame = select_multiple_zones( + frame.copy(), + self.zones, + all_tags=self.all_tags, + alpha=self.alpha, + color=self.color, + normalized=self.normalized, + thickness=self.thickness, + filled=self.filled, + peephole=self.peephole, + margin=self.margin) + + next_frame = select_polygon( + next_frame, + all_vertexes=self.polygon_zones, + color=self.color, + thickness=self.thickness, + closed=self.closed_polygon + ) + + return cv2.resize(next_frame, (0,0), fx=fx, fy=fy, interpolation=interpolation) diff --git a/opencv_draw_tools/__init__.py b/cv2_tools/__init__.py similarity index 68% rename from opencv_draw_tools/__init__.py rename to cv2_tools/__init__.py index 8ecbeb9..9288a8b 100644 --- a/opencv_draw_tools/__init__.py +++ b/cv2_tools/__init__.py @@ -1,13 +1,12 @@ -from opencv_draw_tools.SelectZone import * -import opencv_draw_tools.tags_constraint +from cv2_tools.utils import webcam_test, get_complete_help -name = 'opencv_draw_tools' +name = 'cv2_tools' help = ''' MIT License Copyright (c) 2019 Fernando Perez For more information visit: https://github.com/fernaper/opencv-draw-tools Also you can write complete_help to view full information''' -__version__ = '1.2.0' +__version__ = '2.0.2' complete_help = ''' {} - v{} diff --git a/opencv_draw_tools/tags_constraint.py b/cv2_tools/tags_constraint.py similarity index 100% rename from opencv_draw_tools/tags_constraint.py rename to cv2_tools/tags_constraint.py diff --git a/opencv_draw_tools/SelectZone.py b/cv2_tools/utils.py similarity index 86% rename from opencv_draw_tools/SelectZone.py rename to cv2_tools/utils.py index 0d654c0..f610bb1 100644 --- a/opencv_draw_tools/SelectZone.py +++ b/cv2_tools/utils.py @@ -4,7 +4,8 @@ import sys import cv2 -from opencv_draw_tools.tags_constraint import * +from cv2_tools.tags_constraint import * + """ You can change it. @@ -14,86 +15,10 @@ """ IGNORE_ERRORS = False -# TODO: Document SelectorCV2 -class SelectorCV2(object): - - - def __init__(self, alpha=0.9, color=(110,70,45), normalized=False, thickness=2, filled=False, peephole=True, margin=5): - self.zones = [] - self.all_tags = [] - # Visual parameters - self.alpha = alpha - self.color = color - self.normalized = normalized - self.thickness = thickness - self.filled = filled - self.peephole = peephole - self.margin = margin - - - def set_properties(self, alpha=None, color=None, normalized=None, - thickness=None, filled=None, peephole=None, - margin=None): - if alpha is not None: - self.alpha = alpha - if color is not None: - self.color = color - if normalized is not None: - self.normalized = normalized - if thickness is not None: - self.thickness = thickness - if filled is not None: - self.filled = filled - if peephole is not None: - self.peephole = peephole - if margin is not None: - self.margin = margin - - - def add_zone(self, zone, tags=None): - self.zones.append(zone) - if tags and type(tags) is not list: - tags = [tags] - elif not tags: - tags = [] - self.all_tags.append(tags) - - - def set_range_valid_rectangles(self, origin, destination): - self.zones = self.zones[origin:destination] - self.all_tags = self.all_tags[origin:destination] - - - def set_valid_rectangles(self, indexes): - # This if is just for efficiency - if not indexes: - self.zones = [] - self.all_tags = [] - return - - for i in range(len(self.zones)): - if i not in indexes: - self.zones.pop(i) - self.all_tags.pop(i) - - - def draw(self, frame): - new_frame = select_multiple_zones( - frame, - self.zones, - all_tags=self.all_tags, - alpha=self.alpha, - color=self.color, - normalized=self.normalized, - thickness=self.thickness, - filled=self.filled, - peephole=self.peephole, - margin=self.margin) - return new_frame - def eprint(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) + if not IGNORE_ERRORS: + print(*args, file=sys.stderr, **kwargs) def get_lighter_color(color): @@ -236,10 +161,8 @@ def add_tags(frame, position, tags, tag_position=None, alpha=0.75, color=(20, 20 else: valid = ['bottom_right', 'bottom_left', 'inside', 'top'] if tag_position not in ['bottom_right', 'bottom_left', 'inside', 'top']: - if not IGNORE_ERRORS: - raise ValueError('Error, invalid tag_position ({}) must be in: {}'.format(tag_position, valid)) - else: - tag_position = 'bottom_right' + eprint('Error, invalid tag_position ({}) must be in: {}'.format(tag_position, valid)) + tag_position = 'bottom_right' # Add triangle to know to whom each tag belongs if tag_position == 'bottom_right': @@ -376,25 +299,21 @@ def adjust_position(shape, position, normalized=False, thickness=0): position.y2 *= f_height if position.x1 < 0 or position.x1 > f_width: - if not IGNORE_ERRORS: - raise ValueError('Error: x1 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(x1, 0, f_width)) - else: - position.x1 = min(max(position.x1,0),f_width) + eprint('Error: x1 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(position.x1, 0, f_width)) + position.x1 = min(max(position.x1,0),f_width) + if position.x2 < 0 or position.x2 > f_width: - if not IGNORE_ERRORS: - raise ValueError('Error: x2 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(x2, 0, f_width)) - else: - position.x2 = min(max(position.x2,0),f_width) + eprint('Error: x2 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(position.x2, 0, f_width)) + position.x2 = min(max(position.x2,0),f_width) + if position.y1 < 0 or position.y1 > f_height: - if not IGNORE_ERRORS: - raise ValueError('Error: y1 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(y1, 0, f_height)) - else: - position.y1 = min(max(position.y1,0),f_height) + eprint('Error: y1 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(position.y1, 0, f_height)) + position.y1 = min(max(position.y1,0),f_height) + if position.y2 < 0 or position.y2 > f_height: - if not IGNORE_ERRORS: - raise ValueError('Error: y2 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(y2, 0, f_height)) - else: - position.y2 = min(max(position.y2,0),f_height) + eprint('Error: y2 = {}; Value must be between {} and {}. If normalized between 0 and 1.'.format(position.y2, 0, f_height)) + position.y2 = min(max(position.y2,0),f_height) + # Auto adjust the limits of the selected zone position.x2 = int(min(max(position.x2, thickness*2), f_width - thickness)) position.y2 = int(min(max(position.y2, thickness*2), f_height - thickness)) @@ -403,6 +322,13 @@ def adjust_position(shape, position, normalized=False, thickness=0): return position +def select_polygon(frame, all_vertexes, color=(110,70,45), thickness=2, closed=False): + for vertexes in all_vertexes: + vertexes = np.array(vertexes) + cv2.polylines(frame, [vertexes], closed, get_lighter_color(color), thickness=thickness-1) + return frame + + def select_zone(frame, position, tags=[], tag_position=None, alpha=0.9, color=(110,70,45), normalized=False, thickness=2, filled=False, peephole=True, margin=5): """Draw better rectangles to select zones. diff --git a/examples/cv2_tools b/examples/cv2_tools new file mode 120000 index 0000000..74cb0ec --- /dev/null +++ b/examples/cv2_tools @@ -0,0 +1 @@ +../cv2_tools \ No newline at end of file diff --git a/examples/detect_faces.py b/examples/detect_faces.py index 71446ec..7811087 100644 --- a/examples/detect_faces.py +++ b/examples/detect_faces.py @@ -3,33 +3,36 @@ import face_recognition import cv2 -import opencv_draw_tools as cv2_tools +from cv2_tools.Management import ManagerCV2 +from cv2_tools.Selection import SelectorCV2 + def face_detector(frame, scale=0.25): small_frame = cv2.resize(frame, (0, 0), fx=scale, fy=scale) rgb_small_frame = small_frame[:, :, ::-1] face_locations = face_recognition.face_locations(rgb_small_frame) - selector = cv2_tools.SelectorCV2(color=(200,90,0), filled=True) + + selector = SelectorCV2(color=(200,90,0), filled=True) for i, face_location in enumerate(face_locations): y1, x2, y2, x1 = [position/scale for position in face_location] - selector.add_zone((x1,y1,x2,y2),'Face {}'.format(i)) + selector.add_zone((x1,y1,x2,y2), 'Face {}'.format(i)) + + face_landmarks_list = face_recognition.face_landmarks(frame) + for face_landmarks in face_landmarks_list: + for facial_feature in face_landmarks: + selector.add_polygon(face_landmarks[facial_feature], surrounding_box=False, tags=facial_feature) + return selector.draw(frame) def main(): - cap = cv2.VideoCapture(0) - while True: - ret, frame = cap.read() - if ret: - frame = cv2.flip(frame, 1) - keystroke = cv2.waitKey(1) - frame = face_detector(frame, 0.5) - cv2.imshow('Example face_recognition', frame) - # True if escape 'esc' is pressed - if keystroke == 27: - print('Exit') - break - cap.release() + manager_cv2 = ManagerCV2(cv2.VideoCapture(0), is_stream=True, keystroke=27, wait_key=1) + for frame in manager_cv2: + frame = cv2.flip(frame, 1) + keystroke = cv2.waitKey(1) + frame = face_detector(frame, 0.5) + cv2.imshow('Example face_recognition', frame) + print(manager_cv2.get_fps()) cv2.destroyAllWindows() if __name__ == '__main__': diff --git a/examples/easy_manager.py b/examples/easy_manager.py new file mode 100644 index 0000000..946e5ec --- /dev/null +++ b/examples/easy_manager.py @@ -0,0 +1,17 @@ +# MIT License +# Copyright (c) 2019 Fernando Perez +from cv2_tools.Management import ManagerCV2 +import cv2 + + +def main(): + manager_cv2 = ManagerCV2(cv2.VideoCapture(0), is_stream=True, keystroke=27, wait_key=1, fps_limit=60) + + for frame in manager_cv2: + frame = cv2.flip(frame, 1) + cv2.imshow('Example easy manager', frame) + cv2.destroyAllWindows() + print(manager_cv2.get_fps()) + +if __name__ == '__main__': + main() diff --git a/opencv_draw_tools/LICENSE b/opencv_draw_tools/LICENSE deleted file mode 120000 index ea5b606..0000000 --- a/opencv_draw_tools/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/opencv_draw_tools/README.md b/opencv_draw_tools/README.md deleted file mode 120000 index 32d46ee..0000000 --- a/opencv_draw_tools/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/setup.py b/setup.py index d358dc8..651c3a6 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ long_description = readme.read() setuptools.setup( - name='opencv-draw-tools-fernaperg', - version='1.2.0', + name='cv2_tools', + version='2.0.2', author='Fernando PĂ©rez', author_email='fernaperg@gmail.com', description='Library to help the drawing process with OpenCV. Thought to add labels to the images. Classification of images, etc.',