From 5e281766bc0b3b9aa627614e128aaa414629dd88 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sun, 8 Nov 2020 16:48:18 -0800 Subject: [PATCH 01/19] Gate task --- tasks/gate/GateSegmentationAlgo1.py | 132 ++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tasks/gate/GateSegmentationAlgo1.py diff --git a/tasks/gate/GateSegmentationAlgo1.py b/tasks/gate/GateSegmentationAlgo1.py new file mode 100644 index 0000000..e054d56 --- /dev/null +++ b/tasks/gate/GateSegmentationAlgo1.py @@ -0,0 +1,132 @@ +from GatePerceiver import GatePerceiver +from typing import Tuple +import sys +import os +sys.path.append(os.path.dirname(__file__)) + + +from segmentation.combinedFilter import init_combined_filter +import numpy as np +import cv2 as cv +import time +import cProfile +import statistics + +class GateSegmentationAlgo(GatePerceiver): + center_x_locs, center_y_locs = [], [] + + def __init__(self, alpha): + super() + + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze + debug: Whether or not tot display intermediate images for debugging + Reurns: + (x,y) coordinate with center of gate + """ + gate_center = self.output_class(250, 250) + filtered_frame = combined_filter(frame, display_figs=False) + + max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) + lowerbound = max(0.84*max_brightness, 120) + upperbound = 255 + _,thresh = cv.threshold(filtered_frame,lowerbound, upperbound, cv.THRESH_BINARY) + debug_filter = cv.cvtColor(thresh, cv.COLOR_GRAY2BGR) + + cnt = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[-2] + + area_diff = [] + area_cnts = [] + + # remove all contours with zero area + cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] + + for i in range(len(cnt)): + area_cnt = cv.contourArea(cnt[i]) + area_cnts.append(area_cnt) + area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] + area_diff.append(abs((area_rect - area_cnt)/area_cnt)) + + if len(area_diff) >= 2: + largest_area_idx = [area_cnts.index(sorted(area_cnts, reverse=True)[i]) for i in range(min(3, len(cnt)))] + area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) + min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) + + (x1, y1, w1, h1) = cv.boundingRect(cnt[min_i1]) + (x2, y2, w2, h2) = cv.boundingRect(cnt[min_i2]) + cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) + cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) + + # drawing center dot + center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + gate_center = self.get_actual_center(center_x, center_y) + cv.circle(debug_filter, gate_center, 5, (0,0,255), -1) + + if debug: + return (self.output_class(gate_center[0], gate_center[1]), debug_filter) + return self.output_class(gate_center[0], gate_center[1]) + + def get_actual_center(self, center_x, center_y): + # get starting center location, averaging over the first 2510 frames + if len(self.center_x_locs) == 0: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + + elif len(self.center_x_locs) < 25: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + center_x = int(statistics.mean(self.center_x_locs)) + center_y = int(statistics.mean(self.center_y_locs)) + + # use new center location only when it is close to the previous valid location + else: + if abs(center_x - self.center_x_locs[-1]) > 10 or \ + abs(center_y - self.center_y_locs[-1]) > 10: + center_x, center_y = self.center_x_locs[-1], self.center_y_locs[-1] + else: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + + return (center_x, center_y) + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + gate_task = GateSegmentationAlgo(0.1) + # once = False + start_time = time.time() + frame_count = 0 + paused = False + speed = 1 + while ret_tries < 50: + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.3, fy=0.3) + + + ### FUNCTION CALL, can change this + center, filtered_frame = gate_task.analyze(frame, True) + # cProfile.run("gate_task.analyze(frame, True)") + # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, + # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, + # 2.0, (0, 165, 255), 3) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + # if not once: + # print(filtered_frame) + # once = True + ret_tries = 0 + k = cv.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 + frame_count += 1 + #print(frame_count / (time.time() - start_time)) From 2799c0aa2700629c8f95c697d3f06738adb7f2d3 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sun, 22 Nov 2020 16:37:39 -0800 Subject: [PATCH 02/19] With optical flow added --- tasks/gate/GateSegmentationAlgo2.py | 162 ++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tasks/gate/GateSegmentationAlgo2.py diff --git a/tasks/gate/GateSegmentationAlgo2.py b/tasks/gate/GateSegmentationAlgo2.py new file mode 100644 index 0000000..6952cba --- /dev/null +++ b/tasks/gate/GateSegmentationAlgo2.py @@ -0,0 +1,162 @@ +from GatePerceiver import GatePerceiver +from typing import Tuple +import sys +import os +sys.path.append(os.path.dirname(__file__)) + + +from segmentation.combinedFilter import init_combined_filter +import numpy as np +import math +import cv2 as cv +import time +import cProfile +import statistics + +class GateSegmentationAlgo(GatePerceiver): + center_x_locs, center_y_locs = [], [] + + def __init__(self): + super() + + + + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze + debug: Whether or not tot display intermediate images for debugging + Reurns: + (x,y) coordinate with center of gate + """ + global prvs + gate_center = self.output_class(250, 250) + filtered_frame = combined_filter(frame, display_figs=False) + + max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) + lowerbound = max(0.84*max_brightness, 120) + upperbound = 255 + _,thresh = cv.threshold(filtered_frame,lowerbound, upperbound, cv.THRESH_BINARY) + debug_filter = cv.cvtColor(thresh, cv.COLOR_GRAY2BGR) + + cnt = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[-2] + + area_diff = [] + area_cnts = [] + + # remove all contours with zero area + cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] + + for i in range(len(cnt)): + area_cnt = cv.contourArea(cnt[i]) + area_cnts.append(area_cnt) + area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] + area_diff.append(abs((area_rect - area_cnt)/area_cnt)) + + if len(area_diff) >= 2: + largest_area_idx = [area_cnts.index(sorted(area_cnts, reverse=True)[i]) for i in range(min(3, len(cnt)))] + area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) + min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) + + (x1, y1, w1, h1) = cv.boundingRect(cnt[min_i1]) + (x2, y2, w2, h2) = cv.boundingRect(cnt[min_i2]) + cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) + cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) + + # drawing center dot + center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + gate_center = self.get_actual_center(center_x, center_y) + cv.circle(debug_filter, gate_center, 5, (0,0,255), -1) + + next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) + mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) + mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) + if np.mean(mag) > 40: + gate_center = (gate_center[0] - mag * math.sin(np.mean(ang)), gate_center[1] + mag * math.cos(np.mean(ang))) + print('mag:', np.mean(mag), '\tang:', np.mean(ang)) + ang = ang*180/np.pi/2 + hsv[...,0] = ang + hsv[...,2] = mag + bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) + cv.imshow('dense optical flow',bgr) + prvs = next + + if debug: + return (self.output_class(gate_center[0], gate_center[1]), debug_filter) + return self.output_class(gate_center[0], gate_center[1]) + + def get_actual_center(self, center_x, center_y): + # get starting center location, averaging over the first 2510 frames + if len(self.center_x_locs) == 0: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + + elif len(self.center_x_locs) < 25: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + center_x = int(statistics.mean(self.center_x_locs)) + center_y = int(statistics.mean(self.center_y_locs)) + + # use new center location only when it is close to the previous valid location + else: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + self.center_x_locs.pop(0) + self.center_y_locs.pop(0) + x_temp_avg = int(statistics.mean(self.center_x_locs)) + y_temp_avg = int(statistics.mean(self.center_y_locs)) + if math.sqrt((center_x - x_temp_avg)**2 + (center_y - y_temp_avg)**2) > 10: + center_x, center_y = int(x_temp_avg), int(y_temp_avg) + + return (center_x, center_y) + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + # once = False + start_time = time.time() + frame_count = 0 + paused = False + speed = 1 + ret, frame1 = cap.read() + frame1 = cv.resize(frame1, None, fx=0.3, fy=0.3) + prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY) + hsv = np.zeros_like(frame1) + hsv[...,1] = 255 + gate_task = GateSegmentationAlgo() + while ret_tries < 50: + for _ in range(speed): + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.3, fy=0.3) + ### FUNCTION CALL, can change this + center, filtered_frame = gate_task.analyze(frame, True) + # cProfile.run("gate_task.analyze(frame, True)") + # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, + # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, + # 2.0, (0, 165, 255), 3) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + # if not once: + # print(filtered_frame) + # once = True + ret_tries = 0 + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + if key == ord('i') and speed > 1: + speed -= 1 + if key == ord('o'): + speed += 1 + else: + ret_tries += 1 + frame_count += 1 + #print(frame_count / (time.time() - start_time)) From d3c6e523721ce8e01a0dad903a309819950d2356 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Fri, 4 Dec 2020 23:13:37 -0800 Subject: [PATCH 03/19] Updated optical flow usage --- tasks/gate/GateSegmentationAlgo2.py | 37 +++++++++++++++++------------ 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tasks/gate/GateSegmentationAlgo2.py b/tasks/gate/GateSegmentationAlgo2.py index 6952cba..f73c1d0 100644 --- a/tasks/gate/GateSegmentationAlgo2.py +++ b/tasks/gate/GateSegmentationAlgo2.py @@ -18,7 +18,7 @@ class GateSegmentationAlgo(GatePerceiver): def __init__(self): super() - + self.gate_center = self.output_class(250, 250) def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: @@ -31,7 +31,6 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: (x,y) coordinate with center of gate """ global prvs - gate_center = self.output_class(250, 250) filtered_frame = combined_filter(frame, display_figs=False) max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) @@ -64,28 +63,36 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) - # drawing center dot - center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - gate_center = self.get_actual_center(center_x, center_y) - cv.circle(debug_filter, gate_center, 5, (0,0,255), -1) + # # drawing center dot + # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + # self.gate_center = self.get_actual_center(center_x, center_y) + # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + # dense optical flow next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) - if np.mean(mag) > 40: - gate_center = (gate_center[0] - mag * math.sin(np.mean(ang)), gate_center[1] + mag * math.cos(np.mean(ang))) + if np.mean(mag) < 40: + center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + self.gate_center = self.get_actual_center(center_x, center_y) + else: + self.gate_center = (int(self.gate_center[0] - np.mean(mag) * math.sin(np.mean(ang))), \ + int(self.gate_center[1] + np.mean(mag) * math.cos(np.mean(ang)))) + cv.circle(debug_filter, self.gate_center, 5, (255,0,0), -1) + cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + ang = ang*180/np.pi print('mag:', np.mean(mag), '\tang:', np.mean(ang)) - ang = ang*180/np.pi/2 - hsv[...,0] = ang - hsv[...,2] = mag - bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) - cv.imshow('dense optical flow',bgr) + # hsv[...,0] = ang + # hsv[...,2] = mag + # bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) + # cv.imshow('dense optical flow',bgr) prvs = next + if debug: - return (self.output_class(gate_center[0], gate_center[1]), debug_filter) - return self.output_class(gate_center[0], gate_center[1]) + return (self.output_class(self.gate_center[0], self.gate_center[1]), debug_filter) + return self.output_class(self.gate_center[0], self.gate_center[1]) def get_actual_center(self, center_x, center_y): # get starting center location, averaging over the first 2510 frames From 2d4ce8e8229b271053b8bd934eafa1e850cc18fe Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sat, 5 Dec 2020 21:54:55 -0800 Subject: [PATCH 04/19] Modified how dense optical flow is used --- .../vis/TestTasks/GateSegmentationAlgo.py | 120 ++++++++++++++++++ perception/vis/TestTasks/TestAlgo.py | 30 +++++ perception/vis/algo_stats | Bin 0 -> 143583 bytes perception/vis/vis.py | 67 ++++++++++ perception/vis/window_builder.py | 56 ++++++++ tasks/gate/GateSegmentationAlgo2.py | 81 +++++++----- 6 files changed, 325 insertions(+), 29 deletions(-) create mode 100644 perception/vis/TestTasks/GateSegmentationAlgo.py create mode 100644 perception/vis/TestTasks/TestAlgo.py create mode 100644 perception/vis/algo_stats create mode 100644 perception/vis/vis.py create mode 100644 perception/vis/window_builder.py diff --git a/perception/vis/TestTasks/GateSegmentationAlgo.py b/perception/vis/TestTasks/GateSegmentationAlgo.py new file mode 100644 index 0000000..1c791d5 --- /dev/null +++ b/perception/vis/TestTasks/GateSegmentationAlgo.py @@ -0,0 +1,120 @@ +from TaskPerceiver import TaskPerceiver +from typing import Tuple +import sys +import os +from pathlib import Path +from collections import namedtuple +sys.path.append(str(Path(__file__).parents[2]) + '/tasks') + +from segmentation.combinedFilter import init_combined_filter +import numpy as np +import cv2 as cv +import time +import cProfile + +class GateSegmentationAlgo(TaskPerceiver): + __past_centers = [] + __ema = None + output_class = namedtuple("GateOutput", ["centerx", "centery"]) + output_type = {'centerx': np.int16, 'centery': np.int16} + + def __init__(self, alpha=0.1): + super() + self.__alpha = alpha + self.combined_filter = init_combined_filter() + + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze. + debug: Whether or not to display intermediate images for debugging. + Returns: + (x,y) coordinate with center of gate + """ + gate_center = self.output_class(250, 250) + filtered_frame = self.combined_filter(frame, display_figs=False) + filtered_frame_copies = [filtered_frame for _ in range(3)] + stacked_filter_frames = np.concatenate(filtered_frame_copies, axis=2) + mask = cv.inRange( + stacked_filter_frames, np.array([100, 100, 100]), np.array([255, 255, 255]) + ) + _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + if contours: + contours.sort(key=self.findStraightness, reverse=True) + cnts = contours[:2] + rects = [cv.minAreaRect(c) for c in cnts] + centers = [np.array(r[0]) for r in rects] + boxpts = [cv.boxPoints(r) for r in rects] + box = [np.int0(b) for b in boxpts] + for b in box: + cv.drawContours(stacked_filter_frames, [b], 0, (0, 0, 255), 5) + if len(centers) >= 2: + gate_center = (centers[0] + centers[1]) * 0.5 + if self.__ema is None: + self.__ema = gate_center + else: + self.__ema = ( + self.__alpha * gate_center + (1 - self.__alpha) * self.__ema + ) + gate_center = (int(self.__ema[0]), int(self.__ema[1])) + # if len(self.__past_centers) < 15: + # self.__past_centers += [gate_center] + # else: + # self.__past_centers.pop(0) + # self.__past_centers += [gate_center] + # gate_center = sum(self.__past_centers) / len(self.__past_centers) + # gate_center = (int(gate_center[0]), int(gate_center[1])) + cv.circle(stacked_filter_frames, gate_center, 10, (0, 255, 0), -1) + + if debug: + return ( + self.output_class(gate_center[0], gate_center[1]), + [stacked_filter_frames], + ) + return self.output_class(gate_center[0], gate_center[1]) + + def findStraightness( + self, contour + ): # output number = contour area/convex area, the bigger the straightest + hull = cv.convexHull(contour, False) + contour_area = cv.contourArea(contour) + hull_area = cv.contourArea(hull) + return 10 * contour_area - 5 * hull_area + + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + gate_task = GateSegmentationAlgo(0.1) + # once = False + start_time = time.time() + frame_count = 0 + while ret_tries < 50: + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.4, fy=0.4) + + ### FUNCTION CALL, can change this + center, filtered_frame = gate_task.analyze(frame, True) + # cProfile.run("gate_task.analyze(frame, True)") + # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, + # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, + # 2.0, (0, 165, 255), 3) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + # if not once: + # print(filtered_frame) + # once = True + ret_tries = 0 + k = cv.waitKey(60) & 0xFF + if k == 27: + break + else: + ret_tries += 1 + frame_count += 1 + # print(frame_count / (time.time() - start_time)) diff --git a/perception/vis/TestTasks/TestAlgo.py b/perception/vis/TestTasks/TestAlgo.py new file mode 100644 index 0000000..e46e507 --- /dev/null +++ b/perception/vis/TestTasks/TestAlgo.py @@ -0,0 +1,30 @@ +from TaskPerceiver import TaskPerceiver +from typing import Dict +import numpy as np +import cv2 as cv +import matplotlib.pyplot as plt + +class TestAlgo(TaskPerceiver): + def __init__(self): + super().__init__(canny_low=((0, 255), 100), canny_high=((0, 255), 200)) + + def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]): + fig = plt.figure() + x1 = np.linspace(0.0, 5.0) + x2 = np.linspace(0.0, 2.0) + + y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) + y2 = np.cos(2 * np.pi * x2) + + line1, = plt.plot(x1, y1, 'ko-') + line1.set_ydata(np.cos(2 * np.pi * (x1 + slider_vals['canny_low'] * 3.14 / 2)) * np.exp(-x1)) + fig.canvas.draw() + img = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, + sep='') + img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) + img = cv.cvtColor(img, cv.COLOR_RGB2BGR) + img = cv.resize(img, (frame.shape[1], frame.shape[0])) + + return frame, [frame, cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), + cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), + cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0), img] \ No newline at end of file diff --git a/perception/vis/algo_stats b/perception/vis/algo_stats new file mode 100644 index 0000000000000000000000000000000000000000..f64d29873ed1d216f02a06d2829e8272206aa0a7 GIT binary patch literal 143583 zcmcG12Y6LQ^EX05??veZkP=Elkq$X@>Akn>O>%QMaB~y(CXg7KND~D`kRnzT6;PU@ zSU^Mtu_6{Eb^$daih_cm`2A-0?Cv@DBoY1oug~+IcgUTc-JPACot>SXy&PNY(lvAQ zir~LX*LO_~WK2yi%G>J(|iD{CUoe>BFOq>=-$_OXKM&}g;GK<2;9Cfnd zjBvBV2@1&BTDizgif0cRX(*q&v=0IF}wl({jB1Lk`q*$S3Us77ybSn@W z#T!&Wt6IetV<=ICQOhqiRox6{_*q7HmX$HJpsw;#rKqdR&+LBa!omFs3P4v`ar|OU zp~h554rG8ozCZ|^vqB~sM8)_=6;VJz4Kf26q9v*R^mI^3XK7j4PTqTn?vM@IkwCn^ z>3_rfq_p&G7BIp3yqnOg+|s^qFfAj5aSZ3vQAeBH9c`#MkM0FR$)c}8D@?ss@@3tS z0oEKNKP2Ek*PyqEY7(Y4;F^LaZEEt$+h2ndsC1s`tLK)%so&h#J5Da9G%*aeh z3CH&sJto{MEj4|N6^>62u%tkG2863+APfJnL<#-p_?f_(1(8ad&qTG7i%a0vUnXD zYCTKEiYI0!CI&Gl@;c#ftQJ1zbH0S}ttXvHPyia5aFq=WWu~TQ$0wz+Ks*FAXr)>i zVQOO*34RHmFBF(X?Q_T`YUKZ!5GR5fSZ-;5Mlj$HXZTtt zgCkARY5ppxayF z2Wwji6e;gdPWJh;(h{K|W(F99lB8cufrZb1{?;58Wtb8`Bd=B{9gXy7VeX=i0G|Ul zMWfazn#ZoXHa5OAdv&Gn5=;sF|I}qq0Ba--dTAe3=JPX?RIl!x;LFdFFFz+>0(2z4 zOd`INB)$Z#5G|CtjIs6Y)gOQPYl0~OS!P2;lqrkJ?n{~qO~?Wv4EK`|Z2mES95U;H ztaq<96oB4!`uD@_&$2iV@1+?7Jwo-7p3~~}=;edQ9vENQFeQKn(uB79K?~+qAZg67 zlBWC8{jeaw569F#QS-sWSF&pwrUYcs3skh6FEc&apAq22so*w}W%`&i(&oO}`_>x_ z1;~OENG2=tR`N;{6Vo6BuCmd5Ip`vngD!?F8zeEQKpjl=PfKH}TCLLKhmE-Dnn6(M z5-4ct3}j^#ymY>adMx^wFJ^D)6JKFcv82Ju!2i)uV49tz z$ubBZlMQd#VA7ou?F+{Q@bpqK=}%%(R5J6=a?Kb| z0e1r$C<6y}L?A1F`zEg}b$>K!FkebeLL&!m*J5J^@Db_E$NR!I`ovSC`X zeJTD_D~Lsc*H}Qc?q1Yay$Gi<+aUEYk(9B}$an4Q|G`63>l&s6h>4_<^M>J*Tf!fP z<{>T4(F-W!Zb4kvl*|y8nY2*Mh@@eBa>sG~AZ(!+e@0BJtk$iP+odEXC%3|$#9CUt zSP}j)SLVF%LekDnh5~Y8i(TsW?|JI9#!^UUz#78@R@PV{HM%E?R3iuaeAk0nZ3V+bV9+3Wz43N3hrcyo@@CQd_r_(~FMew7yCzS4Yxq@Lz0MqC{ z*dH+3(uB3MhAJzOk>+D&q8~l$Og*EIncCr%4uc=8V<-SsO?F7v|KX08c3sr-p46Bq z`WHG)YEvjfVOo?hY#XBlM(JL?+?kOjv&RMo72 zWln{nu#%}9easfUYM=h5J(h6^#1>;MP+?v@YAs(;VKqwK2mWa5CYmeP%Tl>sPC!wv zmc~)hVwj9Bc8aB_tYI%jW&a~PGqHu^?7V|&DMw3wQd$VMD0qpUX(OuiORI+7nTiEp z0%#szpQF+5OQpJ+fe<#TEWy>Me713-(8DN{TT<#QSD67jI^OrkN9dRY-0G=`Qcp0a zk^?FJ%wR!U>Mp1gi#}#t=#gt)if&^lKxwIs)a?@JatQ7abhwZ*?sjN3nVyzjfSR0- zRnwx6+4sGc8I6i&8VZmNutG56aw~??SgJn=cad-UEMY4tn=Ci*Q`w-fy zU^4-CRxn%dLZV>2TJ$l?9zOiqQwus83fOFKU`$$>D`Qaop>R-cL?El^p|19)iBDBo zd!mtHN&uXk8s7tzSxJ7fj>7T7vU_29PK`qg;(NggfG|Azw#YRYf&1-CV^u`)S##J)UVCA)RP(c$G&iW#N^+>_d25W|Z_ zZe)0rJ=V{I3D*>{=wlu@cI<@@OD;DQki-0F2DqZFou|x_hg#%7_eg+|s&%P6p(a0< z-mcGSn7My!FKtcAkXH?h8}lWlf^x7WMvFe?qX$0edDpFwTnT{R-T%E_pc$fdLtb~{ zV|m{!=1*sV|V}D>th&!{9}F_mC7zYI?_;pL_bUNTMT>z z^)(fMP9O85l79`_8|nc&%m#o?ORjZ^&`Uvrrb8i<%gV7%3v=?UbY!2U!J=wmQvE&7;W|4?M< zi#Lxp6u^3*67*hsbe|7qygPJ83&WHEdpHtBtO3oWN!iSBGpMbyhm;+HeL)b`3@O~W zp$rEDf#lwiJGibeJ1qLx%DRu#+I@^1wpO&E5FB?K+N(A=CGIMk;n*ICy*Z(KEi1 z1bl%k*)h>2SJ(7bNZ7g9jtkfK4UmO&)Zt>Q2ghA`hpu!emeJA*#n`9B_Q4M=>BLo= zsnR#ou0#7)fwtT?^Jg5Sfj;Kb#ed$)9$sK5ASbpME031s(X5AWo6eHZ6K=KX=8NV0 zFS(rmg%#p|b<#O2;GfO{DP(xIK$944ilJcYK#%c{`AquwMeh{pfW_JdZ0AQ|!^l<% zD&5E~q>ouYVaJJ=yALxI5L+y_WLL8ELuP^?W!F5l5z5fV3^(mHdeEkMh63CJq)jb; z25mb(LDD4?L#b2rG3)(yta*z)^$Z1&xKDIgJO78Cj5L}C)~`_Nxy426AbN!~#oaT# z{<1)N(4Ull*BoX@$&ALSTYmBzrUa;K|G^(b+X3NFZA48kh4Uz1sujwl{m%C^mh>@S zzq9qMmi2L>CjqFhb>OhImAf{qFd5uaeYBrJIepO3GUl1O`v#3NObNJq?eN0F-nX~p z5yR;?5>9#WP}BEb8f2IfK%Yf3s?!=QoOwZic3LL2LZ3TGbXBgPRwSimh6-rK18~Mz z^f9mhx%QjGs)Y>&&;;j}cW;

~k>>3c+{Ru6+U&>j6QA-4($ChZj&7k}-xIW)Wi$ z3uqz9Yl068z20sd~w1Q?PemI=|DIq<0ImIjn74}xR#pY+s3&J?a8~T`kl&ey1#91g637~8ydS-n~%BV7= z0lkBBcUl@81R2>BCE_T3_k^bF@5_bTNCL9-?^L=9l@6z2^HD(Yey@ytPbXJzX07ppQBmRR9|UUI9fruGjxs|B0G=;eeLFf18N#q$N!iAp;v| z+{+R(96iwo468rcz4!CGv4fES_)vn(k8M=09F=npo<1xWHU-q51Y-Zwxfd-~w(rf_ zP!_Y3_MMPVy<7`Ul$I#%ANMO4K zzxDQh_;1bcukarB?h;_niTd?+T_`AoA4mX0@*fU9cpGUVwxGowXBX6DWtKbKa6`ZU zt;sLX!+Mqg>a+g;-EhfP7}iX7JZT$f#6Z0IgZKe!1NXyKK>u@VLV6JBV5Rty1Bsa? z+5husR9AjJQ;%D;#S#2SPDcYC6uPal94h!zz}6*89^FtcSQd zMHO&?x`a;BP{iEr)2liNS@y|81V{3)uP?%Cq6QVhsQ|Jq|3@LOIJpQ>$OJ7<4M_^B z_=MF$Kct3DA2>mac;r~P7AbzBYpo~lMDqYcth&&sMOb~^9Y+B-bM)Xv#>z%;$YPtJ zqUd8j@O1RTk6&G4D1bPW_`lg$(BWkgPN;c+;0R(-8R6FAf6Rw94pC-jfbW_-XQ!T6 zy8B{P!;}CP5x#g{riHhr5AzlAnG_VlmoFYrB^R=e)PMSzseA!L|0UpNsgAOS^C6gHNuCEXt=qBn6JDV_ycQ661jiXwQ*Z_;9OJ!ke>grcMBtaTCBIHKsC24 zNs6Cg2=V{moL)`JZp~_V?hOM+G&bNKt{AW1LrqHp5PZU#qXzylcl9|MI^P<^KL6=d zLZ{N{aI{<$OeqA%U142Mh1~H1b1lxFNb&d9(n=U6`k2jsYf4W}XC4>P0Hm5)R+oG9mamp>{G6xsPJwpe*>u z+}ib)9V4IYjI$maK(SmZb{j3ACCM^D)IT=I4(?VtJN^1n2dWvS1Z0`dpi*X{i=Bii^8Ud8v92VEA}SI)5(%)IIST((pyX!?8N`xqw8g8-gDKLjtU%C*#XZ9xN@7xM8O~l(JBZC|CsNr znqE42CL)eHz4yu5VAm|j?47)yj`QTVT0XV-wQj(vly?8T7k`{(fWh44qhJNLP_RW? zAu6m-j7OCS7kgnM9?GWH=_tb(fAt+Fn~i~pe?Hmlb;Y7h{eQ9ctW*TY(LzeYoLh%$ zaUPe3{V41Sgyxe;#j!|&wawah&D%R)+KNN`voGeYsq@WH+4a0%iBMS@VmoSr%4r^` zP(cy9+QwiVBH1LPm26YLwjqzQ^i&NjzjS~AtBr*i9@<1=$>wl+)&kd7j?k@pHQ zumolX6>?2ZiK@!DTTrjy76h^aNvPkkdWz#7>{dGMZaXM-c+>7VZek4zCWfJ*T%UDw!=y=2A)enB$i`vNdB?syz41G`w@Rbj#CMc2)$6 z1khi;q`yNeU~wN@jQmhVuOQ0VDVEqCXwHA}FsAx(<%$PQT8+W@+|pzQh(DtDztlt@ z=wn`}b!N}kvmmAtz^wTXQMq(>A5IVOpsUuX58_JUB+|8`WlzMmmY1=u<%!#vE=5=( zeeT5v&C}6%gZG9iRX>fW0twjYd1TW<%ggHmQz=f~QBJH52!57;UCwGod@q(1OmQs= zo|sKGU$jIA{j{C7Z$QFK956`$6!}uynl>SoRZA#xXe9Y8T&tC*+l>60K+yvq&Eq1` z)p1`nf8n=#6C#$BLiFKOe^}~6))oUxAM@bl8JAD3f>=v{^!s%f8tWHoa7CYe(}1F4 zL>xFj0u$$8skgVOm3%i&Y9zqQiRSW|Odqw+RsfO557yvlkgraj<;+m3KRi8<>`Muz z`R#Z(jM6lkK=d)YUZ@+_>8E7qNE?te$hd_f63!L6Qs&^3*ni-1m;^9EIym%vVez#h zdN0+_Z_Sa9X%v~raP}gQz+H>Dw}a4S39y$12ec(22Fwx2DuqUiZ(ffcu@jzef@5|Y zV6R6~@mvsMy#$JujMkHvepJtoYg`g=uat!>yO4~jv`h|Z50;M|1(xSj-FD;GH!p5XRP7Q=lkmtqhe2V72*fSdJo3N}(KZf}*r3T4s9{JzE0C0`$d;3Em-#EPXL?_pxR za6IcO~7_h>~2T~N_Q+;gnvv#byV4t`M6<@|8oDyzAJZPXAu^IqTStc zq}7nKkI+n)PK)*ia{sRms>SWY3N%C|`NqKFCnWu2z+L*7s~2tGp1FAjyz@2y=5lTH zD3z%|XL_`cfKn0%Y>u6b+DJ$M62Xg(qoty9*q;LTv&9f%i!01@I~nq%`SjHbiG8zj zxuDE;bcjJ`NHkC?O3i}avPT|gBoF(FjomK^H*Ywy^wDfQ1v7K(#{@cvg>UU9v zN0E()+uS&blxL7r$h$xvJ7#s93|<{)uhXj=$JFzx>=b4hLWokm%=tPWMFVE-T#=y{Q^%ppRMdj^D}+3gOgK0~3(HoEiRaD*g`BM;g0O_;lT#KscWA1yY$mQ=p3}BpWK$bmU5><&M z${{ks%04UiAw0O1_q`^Jo7@`MpW_%NS;T>pYu0f9rDHVmyYA14+yo-BPQ8o^NvmKyVc)jbgO2Y zv8$8-%iz-KxII*M9IJNwmk0)w04QdX=v9O0Ns6D;#~V(R{e4>AM+)k?0WYq-e6neG2*I%nU&qvcbCyD{N^-;HlizjnoO&O6VbAj7XZsqa z1Xuv=!Km+EjHb}XeCvUz_TPPVn_>QPEEyh|H3l0XP;~WT`;GoffW0DGXp{vN`k18$ zc6})LCL%*65Nq2j^T+_X7Qqqn(h9%ODQ|A_&>9h6!*zf4o4`R@1OV6v)()4X=N^;o zwRD_mtF4~|z*ufKZ6i)q#3~d@<}Ki-C}gNWa0-f1L|HP1@@^#*@QN6x&7ksowymr5 z1>#sFfG$r3>sSbwSb0>JQAsVLKp$HLe--)_gLq%FzX}t9FM(^Rkyw zOYXJ8Xr!2EKQs*P=G&234hqVNDgjyhej24Rqy_DeQdgtJm==*yP(^4O97dz_pu^<= zIcgF1=6Shbou@wDLg+Npa!b>Cm5F`Zbkc(QX4AOZ@BTThJgkv7t9@~*!M>4(+>&U| zrC=V;aU|gNe~P-@izVOc-by^G;4>L=Q1BxnPr3uq%dezf{VIViLVNRJgj@A;>Oq=q z<>(TKPdI+laLLiZ63-brSi%e5fJuyFKotsQJ9}vOcv2LwNc%n2J%uYx=3Q7(vlrQ1 z1~GOYF}5 zH7t(Gc;JpmJRpw$#9c(KyF`y0OS7Rd)y9jr)x|ALRr~lkvbe{gD2w-^;ymt0)J{pE z5cQ0n>bjp__Ig_B=Is!%Zv(RMNL`rMonK*b`T@gM0w_#uvgp7B8Womck}nOpfM}}9 z)eeKL7fazVaok?VF2Da(?(!XhcGx&lek^_`OWwl7zMoxBgI!Mr9x5zciI|aTAx!~Y z*vd!x946eq*oMh?ciM(^#UjZ+X0O*f&Z|-yOp-v3j9G>vv5jf~{6A9yv9`Z-2L6y+ z&fT$k~j}mvrr~)ITb8Jo2b;ZkSONZkqD(wN@|Bv%G0VB0gncC zGolc01a2#Er@eMwi%||qZ(xX=_G0tZp9G~Om7v2CNapVtGX61pe*e(Gj^|Sh1+e8n z28!O5Eq>y_gI~XcBT)(b>-`UTkz}$1mFnmh@Q!^ZD$_GX9HC;SNC16kMy&ptSWTCI z(F<-FYsb)|ZCm}it1)J_1l&w40daG*ja805WhgJKHkTCdUkkG61;n9>huxe%+yhjPXG zFoD4?r)Fq_S?;iHuPje&z_;4#XsZ&Ho*Bjt#c{?L%{>f`JREEmXTC#E{Br*8rCEL7lvz64zDjsZVJ-O$4@CBTBBJz9RR!F{S~HIU6f0<4+nxMsIJk5&iM z1x0{4f=@8&RZvwPQOPY~$F;cz(d!7m$lK-^XW_7PWO|Y}hxD|*K4;;*^+&_6D*;rz z8gy6%`YaX{DQru`ZsFhjKc(a(`aoi4dFfggcdo$7UE~6!k9lnIvKdzzW9K3Plzb7i ztdB{cqH$!y*Kd8sCZwb!(tL5bDM>3TFYp<8oGYlc2o?x#@u9AJXuC`w`z9^6%i>CH zFDlBnJwd9Ahgnd_Aj;@tU2(`cK;CH^pvuglG9_^Tj)J6IGA~r(l?3~i`jrIsI3IfJ zFw7(VQ4y#LKuc19WJM9bpm4~?#TAZLQbKFDmXRB+4Y`@Bk121_$9%4A$AO0#LOdk^ zvUDJ9#i6n6K0xT(Ol+t5$6WB_<70f2q4p#o(Jks*Y09u5R(MzUWRfqH?TdBb-E+*X z?`|mbOqWM7MiP*O*AU?f-2az}98r?G!???D9`i1IK}GomW7}3jSM>B3ck&RqDuGzL z=bK0>Dq=4X4p59|AS)>lNG7{L8!9STKmsyW`3j8)eN>h#1+Xs#Gqo?K*{BZJ&HEI> zB?0t#9r2zr2g*IQR;0pj)>0mbwj{ZtVC1}5l##nVt^Q@Voc(J7%)9_TVyI;HILAcy z+#LW{Xsaalh!vAE8O3zcym1GkPJF zTV?wgD#lS_RE=GlKP&Scg zW{;q#(WMG<>LPC4^ZS*hxz#{do#7;@SdIK+Zuom_olZxw#ghOA zjuXz|N{(hGu1o23XAWuhdRw21WT%YfQbAqlC8xffymYmtwwBgCUEIibI~N^!e87%~ z@z--p42$c6q1Z+&1*SMMagf&K^6|9&m(7EneIcyUFo+BcYy()5TZ-aM)^zQ`_?K`vC;?gOR$j`-xhkaIA(!=wmVbD_JBap@fLDbI<8q2T z@}H7VV3%lU3EAQj5l8HWSlyT}G&p)1-g;?Gr^(ljG)xKPuxmMk3d3>BLLoaPa;{K) zZIdAk^f7zClzHXMt~(9$rlQaN^zG(de5W)Nqn0q+Z+KnAsX1$NJ z5;Zno7&Rnk3pqQekT~{$r4Y!X%(hT4x=gbb#kQ~Ma;stXj(=oa$hya1OSuYgbgbVa zMVoxOAIpyf>>Ze6z`Ta!8=Q(`haptAGEes>xY<+4XrwWIxT+U8&$^$(Dv8l31jk*S zH=tO>_on0QkcNW?LW^kV`rAva`sM`IYYF5?3!Q?c3&3$#SV%wz+#)idVP=FnQ#e=Uz1eSN3Ha&9gi|javH;{mg zf7A(d#(eNw?_-M)P%Qy>-wKIEq}+u8K75TTq1p7Yqls~%kb3fk*i*Y17j6ppwFY1m zbIZ_*g$sG`c)EQuBArqv!%nBv$zVGLM|2W{7Dr#}vVRqv1rlInlDYI)*9t$qo11zg zP6%uO8i~7vhhnE|+g=W{sN=$B?Q$M!gp_Fq;+ECD*wJ7kgk)=5p;70zTJ+or(^vv- z?i3=~;mlMLG;Jgt|MuYdg^l0`lRyqDD>_gJj=Q?ro(KV2G_&HctL0LSmBRKmHEem$ za}Dq7YnT$qf!8hB;dT2zyn9Z0)Q~#MPG!Y@M5l6#TVaIaxcv|*d#k^B+e{qJNWgo^ zLPYW+AY20O{_Di@a118dIpq;iFbs1HiS%&iLZzLs2E{<==wr^THe&9GGf;yP$YHRa zNOCm4gq`6y4`DF$2t~V5ghS1diXXzY41LYp0?gcK-Xf^q{-{z!GIV=C^Xn4dpl1M^v;2EGeWp z?jCw|hmS7Ql7l*qUygH65SfBlZLX+uJsq9<=Xfw4>)>(rVb4#V99|{ZiX>5RlDqfn zp1R#ViS>?^KQSrJhY!e#$Q&Nxh8|hV+d~5AV}5;l`s8T~R~Y8a?YAb3ow$;;;iXBA z9{nHALTV2}m~W%LRw4(vqgC84D5P0e-&yJZR-N5IzKmn|n8mKYw|k}A5zO-NC#COQ zGj2Rr{fJa5!j6;tpiQpAyiHWlA`;%x?lyTXEXw?z5jWD;f8>ZhHVcD=iMH ziQhHTa^$gmv51NnNnRw0j-nDS-g*kSt93HVpUgugY<8h~U;m*!1^?6<<`CCA2mjh| z?&}k92vprzo^x&@0{zIL?W)~_xHML%_{IV zv8Ob(jE8p9Wot%>Wn4C-kZVMi5Bl`wT}K~9nkm#q{&*`2ofhUb8f_nWYP-@?PSryp z?-@{c)TcIo-Ql!drGbeW~R*fR~{7DO#DZ}iDVrPoiEYjtk0&aT7O5*gq3#^`)bj{@Cw(PPa(7jjJV_iylPo3HhWhBV+Jwjc8XYc1Z3d=zE{W032=hgo zRUUkz&sWk;KxH%pE;HlS3!|SO_6K|e6Q3!Px^?_ozVd4Nw*kbk==I-&ujtcC&FEYk z^!3W_d*)D?yt9Z!85p%?Ud6*VMbPiS+b{6>TAt^-`^~Ch2mWSfce`-(JW7?vC=At( z%g4hSGEXx#hfcQ`O(9kdAFxv^_b#toIsMr)xWBaKo!UsFvKhj8ns>1<9HRL#C|Qr9 zN)h1X+?@T?Z4&z|wwumwf+6}}E!q^DDzhDu1xdFTR#L0xQBR$xrk+-*UQnhlc1&M7+y+reFEswbi~(fPGTaPK5+ zT)F|1e~Tp!4KJq|rc zY#Pp#8j7`F>Aq9OjRJ5KP0jykc9MjFKBjU2NM5OS7J{Q_>gnrLDj8Ff);{qC@}Lgw z@@Kp8j||b%k){_LE5=JtUk^UfZ$<)0J{3mqUP1b`%n0iNPmX<7X-JtV=P3{BRvIUBDI z4TVKJU01XhsF?L4kj^%pTvd6Bs;a1sY;yj6g6C%38Z&Z!9UOyHk5FjNSIpOD>p|LG4(wM_k&_@WKBC3kIXbr2!S>eDh zcDm!-CvLbKw>&G99`OE~bRlU6XoIGXDY+06b#mYncNu1>$L<|J@S2?lE5>`~2roMD z(4a>SzYbr*@}_6X?Q3Q6C4u*hU)G3;tTyu-_EORJS0O7%$=jE8y#LYss`b}geCN{G zmB?y7puy#sw>IWet+*-`z!lnWZvQ>2eyXnaChDy}vRb-Yx$*8TvEjNHj@8*^WrBRX*6Ayz@h!smh zGt|Wj1-NWVgk;b1TNwib$n_A#3lva_hEs))u@k7yHA$t?w)fE_*|iAADcK$zMYB1z zz9ftYzSVURRvVwaUrs$#?qwqUO(bCtjw0L$sEjT{yJ!kiqd0`7`%Bw=JA_8zouh`Z z0F9or;U0XVulBQS5Fc&FHdamH${T0*C{t=2+H5PZRu50a1NJo-u-&Q8Grl@eD+Ya+y-Q~6Uk^twQsiisrdU7Zovh^ zbFY7Kd;Bs~+mee$VK~&4@-+G0rO$hU<$6o-r#O4?i9V0>q{p@FEsr&np7ElOoj_^7VZO3_b<4YFzRp(z zjz61%OZZWp9XEk`bYJDwWp#T@R&y5>E06mO;*AT$YV$`d;~QZp=wt4C{^grLd+ui3 zRvr4(z89L!H@r>-^_<6rI~RRlv13oe&hF$$uCe3PJ*QLK;uy(S;@uj%B*%0P1o-71Yi=i12^4*BUm@|`3z^f624 zo;=&X2_C=@acejjT*bNVxL3dDsg_;it4)+y6ye(VnFI|lD`CINKj!B1Ro)G?L_&tx zVkMr*h|Zy!Da??&Kk0e|`Yl<~YHrQ7#%4Q&kw1GUQ04$5dZee3Y!S6=zzwY!4y^@6a$j*D?PLI=+D zG2btKQ?UlGf#R*_t9I)BSfAqTDmq%m$pLe`l_P8hRQIx-g=9JyhTo;ebKyrmD7((b{ zNfcirQRHDZ>PDKr(>8bbf-61mge@fUiTg;5c&YcP6u%rV8LbP^QB3ujD70$X!Q3y8 zPf>+Fqe5lqw#H09v-~_VRu^mk{lK(&aq#>7wDO(N=W6>^v2Rri)e~Z?vG7>*F<-pYXKh%{Z^) zcl^XfztL5CAwqG7npf-=o0QKrJ#kT|mAvqOgChSpWZZpctp*|;E&7;N$3SRb zC9F5w8V?;m_}HzgP)Sm5WXoV*!kMPC7wql$%SS7|K|dy(I}zXV2ejoWK3yF_lp;W| zx+_l)4+?#c<{KaROpHk&3!T(g`<|^dbY|N}d`PVH!}oh?9uF9dyu3is0mS=vn(qas z!NV#71pAXn@m#jW&;)FvXrP@cK)Y{6_*H^oX3T!3W70Yv1kFp*cFit{T&+rZ=#r0C z`*PUDAbe#TcW-=WMuYv3O`J-gm5s}zbUd3V4`CvG%P>8c$X!qgAaxagn*4bdsi1E^EoC@d^ zLR8UU&qksMXBEtjc@NI`x_iHCNm6*M3F)8QTEuwqB#ZO zX>;lx1qJ3&ly+`;BEwT=*y+3-`bp;dQ|~0&7xeh+wBm_cq4{uMV)4N$J?>jFQWCee zP#bHIG-pwsJRbbm+IfG=aT8G}D+tPSbMCx!x>{dFvLL z$R$N2yMUJH3y-n^X0yk;HTv!~u0M%aRyFcg^6qFN?v`jGz1rUu*ZwIieXrNeJyd$` zK*LVt;yBqr(y`r-K7yPJKQ?8~;5AEq=@qA7ccqOgY?Vk2|ym@;LBM6W|u;z6(z4bUkm zg^@nQFtbOSyZ6`SBE&mdn-uvFY5Y)JD>E;{ygNbw4vy4FdinqJb2JXW^$k)v4BrHhY|Yt=Qw#R*rOVG7)K zq$udXJ&)LxTahLqcK19vVoK0&g1lZ@Chlr>U~pepH=>t8>Kv72y-|xDxNK*G(AKNW zI%QU3Wb;CND{(G3{3{RSH9FYluMs#x$yj~s@u+cp&!lXBmO}W>BNuVk16=#2R4$bSqQTq?t7%-F5hL19#i$s&S)8XEB=v#)j{7|oVo0D0FxoGu+qb_IMp*#CNcKc3CAljjr zMRG92M2js+KQ!-TRF~;$9Xmz%PBy;w)(6jTT#eb>u>A4vFMRS6cSQ33T)?iM zA!LX|M=wJje4?jtgq_oe+nVFgu1 zeW;?okrgSAy##XD!85Q zKoD&#u>SpU*!+v0(=f0n1UGfR)dfItAFT@CiSrf_r`Tq}*+TG%v+tAgJxZUtBu35% z_a7RbUGm`HFqVH9U-8Y$9jAe>OSz~y0>M2vL=(&x-iAxHJP|Xn(Jif z47UdM0{r#e8mQ~E7(=atPq9ene_#Y^5=(rx_CGYd4lg0$Ph8u-5z;FnyOwL|`$*E~eQU z?J#v}XMe@FA$F@lM^$zT#Y=5m4&BAab&Ab)5osNub+1=Jv zt@q$fupVuIW)0uH@rZohFXw`HqUM8#uVlk+lmO8Arivn=q%Eu2CXBfo^cJ6mgnVig z5qpMZTms&_jlvRy^Hu_EjhHK1W7TbabLipbtqoHGj8byJRMS&#?eG@QJyR1Vl>{Vf z*lom`Wqb%73U}n&J&wG;B?T#V`)oMzS>xH<7T`WOA1Rb5dgKuLL$~4VfV;oDlz?4{ z2*(*lwh(9K7#O;_W$;i1Z8*iF;10Ee*O9A#pNpuyBlp&uXAMs?WF8kyS#1+V>fM{T z+Tlk{iPZEY(RBSjFlD%_)z8Em!j&E!@#9o+p30lDidN9l{5~C4PV$pDtQm2rhs!8Z z;b3t2(%rW#O7UygSxEEZ>U5*pH{ZVG0(^I3bJLm(U|!~J1a*rt*^y=BT^D-;!LQWv zEz4hIcW{T9%mA$SIZJqc)I`H9{>hDxZ=0Q>cwkZf3Xv)z+L5{XZSnn zy5`g7W472%+E41k-lC7WfG2dAfz1JbZ6(KG@x)5!MoDb1usIM(2jOXciA%6!$2eyG z&+XIh!-jSD;9vGUaqV#3vPY?9cM!fapZgbe>Es?;lC<6zo}B*7AhfJhq&%y@ANd3X zdziP(v9l4f09#e~H~Q*ogP_K$3!%^|_04H7Jn;Ps^9}R;qU4;u1=W5jXcmGa=ZH0% zXxeH{PhPp~s5_B4ka^Mvd3*u#s1FamqEBfl*ms;&jwuzrKdX4TzuL;}MNhQ325+-N zYSjxHbMEdEY|yre;mu+t-;pxuRm&%9bkeNaiE4ABlMu(uT>87xiFVt6coI=j5}=OJ zjB_x;Id!BuRC>|dPN54CMI`~RE)=$N5uLb7vFBN0PfhB&bGxD@(pTlIQC}r@;VO#Q z&psx}*RpxQ(J{S@ z38xF3yAsH!TKSX0QC->CZI_aA?immKJ=hAkTM4i~<;I#{CuWp4S#mvo>kh5`)!9Ei*p)F() zXNfbjUbW_zTUv3Ju9nhyGYZv*oaxNn*Tqtz-9>OuJ-MQGUResA`H2sXBRPwB@(&As z^_@eDy#B_5e9lAfoK6n+B*UCm`K9{HOV2jg1wkdJGCD{fbNLS$F{d|U$vl4Z;`Fz7 zt<^1~Wte60mN_gJbG-559apnW-A&*d78mZ^ zBYzj$6gg=;_=^6kgfDcTL-tkpodoiEuU`ISUsOED!&&(P_eQ=nh3CqV2r?g<9vs!q z5Yb#td~+^eFaH|Ttxx&k@=fkh;Oz}U5VVEi!BI4igJy0i^3DqPwv)2wiHaPVh(uHN z;3&$J5}Ms5BthqbR=LoUzb(4<(7iLz+Q$he%8CI#|Co2bHLu^xb8zLN+FuW^Z*pvu z!KU-UbeL_c%_-$y{26*wUeCbTuS2i%sOusHyCWTXu+vWyglc%2!J@CU_({-EZG%wIWS5+nVp9N>b8a_F?2Aa^$DG66g2E!Je?k+v&#B zBQVmU&{2^$JoLpK_ikTcm{*>=yH2fJmZI@baA|c!PI_>ZOzkC_o5djMBm%>y#cw>b zZX-5o=l3rDYR{8ggMwXl2vHp&JcZyZ`lpEgVou+o2}HydG>rsuyt`}plfogIstuPq zsLNkSJI$Cs8^@gidSlY(Yr=1W@g(MPp=ACe{**{rAjxy&aCNA&SP9TdVWG9who~vH zn1xFW&Rv&8vu_+!=3}GV6sy{V7oPs16;=;yUirryQG&c2Nay_G?m>%B{Ba*f(2fb( zMIucf^PYJvw@*r&ZkSb9Z@eexmi2~LLX{Prw`<)evnTQ38ouRS4moo_9^|m|!~tuJ zp7-DYFSU}SlcGLc!mON9;)0h!!pzZE=Zh{gV%e#RgfMG{wa^ShC`!70g|5Jnvt|)L z=wmL=t~29};tX378=#T90j=eUH*!lNa@0E$J)SxCiRd4lsBy8d^H3n^jqsH9Q09nU zsz?01`M%#*!%&idTQHlcK2gr~e~Za0=YlPE+}l!YQ!o zwDjzMsM^YG2WHa8Ec)@5$yJWtYbbyg{yM}HabFeIQY*AWUy=}8MMt7XF#koGHk6nG zkE!t2U|n+ad@$qPp)=r*k$^1m8;u!P5K&gIsvtohvt+FYro_Glr+@?`dU4Vx6jX%u zZ9HK9F?TO*+Bj{#1y_d+Nc15}{X|)b*rM}~xov*WZgZkmAXvu++-%fRnS^vBnW_K4 zNXKN_ccH`7?FmDWg5Czy7-|zz{-ki+mFv5{IMhXU3j0Ogg6D)jzwI*Y@vbjaLO7KK zXio59ArDh-@x-)DylV=^6;j0A7_HEdn(80ez(cx3TvykYM*2NXZo}|IeCOV4&wtUcKt+zMPyf_!of7Bs;=8}h zy$yE9&j&sZo@#Vwe*JEG<24&^ItbVE#rA(4zq6`8pH7v@$^Cl!;R)DE&uew|vz(;} z5fy4mw|`BsZXZ7D2bJ_49a>-CWMoA9wGiRO(3=A+?H~H!P%-iy67?WS4jT(zy3~*d z$1OF@l?G&f9E1Kt-HvxGu8D6OT>TpsISHtZlHLryQ02#$-yIAb3E2A{$1D`u2|))E za5qmghXr_Q)_-bXGE|{OA9MfrzkZwk)MD)9Y#>MGn$@=UC-Y0rIQ3(yVM+i|Zt3x( z!+446E#; zWgrXxutW*{=lGex8WjjOUlxRPi~ z#&ub~F;-W>t+zm$Gac#93oez(hg@+7nK+3p7 z`N*FXj=OHH8Lwi2&#8twqe0Ncx-t4MAaluG!_mao60^bIL8&21;QQmzo*O zu(-t=$G_x=Sc(ho{A11tH>*)(%R%U68=!Tb4&bbvq%KR~5jnD?F@o=9KEVJZ74t%xkyDyqev)QminqSO3}xhIC)5^rTv%UX1NVu+<8Uc&~B-O73L5Bx|SmG zWLQrGxbe5cdXg$98zk>WeAqjJOWFicAuAFOVhx6y>$;z<^RTOI=jd)Iz=L#P?YV+rg#-dcw} zGydUz7s90%Ab#BjK$K6q8Z_ym=vWDj&=ShCGgigCk$}Xn&+$WB48&3fCV*6ZHHo+W(X1RxO1|B>_8Ip+oooQLYNs(cak0+kmX~DJX#4;(Y&)t~(qH z#q&S5}#9vQd}v(jG;nz<1hSUR&Vh5_V1?8gjdi8WTDekD4JhyK!Kyx zP=G#WyB(t!y#DolNX}~mvcOPm1w@KVLi@|ACtc}A|r%{ZRi+9;p&6uR|4wbuMiwJf7)Cne`ry|O#hA9DvDlhRPis3r0UBc3qY5d?yU!9NJW|$IS z-$PK)Pc6_Bny$Yux5i_*jbH;(HNFi^XVJ%O#`7IQt4qMmiso0zisUq&*|bw9(4~)= zyt45Z*F5m3p@1A#kxx&DrrFZ7*v{a$(1;RXl?95{E69|#^W`5g-z31Q3KXqLUYtw} zSPfT;4aiBp9{sb^V9S@qcGGT@KIZw#N$cm{hJH)H-S4Xv0hpUuo_*x#AQ89*lEy#g zu0BUY=UYQlNg#*OMSo~By;Bp3d!U;o;GNBHeRzFVubS-)Qv&X0w!6yV$qaF`9bvJ_ z0m?`0m@n!gk_6zXikvU3}R;cHvkXf8x4HS*BGH~&Qf6NO-K407RQ^YGs zAjd1z>J(D~81K(XMRuV3Gf~M_3SI$rth>!ld}&VV*c}pX1MbFa-wyIUx%>ilE=$;; z%FpN^%ML7j|H&W*SOPiTSIXp13ddcw-nC?;C1z&?!cKEHpV}SviL{XHlNG_jt39!{iEDSX~5HM?Jd0K!f@O*7h>e+SGRTzd|vUe=L3%JnyoLjZXev9O6(!w*6J%xMr&f=C<;17A!y9twS-b%8JccQgFAt96-qW{j zD`RwollqS2JBPJ>^j^dEPC6_O5iy34IZ97m{&_?dOyE=;4tEhcjjg#18nn-h9XH!m z;{|~C!#vjX<%vbl4&4kt%X1U&E;h5>Z3a6hnya7ckUntp;{FwX7wvJEx(chKtQM9% zdHmEw^eFU2$Jn=X?#sv38*G%Z_Own<>4vZa zt6I>`CKuDdBJU(#vrw;=g);)_?rS3Am={Tw`y5+T@4lfTu>>DU+J3QOVkGsxt4PgF z1OL7|1G&{Q?>csC|CJa?dnM5UCGs^dGrbsvh#*V4Z5L`=Ks_5Kj=6Eysh*g&`bnL; zo^SI+`;OR2MRjWGf9%XW!~3L;I*@3zwZUh}qxu_=!WioJM6^9(i>3oflxs_@VhEyW zCe}i2SU+W!Jk%lw0UfpP+3>`s=Yc}E!Jx>s;{o}-V9t!!897il-*9!n29#YEi6 zqg%;q{;1rH4)Q!!fCop>q>VL;7V@C2hEMvuU##A(xChjI;U4z;yX!!603S(*c!RaH*kCY=>#Oc3-x%h}n5kH5(s~c_{Gq=VaB76W`v#*b|D9l>SNq;P12|5fF1U_$Wi%JoJ`o+_y0g?uU>FuBc&l|SO7Q36unNq||q z$hN9XoDy)iRa>S=y^xvYI!Vo>#MUm$^e+?Bls@M5C4QXt>*`nVPL~ZR3oQX-Wzom% zU#Z5JU-~|4C;%xAyE*a_UuAl2pPh)0vQ1M6Xl}I6k7Fgw51kRo&I#O?JR}}62${n%Tnn}Pekk^0;L?MkI#f57{f9l8k-yhQm%a#NreoKK5 z^A_}T%A@b}F}YM$T5l*o;;$9>=^2y(L|^xzkjvUTaRw&=8m6tl70-i`dj?{vy)+}k z&%ZKk3A{AoDQ*gJ6vr)v;V7D$i10W;Gpj7xOdqpW?8NKO^o3z_@$&1z!&{*A?KW7k z#laM$h2m#IF1|F4-GOVSIG?Ru{M^_#Ch@hJnr)q=B2jdd?ZT&I!nGPh*)7~~N3&P= zOeb~C@mr9Cx41YsMG1XmC8HV5nC$O{E#5L!l_39GZgD>&A3k`QfPeCj`NAj9j3{{$ zOG2Bk*IvpVxst0S&dVpM-%sHKUS{>7e&b{AsdsjNkpqw8CdHpaul!W!Ro(C9WM~zG zryBGZH3mL?%(ZpCsrT%8xKXS>4>a%pz>BIyUkP+B3i|bP@icZa&fKD_{*c#jzCwSR zx*TQ8vNl0K${zLSmz6RN^Gx7VYkKdy6h4Jz5x%rlLazu$Qa-SPZW zSjT$mdT`W;r;+FvBPHmROpyf|%@kRXKIXe4cHOt|=vc!Pb6^h10hc-oj?RtB7Wg_hkyO~`Pdm>) zT;>Px_7~1uhpXaHfBzRJ1|cBfV%wL--+Cca^OmMG8z|bWXLK|_%7P7iG+1}ZD}Uaj z+e~>Dsm)vk=r#*}O*YI{ANm(fOjw}WOltzQIRbx6*%P6PPhs^~)HAAf&wFmpr_FUY zzF+6Hci}foeq-lvWxvkSZRY11H;OiErF(s!(NcW&RngP=sB*ZxtgZu zq6#UZhLVb~36WJ?>eY7Ww%D6sC?8#x)^zq7B_0ls*;`5Pyb&`C)=|S_gJZYfgOp$P zJxlGp5&J6)@NMrL+Wl~%xSr3Ag)XR(brscW+d^c*fJNA#1`BrUh5Z%QEb!G1YWzuT zF$%$_ZYg7gbrbZpDKh-G=J!{450S7PZ^?TmbSvWUUG#qu^b5gP^b@H42B+^ZUBn)> zsbOw^=iq?1pH7URUxe_Tn!tmv=yxajJve=bsiSsas|O7^Q+DKr2>Q9A{W@$hsrSYs zyM98Jq4!tcKX=%5>uG({?2O~haZDZ&Xow@qn)?=Ip7>l?`kb9TsX4uPa~$2x>(L{2 z!t)s3F8n~;%T z(B2SCis1Ntaj(wX2%2_=ky_Md`q-%wVGZ}bcwLXA#t$n4>?K-r8jB`qT`v6OZIMK> za!T%%djj0JiUA=9o1-1xit&42J#%EoRD?WyynNP;2k%7_D4+*Jq0Ishj_Ote>Q+PE zEeC$vO}{-C^%ZveHy+*^F8ju_x?6fL()8oazjkYgu8T08CuynXClG$x>6V6O^dqbZ z`Sj2d9myyH(WV>bEse(Ryg2w_-8*vN%R$q%h|q2%8la1TnVW~BC7vh)5031eAO8q!V>l-zkWXvnPYOrTq$I{H4%pMu*^YW}Bu#$K&6*3D z(KPF<{CUI=ci)2S$P||L>#h0G^xzOp3%$uBb(?z~zu{&fz|{1S3l#Q37EY$8xu)Ej zQ>WYf`FL}r3$uY7?~M!jlfsepFfL`21dsK!7gyMBaNfQ5+~V-yD4HJ=kB@Ph4iB5k zFd>apw3Be0>PBpW>&bAXhK9E*Dg&JeZm-EF46%$&qtA4FT^Akt~j)!+M|l&&7nk-OiE{0JUEhO-WxQuReb(1!j&^d_(RisWDLUVY#D7_ z?G^!BxS}S3eCm)tDI8VD2hKW%B#)etiCbHtK$u==(ZeiR;^a+aKRh_H4t{-$9T%pw zrb!uJSJlKlm$3=S@l5~72F<)DL_XnJrY z&Acn9-mO{7!@#iUV~*XiJZskmZ1*ML-n4e7-bHcL0`O%mY$dVeZtAJ??(>q{ipOS^ zz6}fNBVCr>A4|8yF9@2tcOD#~*#wjOGa6@&qH1&az&aGSO8Qar zkC#xkGpfB-NTgQo9vtclH;GOYdw%3yaRgL}WJ=i_c7|}A{!kB&qWKfi{5g`Q z%vNFcdi=Z3#|`VprJUc`O(yQvSe%qf5~|k?!RLJ2CBLY&lLucFBXIDmq8C-NRY$$mYLq;O2n`TQ5MU z#^PFM|L~I(1v$;aa1_lD(M%)!Xa|0^<-1>gy4x~jfuo@3s=GkbzBio*Hd*vB>CWYb z&G1xRYV^%h=hBv_M9TUACoAi{TTYck9B}50TIIfKnQpN8Tqqp5mOG}<;SP>US41!B zvA!>|kuQiu>C*9&4b|_4<6y(1w>ELox#1lsSLB9TEwa;JXyeZlOA4g{j&c$P}v8v7I6_BPN~%RJDcR z$gWtEiDq-+d$fb^dk@A9-`j3JjPrearyp6nLdk4lIG}mS|3AFFg&SOSU8^}-oEw#j zl;1FdN}=WaWAdB8HE{Aio&;f7)9$o7ll{bhM!J#IwB*bnqKTcp0#700%WqU@Hhshp zJ1UW`By;?tcZyt>e3;L>*%%StDmI74#!26Uujo@RSPOj63B9K-dB<<%28D2L`r-PV zei!E!@1Dcv37UoAD4M@hn=jzwVoc*U;alEHI&7GChZ8HD0YHq2=%cgE9(`?|({>MzqRn4lqZcY%G^hVOa`e;~ zOw$=-w>NmC*n#|LdT@v)2-#sVIvAMV&J_WvNi*BTjj@sgX}$dE8JUbCKoV8T1OCJb zea!hEt!#MzE79E*ki(i$3!}s{DEZ^KYcJ8Ymp>H33sFvY`ux6mM_<1F6~mN3KCW;1 zlfrS+E!{O~W=1cvEQqu=+n@M+d(qj?AqGPNigY13ZqmiOrXYgB-Qp8pcPp``;cDcJ zv;jp{J9y+z3dc>hVpo_%qZsaIB>LGfe!<0|#l`6Xygd>iAyq=h=8H+7E0bsn-(6YB zu2w2N3QWDw61w#GThD&*@eU|x8^}jO*hll26!RgX$UGiF9$9KkHBhqM23p&c9l$*#aHpx%lM4Q8f9pT#xd0Ix<`C z=3(SHhAAFe5~tnUNN160fqQ)!c(w?eNu6NbQ$p3y6~uFknMoF9G{t66Uq5Rh%~*6d678hGzmR8iYB#zQBFQr zoA)7q&K(>4*7UAlhG7r*8=dLA62 zNu!ZRc4Tf@o@;d$!v-!uDJE&cTBplnf%v9amn&tWRwCqlc9WF7<4Qu@dU6rwp{^X! zes~Rm4Ucp^$3Cf3vO8-W*(dR?4B}80K6~H58KaS~W<(?_Z`*ybYLOE_`RxAPpT^z5 z-=g9vmH5HtNRDpl^Zt?}R}VaK-CRVrEIk{);NS^kvz>uyJpy~|2ZXIIfceKfJ>$!1 z_wPZ#2!CU#=}AJ5M@Os& z_$F6dXPDsy?K{mritb18?mL{MM`GW2ukSW&KtFi!uYQ$k%*-bR(Y-sTcJ5MR5)wFk z)?!MFvbbKoD00xn0V_76TQ=PO@|jY|`>|ULw2p!)Pf0^-!N0R&^nmxyS6*d!^D1xN zckcGe=MfxGeexHzw|z8)?n0L>RRll)CxWm*6|_FePhEOv8r`Kx=M&EB2b!rEWABX+l@G zDP_lo8OUhzu)p;W6LP@XwE{)=PZl;`^{s06L;VT)^lw8Q8g@7-gadAZ>R;aL7z6eq z32e0WEQ0R&$6UZ8O_n30k`0i%7fi4Neui-VSvdp25Biv0+KhqLLgo$$0G-R!Dm0d@ z7A=mu2k7)Mci#5)q_2L>G87<1N^`I6A1NDZeSm;534kKka2rs!oyZQ4cV!gxgi_x% zZ|{6*Yh8UbEXzOOF7JM`NZ*nW{tJ|sfGmH6%9rPf^eAgxYkFuEBc@AwTB&;|a3s14oRxq?A4lkqM4~H4kpv`q3ZZX` zM3;%oRYr5~>rCP@-F1d3fmmB~=$=Y$8CdM#BaNN=!kBykMLmTg8&Zu5sO9@ne?_wJ_(6s{EP~ukxl=Dn!cSk-NAp) z>*5l0cq^zz2jKX+O~wwqmTJnr6FaeoQD}BfgCoKcgK-x zMufS--6jD>wgrT|Q6-R}k14WUV>y<9*G!XwtRxz2?ua&7>%igMO8nItKg+Pd72HwRv6a;IV5-ga@P9vhydAtfuo>O(`y zPtTmWuJn`BUmtCa3j<7=8*@}aT2<{=S}hZvIgEvV+qnPvHp@<}nTh;L(YbX-l%20i z-A<*-l9s2-H@?I)3U<^y7Ll9R%Ew3Q!ldF1bUaq`#SFSg0>{LwM}9b2_VrXnny!+F z_bmLu^r_f0^N4v7Hec*|dIGi|E`;$%S^4b?-u+=zNIU>*g)Mkh&>TGxR9f@AS}mI6eGpKsXL8^0|44i9_^67gZ=90Q zdj|pO0@8cU(tGbsLV)CEAtd1@ln{`PRH@RX7ZK^8AiWo*BPF2pt_T8(fbf3LoSm7w zJNxkaJ@4xum-(2pXJ*c{(~n#YI_XT*AtEXtUFis@P2(nAJO2y(=v@__-UK2V}c1F!QCt;-M*`(0f zpNfXLqybSo+Elv}wmtD;EWxW5KMyH8t1UTR35>gFAn9p!QA*bx65)pP6-7U3a&1Yq zAaSW@b`=(;r-N4kKw2GXkKg$eq>^*zUb71%94-65EzUep&HX z|8Q{f!RHQyB1AY#+>F?hcSIzY7-A2I(#~)jXE|8?^A~+aoNKilL|{n)wPySAb)!MT zo>xh8Zv#L$FD2;I+?SNWvj_GmRT=sy&Vm5}p`|a#$!fRX+-ieyQq=y(7xg$v{Ac?o z$Mtw>{_6hUBpGt^)L4*)l5l+dSC}HrD*C0U>oT}^NGzR$`A9V`KGxX>>3UkJAPjI0 zix~Bz&FV*So*!Mvi67C&3c8mx$=8h`B0sM5&3_kL#JcSNd`|%~i)XC78m9m{!;gzk z33u7=eL-4Px|`}x8{=(XE>XMww?VJOzN@|eW?;rOZVPf`<~g^sjwi9q7i7?m@qZrr z4j|n}K4?7eLNrZ)WYvdcQAx;1TTdqR4u%& z9O%*lX?5njxl^>ifTqA5~5RW`{yDF;1f7a7=+`` z`H*SiQI?AlX>jFn9*SNxbJ8r>mq0xGEU%bO3?TEwS!d3;_8*>D&5zqchn|bR+IkC5 zbgLY*+!c0o^5eF!uJI3r?fp>Za7m*c)Uu=wUlgng&m zzJ6}T-$X@T@xxv;|~6# z8b~Khz7`iTf&$X@?%cAVL1#oZAe65A`>tLx8lJgoo6 z=Y?Gs#jj>+SV|3-3V}*Zr5@?5zmo;eZB`KlO}qD+^UZJ7sE;yqQY+I*&E`=nh6&z5 zD;AwhM7F=TLT!Su>8E8o_5XI;5vesRg>rlksD)2l8(Q#3Yl`|+J?~$Y^ZM2D z4s-Wh%79-@kGqpT*`i4(9>bTOzDBAg+1`0qCoroVU@2<6i{ZQ`#OXbJp04KVPKs!D z@v%534JMmQRMtigp|2Rb)-bW`h*vE`{`CuYmmi{rJphpQGp3Dp+$Y{*_FBZlZCcr| zZOwU8!I3jY9X`UMG03;D^xxfwo<-`N+G5)|R3up=|83M{2&Pmr#ob}KPll*YNl@{u znM3oSszt~e7WQ=Bf?5Pt5;JR^^0@3Dc&}^joi833sbr;e`=Ia8Rh=a{E}LX>ESP3? zFNIeEs8|0gVec?H;)z`>rk*` zb|{m_-m5lcYrD@~_5lXblH&WXzACbE3GAzjr+xjyrYj>|_8}g;77AHN%K0IoP%To| z2TcOL9p1`iAK{L){pH2M6gSTvY3;I-ZmBc<$$fBhByxBWQWD4GmH4S2aqq&pYpFsi zLVme=wdC={JNvlo6TZVubNo0qXHlu1#uNQBk3~yzW&Z5HD&^J1E-Pp5!W&MN#mh#_ zAhw;B+P2rYI5~|CMz_)OB%fT}$2qdZas**H5;7orpc2?gZvktkUT!W@2r*hV;_W@Y zla+r`FjC7PR01p)Iz3D4|Iq#X=ux8}sVwLh*{mfcT{^>q)-nm(GN3G|dvnWx=#5{{ zF{d}*oegSEdoK?`zXwvkVX>(g>>8OoyxuGM+*%yxl%Ft)7n5?UC1e*Gn^;lZItq!# z@qP-T0EON@YypEF_~ zdH;%Tcl|3~d)#wrs^yP8Q&|5>_K1<(qNQGZ|sZNNSv)}oGbNNr) z5K{It)l2FWv&I<*!BBdug;z>gL+Sl;RLhvZ3P63TlvIYEmYh_PWS-3R62)G-UsOBa$ZrX$*0$b=c@;$Pu-=NiryuvA#U$?k;a+TNo=SlG<3LP>K2~7M{2d>4 zhUiEI6Q+l>S35h7Oahr@>2{Jdf>G1e9ZT9seRh~j!xG7dP#xd5v_X>#gOs~Bs8b3c?T?;hS-Ik&4A*>`ZeM2GHNMGwf$BJ;4Jp5WVP#uLG!8L&u)BU3NW!bDG4=-yWeIS?+=@ClB(B{rGo zU2^||!zDge^Ib`6xNqXABX!rTF|+)r@n80Sceb({qJH)L?;c;8zT4E3AMWgW3(oN=R$UJufl+lR$^IG#0*QN|Iw^v}(F`A<|9SE0D9 zK2q?`XV8FG+maZ1=E0ps;W>ZkP>2%N`HcHc9>IDW(qz&)k8l1|ueQtD6%=!Eb#5dK zdL*`lBX(e|>p+qz&|oM~nQMaWAKc608oT zc;KpotkQRmkC_KJXvjvqc?{&A6pWfP6{t%{%~n&^`v*EUfc3}ssZS1oDM>c`oI<4z zkqz0s&em5da(Uuifml-6u#lxQL=-GFSE zo@@N1JP%S;Dosh5RRR#HWW%m6I5x+OSfy0UWyyvqyBRiKwJa3W$pwx=BHc)Y2Y~{$0f7y7wErKrp z%KuHS-B7w3T+pe5DVwbx0Ezd%@G7B^ds_Q>XjR!@=FhoetRO%R8los1X(PfG<$Z>T zHN1%AC~o>#DI-@zOy3Gkv24VHPokgNU*f0sRB<~9(EkBmQG)`&xvLP%3Wll_-GLsu zD-Oa&SF#arXXOL<0T_jz?1}bK8cv|YlK9|x>jvEt;c6IvxyQ{yC!;{)H+%%NsA<3` zoUI7wae>nzuEh(bP#I`!*Zw@~;&lo~D#hH7ejW@Mg)@$DzNIk)It*=@dk3_uGHMJ0 z>)w{jQ7{w4b8Km`Lge!0lc!iqaK_J6H}o?*JCQHYQG+vdV$~GYy5M^Mw`8@>eRgla z&^IzUbS+N)Ed3k;(3#qEq92rO;WDotStHt037YGo zh+RVtG3+Viu}nKB%`DhIL7wC}NXR2E-C$h0vdobdUJQ6e05X8vCj|iE&zqI_*jGMY;a9D4bx`Zq$lrR`(v3>TkAKFA5a)igj#AYn8~BQ z$BZR%a1|o!<`-@Ba+Br?o(trh78gubV`AK0c>cu4I@M=n%_433K!@yU#7mvd&vgJp zA8W)9wMN}K8jffpPeZ;kL&(d=^U7#tg0j-chN2P~{MlX^=ujC2ty>1q0EGVJExqDk z-5iJ@$IU8);B=Qwc)i$@2bV6keBXIAN>Hx8 zv+#1=Ntzr_X$YJ7=*~`$8PdmU+M)jTaFGEb@ zUj{=}0p&Dw*5p7HvG&ZWnV(t~YElG;-`u7RR< zFc%QsnYPc~ZJEDhYaS)~|mWQ{H*r2A4@%onO6l!nk;*HUfpze4R$yS8OH0$iOvs|wY!;nj0%@18v#L;sJR>y%mS1_xz_Y{(Pz@Cr^u zLSQdA+R$qlmk!5=KGuze8CUM!4^~4q;_X7dOTrrz=^ACjF2W#s51XF8#y;d!4S29+ z!`z3A<%LeWXcy;ws2xX`@y;YU@Jued1cPWIu+R!klx#>M*p$I6AybUIUvz{!EG{@U z9B%C4Q4u7%YKC=8qj9;ro`zT~8-6Gf=$mB3qzLO2lSzenGSm_Aor}_{$%Z)>6-Fl; zaxM>F&=p7Tu-BWw?m_iYKV0|;Q3&BodN%?b?}q%7f>Ai%(PnlAu=%EaR(zk{l>M-hz1VT#SO1cnom6ro?LR8l;9PG8*N2#3LV^^0Z;gM;v_tzAWd0kCHoQ#ls;Bm z;=j+X`E)fz98V+OtPnM*QQ2C{QpUP0*{~`1CQhNwx6WRct>NA#+$~!xzAKspF-ke( zMRhLChxBi<;TfcRH-V4CNP#`F4+cpeE1Af~2;G`&fYvl8cMpo0U~tDqXs|U;+d-j_ zon=FUU((29#D~FGu2$XE55Ab&;!IyVXhKT$XIuE14RaF<5cn za-C_Tp;?vf$FfUYInx8Z5R1|#*2Feaa&;7S>6qR`QL?Y#&$Ws>oz5OD$09C?@48LWjVjJORk1+3r{IS zd&p&83$>hk5?+Lj%B>x=Jx>$$qwVx8sUN}L)A6HD?}m2W*zOb9OhlQa28z^QB?EDU zSmy)}lor3c-w&Jm`f;estYleqd#Bc;-H>hHJ?p-$+|5BQvy$ak_5Q9sI}qy0;jjMv zbAMleWD!@5qbBaws;BxL*bVt{dcD%iJ4MbD{6Fn5dl!9n^MVPgUMmKwa^)uQ|BpIn zIPs6~}8Q{K)L(lG$?8Mo`Wxem1r|P+! zofK(W(=HSv@Pr7I!%r=)+EoXIPG7uvcXATftQRBx0=v;3Pg^q3y}B&W)5qfKLEGQ} zpYM<1BeqduQ{xa17f!T2BsYQzLkoQmF7FWB$t*F|g3lH2CcQEEhWki*+reI1%@<_J z8#`=g9bl1(>1~QM2RXDQm*Yj?bC7j%lScs+pbW0TAo?J-oObz1-X)XZDp8({idxpI z4p~p0TOGolw)I%oL4<2tTkkyE`gY$sMc^SC+@ZDNd{SV|GC%EGDCL2y3VewMcXuT< z5Pd8PamZDA7~Fgw_B`Jt{|J|PD||iLsx$LYPvrNgI5_yL)STazmdu@omh2w3g4Z*; zdj;99AYPmZzw>h4%FL$`M7W*3x&Dg>KsO!}19-!!FDC=3YIvBmADgwknm;9XMy z{%|4}TPwBC-5D?hL^KAA>f^z|UOEuzt!s^)Sz&myzEoD{Dr45G&UK!1qk?l+@}e&+ z8AJy~C0}UGe{}rq@+ey}yABY3DF!MM-kfPnVgbf%k zoUi|XNdBzK(XJ@SK;sUf5DfZQQy#_Ud%Ol-&9V`1e+@F=&<)E582VVhFK>%vo>05V zhUZpu7b8Ea7XsX2Kj6}D&0(q)HQ<`--BeJlRvfLC-Xbyx)Zzkog+K*^EmW>qY~9T2 zveWwRVNQCMM!K(O(!I7quQsU}FmY`aS<>=O<8u{p{huB?z59hH2+YR@5XbIUp!Tsy zeBftlD|V!AI{S9>-%hec!@;&|?em#FT|QEGa~<8-&G`1x_zif4UnE^<>x-XL26V@r zIckDyVl`wLmecsnFMb1F;SZ(pw`csiVoUoh8#aGm4EWiZ4i$4Gek$Z(j=%Zt$c#B2 zLsPY2dhI6#n!$ncdqU~FZIoXlC-SVTS850i!6lA~L*;V$`=VQF7wPP>n!=Yd!|@*2 zAoG23N*8WRZ4g>TFD8TKdN|(o6_vG1sXH-G8r&g53>Xa0Cj3S#Vfe6Dl=W?D!Ua?G z?C>R)2|%qjbw3)Bc0ByJJgOYM!;4oUXI*$Gv1fNVJz)yZv1|EGc#){e38UWb$vtlK2<<;;)ttU(Ns~2R+~I66R;@wx$XCCPLATj>3Gx8mO&=@3o&c3!2e1GW z24-#ZIi5_ib9>Q*gO{;$M_;QycUd%o zW`6$mh`g;|e_@NwzmSW36CrLm9%FWrg^p%n^M;8kBVCR87IP{Zcv%tCcmDF1e%@j( zT!_<*Y&r;d0TzS*k}SS8>_pkl*M`s1`;2m=F`FLfh`7(*t4E%r>AymDn{<vr2jldUr&kOVn;W971i=4guL1U5S@p$v2wuQ)6k z7sheVs+eftG+@-uEe3Q6sVQh2!T6|ENqT&8P!S)CEBk`EsJ}B+G1nR@YIa@h zU`O;6*Dc3-9Q$lcu@fs{My93pBDQ*u2fnWT*@{+SB*W3&XbVn}H9L+OF<%M?SFR__ zMYY5YUY;oMFQE@~EL1A-a>U58op2A3EECPn?!cQ&&x%zrIF~4f(z^7kSu`A>pA=5y z*`@0>oVVa8tn8iO&@~EHtwKZ}eCGEbe<*-SljoP$q*V!s7ZrL;d*S9%iDDMS8r#Ka zR8Ar4v(?jQpSX%+em2o>Nm5QEFAt9y>m!nq=wlf$Y8{2+1aoqZ+eL;vSaS}H4!g9w zJ#dn`((8m8FbXGWkUY$i11FyxC=HtCj?1^XQCr~L?~BubQ8yy6q#Ay>PHAoTIGQAP*_(mSYdT!q9cmBRP+gN-iR?Pgt81CKM`iHw(>0rjl1QaY z62y`Eb}d<9#S^QpDN@{YReNKs%n z;(4q+?Tp!TI6D$V#3&}}(>ucRRtcLxu3&L-u@W(q-&#Isw-+VMV&k;@xd~W(! zWzv%$E!@5qx1F|lN;k?j*o{|PpUJ0Js#UqX06}4O^X{KAt0aU6-pmfVvq~qKY2~1Vx$9bSeNVcfdVbGoew4uvPiv2?m>+$tyJx3t z{AN6!*4Za>&*)NgjZ(0?EvUm2@&>ypB(*0JcJ~byOhiOz7=rMkf^9=+WN{2SMOH`D zcG-bq0*(kA)|J4~aYgwue{C)@4wnam=!ci1-y0)HXJ^qNH+n(88_ZSv?u#VW_~ijWlv=g9lpmY z{V6q?h6vK{JZPTnl*$rD(?9EJAWbq_Fq26`lPc*2Xw&;zDyLEtu)8L ziMu9sAeN*jP0uMNa|(!TC`fjB+wNayl)#n?@g;!)qt0j!;7kbY9U6`3IA_Urwz;L3 z#<%o(q5F~DqZhA(whI{Ws)r)fLwe-=MESMI80?LZNMw?5ayrc6mP9yCMJiX;U-N6# z=(o9iehU*W()mIHjvylz@j>F_@dR|BQ9~d!b{G>*!}B47(d)Yk#R7lrZi8?JgJr-d zoWlv{8(QB$_bKRe#K-F2E^^uId+?SkuzJJ&zO#1%r{@yZ{qp4oZB)9_;~O>K8IBMb zai_X<9cZp?0W@nu`wp9a=0|-jFHpDHY0o$*sQgt}qena#6xzzO^uO1{e%sLnjuvA-QwEpPd0njjA`_=I$dD`)35=lPH^ ztJd-9r}iEDX$aPxP7XHOfLF^yewH>pH`r;~qf%yUvbI*~H8=BC_+{GRzBoCO3W|}>ZxnBt z@Auv=>D@vShY;C3p%8sxesHzEvT@Z68QkGu8s1J?Qg-GyxU;&&ZoV*NZ+|`WP#T#K z*PH}r&J_Eb^8nMiG6Yc0%#%;%$c$9iwmv56&SH%=`=0N_X?JVi1!uR7_P0Iu)etyj zef;v$rzN&iB!*!oHDDBx2hmh|i|5X9rLbKbv4jCR6KDN0Unp=c^F3DvjKVnvI1>WM z;{=CQzqz4PhKz482u`iC5pP%ZUAbeAI-G9&CzeGvOna_W-dnPv`qf5zCw(})8$9%l z&-W!wnu6#P&2!P4)__qs*U((jiQsso8t`~02f9BY&VWAFmU(sCq%I6jLN@%!OuWY( z!4P&G--)XraoLckrawJ9l;8>vcb!b=l|I(Q{8i^|Zx6X!HYE61Vn?#*Yr)GeS@UZ$ z@vS8r^7m7zJ8|foPmCe(DEepznLvkwOxxH(bKXx|gi`k-yAFEZZi9Z=^jq%4FSXd7 zinIA$_Cw|%rc}a=>0|Zpl&nuzNpE9(UZq=qo;%3EMb_2$ujq7qJS+7=PPPuA+95LABYxVFS z98(n7hOc}`uW(Ab_AD31PamtrnId1btTqz)xL>XO{I_-prZsY~vymCBmJD<$#`+k0 zH?A$tH)`r)G%Lb(CTJlY`KX0DXDDdt@jq}}yfS=TWn?yQD`o3pFRmhfZYzF40%Z_3 z3=v29aOrceiYo90S=~=A2L20_719)ICLmU6r8saiH=IGl7IEBKYDWBK74?&gK?)6? z>|i-b4V#`;t_87Ev&GOIULWiW4P6u~0FlV_jPLANl7Lsl;>uaql=;4vekJzP)haX- zf$j~Ie1JY4$7{UkGL0oFukilTZn)|t5ye4r!WdS9L9`ogRK0(`>$?^HC^_Ow?+KsU zQFnXNaia_FnpVQ%43g-UV>|}p7VOO&xZe4kwzay;BCYY)b=@v|G0#?Cl!_qCRrOT* zfRuD^2>#4@u2qgIX@*eu#yJ|LhmA~427mgfT&7lH_xq&ZwO@cyde}%UCn)RUp+en` z72GazD2eBBBn@E^FQMj4pG-g5{(0^YokHQOS{IP=6lya@)>hxFp1KbPg}#5y`X#pJNIis9=n?sf$&(hZ zGvp&w4y5F@fesd0C6|%uE92$Mq4Q1~Fp9PXXp&-61v(s4+LvngZriR=FkjUEb4vb1 zNCJczh_%-HIzVh`-q+u}NwlI;f51PayrW;wahcxve4rryN?t?_K|8xRxhYlw!>N?F zW(Uxj%rRbSF9Sy5q>{LHB*6!|JAp43A8YC5RP$$dg2ThRy08Cuyl<)Al1u4q3HX2y z!7KbN2>&=-JixEt@IaC854JQ;`M42de!%a%-aqeME#{V)AL_E2_r8~8&QYi&`th{% z4ZzP5I9{26S6*d$WAi1EQzl6%^?^Q`e&wmF9QvS>k+Y+@Lj@hwWPT0xM!@ zqgJf=V(Bl(v2iw)sn;U-G+bH@+m|XNB9rc@y%I}%g++UJ-WpgQ`dA0X4@*+C!9bh` zPeaL(S{2}YYFH8DC{Q3zq<)429MACV*jb~m1V9-n8|F@AE&?F3Asy5-^Us;6;AABm zb`#!1n)^{nHD$xxtVZ+Idxu)s4IlWK>V|%1kEY25I^aXAEIK`62y`WDvQ*FY#p{*& z!Z=IaWYpcjk(11-UV9Iz$a4(L0j28Ec|{^zIZ+D8Fse=nCN>Em6Q{O3cIxU}ef3iZ z_E;W}J_oN>x0|r}2OPXh?iDNl8c6Qt#%48O)Fgi;oSB)HI*ag``5zB=w~j*Mv=#qM z*-&#PaE=kH?8r21z$l!L31?Qud5)w;`dHtlFZ5^b6IdUjz|42#{_J@r^|L3#U9F!M zkQV@KLD1*%Tl7V;$@e&hnU=QX|>X)8_;0igCu~9B) zFNL8IZKu|D>74)yHEs^ z`~xrw=R(5SkKhB{I@Riff6q40@Qv_2AP+<-=SSB`27>a~!%I#9Kp? zoX=ZI6$DHU6xXUooscr6bfn^AT4pt1CTi(V7MEuRQjg!VC&wK3902uaX z$vl2U;yryl#d4fcupW^|guc+nlU@WqQ8NBW4L%pK!At4TI3+=u*DFZSM5EA%ftXJ+CYTn+t^o*UeSYA5KLO z31j#6?Us{j!ncq4mI8X6wYuA*%h+E6`=aB$bw8&2WsJ-EvDDieLm!Ptw}vM^Z&^r% zY>uv~7ya=Y3MQQ-Pf3UEA1gUa+=oMUO{8tij{U8^fTOT%*pa>+sVvsAA>{>q zg>hh2If#)~ilZgs9iIPMOXjrSPWm+nIkzBfg+CfFX0{0AQ1@2err?`Phv!ec{HIC&aL$}~B;op%NK}gQ1kZWiamSs``d-A>$iO$% zt+rJ-{pVOST0-8mNC;`-R6$sB|H)TZKlQA|gFZ%I_quF}YZGwFe>V%P-_KZ9} zWd`(Kkzp^E-z43}C|)>yRjBX;t_}ir-E&pwZl!c=eNwty;=K)Epcu%TNNY4xbok|` zKP%vpT)8+JZXn+&CPXW8*eZn0&58?%4&!(%x+69|*M?tnx=*kDH61q6yCkGIHIApE zlLoma*Fu_)@Sh7k3LLIasa6Mh-a)$Mt^V(dWGQe1j9c#wgNEg-@2Hs0V)ybr2yee!n#CO;4})i%$P$B*op1d8j|($J>ybU6#aY2G zv=*Od?IpMSk;rL6B|9^(!pP?H+w%b!wct8_Gtg0jSS3@ zKZGErw9E4i9vZ*Fk4&Uezl&JGx)-hD_55@wtkdg5Sruei1jTRZqh`RU)tu;{nf77gBo_^&!Y~>%^XMHl&M^xn z7eBZi+{Unz_LT<!TXcXYfTC$0QR z(k*Rr61z1z}9FfZcWY6-xJnDxvVK!cauzNGM_@8f=1^hP_)m01+e#z%r zj}uYSb|j-V9P*g~M@P)%>of)KUYK)2MwwfJYIO_0J?s*lSoXvUQf;-u8}Dhv+jYe_ zKLkTm9RMhMAbkcAe4v9RB&A%#wTSdt`t|t|XP%6}N`1pOu}d>R9Wzq~rF1oy9 zAGh8P@5!6ut#%0Xth9o*gH3vk#laNax)N?mQ=VElRrHLj&gJE`>`$hiM5v9>%yifdz(hmxiK#QiQDoNLW_!(TLqo!~lS z(f+0~$kIACrxjkQ)_x|!#pw{jwYJ8{jcvbcq9)?b3bM%CKo6|NsaDsX71;?*DzUIx zeiyq^csu6?JW>es3+Wrm<)|G@CGH~V#c`)D&;4_eeH$mc>?OWqwHA_5WjbV0s&AD0 z0P>*uf-DKoyRX3BFHu9t^}godKLE1A7bNk^unR-KnhdYj*3aAjwP`2l#vuCXxp+O( zh~Z6O6%%n<;ALpA?M3UPoZ3+t-O*-}76eJC8k?s4|jhcpB>N`~Zx?9t`ZV zvb-*!TYKC72@fto7TVFaTBrB-=|p<6-{@WU`^zU&|M?ci!}1jtrMOjNs7t2o*H0zq zuZIZs=I73f?Pl!HEKXPib^A9RawnmB1#f=VV-l{{v2$)*NIqi@x}`u}JZrjJ14d0{ z7~y=yICZwQ>F@yf`0^S=)~sD$w_@Rwz-gEnp5HpuvHC>pEjZkwgzonw2#aB^#TK(G z@T;NY^0TKK(>rnIC2*GL*)jn$LZZ=V69aMG(HtNv)h zFUS=hbUzZ;-&Z5%j!7Li3plBKftSuqCPTECU#{TiPMJ`o)u968n#285_?B9J?i6|L zS&O<18_<3U)Jevj(*MRUwp9d%$CI*x+A4B~PMpy&nu1Mft8E zDBn}MmaP+eZ`Y)ijrQO`$%dcJEdQin6n2YtGo3uI-I=ZYfvcl4KxCcQE3RYB9>gaZ z2gHC;I0q8WbBuE%csnFs4UqGkPX|ge)^$lI^dc#Hgnol zA+N^I#ZF!DUF-B`AFTJY4LKwn4^-MWH8-{RYKy*dsL(Q(KXu-uJEl;jSZF^^CLUFF z$lK2`GUFySVAO1N1!gDCbbHFoE1m|D1u-fKfQli;aSh*NtEA*W~%O zO@MLx{^X*6q}@Xgje*mEQ8+c9<{W3M1I@1P-8>44zh4I~-O+-K)do%jM&W!zZwPg1``PjIm! z*IKr5dKt4|zj9qz=Tzd37OOVnr>XE^7`1xZr!Pq^GNxj{sHyZIoJ4mol4r}>zuNH* z2;7j$Og}gc7=<&6a7OZZ*KfnPYTrfw)X+vn=kR<852N#LjMIQoIEN9=7QW*wpM`Qd zfSjw7%|E^4>NbBk4H$)!sukOqvzEGs;9_1)F}a2hZQCl%thFZkkI z{$S+P{S&c;Lt9_2e>UlGe>e>og>x0*{DpDqJZb}l{c9*nZwXHgiY0tWThhr0eX?Z2 z#+whpe^uS`+0yj0k?rRxp>dX!krjzN;kDsF*2itdg$xh>grpDt z`OopJ>IC&pBm2F;%MDkrhYUgLpAPkR2@%IdvRWfpq!KbfSdB)$T@t^e?Wkx--wr#6 z@nc2(dqjqJERX8lJY!m;tFJPHvdg;V+WWML-`#A$D4c&2&P05HXq=pc6WSFv0s7q~ z28_a)naplUd~p^((7eG{A=o7~f5~6@U|KQ>8K=&GQ8)_{&H!JW?8JbRyzX_keuv-r zox1^}aJpzQQo;|lcSuY)ReN#Bo6=uEIS$AN_(L#+ISgZbNN!P5^3@Rq4NjMgEu3U5 z0!e?EQ=!4*Q~oHhX6NSA$u5CPh;iCn^!Gz9I~P{SYl9p<+hJh< zrEcwLTFm|gALzbS5!;PEmN3v~fVdhxB>SI7|2T+lqr^FJ1Ut~XHuKGA$dv7A*p%c8 z-RjXVn%ASsr7{pZ%47&Qc4pH-aXhJ8H!X|(n&6$>A1hQLM{Een`}#?TFm!9MFb0gI zpgR@eoXS(t`Ib2oXIpUfA|+>Atpp*Nnc{&xM zc@3b-W_UJ<`gA`g_| zgtFlWx%`tV#Hf%L6hv3*11gK8gH2zx_<2a#Sy+A9@P}Gdr-Y%2%T(6#bAH(F49kIR zsNRdv!dUpATfHzx#zp>}{KVHtpz&?U-cK(6gpE9(Hl}ki3>by87U2{^x$|(SL}aqz zXDaefmTV}TbqS}4eR1Msmo><$VV8?OuACWyyLsRY7=^O|aC$13MR1)*uhT5kj$5+f z2f6%{f>Fqu5%Ns5tTB-x(ZSA4^Lkru?C{6@X%`XqCmVjckbhDzstc-!=qWK485JAu zjPmFl9ew{mqteS1&JV#Toa6y- z6VnTNYA;n8Tx?bSeo_A1TNe7mX}~C)?+NE{#;J8YtXGA|mZ9UR-*WaN+nQ&MJ^-U| z27+s|NBZKFLCsc+^H%b19UbG;I1L!W8BWX7o<3~Pa?qbEkQRkg;U?>JXrFT8rWWuY z(-`=s{t!IjCp(NSio?+10Bw)qAMvr+UWR*)Jrm%&hnGOuZIY7RCW$ENqg_=S6dsUd zehQdKz8-eB_K=C9cC&yM!8S=Ogf%)W@mygR3fSNGo@&0JKdbA-V0vNj2`MNjQfIcykvQqofqjBfA$4oM~{lI z>(vUg&Q`DJr{27QMUqoE8QWC|r8Dq`AL4@0X{5TGuEFmGF?cHG>qQm$Ck3N${sDB7 zmD7`rE_9|x+xuXU@6OquV|r1T*6c+Br-L8{jKUcRP*%aCVpK~}{~P4i*g32XkC<%4 z+x>m9%Rea?g*^{pPfzOz?9EYUryYtud*NrzF4jRd{NR^=QZNdCVc@r~vKXQ-PtxGD zdJAJx%|b)}j5a39bx!%KxHFDz__*iju~47jYza~81h%tJa^4U8OwEo;Jlp^A9@4{N z^=vum6wwN^n-2V5LpNX)&IyFm*q3d#+NH~U3g5EZ6Y`;SC8foz+1dsnMwQs7=?2m;oO9e*ZIlu`?!9S%0c>NU&0W7I1Lzu zQ)JhqICd|b3!*pt+;sqKCl_7RjTM{8`fVkU}JOZrM(tb{7}JX zP^ig(w(PMCq*??F0uMz%gzT2znR!UQk zsI?nEk}-)akGY6ePMije!uf=7juR)?K?hD%fXhynoA!g#fKfRAC7jj8I2|wOlSl1) z$JVL?Q%Ty)Y1=%lhjA|RT`yL3SCR&J#B8td1yLm`I8?lh5P1bT=^?h4`;wolb)efM zX?|K9m6}9egFFU|nu>^jS?P;2(W*4L&z!@9*!@!J%W1Dt*r)@ZI-PJDSA_wiaFVl+c~!8FVkgw}ES5C+)>z<_Or{=#^H>}33jYPd zPc9?4iu8-gnb2^&D$MT&kL_+4v*99Sz!1)kxFFoZ2gb8+gEQlQ(8uZzw}y|7oToq!kBDq`qrajxCKYvf^sSEk~SinZL|On2Fb1&D(;XHN#mTbmNz?tOGD z#ATC71z5BXOnAXd&5Lb-g7pg#;f#R1|9n9LzM=eAcy|9>v+(a(3lSLko?U-B5~b_z&Vbd<78S;Xs-+V`q{Al^Zdk&`o5XaU0hn! zIn$C=uq4kHhs-e}nZMEsn)%=J3O#!Gi6`L%A5e=G`0wulHx^>jMm!0}F2Jkj=Fc1M zCEDLjTz!JngV3QT2*aI(Y^e0HIK+WM{+|^~bfi2UbZjR9PrE-MMVLFXcNko>K-mxc zNbRZXdoA-;HSmv3A5Xf$J0%J}iI3GTck=Ef2hDa_k8dyiGct6k%l?jc zk@n8q_|f+zPekB?`+g*)4wB+9!QMnY53`0oiEuiyH`O=vZ1;c>7hm4iWro&$2F}tEKBX_q|}}cN6OgL z28=?yj^@RBJs=KtM+Qej;~Y!MgMScbBR&>oeLH>=$3`~%OmB4auNxkI5Bq~`;26xM zhd|V*Ohem-ZVxeh=c~j_x8uCw)}aL54`1QONTWbMM4?jgV;m%XN}m2R>B@}uxB_KE z&1oaeNe~NKf67dfo-P3TSf3xbvM#h-H>9caH2lm&{z<_|dg4ao@@*Cx;y=|;eIG*K z_CS2ltt$<>rH?1z2?n3^=lAT`J7g% zvo(8*RF)=A14iMblHc}VUz`ayHyqFP68rehjy>stt z&U9HrRwsE9_m2FyjJX>y3g;`rIm8!d?u~1Yx9tE|S`Mlud$obnfKfP8;bmtv86PIc zB5ZHBvgG<_;Qh{z?et&9ant=FHeeLuf(o%`tj9C6j!6?L< zIdXcDirX|7j%7to8H~YRVWveVTDlpx=JMQ2A+WcFTuZxb8$ygKXzbjL- zOb_8~@#5hh4s7XD5Pp-pw+vu9g0) z20#D)9D%L!F46b50i$qIp8kZ?F}?0i#Av8UE}zUuv-99UfqL!}=Mq0i$qsB%JZSIP;y03Qm3sIN6(! zD&!fjp8=zA+C+s^9t!(Dtjh2s8-7sBKUuP&P_L%p4&_a(Q|xqadMtU(1(1i6?U+=i z{tmhfJ?!5HU=&Vb#BIvUiu0%+!U?rrrZ13l27(hsAPpRs@brrF+l5qN0^a#>8t@AL zdcyx59}NB2Z9G~JwO#OLj+*lL`pbD_mEq?_c?9)=vi!@$A~fP3lge1Tw~$4Hz|MGDN`{VUSJMQep>rus zIG1w3ZF``;NYir;*MK3^K+1x~>nf@cd#{Ml5ZkE09u`}U_^(n8+Ldd7e;vi%opbEk zdD)hq3!u)gZ1|a@{F8$59!PaH5HeuU3;TaLmaykl(%jpCcSbh+j79!Q!F*^eME3v7 zv7jd6^BYf2xGdR-w=?nr>Fg)7nzi^NP1`LH0z3^5Cu`5c`;?G}7+D%Yu(P%q(m}=a z3Kc@N-VJGU6dKm0{(d;$dK%u7s;dZ0D4p2;SCiBIZCY_{*q2i~K!f6G#M=k?Fn$2W zdrbAb#?;dt+Yj43O4!BzSA){jtd=k3n}M0Y-phs`ipxJK81F#^c@4@#>HpPm^d-CM zG=*PcII`hqI8w*#$xDh1cm^3|ZUC<&0s0U(QX?jR{9r30E;>AhY8o&_|5szzq~rwg z3uLBvO7UrIUIT_mDK5|Tba{FdIh#nLp^rsbJ2O<93x3wUf6|^&M}X7ASnBKyaqX>b z`|?8R*&<}+sEOYW_oJDkf|Gf(Lo=5-RmSKP?Gc-9Xy&XRW*?8Pft&_SQqO6BIn5G3 znz;whTQ}&I2%5Qz6MlM@s@`fpnz=gNs>WMe(NU@6iFeMgvC@xbjw?*phc{~JCTD^l zfM+!{b1!?pJ6qWeo!W~3(!-IZj~~q(XL^A;QuD%YpMN-#gTRQ8;rD&Ktb$`pzj3*`jdLTAN^yKRe)N z^843-)1YpLA^j?K=n3EFIY;NC3M=Hhq`uv@OtK*6X|s(ki?jD$RSV$Gf+q~cX$C}7 z&G_J65$!uPWju8^jQcjV`=__A-I=evn|ERM=d3hv3~UMJL3^cskMB zl&=ElGG)WhHkE%;FlzMQ6Ghtcf|FCYn?6>L_PgJ0d0C- z`g@$8Eq^QKlgq=uhIur0V7V$yx)0M^zB6_E6qLe_sfK^V$Ku>}m0*?<#)C@e_FIB+ zmZLYew^`f6OuUg+U~$-XxV-tRAWJsP@*nKUlvH{7z3+WaOygxytozW5B4T>JOZJ)I5UA(;OfieXJI#yT8b~0^+x9D8%}B zaZ2P*5Z+Ma_0U-vKT&Ul-qdH#xb`2Oh<8;s%vEo+Jjo|l_tiuZE>FX}=u|mD*-$eW zN>m9D8`$x{6{`2|sBD?0LBMD+=kTF>H$j#9bR?ZpmjR=2QZTALnklWXg&Iu?*I0T9 z+{(ZeuTPb3LIGyRLu|k(oTmt90zOzhjkDgE;TbE0^#pt4@eT5W(|}PpDKDO#fp@&l zcYk%s-(9P>uA?i1$sO;wpgCn&N$F?^Zf89W&FMwYK^8e31@(*_uZlZxd3$ihMuAwa ztOYFZ=3ZR2X?G<*JDzhxWkS?>n-q64H7uo1bRU}ERi>HFAj=xjcCf^XnL?8dX^c;Z zqDO#R(?eBqxVQFBuR$Yws^}(>BRl^ef{3SH8{s92@?g~K z*;PM->h=A-!xJ_PjP(u($yaa*l=-&qmKNR~&WBJR?A0QhIxdSLZ+~0mdNa}=Sq&JqRKEfz z8${tHiksQ%#^)R(prdT~nGy3{kZEN@&PWr6bGFo#$y@4*84-_dlLxF63p?O{h{PyX zm&@nV4R4OO`QpJj6AG+Yj~T^am|k9%EvNR&89f`<+2KSKoDY`uDBYq?qhRm+)dIc@ zfW0iXZd(-YU8%cGMuhYPPH10t&R_!gDDgTRLbpbt$Hg^DT$q0k2Fk)+{uGL$z1ne7cbZM<%0uu|_Q*E^3H%V2`3mWh&ETQl)n| zWkfYUTUTYfo_;oQE{ z`_$V$>1rWs+sYqY+0%B`BkM&YTZ50+@x8Ec*Rg=N5Cn5&oIJYeQNJTIVC0c?k0qQ` z5!VZ6?fIL+t5t6dZPla+Cy#C;GqkY|28_ZVb8PHnmj^LNKv0&Q{m*u*e+64chgD=Zx4!Yx?^{E2%RW}T28{x6Y;<_V3V z{CDs=ax&kyb1gppE)G)DvgAR@K5a2pFU3+C4u>o{hNChM%XGac`YkTj55cG@id;jT z`Nq(<{M1~TdzM-=#AOw$J$z}y$p`fHb%@5`dAvRVukim!@N;>oHT` zkG(cEFHR*jjg+4TjKZnM;LOxFD$o1CQHgN68q^wFu+d74S=IyA{TeX9`MPH50Wo&z zh^Ss65v3#CJxfQ&#fC>kmM>MgbPS3Smy8bS)i=ZnjVawPBsMxCDwaS$$_qLF=`UFr{;wc=Bi$*^Au>o3#PcG9^2O^(T=t*;pIC@WG{lFELDhr|LZ|2M z9UC5u^1kjU(st@4F{;Cm@F?b|!wH?^G2mrqc473GkdZnI7fy@*dShiy23guZ1GZy= zFU1WQ)x&?7x&4l&@0>yW&caoDbc7Q9-iB#A%6$V{xJe=dM&XR4jkkfQ?!?Jj709Z! zwgIH`2P> z-RNE2WfedC<&mYIH^ev%oCb`<=`Ks-bozGI%M=ZXO*Wuo#LrvamIQoy}E zNL+1>T_#ftTUP>d%ywN515V>G7%&Rw*MyVJat@q4FI*p*)r`LA( zj)@NK<+WHw>R~^d%79VGf1?4j=G8G^PQ!pFvih%ax2krXCiFDNY50AwR$u}x5w zxqg)6R3B2~m()`U`sGmi4D;P;95$Xy1_0&*xnB{C6Q=>Aa3-T;mfZK4scN0F$^hN1 z{{pMK2EkAA-696O!e5fcT*`OM>yd(gR&OK(5RTwKLNqkTY`_rCGQjk3B|ZqKSBX`F zgT0nhWeSsN6H#OdYve@`1BQ@`28_a42U|BGRa7*%PYY$l$iOz3jQ{kpxTG&E<+9-?&x5LkE*nz0qzwZ~29ym` zBV%}no6F6#cJd3xatDf-hQ~pkY$cC_Y>L!|YEqAdMUbLU?p7d|RTQ$F&WBg~$+Bec zC}Sgu^>7h(LPF|Z5m7OreyAlg*QgXWeyAlWV9AC_Eq-+&y2{4?1GR2R?&?;Mc{o^k zdOXOJ*Z8;}YMFI63~Ko`QPFmFPsAr7Fbvja3gFR8Ag8Wq;KxR3z{oXlivW#ek)iOK z!K>v^I$sdAf}n?bSM<@(H4BmLYqdCAj{M>5D~$Uf%U5ap7N0~`baAUVM%Q;o`OInV zH*r~&|1DXqb04TZ4E5o<#y=Fc_g8gUR7PS(zM+CG7E}I}jwwIZj**qSmE=s(&n6;5 z>s;CUIo~W(oWGci{Xj5*AA(ntPe!LTxg8o43twTc*>k-TD79q6&+KLGW>3{c?0sYQ zN~}>>X1o*wdn@lTvN)ze4R$ zyS8QF7d`ug_KJ-O^13*f_Xk%j8*196Xi-v;D~iZYMjfbbD8aD zBO4wm9){4Zk57Q(_;B2z%jJpmb?N7c60O)C&8^;mv#jpvcK!+9KH2ay5v4Ge4U>Mk zMEdfA(2si>ihd$RuPA%q%u59~&GwCZLn4$7Ki#^7I{$TMV7L*JU(EQDc8 z$cCS3%L_=|L}uR{lapKKAk3}%enou?_porGNp`+QY<2`c*o5#Kf;e z6&5d>zWjyysiHIe)P7Ef+1b_36m4fUy$uO&{kDJB6t6%DNQ`HnapFzw@T|eT>8>h% zp8b$=?aLHj154G`n;R=UT@E{}@$khC-CCvBT!3^Lnrg(eG@K_(Op20bC#2VtlKyh~ z$|>Kh-8x z8JR4n=V4EC`x%)s^{6e!`HqPGO| zyzbdGehu#wz4_6EKNCkt7R4UlE#wKu%VWS0HA$sl3+-zP!In8x=ZY1U)Tc2E_zCnQ zk#WSChD&PiEv|8z&$Y&*vB*uyKBnModTAG!DM=-@V;S@mTPBkvaZXXb3D#m!ZY5O6 zE-Ppx-_b{q+(|+MUTvN*;1@Z3V?qXsoR1D7Dc>>Kh&RPm`6o|d%S?LYGil^|n2=g; zDBlCCOX@9WPPrRkaSRob0i#CSlc>kWL&rEdDd!(SmTaI(k?(@bKPec6Glp=s7RSed zlUD@Ry(d2h9NaOkF~(^m$2MRT&JHPna~IFO8LbF?EcT*S7jx9;}S92nejxC$L!ohbE+o>0c zSvH&?*@(Aq5rLdrO8!Z~sL9Vk7v`xS(`a1ouBTzxZ(lA^yZ*P4hY2T0Y}pWcsf;aa zzyts58vlC}Ant5DH8ErX%&*RdbF#9^rYg4y?Vm3_4H%lN&+&Ki?fe?#by@P(jJNNx zMo}-L0V8ofbYq-Ia*n_>J1#iPj_M~C%4>zxfcUY_kF9j{e;l-gQ62O_2T1ML+ubX+ zS5&{~ATQMXT&0lDRSL0CVyoU)|4b6k0kX4nTt4;6m?YwP;#1`L8s<8E~<1M|P&N2KeRy1}p0v~KuLYuu5{ zwHJN?#+mK>#a*5ickjhQqRJNWFXS13L!sAH)@QUXjhl$#p!L+C9#Sy7S2jhbVb(}| z5^r~*@dr3wAOl8``7jpVJa2O6S);z}ux-Wi8rUI*_k#hWa1O&H?ZkMFv2>b-hLXw8 zI1Lzu^B$e^`}Fh$x=R;^z(ybIQRfUN-c*4LL&=)+A02;7p*s`A^Y|eciSyw)T5EB4 zIFEGU+(RaP@eeo>4hJJug)w&nM&h)463+QVf9H{|k^af4gnkf}yNmSDy@1oOL9*L3 zob*n$*}QPfxrTDj*h%~{Ho5-Y1*Hn!nD)y%CY?2|i`p%A1d+cV-57zQ6d~6mm4zKJ_Hl+As9I=w}^5wNQEROSxy`n1drdqrMspsUkaSddF$!tz<^OW=`7gq>F78~ zb@a#WC`ePpWlh-jDodi<^mayyaXOwD_A-Q&#)Y9OyG%BKy&|)te9HA!uOjkr6<9jq z?&H9gkU|{6-qCK*Navl1`1$epx|NVaMK=6|uza*V>3|crwdDpGJ~FmTEt(8ni=Rmi*;M!vB41`aZv-1Udk~94vKjm z^}nEv;3R7K@#%kmRT|#Ul{zoayXFp-jI zw;w-zk^!SoQ(!S=u^S9mEw~am$831l#M@hZuOI_PA*Mh`yDZ<<`pQztw51QMADjk^ z!ucA5wYU1>EZwS2kHpQO(R}$!`;lGJv;b|-`O?;aQ8@R}ICuE&0oL*(P~hOGH;L1y zc?O(+`|eHyM&T?@B36_yPOjPvM_id{&Ck9vU=+@-gfkUj#m)`$UTW+mcvg}dEYAu1&;Ghi{g>yaO zEbqJj`EX}KnJOwaoHGw_8iJ<*LpYBElf4t$)a2Cu3$GFyxd#TLrz@W>x$^R8SCK`Ng5-Negv5AXimX7wvn3lOt_TD|#!2maI3$>Ymy>vyfDT`$ ztN~Bv(819*$$FR&;xX~DIAnHXkZQ=xH;&{&28?|SKLrIL=6@(5pIG%11oIQY2Rh){ zjD1HwrVqhRcy_18X~58gP>v~)y9Y7Tl^Cbq@f_X{=lO^Yfx9b@+l5uRLpP)I6d5pt zb0iIMH|@?n1n&%cP-?T4P2bOGw<=tS!Y&%apBO_zTCg4{{|ie-q#HRKojGrfU#gx) zQFo)o=s60BLhGz?)1JR`wFt-C()h{72|rVc!Vv`Hj6C{m_4L^%uHvjruhaR}>n;{a z7h*75X)tL#zIt*j+HZb?$OHl}Pl=nxLQd{>H+tu;`QF9sM2M7@vJr3J7GrVToAOTz zM&Z0ei0=}-^Nyn$jptTibvO$*d0STY#c9AuoDZiGPLJrKIa;Z*ckoY#8RrLJBu-n@ z6d-39j8nr)gM-J&jW=G>DkF%@V{i)m2E4+*fW|EJ!cID{p%Zp_>8cxVSMa5S0i$sK zgyGsh`>q#z#Z^M|*xLyw##Mwe*rcriBXPO|Y3GljF*xZU)z+SP?~=fI6E_AQijMNSy8rK*#lodWVLE42Ym4 zsSe~UB)ySNW^Zt%7|0D6g*-PA;Y%8f6DQjb5Il3DVqm%IlfLvvSOZ4k?BO7+7DtqR zS2h%3KLDd}wk4cZd9gIn)_e; zKkht5I6WIxZvX>E;ao~MJqK2gGuxs$6Q{)WL!q5TgL>6oN{_wa8g9TSoXxQBk;2W5 zyB3N!DzM{NeieL{C+X%(J6-#LdaUocGGG+y4s@;#63^wtx%tA7y=T7!AH8SXjqQ^c zO!SA-fKfR65KgMe>V-4*^p6dG8ri~S&290x(l`GN1kN;thGoNBf~3_yX3U0UOK<=EzQ>wU#Gg#U553+ene@df(q{h2+hxLC_7q=`@an6c zoG8@@IoZjIzOT2-UP2UdzH;n#&<>8d6W1+qPe4#Mg-v;Z{1tGZ#il;3AdJkb+a`74 z7(WG(Ig}2Ld$M8gWGq3H9i2WZdQCMHNhnc>J(9{@*y4Rcndh#oS?W!6hajr3Nz`e1cOT_2ZYLmXE3Tb^%T{KFad z*@%ayM>gcrJ%XdMn?4rXv6{mOFAQ4Eg_2W**uBQE?P)ajK*#h_g*Drs%ZhPXlZE#? z65-pce7iMZ6q&OE)Slssv-s+oh3_9inx^v0wpM<1fVRVGUz`Sv!daGZ_V>LYRj}^v zzGvqA_6{7|8c*mTT~s#JaP_)?Ro$?N!m)D>bw++O53{20M|Lnlbo+|Q+Y?o2!cn<` zR3Zbh5xv0*r#y)M6T#k4Aju&tNAC$~LZYK1VB{mA2z3J-x25qzHP;B5D+Qu>-CEMW z%~RE!f^4(<&d-2RIKLpA4~X8*+m)p*xXQ@5NIy5W0i$rv06NxLis(!V3WW2mR|gz< zm)vE^h9B#-{F8!F9Z)Ew6#umaH*9x~6B7o)^`5x(xn=r*5S~@kulL}=r$^pRf$NMj zt{{rHn(s@*KIvuQ1Nl!(s=u~`=rck0r+wb{)ljKm4!kVdZR!E4)NE`X14d0{AK@G$ zma+p`@c^XBz5^(${UVho|bB5%f6pTVm z2infzOIGAw&e=Kv912IcQ+RG0U$Po73THRc@>KUF>!p8g=F5KqMy7{FlW$2`W+8AI z5{dz%aK;f%%Es+QR_SMHjee1(YQ1Zdfzvn>28_ZP1ZRqbETk^!6&e$RoVMg9>tO*M zHLIitmc>b|*U&MYyVGaS010!@%4F2v_zyS zwHGg1wX}BLsyrl?p!SNTG)3ZiUJ-EV1glLmkOo^LI6uvZ0Zf-3c>4&1btD zI=pXI!P6I7%-#`C6ACQlJBDD6qRgndbgpR8D$ zTxv-jCYF~l_0zUfagCs}%Q(2|*U%q=6Tz}3rZpPLas?cK^kpi~-X(k45zwfw{+fNoh0^vwc2bQp_Nxm70e67j}OUKMfagQJ=fV zooc&4@tF;c#N0-hx4ru=wM*e=EBRD9zeXc5PXY6sA3x~3nVmTz++!t(i+n3m5K-|$ z{eMb-$^tV6_vkkuD%@lCYJVnMyutpAt|qKMv#&dOqsK#9>S2J&(4J^t7#fX)CXUSi z=6k5>{DsG_-SfYIMQ}mL)_{;PLxE|@v2180W=R|`&6Zr(Q2Vk}&$t-`!Mv6Q*G&Uz zmiFeEhu^_FdvVKI7ap`JM^<&V|I%ofWxdH|{*SU%rmQPF3&|3-U4QK5;g!y=fIEL$ z+AniY?r)~VLrvm%H%EJx&{DyvOT6LX3AuAT9>ecD9lZN;5M_9&%PfKRJ zje)(!(j*kgAFU4E@Cn92dkPbOy;QSJs9CB_>oMbb);mPXq|BPT{#r}2Ztmf$NDcnQ zqe&^l+FEE2+cq5&QPqWm}W2L@V%^pOvTpb>a2yGVGq_~wT1W%0Q zLhIU*QXc=fUsXAmu8DA$bDSazy_q}xlhYv4K1XgAeq0=%qej(#R(|}kVlWB;L+AXs zJ;rf`$&9k%T3@cP@${!`g$>bHnD%FK$_{0JMk~zvGy4jYuGD-=*2R!~>^=Q78VOAt znV%579a9$7T^N^gB6)%13e#vL<{H9m<-Ni}d(57)`YKKx{5tJxwZdpX&GOzn%MTz_ zJh8%PG|UR#;wf((rn>a$=Ao|e@zM?x(kELuugWjkssI@DrKqbY`1u=1 zIPNh`G>yh#m)2+`__qYk((DLuE9<*W2jU)Zh;R@u4lRhEB#mrgrawMweeXTH=&J39yHydm|GCSHfwNUBhH@ zb~F-tE#dQ97aRT1L6us>PMr&bYHH?|mn~lcGlOZgw)}n<@*T|Ec;iHb*SAb98GZ3D zm${mgC{wTR?T7xEttY#T%a87N8@cr^6Qit0duXUK`L3qAa&J`fvN(_MzfiYwl> z>;_XBuGul>)*u`Ze#qnMpW`|(t?YLykX)mYm{g$NyiYvXBmFM*PndB19~2}l=ljmo zu6uq3W-y;hD$Y)$k(ew|$^&oAcZ<*d_R|A!t|cd^VlU_*qhV?^60Xe9`z$o=_dybvIAcM4aTP3UZ z=E_+;_TC(jXtz}t-lt6JuEp z;f;M=yu_KQd_t;`6F*5Bi80yEiyNL z($CA54OmQRwyix)CDYYtB<3>W^R+jhOFyQ1kuWTs*f1~2KVz*ErbZ(%$=SjD*Bi6W zw(j#9=8b};@JF9jD*{$IVQMrI^C)4?^~QW%tl5a@+_6ZuO}X$2Q)XMCzChvwG zu`Bqw?GtIP?g%TmR!+y;w7=t~k@Z%VyFQ0*CH36;B$gUJ6Olw&v%rAdz zJnW4>0!DuCv%18sx#SF^6`$8=BxVW1w3da!EPW?1$KsZe;B(EZ4nIDi9Ff-WWJ4n{ zD-ou(EEN6NR#Uq?sDr&Ue|nwye=Q*&NDWh?k(iXMSS$;L8E|G6z5tfm-LGHHPJDNR z6Q)KZF`E&lwJa1S&p8BrXYVZIUW?lMoG>*SiP?!Tt!1Gw`F07f7rqx}I$>%w60;X! zTFXLV`uog|E0FvHf}@so{;YV)Aty|YMq*OUU9l`wK?$C25=)Irk#Shly8oA`yOde&*ZM(!xGaU*M#4*!;1svM#IgoY(zYuhK#~2z(@Yr%G9pX; zGj$2%%C+4c>m|g{e)}P}Gh{;}d&{a`Po>eZAN!Ebuhjai8pyyOdVj{GQ=Nbr?LC4# z1`0u=yqZ%hm#}B5W_juikcb=_ve{SY3QrMY*$`|tLK(9oUZho%tZZgE-Z}LtrEkA@ z5jGdHRKQQd>&we$&Om}rr^~!x#cWN+j%Bv(nz{9(@cfja90)E;FOu^3pOBg zzGhKVgt0i6m0RBY!gg0!8p=-1ym--L~V$q3H8GIJ+ChuO>!8b zi1FXYHyMOe=l!6VlqOwfxXdTC-`ZC%Tz{uD?hYeYgZS`$E1ss2A)5rk?dA9+oH0B3 zgpyxh{UWo2%b=o$dCI4$j?|=%EaBU^`qnl2zRhYr86Byc`bFrc^<&Tx?JMnAa^vt< zRUu@K-SX>U)LxW40&BJEVOA=GRTlQFi5&ozX8NIBZJXVAg& zXV7d4pJED=X9dRn8uuVibD8VBNje_BRJ3mgXc7E-oK(#k)a*R(X7l#U*U+yy{PClz z|K@uwDjLl;WSBPAG#a^RW)kK!CO~Y{oL9Gj!oF+TljrRQA9tDx8jZxN)DAZrEO6dy3JYMDAG75?6Z1h zP1%shwEqhv;*eZ*+N}+NIu{V&E3zXHI2v&}Mr3vHQ!%eRn{tJ)0Kd zu04^=c}>{}W=A72y8@HfJMG*3f3fW-KSafm=7b_IIkTaWEkqL6A>QAJ`{v*bc)$DS z1($bw1g^D}qtQsrIKuR0v)nc#x$O&@TJWNq7dy3~>n9CUqmh`*L57=Pflf8p-Es!t}?(L$@olEYIYt z*Mi}9?P~pwPqX2`%;K2p8*wuncK#dpQcxYOyKAv^QB3}6W(k6{@2@;PAhgHG zW$i%Ktt^lty^9sEMc&}d8O@&Qff)sppooYU)C6bz*fb0AlcbS-yhjb>?cIG~gcX@) z-!Vi|EL(Q0THd^CG!mNP-pm3-Z#PBBP=Vp+(p%+O8HM5N##CdQ%cv+9XWSoF=<=pR z-?&T)kny-Q;akiXE<@OmWwURnSt=)jW;5^)dl+GZ%kPgF3N8PB-l5CK<)_O{O(oH2 zBxYG3U{WRtU@A4|`}DrMD!hx!`0Z0WE?svh7MQd7$Y(<%F>4U!0dGuxnFHnZWkGN&^cCeCeY#MMo##jTLxfu|4uvnpPm)G9 zQHZ=tf2VEIKKtec?k`z85l7+tm#a*g^WrbCZ5=~x?LN1>TGz0My)2iI^6JuN`QAp& zR}JavGK^WH(|4wxahX~^kM=PecC`5gwtse}DP=o`vdHe~Y=YW$XKrVL)?VbDyLE@9 zj{;}b;YVp_8jbAjXVlx7-k36mb$s~@oUMjAVQMrIvp!)4vJY<)(=0NhWEb4))C@;? z)C^#91t&WiiP@hpTk$PQ_3P|_evNyKz!k!3@_D;LXG5)=pGG4wM-k>S_N8s(xuIy( z_M5-p@K8E#%d2Z!$nMoN?bb6r3JY~EZWes7BvYl*rZY`KbSS74AP1Ii3n3!M1 zzoXhpxi=GR0>bl;{AySzGo9wm>U=-ShF%ViLHH;5H&yf#`%prho`_sxxv%KZP3clR zx~0)b%<+V2Nf~7ee3mrpgSg%}h`M&otTp)0yTH_>42orTOKXF~xJy~p93^FPqIEYt zjEtg1uYa}^3VXi0nVZV?yy7yoWIikZIq==M5ZvkoF*&?djb;*Yrb!v@;?NAr_>(s0 zN4NbEoM}=2X2$JA&fW(HwuB0yxT#CzFmG;%<05vI26c~}+9kyRh`U-;hnZ5U*2*=sa}SpZ_uoFGMy-RjHJ zP!tsP;R*uNa#2;{M*91S7ts}UXc5ug*2Ok2iOibq0I49Hc9e=ar|9lH17G8?AztE4 zKPJ0vs;c-&(#Woq!?H3{N#fa6+vMWD13F#7&BI%t`zQAXV@DQJmyEn-H8%V z$I|g!Gm!I3aTw|OX{tT*3uIDV8Px4QQX0L$cULBU{Heco{Us05(GVuHg3hEd;YAs3)qi;osE-gU-M96mGk<>sOkT0TCX@6kIGQ5EmrhO`_9%Gu zcut49#Qc{>ffmW5K*5=L1EYDaFw%X-3@{_41`&qn1X$fM0*w&V?e;B#&0{EA>9Atv zs`qccTscR8+oOC(S2K-w#833d&m}@~D-;F{VHueT#lG705bH$1NLHTG8hAzm)P7{- zm5Y%Ffm!f}lW)9ro1&W4Xe4G0d?OLPr|4zwL*|{|Ex=GUPWapFL zxM18WrxZ965Kb%Y1<}}5B#lO57Q*K`SBeTd^>P~|pZa!>LIEE$%y17xbu#{+9_>kY za$oW(O(ko`eDC%&8rfb3^ z4n5`)54qjB5e3B_#($mqt=k8G;6?yRkqgDo9tF(z8J{+5HBYpG!_V@l&Go8>B0t0~ zY%nmiEro%GJ&X$Pjafb3)!$_td3G!R^Tar&tg&X>(0td1|9s80r$=0x55=lSYL0|N z+5oKlqH5}DG_osn!-i7Imc0{YX?ynz_l#qeS(L z)6`|ur=;uaNCuLpm4^(}Xe8!z>g?C->)RF>_qBi-foDHi1U|LO3>uBZR0C+2tNc`m z6Iqp|kEY*C#enOwTB9LMaG1kHk8+rm_$*bzV$Sw@FQ1%Q2xq+YU!EH0`x`LlvA#1| z!Uc*(E*fz|>ZgM&p6`pSH3lpM6UIzY6l%zs^rfhvwW8os62f)(h#OY&p<6(wBoE zlk~)G07!4cHiP?p`U-Y@5g%n--q@qb;5bT8pv_W^MqrvB5Dx|M@W2!$Jq_U_#CRss QCRn|k2fMbU(O0ede>dqAr~m)} literal 0 HcmV?d00001 diff --git a/perception/vis/vis.py b/perception/vis/vis.py new file mode 100644 index 0000000..3b3665f --- /dev/null +++ b/perception/vis/vis.py @@ -0,0 +1,67 @@ +import argparse +import os + +from FrameWrapper import FrameWrapper +import cv2 as cv +from window_builder import Visualizer +import cProfile as cp +import pstats + +# Parse arguments +parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') +parser.add_argument( + '--data', default='webcam', type=str +) +parser.add_argument('--algorithm', type=str) +parser.add_argument('--save_video', action='store_true') +args = parser.parse_args() + +# Get algorithm module +exec("from TestTasks.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) + +# Initialize image source +# detects args.data, get a list of all file directory when given a directory +# change data_source to a list of all files in the directory +if os.path.isfile(args.data): + data_sources = [args.data] +elif os.path.isdir(args.data): + data_sources = os.listdir(args.data) +data = FrameWrapper(data_sources, 0.25) + +algorithm = Algorithm() +window_builder = Visualizer(algorithm.var_info()) +video_frames = [] + + +# Main Loop +def main(): + for frame in data: + + state, debug_frames = algorithm.analyze( + frame, debug=True, slider_vals=window_builder.update_vars() + ) + to_show = window_builder.display(debug_frames) + cv.imshow('Debug Frames', to_show) + if args.save_video: + video_frames.append(to_show) + + key_pressed = cv.waitKey(60) & 0xFF + if key_pressed == 112: + cv.waitKey(0) # pause + if key_pressed == 113: + break # quit + + +cp.run('main()', 'algo_stats') +cv.destroyAllWindows() +p = pstats.Stats('algo_stats') +p.print_stats('analyze') + +if args.save_video: + height, width, _ = video_frames[0].shape + out = cv.VideoWriter('deb_cap.avi', cv.VideoWriter_fourcc(*'XVID'), 60, (height, width)) + for img in video_frames: + height2, width2, _ = img.shape + if (height2, width2) == (height, width): + out.write(img) + out.release() diff --git a/perception/vis/window_builder.py b/perception/vis/window_builder.py new file mode 100644 index 0000000..3e054cb --- /dev/null +++ b/perception/vis/window_builder.py @@ -0,0 +1,56 @@ +import numpy as np +import cv2 as cv +import math +from typing import Dict, Tuple, List + +def nothing(x): + pass + +class Visualizer: + def __init__(self, vars: Dict[str, Tuple[Tuple[int, int], int]]): + self.variables = vars.keys() + cv.namedWindow('Debug Frames') + for name, info in vars.items(): + range, default_val = info + low_range, high_range = range + cv.createTrackbar(name, 'Debug Frames', low_range, high_range, nothing) + cv.setTrackbarPos(name, 'Debug Frames', default_val) + + def three_stack(self, frames: List[np.ndarray]) -> List[np.ndarray]: + newLst = [] + for frame in frames: + if len(frame.shape) == 2 or frame.shape[2] == 1: + frame = np.stack((frame, frame, frame), axis=2) + newLst.append((frame)) + return newLst + + def display(self, frames: List[np.ndarray]) -> np.ndarray: + num_frames = len(frames) + assert (num_frames > 0 and num_frames <= 9), 'Invalid number of frames!' + frames = self.three_stack(frames) + + columns = math.ceil(num_frames/math.sqrt(num_frames)) + rows = math.ceil(num_frames/columns) + frame_num = 0 + to_show = 0 + for j in range(rows): + this_row = frames[frame_num] + for i in range(columns * j + 1, columns * (j + 1)): + frame_num += 1 + if frame_num < num_frames: + to_add = frames[frame_num] + this_row = np.hstack((this_row, to_add)) + else: + this_row = np.hstack((this_row, np.zeros(frames[0].shape, dtype=np.uint8))) + if type(to_show) != int: + to_show = np.vstack((to_show, this_row)) + else: + to_show = this_row + frame_num += 1 + return to_show + + def update_vars(self) -> Dict[str, int]: + variable_values = {} + for var in self.variables: + variable_values[var] = cv.getTrackbarPos(var, 'Debug Frames') + return variable_values \ No newline at end of file diff --git a/tasks/gate/GateSegmentationAlgo2.py b/tasks/gate/GateSegmentationAlgo2.py index f73c1d0..e9f440b 100644 --- a/tasks/gate/GateSegmentationAlgo2.py +++ b/tasks/gate/GateSegmentationAlgo2.py @@ -19,6 +19,8 @@ class GateSegmentationAlgo(GatePerceiver): def __init__(self): super() self.gate_center = self.output_class(250, 250) + self.use_optical_flow = False + self.optical_flow_c = 0.05 def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: @@ -58,8 +60,10 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) - (x1, y1, w1, h1) = cv.boundingRect(cnt[min_i1]) - (x2, y2, w2, h2) = cv.boundingRect(cnt[min_i2]) + rect1 = cv.boundingRect(cnt[min_i1]) + rect2 = cv.boundingRect(cnt[min_i2]) + x1, y1, w1, h1 = rect1 + x2, y2, w2, h2 = rect2 cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) @@ -69,32 +73,37 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) # dense optical flow - next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) - mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) - mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) - if np.mean(mag) < 40: - center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - self.gate_center = self.get_actual_center(center_x, center_y) + # next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + # flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) + # mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) + # mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) + # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + # if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ + # (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): + # self.gate_center = self.get_actual_center(center_x, center_y) + # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + # self.use_optical_flow = False + # else: + # self.use_optical_flow = True + # self.gate_center = (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag) * math.cos(np.mean(ang))), \ + # int(self.gate_center[1] + self.optical_flow_c * np.mean(mag) * math.sin(np.mean(ang)))) + # cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) + self.gate_center = self.get_center(rect1, rect2, frame) + if self.use_optical_flow: + cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) else: - self.gate_center = (int(self.gate_center[0] - np.mean(mag) * math.sin(np.mean(ang))), \ - int(self.gate_center[1] + np.mean(mag) * math.cos(np.mean(ang)))) - cv.circle(debug_filter, self.gate_center, 5, (255,0,0), -1) - cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) - ang = ang*180/np.pi - print('mag:', np.mean(mag), '\tang:', np.mean(ang)) + cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + # ang = ang*180/np.pi + # print('mag:', np.mean(mag), '\tang:', np.mean(ang)) # hsv[...,0] = ang # hsv[...,2] = mag # bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) - # cv.imshow('dense optical flow',bgr) - prvs = next - - + # prvs = next if debug: return (self.output_class(self.gate_center[0], self.gate_center[1]), debug_filter) return self.output_class(self.gate_center[0], self.gate_center[1]) - def get_actual_center(self, center_x, center_y): + def center_without_optical_flow(self, center_x, center_y): # get starting center location, averaging over the first 2510 frames if len(self.center_x_locs) == 0: self.center_x_locs.append(center_x) @@ -118,6 +127,29 @@ def get_actual_center(self, center_x, center_y): center_x, center_y = int(x_temp_avg), int(y_temp_avg) return (center_x, center_y) + + def dense_optical_flow(self, frame, prvs): + next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) + mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) + mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) + return next, mag, ang + + def get_center(self, rect1, rect2, rame): + global prvs + x1, y1, w1, h1 = rect1 + x2, y2, w2, h2 = rect2 + center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + prvs, mag, ang = self.dense_optical_flow(frame, prvs) + if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ + (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): + self.use_optical_flow = False + return self.center_without_optical_flow(center_x, center_y) + else: + self.use_optical_flow = True + return (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag * np.cos(ang))), \ + (int(self.gate_center[1] + self.optical_flow_c * np.mean(mag * np.sin(ang))))) + # this part is temporary and will be covered by other files in the future if __name__ == '__main__': @@ -142,17 +174,9 @@ def get_actual_center(self, center_x, center_y): break if ret: frame = cv.resize(frame, None, fx=0.3, fy=0.3) - ### FUNCTION CALL, can change this center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) cv.imshow('original', frame) cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True ret_tries = 0 key = cv.waitKey(30) if key == ord('q') or key == 27: @@ -166,4 +190,3 @@ def get_actual_center(self, center_x, center_y): else: ret_tries += 1 frame_count += 1 - #print(frame_count / (time.time() - start_time)) From c33f16c2732818c9f181fee22d60c7fea0ed8bd2 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sat, 5 Dec 2020 22:04:50 -0800 Subject: [PATCH 05/19] updated vis --- .../vis/TestTasks/GateSegmentationAlgo.py | 120 ------------------ perception/vis/TestTasks/TestAlgo.py | 30 ----- perception/vis/algo_stats | Bin 143583 -> 0 bytes perception/vis/vis.py | 67 ---------- perception/vis/window_builder.py | 56 -------- vis/TaskPerceiver.py | 4 +- vis/TestTasks/TestAlgo.py | 22 +++- vis/vis.py | 68 ++++++---- vis/yukiVisualizer.py | 96 -------------- wiki/flowchart.png | Bin 237062 -> 0 bytes 10 files changed, 66 insertions(+), 397 deletions(-) delete mode 100644 perception/vis/TestTasks/GateSegmentationAlgo.py delete mode 100644 perception/vis/TestTasks/TestAlgo.py delete mode 100644 perception/vis/algo_stats delete mode 100644 perception/vis/vis.py delete mode 100644 perception/vis/window_builder.py delete mode 100644 vis/yukiVisualizer.py delete mode 100644 wiki/flowchart.png diff --git a/perception/vis/TestTasks/GateSegmentationAlgo.py b/perception/vis/TestTasks/GateSegmentationAlgo.py deleted file mode 100644 index 1c791d5..0000000 --- a/perception/vis/TestTasks/GateSegmentationAlgo.py +++ /dev/null @@ -1,120 +0,0 @@ -from TaskPerceiver import TaskPerceiver -from typing import Tuple -import sys -import os -from pathlib import Path -from collections import namedtuple -sys.path.append(str(Path(__file__).parents[2]) + '/tasks') - -from segmentation.combinedFilter import init_combined_filter -import numpy as np -import cv2 as cv -import time -import cProfile - -class GateSegmentationAlgo(TaskPerceiver): - __past_centers = [] - __ema = None - output_class = namedtuple("GateOutput", ["centerx", "centery"]) - output_type = {'centerx': np.int16, 'centery': np.int16} - - def __init__(self, alpha=0.1): - super() - self.__alpha = alpha - self.combined_filter = init_combined_filter() - - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze. - debug: Whether or not to display intermediate images for debugging. - Returns: - (x,y) coordinate with center of gate - """ - gate_center = self.output_class(250, 250) - filtered_frame = self.combined_filter(frame, display_figs=False) - filtered_frame_copies = [filtered_frame for _ in range(3)] - stacked_filter_frames = np.concatenate(filtered_frame_copies, axis=2) - mask = cv.inRange( - stacked_filter_frames, np.array([100, 100, 100]), np.array([255, 255, 255]) - ) - _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - if contours: - contours.sort(key=self.findStraightness, reverse=True) - cnts = contours[:2] - rects = [cv.minAreaRect(c) for c in cnts] - centers = [np.array(r[0]) for r in rects] - boxpts = [cv.boxPoints(r) for r in rects] - box = [np.int0(b) for b in boxpts] - for b in box: - cv.drawContours(stacked_filter_frames, [b], 0, (0, 0, 255), 5) - if len(centers) >= 2: - gate_center = (centers[0] + centers[1]) * 0.5 - if self.__ema is None: - self.__ema = gate_center - else: - self.__ema = ( - self.__alpha * gate_center + (1 - self.__alpha) * self.__ema - ) - gate_center = (int(self.__ema[0]), int(self.__ema[1])) - # if len(self.__past_centers) < 15: - # self.__past_centers += [gate_center] - # else: - # self.__past_centers.pop(0) - # self.__past_centers += [gate_center] - # gate_center = sum(self.__past_centers) / len(self.__past_centers) - # gate_center = (int(gate_center[0]), int(gate_center[1])) - cv.circle(stacked_filter_frames, gate_center, 10, (0, 255, 0), -1) - - if debug: - return ( - self.output_class(gate_center[0], gate_center[1]), - [stacked_filter_frames], - ) - return self.output_class(gate_center[0], gate_center[1]) - - def findStraightness( - self, contour - ): # output number = contour area/convex area, the bigger the straightest - hull = cv.convexHull(contour, False) - contour_area = cv.contourArea(contour) - hull_area = cv.contourArea(hull) - return 10 * contour_area - 5 * hull_area - - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - gate_task = GateSegmentationAlgo(0.1) - # once = False - start_time = time.time() - frame_count = 0 - while ret_tries < 50: - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.4, fy=0.4) - - ### FUNCTION CALL, can change this - center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xFF - if k == 27: - break - else: - ret_tries += 1 - frame_count += 1 - # print(frame_count / (time.time() - start_time)) diff --git a/perception/vis/TestTasks/TestAlgo.py b/perception/vis/TestTasks/TestAlgo.py deleted file mode 100644 index e46e507..0000000 --- a/perception/vis/TestTasks/TestAlgo.py +++ /dev/null @@ -1,30 +0,0 @@ -from TaskPerceiver import TaskPerceiver -from typing import Dict -import numpy as np -import cv2 as cv -import matplotlib.pyplot as plt - -class TestAlgo(TaskPerceiver): - def __init__(self): - super().__init__(canny_low=((0, 255), 100), canny_high=((0, 255), 200)) - - def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]): - fig = plt.figure() - x1 = np.linspace(0.0, 5.0) - x2 = np.linspace(0.0, 2.0) - - y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) - y2 = np.cos(2 * np.pi * x2) - - line1, = plt.plot(x1, y1, 'ko-') - line1.set_ydata(np.cos(2 * np.pi * (x1 + slider_vals['canny_low'] * 3.14 / 2)) * np.exp(-x1)) - fig.canvas.draw() - img = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, - sep='') - img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) - img = cv.cvtColor(img, cv.COLOR_RGB2BGR) - img = cv.resize(img, (frame.shape[1], frame.shape[0])) - - return frame, [frame, cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), - cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), - cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0), img] \ No newline at end of file diff --git a/perception/vis/algo_stats b/perception/vis/algo_stats deleted file mode 100644 index f64d29873ed1d216f02a06d2829e8272206aa0a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143583 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

~k>>3c+{Ru6+U&>j6QA-4($ChZj&7k}-xIW)Wi$ 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 QCRn|k2fMbU(O0ede>dqAr~m)} diff --git a/perception/vis/vis.py b/perception/vis/vis.py deleted file mode 100644 index 3b3665f..0000000 --- a/perception/vis/vis.py +++ /dev/null @@ -1,67 +0,0 @@ -import argparse -import os - -from FrameWrapper import FrameWrapper -import cv2 as cv -from window_builder import Visualizer -import cProfile as cp -import pstats - -# Parse arguments -parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') -parser.add_argument( - '--data', default='webcam', type=str -) -parser.add_argument('--algorithm', type=str) -parser.add_argument('--save_video', action='store_true') -args = parser.parse_args() - -# Get algorithm module -exec("from TestTasks.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) - -# Initialize image source -# detects args.data, get a list of all file directory when given a directory -# change data_source to a list of all files in the directory -if os.path.isfile(args.data): - data_sources = [args.data] -elif os.path.isdir(args.data): - data_sources = os.listdir(args.data) -data = FrameWrapper(data_sources, 0.25) - -algorithm = Algorithm() -window_builder = Visualizer(algorithm.var_info()) -video_frames = [] - - -# Main Loop -def main(): - for frame in data: - - state, debug_frames = algorithm.analyze( - frame, debug=True, slider_vals=window_builder.update_vars() - ) - to_show = window_builder.display(debug_frames) - cv.imshow('Debug Frames', to_show) - if args.save_video: - video_frames.append(to_show) - - key_pressed = cv.waitKey(60) & 0xFF - if key_pressed == 112: - cv.waitKey(0) # pause - if key_pressed == 113: - break # quit - - -cp.run('main()', 'algo_stats') -cv.destroyAllWindows() -p = pstats.Stats('algo_stats') -p.print_stats('analyze') - -if args.save_video: - height, width, _ = video_frames[0].shape - out = cv.VideoWriter('deb_cap.avi', cv.VideoWriter_fourcc(*'XVID'), 60, (height, width)) - for img in video_frames: - height2, width2, _ = img.shape - if (height2, width2) == (height, width): - out.write(img) - out.release() diff --git a/perception/vis/window_builder.py b/perception/vis/window_builder.py deleted file mode 100644 index 3e054cb..0000000 --- a/perception/vis/window_builder.py +++ /dev/null @@ -1,56 +0,0 @@ -import numpy as np -import cv2 as cv -import math -from typing import Dict, Tuple, List - -def nothing(x): - pass - -class Visualizer: - def __init__(self, vars: Dict[str, Tuple[Tuple[int, int], int]]): - self.variables = vars.keys() - cv.namedWindow('Debug Frames') - for name, info in vars.items(): - range, default_val = info - low_range, high_range = range - cv.createTrackbar(name, 'Debug Frames', low_range, high_range, nothing) - cv.setTrackbarPos(name, 'Debug Frames', default_val) - - def three_stack(self, frames: List[np.ndarray]) -> List[np.ndarray]: - newLst = [] - for frame in frames: - if len(frame.shape) == 2 or frame.shape[2] == 1: - frame = np.stack((frame, frame, frame), axis=2) - newLst.append((frame)) - return newLst - - def display(self, frames: List[np.ndarray]) -> np.ndarray: - num_frames = len(frames) - assert (num_frames > 0 and num_frames <= 9), 'Invalid number of frames!' - frames = self.three_stack(frames) - - columns = math.ceil(num_frames/math.sqrt(num_frames)) - rows = math.ceil(num_frames/columns) - frame_num = 0 - to_show = 0 - for j in range(rows): - this_row = frames[frame_num] - for i in range(columns * j + 1, columns * (j + 1)): - frame_num += 1 - if frame_num < num_frames: - to_add = frames[frame_num] - this_row = np.hstack((this_row, to_add)) - else: - this_row = np.hstack((this_row, np.zeros(frames[0].shape, dtype=np.uint8))) - if type(to_show) != int: - to_show = np.vstack((to_show, this_row)) - else: - to_show = this_row - frame_num += 1 - return to_show - - def update_vars(self) -> Dict[str, int]: - variable_values = {} - for var in self.variables: - variable_values[var] = cv.getTrackbarPos(var, 'Debug Frames') - return variable_values \ No newline at end of file diff --git a/vis/TaskPerceiver.py b/vis/TaskPerceiver.py index d51bd49..e8b8609 100644 --- a/vis/TaskPerceiver.py +++ b/vis/TaskPerceiver.py @@ -1,5 +1,6 @@ from typing import Any, Dict, Tuple import numpy as np + class TaskPerceiver: def __init__(self, **kwargs): @@ -21,11 +22,10 @@ def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]) - slider_vals: A list of names of the variables which the user should be able to control from the Visualizer, mapped to current slider value for that variable - Returns: the result of the algorithm """ raise NotImplementedError("Need to implement with child class.") def var_info(self) -> Dict[str, Tuple[Tuple[int, int], int]]: - return self.variables + return self.variables \ No newline at end of file diff --git a/vis/TestTasks/TestAlgo.py b/vis/TestTasks/TestAlgo.py index 38f3c7d..e46e507 100644 --- a/vis/TestTasks/TestAlgo.py +++ b/vis/TestTasks/TestAlgo.py @@ -1,16 +1,30 @@ from TaskPerceiver import TaskPerceiver from typing import Dict -import sys -import os import numpy as np import cv2 as cv +import matplotlib.pyplot as plt class TestAlgo(TaskPerceiver): def __init__(self): super().__init__(canny_low=((0, 255), 100), canny_high=((0, 255), 200)) def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]): + fig = plt.figure() + x1 = np.linspace(0.0, 5.0) + x2 = np.linspace(0.0, 2.0) - return frame, [cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), + y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) + y2 = np.cos(2 * np.pi * x2) + + line1, = plt.plot(x1, y1, 'ko-') + line1.set_ydata(np.cos(2 * np.pi * (x1 + slider_vals['canny_low'] * 3.14 / 2)) * np.exp(-x1)) + fig.canvas.draw() + img = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, + sep='') + img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) + img = cv.cvtColor(img, cv.COLOR_RGB2BGR) + img = cv.resize(img, (frame.shape[1], frame.shape[0])) + + return frame, [frame, cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), - cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0)] + cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0), img] \ No newline at end of file diff --git a/vis/vis.py b/vis/vis.py index 37f3efc..3b3665f 100644 --- a/vis/vis.py +++ b/vis/vis.py @@ -1,43 +1,67 @@ import argparse -from pathlib import Path +import os + from FrameWrapper import FrameWrapper import cv2 as cv -from yukiVisualizer import Visualizer - -# import TestTasks.testAlgo -# Collect available datasets -data_sources = ['webcam'] -datasets = Path('./datasets') -for file in datasets.iterdir(): - data_sources.append(file.stem) +from window_builder import Visualizer +import cProfile as cp +import pstats # Parse arguments parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') parser.add_argument( '--data', default='webcam', type=str -) # do this later #, choices = data_sources) +) parser.add_argument('--algorithm', type=str) +parser.add_argument('--save_video', action='store_true') args = parser.parse_args() # Get algorithm module exec("from TestTasks.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) # Initialize image source -data_sources = [args.data] +# detects args.data, get a list of all file directory when given a directory +# change data_source to a list of all files in the directory +if os.path.isfile(args.data): + data_sources = [args.data] +elif os.path.isdir(args.data): + data_sources = os.listdir(args.data) data = FrameWrapper(data_sources, 0.25) -# TODO: This is undefined and should be added later. algorithm = Algorithm() -yukiVisualizer = Visualizer(algorithm.var_info()) +window_builder = Visualizer(algorithm.var_info()) +video_frames = [] + + # Main Loop -for frame in data: - # TODO: benchmarking +def main(): + for frame in data: + + state, debug_frames = algorithm.analyze( + frame, debug=True, slider_vals=window_builder.update_vars() + ) + to_show = window_builder.display(debug_frames) + cv.imshow('Debug Frames', to_show) + if args.save_video: + video_frames.append(to_show) + + key_pressed = cv.waitKey(60) & 0xFF + if key_pressed == 112: + cv.waitKey(0) # pause + if key_pressed == 113: + break # quit + - state, debug_frames = algorithm.analyze( - frame, debug=True, slider_vals=yukiVisualizer.update_vars() - ) - # cv.imshow('original', frame) - yukiVisualizer.display(debug_frames) +cp.run('main()', 'algo_stats') +cv.destroyAllWindows() +p = pstats.Stats('algo_stats') +p.print_stats('analyze') - if cv.waitKey(60) & 0xFF == 113: - break +if args.save_video: + height, width, _ = video_frames[0].shape + out = cv.VideoWriter('deb_cap.avi', cv.VideoWriter_fourcc(*'XVID'), 60, (height, width)) + for img in video_frames: + height2, width2, _ = img.shape + if (height2, width2) == (height, width): + out.write(img) + out.release() diff --git a/vis/yukiVisualizer.py b/vis/yukiVisualizer.py deleted file mode 100644 index b17831d..0000000 --- a/vis/yukiVisualizer.py +++ /dev/null @@ -1,96 +0,0 @@ -import numpy as np -import cv2 as cv -import math -from typing import Dict, Tuple - -# Get input from webcam -#cap = cv.VideoCapture(0) -def nothing(x): - pass -#cap = cv.VideoCapture(0) -""" -cv.namedWindow('contours') -cv.createTrackbar('blow','contours',0,255,nothing) -cv.createTrackbar('glow','contours',0,255,nothing) -cv.createTrackbar('rlow','contours',0,255,nothing) -cv.createTrackbar('bhigh','contours',0,255,nothing) -cv.createTrackbar('ghigh','contours',0,255,nothing) -cv.createTrackbar('rhigh','contours',0,255,nothing) -cv.setTrackbarPos('bhigh','contours',255) -cv.setTrackbarPos('ghigh','contours',255) -cv.setTrackbarPos('rhigh','contours',255) -""" -class Visualizer: - def __init__(self, vars: Dict[str, Tuple[Tuple[int, int], int]]): - self.variables = vars.keys() - cv.namedWindow('Debug Frames') - for name, info in vars.items(): - range, default_val = info - low_range, high_range = range - cv.createTrackbar(name, 'Debug Frames', low_range, high_range, nothing) - cv.setTrackbarPos(name, 'Debug Frames', default_val) - - def display(self, frames): - num_frames = len(frames) - assert (num_frames > 0 and num_frames <= 9), 'Invalid number of frames!' - - columns = math.ceil(num_frames/math.sqrt(num_frames)) - rows = math.ceil(num_frames/columns) - frame_num = 0 - to_show = 0 - for j in range(rows): - this_row = frames[frame_num] - for i in range(columns * j + 1, columns * (j + 1)): - frame_num += 1 - if frame_num < num_frames: - to_add = frames[frame_num] - this_row = np.hstack((this_row, to_add)) - else: - this_row = np.hstack((this_row, np.zeros(frames[0].shape))) - if type(to_show) != int: - to_show = np.vstack((to_show, this_row)) - else: - to_show = this_row - frame_num += 1 - cv.imshow('Debug Frames', to_show) - - def update_vars(self) -> Dict[str, int]: - variable_values = {} - for var in self.variables: - variable_values[var] = cv.getTrackbarPos(var, 'Debug Frames') - return variable_values - -# Continue until user ends program -if __name__ == '__main__': - while (True): - ret, frame = cap.read() - - frame = cv.resize(frame, (int(frame.shape[1]*1/2), int(frame.shape[0]*1/2)), interpolation = cv.INTER_AREA) # Downsize image - hsv = cv.cvtColor(frame,cv.COLOR_BGR2HSV) - - hs = cv.getTrackbarPos('blow','contours') - ss = cv.getTrackbarPos('glow','contours') - vs = cv.getTrackbarPos('rlow','contours') - hl = cv.getTrackbarPos('bhigh','contours') - sl = cv.getTrackbarPos('ghigh','contours') - vl = cv.getTrackbarPos('rhigh','contours') - - mask = cv.inRange(hsv, np.array([hs,ss,vs]), np.array([hl,sl,vl])) - res = cv.bitwise_and(frame,frame, mask= mask) - #cv.imshow('Viz', np.vstack((np.hstack((res, res, res)), np.hstack((res, res, res)), np.hstack((res, res, res))))) - - - - - #cv.imshow('nine', np.vstack((np.hstack((res, res, res)), np.hstack((res, res, res)), np.hstack((res, res, res))))) - - - - if cv.waitKey(1) and 0xFF == ord('q'): # Exit - break - - - - #Cleanup - cap.release() - cv.destroyAllWindows() diff --git a/wiki/flowchart.png b/wiki/flowchart.png deleted file mode 100644 index 1bae9db9ca9b6c5de93ca5d768ce9fb2fbc40bde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 237062 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 ZM&<6ERQRH#U Date: Sat, 5 Dec 2020 22:21:59 -0800 Subject: [PATCH 06/19] merge to new format --- README.md | 19 ++++++++++++++++--- tasks/.DS_Store | Bin 6148 -> 6148 bytes 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e67e989..90e88e2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Perception Code Overview +Code Quality [![CodeFactor](https://www.codefactor.io/repository/github/berkeleyauv/perception/badge)](https://www.codefactor.io/repository/github/berkeleyauv/perception) + ## Installation We will use Conda for managing environments. We recommend installing Miniconda for Python 3.8 [here](https://docs.conda.io/en/latest/miniconda.html). Then create an environment with - conda create -n urobotics python3.7 + conda create -n urobotics python=3.7 activate it with @@ -13,7 +15,18 @@ activate it with and install all dependencies with - pip install -r requirements.txt + pip3 install -r requirements.txt + +Then clone the repo in a directory of your choice + + git clone https://github.com/berkeleyauv/perception.git + +and install it + + pip3 install -e perception/ + +Also, our training data is stored here https://www.dropbox.com/sh/rrbfqfutrmifrxs/AAAfXxlcCtWZmUELp4wXyTIxa?dl=0 so download it and unzip it in the same folder as `perception`. + ## misc: Misc code, camera calibration etc. @@ -33,4 +46,4 @@ Visualization tools Code for testing tasks (Ideally this should be placed a separate folder called `tests`). ## wiki: -Flowchart on TaskPerceiver, TaskReceiver, AlgorithmRunner. \ No newline at end of file +Flowchart on TaskPerceiver, TaskReceiver, AlgorithmRunner. diff --git a/tasks/.DS_Store b/tasks/.DS_Store index ab8337441c865e6d0bf8b50c59a4ed5d0a283227..b0e5925f4e4460187a9977a7d06f768da65d8987 100644 GIT binary patch literal 6148 zcmeHL!A=4(5Pd~77>wbj$31#7(W{pY!Gkvwy(28E5Mh_FXu@qT{)4~Z_xS<(rfq;N zka#l2%#ikVyYr@<*Ufed0MmXMTmUTq4Hm)LA*(GW_oZxD&G!h;*cb_VNH9c-?uNH^ z{6z(1?V4PzE~Z#;?N;m8y&308f1LN}A&x>*3eZ-uyD$nNrzBbdHyvIS2^z(eoad5!kQ-;g<3nl`QXgy#H_Z+-n!ncw#rA^6v zwe4+*oCJIsPfZn21yq3_E5JQltkHC+wJM+rr~*p``-e} zKozJeu;(sYvj1Q2KL1xqdZh}e0{=<@Q}5h$+DysstsBY7UK_DIu!xCY>2OJ5!*0d& fm91F*+p)&}NE*c0W9g7RH2n~;GH9g={Hg-q{>6oO delta 92 zcmZoMXfc=|#>CJzu~2NHo}wrt0|NsP3otOGGUPFoFeCzT=EOpEM#jl@%o>}oG8?gM pe!#5AyqTSYp983E^F`+G%#-;=bU8sf4*)U4WE&po%`qZNm;uZk6)XS% From 71f2e9fd4d4556c9ac54dbbf236b2749fd963f87 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sat, 5 Dec 2020 22:22:42 -0800 Subject: [PATCH 07/19] update to new perception directory --- perception/__init__.py | 0 .../misc/camera_chessboard_calibration.py | 178 ++++++ perception/misc/combinedFilTest.py | 60 ++ perception/misc/combined_filter.py | 55 ++ .../misc/featureGray2_higher_order_fns.py | 93 +++ perception/misc/hydrophones.ipynb | 195 ++++++ perception/misc/nonlinear-regression.ipynb | 153 +++++ perception/misc/optical_flow.py | 61 ++ perception/tasks/TaskPerceiver.py | 18 + perception/tasks/cross/CrossPerceiver.py | 9 + perception/tasks/cross/cross_detection.py | 98 +++ perception/tasks/gate/GateCenter.py | 22 + perception/tasks/gate/GatePerceiver.py | 9 + perception/tasks/gate/GateSegmentationAlgo.py | 108 ++++ .../tasks/gate/GateSegmentationAlgo1.py | 132 ++++ .../tasks/gate/GateSegmentationAlgo2.py | 192 ++++++ perception/tasks/gate/archive/detectGate.py | 198 ++++++ perception/tasks/gate/archive/threshTest.py | 209 +++++++ perception/tasks/gate/gateDetectionVideo.avi | Bin 0 -> 5686 bytes .../tasks/path_marker/PathMarkerPerceiver.py | 9 + .../path_marker/path_marker_detection.py | 256 ++++++++ .../tasks/path_marker/play_slots_detection.py | 234 +++++++ perception/tasks/sanity_test.py | 65 ++ .../segmentation/GateTaskExample.py.orig | 86 +++ .../tasks/segmentation/aggregateRescaling.py | 80 +++ .../tasks/segmentation/combinedFilter.py | 58 ++ .../peak_removal_adaptive_thresholding.py | 570 ++++++++++++++++++ .../tasks/spinny/spinny_wheel_detection.py | 112 ++++ perception/tasks/spinny/threshslider.py | 185 ++++++ perception/vis/FrameWrapper.py | 103 ++++ perception/vis/TaskPerceiver.py | 31 + .../vis/TestTasks/GateSegmentationAlgo.py | 120 ++++ perception/vis/TestTasks/TestAlgo.py | 16 + perception/vis/algo_stats | Bin 0 -> 5441 bytes perception/vis/vis.py | 58 ++ perception/vis/window_builder.py | 56 ++ 36 files changed, 3829 insertions(+) create mode 100644 perception/__init__.py create mode 100644 perception/misc/camera_chessboard_calibration.py create mode 100644 perception/misc/combinedFilTest.py create mode 100644 perception/misc/combined_filter.py create mode 100644 perception/misc/featureGray2_higher_order_fns.py create mode 100644 perception/misc/hydrophones.ipynb create mode 100644 perception/misc/nonlinear-regression.ipynb create mode 100644 perception/misc/optical_flow.py create mode 100644 perception/tasks/TaskPerceiver.py create mode 100644 perception/tasks/cross/CrossPerceiver.py create mode 100644 perception/tasks/cross/cross_detection.py create mode 100644 perception/tasks/gate/GateCenter.py create mode 100644 perception/tasks/gate/GatePerceiver.py create mode 100644 perception/tasks/gate/GateSegmentationAlgo.py create mode 100644 perception/tasks/gate/GateSegmentationAlgo1.py create mode 100644 perception/tasks/gate/GateSegmentationAlgo2.py create mode 100644 perception/tasks/gate/archive/detectGate.py create mode 100644 perception/tasks/gate/archive/threshTest.py create mode 100644 perception/tasks/gate/gateDetectionVideo.avi create mode 100644 perception/tasks/path_marker/PathMarkerPerceiver.py create mode 100644 perception/tasks/path_marker/path_marker_detection.py create mode 100644 perception/tasks/path_marker/play_slots_detection.py create mode 100644 perception/tasks/sanity_test.py create mode 100644 perception/tasks/segmentation/GateTaskExample.py.orig create mode 100644 perception/tasks/segmentation/aggregateRescaling.py create mode 100644 perception/tasks/segmentation/combinedFilter.py create mode 100644 perception/tasks/segmentation/peak_removal_adaptive_thresholding.py create mode 100644 perception/tasks/spinny/spinny_wheel_detection.py create mode 100644 perception/tasks/spinny/threshslider.py create mode 100644 perception/vis/FrameWrapper.py create mode 100644 perception/vis/TaskPerceiver.py create mode 100644 perception/vis/TestTasks/GateSegmentationAlgo.py create mode 100644 perception/vis/TestTasks/TestAlgo.py create mode 100644 perception/vis/algo_stats create mode 100644 perception/vis/vis.py create mode 100644 perception/vis/window_builder.py diff --git a/perception/__init__.py b/perception/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/perception/misc/camera_chessboard_calibration.py b/perception/misc/camera_chessboard_calibration.py new file mode 100644 index 0000000..3df18fc --- /dev/null +++ b/perception/misc/camera_chessboard_calibration.py @@ -0,0 +1,178 @@ +import numpy as np +import cv2 +import glob, os +import random, string + +################################################################## +# Measures characteristics of the camera so that +# later images can be undistorted with +# cv2.undistort(), camera matrix, and distortion values. +# Undistorted lines appear straighter and aid in feature detection. +# Requires a certain number of image samples of a chessboard in a variety +# of positions and angles. +# - add more diverse images if undistort_test errors or the result is swirly +# +# It's possible to use a circle grid instead of a chessboard: +# https://docs.opencv.org/3.4/d4/d94/tutorial_camera_calibration.html +# +# Most of the code is from: +# https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_calib3d/py_calibration/py_calibration.html +################################################################## + +################################################################## +# Helper functions +################################################################## + +def isFromWebcam(cap): + return isinstance(cap, cv2.VideoCapture) +def isFromFileSystem(cap): + return isinstance(cap, list) +def get_frame(cap, index=0): + """ Returns ret, frame just like a regular cv2.VideoCapture does""" + if isFromFileSystem(cap): + if len(cap) == 0 or index >= len(cap): + return (False, None) + else: + return (True, cv2.imread(cap[index])) + elif isFromWebcam(cap): + return cap.read() + else: + return (True, cap) + +################################################################## +# Test 1: OpenCV's tutorial dataset +# source: https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_calib3d/py_calibration/py_calibration.html +################################################################## +# images = glob.glob('../data/opencv_tutorial_calibration_images/*.jpg') +# test_img = get_frame(cv2.imread('../data/opencv_tutorial_calibration_images/left02.jpg')) +# checker_rows, checker_cols = 6, 7 + +################################################################## +# Test 2: Munich Visual Odometry dataset +# source: https://vision.in.tum.de/data/datasets/mono-dataset +################################################################## +# images = glob.glob('../data/calib_narrow_checkerboard1/images/*.jpg') +# test_img = get_frame(cv2.imread('../data/sequence_47/images/00001.jpg')) +# checker_rows, checker_cols = 5, 8 + +################################################################## +# Test 3: Pictures taken by oneself +################################################################## +# # Files from my computer 1 +# images = glob.glob('iphone_chessboard_imgs/*.JPG') +# test_img = get_frame(cv2.imread('iphone_chessboard_imgs/IMG_3413.JPG')) +# checker_rows, checker_cols = 7, 7 +# # Files from my computer 2 +# images = glob.glob('*.png') +# test_img = get_frame(cv2.imread('GIKTZTK9HV.png')) +# checker_rows, checker_cols = 7, 7 +# Use webcam +images = cv2.VideoCapture(0) +test_img = get_frame(cv2.VideoCapture(0)) +checker_rows, checker_cols = 7, 7 + +def undistort_test(mtx, dist, test_img): + print('camera matrix:') + print(np.array2string(mtx, separator=', ')) + print('distortion matrix:') + print(np.array2string(dist, separator=', ')) + + ret, test_img = get_frame(test_img) + h, w = test_img.shape[:2] + newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h)) + + # undistort + dst = cv2.undistort(test_img, mtx, dist, None, newcameramtx) + + # crop the image + x,y,w,h = roi + dst = dst[y:y+h, x:x+w] + + scaling = 500/max(test_img.shape) + cv2.imshow('original', cv2.resize(test_img, None, fx=scaling, fy=scaling)) + if dst.shape[:2] != (0, 0): + cv2.imshow('undistorted', cv2.resize(dst, None, fx=scaling, fy=scaling)) + else: + print('Error: No valid undistort_test result') + cv2.waitKey(500) + print('Hit enter to quit') + input() + +def is_recording(): + print("Do you want to save the images taken in this session? (y/n)") + user = input() + if 'y' in user or 't' in user: + return True + if 'n' in user or 'f' in user: + return False + else: + return is_recording() + +######################################################################## +# Start script +######################################################################## + +if isFromWebcam(images): + print("Is recording") + recording = is_recording() +else: + print("Not recording") + recording = False + +# subpix function termination criteria +criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) +# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0) +objp = np.zeros((checker_rows*checker_cols,3), np.float32) +objp[:,:2] = np.mgrid[0:checker_cols,0:checker_rows].T.reshape(-1,2) +# Arrays to store object points and image points from all the images. +objpoints = [] # 3d point in real world space +imgpoints = [] # 2d points in image plane. + +# Gather data points +count = 0 +ret_frame = True +gray = None +while count < 40 and ret_frame: + ret_frame, img = get_frame(images, count) + if ret_frame: + gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) + + # Find the chess board corners + ret, corners = cv2.findChessboardCorners(gray, (checker_cols, checker_rows),None) + + # If found, add object points, image points (after refining them) + if ret == True: + objpoints.append(objp) + + corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria) + imgpoints.append(corners2) + + # Draw and display the corners + img_chess = np.copy(img) + cv2.drawChessboardCorners(img_chess, (checker_cols, checker_rows), corners2,ret) + + cv2.imshow('chessboard', img_chess) + if recording: + name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) + cv2.imwrite(name + '.png', img) + print(count) + cv2.waitKey(500) + else: + cv2.imshow('chessboard', img) + print(count, 'no chessboard found. skipped', images[count].split('/').pop() if isinstance(images, list) else '') + if isinstance(images, list): + os.remove('iphone_chessboard_imgs/'+images[count].split('/').pop()) + print(' -removed') + cv2.waitKey(500) + count += 1 + +if gray is not None: + # Get camera matrix and dist + ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None) + + # Test it out + undistort_test(mtx, dist, test_img) +else: + print("No images found. If you want to capture from your webcam, uncomment the 'Use webcam' block at the top") + +cv2.destroyAllWindows() diff --git a/perception/misc/combinedFilTest.py b/perception/misc/combinedFilTest.py new file mode 100644 index 0000000..0084045 --- /dev/null +++ b/perception/misc/combinedFilTest.py @@ -0,0 +1,60 @@ +import cv2 +import numpy as np + +from sys import argv as args +from aggregateRescaling import init_aggregate_rescaling +from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim + +if __name__ == "__main__": + if args[1] == '0': + cap = cv2.VideoCapture(0) + else: + cap = cv2.VideoCapture(args[1]) + +# Returns a grayscale image +def init_combined_filter(): + aggregate_rescaling = init_aggregate_rescaling() + + def combined_filter( + frame, custom_weights=None, display_figs=False, print_weights=False + ): + pca_frame = aggregate_rescaling(frame) # this resizes the frame within its body + + __, other_frame = filter_out_highest_peak_multidim( + np.dstack([pca_frame[:, :, 0], frame]), + custom_weights=custom_weights, + print_weights=print_weights, + ) + + other_frame = other_frame[:, :, :1] + + if display_figs: + cv2.imshow('original', frame) + cv2.imshow('Aggregate Rescaling via PCA', pca_frame) + cv2.imshow('Peak Removal Thresholding after PCA', other_frame) + return other_frame + + return combined_filter + + +if __name__ == "__main__": + ret = True + ret_tries = 0 + + # for i in range(3000): + # cap.read() + + combined_filter = init_combined_filter() + + while 1 and ret_tries < 50: + ret, frame = cap.read() + if ret: + frame = cv2.resize(frame, None, fx=0.4, fy=0.4) + filtered_frame = combined_filter(frame, display_figs=True) + + ret_tries = 0 + k = cv2.waitKey(60) & 0xFF + if k == 27: + break + else: + ret_tries += 1 diff --git a/perception/misc/combined_filter.py b/perception/misc/combined_filter.py new file mode 100644 index 0000000..84bec1f --- /dev/null +++ b/perception/misc/combined_filter.py @@ -0,0 +1,55 @@ +import cv2 +import numpy as np + +import sys +sys.path.insert(0, '../background_removal') +from featureGray2_higher_order_fns import init_aggregate_rescaling +from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim +#from workshop import draw_rect + +#format: [video feed] +if __name__ == "__main__": + cap = cv2.VideoCapture('../data/course_footage/GOPR1142.mp4') + +# Returns a grayscale image +def init_combined_filter(): + aggregate_rescaling = init_aggregate_rescaling() + + def combined_filter(frame, custom_weights=None, display_figs=False, print_weights=False): + pca_frame = aggregate_rescaling(frame) # this resizes the frame within its body + + __, other_frame = filter_out_highest_peak_multidim( + np.dstack([pca_frame[:,:,0], frame]), + custom_weights=custom_weights, + print_weights=print_weights) + + other_frame = other_frame[:,:,:1] + + if display_figs: + cv2.imshow('original', frame) + cv2.imshow('pca thing', pca_frame) + cv2.imshow('other filter thing', other_frame) + return other_frame + return combined_filter + +if __name__ == "__main__": + ret = True + ret_tries = 0 + + # for i in range(3000): + # cap.read() + + combined_filter = init_combined_filter() + + while 1 and ret_tries < 50: + ret, frame = cap.read() + if ret: + frame = cv2.resize(frame, None, fx=0.4, fy=0.4) + filtered_frame = combined_filter(frame, display_figs=True) + + ret_tries = 0 + k = cv2.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 \ No newline at end of file diff --git a/perception/misc/featureGray2_higher_order_fns.py b/perception/misc/featureGray2_higher_order_fns.py new file mode 100644 index 0000000..f7e4e47 --- /dev/null +++ b/perception/misc/featureGray2_higher_order_fns.py @@ -0,0 +1,93 @@ +import cv2 as cv +from sys import argv as args +import numpy as np +import numpy.linalg as LA + +#Jenny -> unsigned ints fixed the problem +#Damas -> flip weight vector every frame +if __name__ == "__main__": + cap = cv.VideoCapture('../data/course_footage/path_marker_GOPR1142.mp4') +paused = False +speed = 1 +#man/min of past ten frames; average or total +def init_aggregate_rescaling(only_once=False, weights=[], max_min={'max': 90, 'min': -20}): #you only pca once + def aggregate_rescaling(frame, display_fig=False): + nonlocal only_once, weights, max_min + #frame = cv.cvtColor(frame, cv.COLOR_BGR2HSV) + frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + + #kernel = np.ones((5,5),np.float32)/25 + #frame = cv.filter2D(frame,-1,kernel) + + r, c, d = frame.shape + A = np.reshape(frame, (r * c, d)) + + if not only_once: + + A_dot = A - A.mean(axis=0)[np.newaxis, :] + + _, eigv = LA.eigh(A_dot.T @ A_dot) + weights = eigv[:, 0] + #if (weights<0).sum() > 0: + #if np.mean(weights) < 0: + # weights *= -1 + + red = np.reshape(A_dot @ weights, (r, c)) + only_once = True + else: + red = np.reshape(A @ weights, (r, c)) + #red /= np.max(np.abs(red),axis=0) #this looks real cool - Damas + """ + if len(max_min['max']) == 10: + max_min['max'] = max_min['max'][1:] + [np.max(red)] + max_min['min'] = max_min['min'][1:] + [np.min(red)] + else: + max_min['max'].append(np.max(red)) + max_min['min'].append(np.min(red)) + """ + + if np.min(red) < max_min['min']: + max_min['min'] = np.min(red) + if np.max(red) > max_min['max']: + max_min['max'] = np.max(red) + + #print(np.min(red), np.max(red), 'all time Domas', max_min['min'], max_min['max']) + + red -= max_min['min'] + red *= (255.0/(max_min['max'] - max_min['min'])) + + #red -= np.min(max_min['min']) + #red *= (255.0/np.abs(np.max(max_min['max']))) + + #red -= np.min(red) + #red *= (255.0/np.abs(np.max(red))) + + red = red.astype(np.uint8) + red = np.expand_dims(red, axis = 2) + red = np.concatenate((red, red, red), axis = 2) + + if display_fig: + cv.imshow('One Time PCA plus all time aggregate rescaling', red) + cv.imshow('frame', frame_gray) + return red + + return aggregate_rescaling + +if __name__ == "__main__": + aggregate_rescaling = init_aggregate_rescaling() + while True: + if not paused: + for _ in range(speed): + ret, frame = cap.read() + if ret: + aggregate_rescaling(frame, True) + #break + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + if key == ord('i') and speed > 1: + speed -= 1 + if key == ord('o'): + speed += 1 \ No newline at end of file diff --git a/perception/misc/hydrophones.ipynb b/perception/misc/hydrophones.ipynb new file mode 100644 index 0000000..e9e2e9a --- /dev/null +++ b/perception/misc/hydrophones.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline \n", + "import numpy as np\n", + "import numpy.linalg as LA\n", + "import math\n", + "import scipy\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib\n", + "#ignore divide by 0 warnings\n", + "import warnings\n", + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[24.64692931 63.9232561 ] x, y pos\n" + ] + } + ], + "source": [ + "# pingers = (x, y, frequency)\n", + "# pingers = [(np.random.random()*100, np.random.random()*100, f) for f in range(2,5)]\n", + "pingers = np.asarray([(0, 0, 2), (100, 0, 3), (50, 100, 4)])\n", + "pinger_amp = 10\n", + "# assuming cylindrical spreading: https://dosits.org/science/advanced-topics/cylindrical-vs-spherical-spreading/\n", + "# amp_detected = amp_source / distance(m)\n", + "sound_speed = 1481 # (m/s) https://en.wikipedia.org/wiki/Speed_of_sound#Water\n", + "mic_sample_rate = 1 # Hz\n", + "robot = np.random.random(2)*100 # x, y position\n", + "print(robot, 'x, y pos')\n", + "\n", + "def simulate(duration=5):\n", + " num_samples=duration*mic_sample_rate\n", + " # get 'duration' seconds of samples\n", + " xs = range(num_samples)\n", + " ys = []\n", + " for x in xs:\n", + " y = 0\n", + " for pinger in pingers:\n", + " delta = pinger[0:2] - robot\n", + " dist = np.sqrt(np.dot(delta, delta))\n", + " if int(x / mic_sample_rate - dist / sound_speed) % pinger[2] == 0:\n", + " y += pinger_amp / dist\n", + " ys.append(y)\n", + " return xs, ys\n", + "\n", + "xs, ys = simulate(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4\n", + "2 0\n", + "2 1\n", + "2 2\n", + "3 0\n", + "3 1\n", + "4 0\n", + "4 1\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "

" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "max_period = max(pingers, key=lambda x:x[2])[2]\n", + "print(max_period)\n", + "feature_len = max_period * mic_sample_rate - 1\n", + "features = np.zeros((len(pingers), feature_len))\n", + "\n", + "for pinger in range(len(pingers)):\n", + " period = pingers[pinger][2]\n", + "# features[pinger, 0] = 1\n", + " for i in range(max_period // period + 1):\n", + " print(period, i)\n", + " features[pinger, i * mic_sample_rate] = 1\n", + "\n", + "A = features\n", + "xs_ = xs[:feature_len]\n", + "ys_ = ys[:feature_len]\n", + "amps = LA.lstsq(A.T,ys_)[0]\n", + "\n", + "\n", + "plt.figure(1, figsize=(20,5))\n", + "plt.subplot(311)\n", + "plt.plot(xs, ys, 'b')\n", + "plt.subplot(312)\n", + "plt.plot(xs_, amps @ features , 'r')\n", + "# plt.subplot(313)\n", + "# plt.plot(xs, ys_fft_ang, 'y')\n", + "# plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABIQAAAEyCAYAAACLeQv5AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzs3Xd0ndWV/vHnqFcb25KrZMuAwQVwwTEtgEM12CZAIDRDKLaDgWSSySSZ/LImk8m0TGZCwiRBYJleQhggAUwxJBACgQBuFDds3Hsv6uWe3x9b0lWzLEtXem/5ftbSsu7Vq6st0G3Pe/Y+znsvAAAAAAAAJI6koAsAAAAAAABAzyIQAgAAAAAASDAEQgAAAAAAAAmGQAgAAAAAACDBEAgBAAAAAAAkGAIhAAAAAACABEMgBAAAAAAAkGAIhAAAAAAAABIMgRAAAAAAAECCSQnqB+fl5fmioqKgfjwAAAAAAEDcWbRo0W7vff6RjgssECoqKtLChQuD+vEAAAAAAABxxzm3oSPHBRYIxYtNmyTvg64CXZGRIfXvH3QVAAAAAAD0HAKhLho9WiotDboKdNWMGdIvfiHl5QVdCQAAAAAA3Y9AqIt+8xuptjboKtAVK1daGLRggXTPPdK110rOBV0VAAAAAADdh0Coi266KegKEAk33ijNnCldf7305JPSvfdKhYVBVwUAAAAAQPdg23lA0sknS+++K919t/TGG9KYMVJxsRQKBV0ZAAAAAACRRyAE1EtOlr79benTT6XTTpPuuEOaPFlatSroygAAAAAAiCwCIaCF4cOl116THnrIwqGxY6X/+A+ppiboygAAAAAAiAwCIaANzkk33ywtXy5ddpn0wx9KEydKCxcGXRkAAAAAAF1HIAS0Y+BA6emnpT/8Qdq921rJ/uEfpPLyoCsDAAAAAKDzCISADvjyl6Vly2wnsp//3IZQv/FG0FUBAAAAANA5HQqEnHNTnHOrnHNrnHP/2M5xVznnvHNuYuRKBKLDMcdI998vvfmmlJQknX++BUT79gVdGQAAAAAAR+eIgZBzLlnSbyRdImm0pOucc6PbOC5X0jclvR/pIoFoMnmy9PHH0ve+Jz38sDR6tPTcc0FXBQAAAABAx3VkhdAkSWu892u999WSnpL05TaO+1dJP5NUGcH6gKiUmSn9139JH3xgc4a+8hX72LYt6MoAAAAAADiyjgRCQyRtanJ5c/11jZxz4yUVeu/nt3dDzrnZzrmFzrmFu3btOupigWgzYYKFQj/9qfTyy7Za6IEHJO+DrgwAAAAAgMPrSCDk2riu8e2ucy5J0i8kfedIN+S9n+u9n+i9n5ifn9/xKoEolpoqff/71kY2dqzNFbrgAunzz4OuDAAAAACAtnUkENosqbDJ5QJJW5tczpV0kqQ/O+fWSzpd0gsMlkaiGTHCdh67/35p4ULbiex//keqrQ26MgAAAAAAmutIIPShpBHOueHOuTRJ10p6oeGL3vsD3vs8732R975I0t8kXea9X9gtFQNRLClJmj1bWr5cuvBC6bvflc44Q/roo6ArAwAAAAAg7IiBkPe+VtJdkhZIWiHpae/9MufcT5xzl3V3gUAsGjJE+sMfpN/9Ttq4UZo4UfrhD6VKRq4DAAAAAKKA8wFNv504caJfuJBFRIh/e/ZI3/mO9Mgj0oknSiUl0tlnB10VAAAAACAeOecWee+POManIy1jALqgXz/p4YelBQukqirpnHOkO+6QDh4MujIAAAAAQKIiEAJ6yEUXSZ98In3rW9J990ljxkjz5wddFQAAAAAgEREIAT0oJ0f6xS+k996TeveWpk+XrrtO2rkz6MoAAAAAAImEQAgIwGmnSYsXS//yL9Kzz0qjR0uPPSYFNNILAAAAAJBgCISAgKSlST/6kbR0qXTCCdJNN0mXXipt2BB0ZQAAAACAeEcgBARs9Gjp7bel//1f+3fMGOlXv5Lq6oKuDAAAAAAQrwiEgCiQnCx94xvSsmW2Jf03v2n/Ll8edGUAAAAAgHhEIAREkWHDpJdftnlCn30mjR8v/eQnUnV10JUBAAAAAOIJgRAQZZyTZsyw1UFf+Yr0z/8sTZggvf9+0JUBAAAgkkIh6fXXpeuvl+64Q/roo6ArApBICISAKNW/v/Tkk9KLL0oHDkhnnCF9+9tSWVnQlQEAAKAr9uyRfv5z6cQTpYsukl57TXroIWncOOnMM6XHH5cqK4OuEkgs5eVBV9DzCISAKDdtms0Wuv126Ze/lE46yV40AABwNLyXdu6U/vpX6eGHpR/+UPrqV609eehQm2X38cdBVwnEL+9txffNN0tDhkj/8A/SwIHSE09IW7bYx913W1h0441SQYH0ve9Jn38edOVA/AqFpD/+UbrmGmnwYGnfvqAr6lnOex/ID544caJfuHBhID8biFVvvy3NnGnzhb72NXvR0Ldv0FUBAKLJnj3S6tVtfxw8GD4uOVkaPlwaMUJKT7cZdtXV0mmnSbNm2YvjnJzgfg8gXpSV2arv4mJpyRK7X914ozRnjnTyya2P91564w3p3nul55+3nWenTLHjp061+y6Artm2zVblPfCAtHat1KeP3S9/+EPr1Ih1zrlF3vuJRzyOQAiILZWV0r/+q/Szn1kY9KtfSVdfbbOHAACJYf/+w4c+Tc9uJiXZhgUjRrT+KCqSUlPDx+7ebZsalJRIK1bYm9brr7dw6NRTeZ4Bjtby5dJ990mPPGJh7MknW6gzY4aUm9ux29iyRZo3T5o7V9q6VSoslGbPthOEAwd2b/1AvKmrk1591Z7n5s+3y+eea89zX/mKlJERdIWRQyAExLmPPpJuu01atEi67DI7izRkSNBVAQAi5dAhac2acNDz2Wfhz3fvbn5sYWHrwOeEE2wFUHr60f1c76V337UXzE8/LVVU2FyTWbOkG26QeveO3O8IxJvqaun3v7fVQG+9JaWl2Ym7OXNsNlBng9WaGpsrWVxs7S0pKdKVV9rtnnsugS3Qno0bbSXQgw9KmzdL+fnWujlzpj1XxiMCISAB1NbaXKEf/cjO8v7sZ/aCPYnpYAAQE8rLm4c+TT+2b29+7ODBba/0Oe44KTOze+rbv99aXUpKpKVL7ed89av2XNOVN7dAvNm40VbxzJsn7dhhYezXvy7dequ9+Yykzz6T7r/f2l327ZNGjbJg6KabCGyBBjU1tgqopMRWBUnShRfa89dll1lYG88IhIAE8vnntnz4jTfsLNHcufGbdgNArKmstMfptkKfLVuaHztgQNuhz/HHS9nZwdQv2aqhRYvshfWTT0qlpdLo0XZ29aabpH79gqsNCEooZBt93Huv9NJLdt3UqRbOXHxx95+gq6iQfvc7WzX0wQdSVpa1ec6ZI02Y0L0/G4hWa9bYaqCHHrJwdvBgC2Zvu81apRMFgRCQYLy3ZZDf+Y69+fjxj+3zpvMhAADdo7rahlK2Ffps2mSP0Q3y8g4f+vTqFdzv0FGlpfYmtKTEdkxKS7PWlVmzpMmTWaWK+Ld7t73muv9+u9/372/h6OzZNrMrCIsXWzD0xBMWFE2aJN1xh63o664VhEC0qKqyVs2SEjtBnpRk4eysWdIll1iLZaIhEAIS1LZt0l13Sc89Z1sJz5vHWSIAiITaWmn9+uZhT8Ncnw0bbLVAgz592g59RoyQjjkmsF8h4j75xF6AP/aYtZcdd5y9Mb75ZgbeIr54L733nq0G+r//sxD4nHMsdLniiuhpP9m/X3r0UQuHVq60x6JbbpFuv90ef4B4smKFPQc9+qjtsDlsmD0H3XILs1UJhIAE99xz0p13Srt22UqhH/+YM0QAcCR1dTYLpK2VPuvWWSjUIDfX2nPbCn0SrYWqokJ69ll7Yf6Xv9jZ2OnT7ezsRRexTTZi16FDtuqmuFj6+GNbxXfTTRawjBkTdHWH570Ntb73Xls5UVtr81PmzLH7ZiKumEB8KC+3ULakRPrrX+1v+fLL7fnmggtYpdqAQAiA9u2Tvvtd66M9/nh74Jw8OeiqACBYoZDtMtJW6LN2rZ35b5CdbY+fbYU+/fszVLktq1bZ6tSHH7bWmqFDbX7DrbfabmhALPj0UwuBHnvMQqFx42w10HXXSTk5QVd3dLZts9eCc+daC+uQIfbmedYsm68CxIKlS+29zBNPSAcO2PPwrFnS175mz8dojkAIQKM33rAHzLVr7d+f/Sy+WhYAoCXvpa1b2w59Pv/cZq01yMg4fOgzaBChT2dVV0vPP28v4F9/3c7aXnKJPQ9deikz7hB9qqpspVtxsfTOO1J6unTNNbaq5rTTYv+xoLZWevllWzW0YIGt3Lv8cvv9zjsv9n8/xJ9Dh6Tf/taeRxYutPvkVVfZ88g55/A3256IBkLOuSmS7pGULGme9/6nLb7+95JmSqqVtEvSrd77De3dJoEQ0LPKy6V//mfp7rttF5t777UXAQAQq7y3HUTaCn3WrLHHvQZpaTbfpq3QZ8gQlph3t3XrbIXCgw/aaoVBg2zO0MyZ0rHHBl0dEt369TYg+oEHrNX+uOMsJLn55vht//z8c/udH3zQZq+ccIK1wd18s80dAoLive2aV1IiPfWUVFYmnXSShUAzZkh9+wZdYWyIWCDknEuW9JmkCyVtlvShpOu898ubHPMlSe9778udc3MkTfbeX9Pe7RIIAcFYuNC2Xfz4Y0vYf/UrBn8CiF7e25uVtkKf1avt7GGDlBQLF9oKfQoLmWMTDRpWKJSU2L+hkM18mDVL+vKX7ewv0BPq6qRXXrHVQK+8YisNLrvMgqBEmkNSWWnzWIqLbWh2ZqZ07bX23+ELXwi6OiSSffukxx+354dPPpGysuxvcdas+Fih19MiGQidIenH3vuL6y//QJK89/95mOPHS/q19/6s9m6XQAgITk2N9N//Lf3Lv9h8jLvvtv5bHmgBBGXfvsOHPvv3h49LTpaKitoOfYYNY1BqLNm8WXroIVuVsWGDlJdnz0UzZ0ojRwZdHeLVzp32N3f//fZ3N3BgeJ5Oos+4+ugjC4Yef9xWZUycaMHQtdfam3Mg0ryX3n7bQqBnnrGA8tRT7f543XU2xB2dE8lA6CpJU7z3M+sv3yjpNO/9XYc5/teStnvv/6292yUQAoK3cqU94L7zju08cf/90vDhQVcFIF4dPHj40GfPnvBxzlm401boU1QUPds7IzLq6qQ//tHeEDz/vK0iOvtse3666ip2yETXeW+vde6912YE1dTYzJw5c2xlGvOsmjt40IZpFxdLy5bZ3Mmvfc1ayghrEQm7dtlW8SUlthFBbq61g82aJY0fH3R18SGSgdDVki5uEQhN8t5/o41jZ0i6S9K53vuqNr4+W9JsSRo6dOipGza0O2YIQA8IhSwI+v737UX5v/2b9M1v0loBoHPKyg4f+uzc2fzYgoLWgc8JJ1jbF61DiWnHDumRR+xNwpo19ka04U3CKacEXR1iTctgo3dvm5FDsNExDUFacbGt3qipkb70JdttjSANRysUso1uSkqk3//e/p7OOMMe37/6VetaQOT0eMuYc+4CSb+ShUE7W91QC6wQAqLLpk32BD9/vjRpkm0ZfPLJQVcFIFqEQtLevfaGveFj5077d/t228Vw9Wrb2aupQYPaXulz3HG0IODwvJf+/Gd74/Dss7Zj2aRJ9sbh2mtjb9tv9KylSy3EeOIJWp8iZccOG0Dd0Go3aJC1d86ebeE+cDjbtll78Lx5tsFA377SjTfa4/mYMUFXF78iGQilyIZKny9pi2yo9PXe+2VNjhkv6RlZa9nqjhRIIAREH++l3/3OVgjt2yf94AfSD3/ImXogXtXU2LLtpuFOy7Cn4WPXLltF2FJKitS/v7Wbtgx9jj+eN+7ouj17bJVHSYm0fLn9TV13nb2ZmDiR+XcwLYcjZ2TY3wnDkSOr5TDupCRp+vTEG8aN9tXVSa++ao/b8+fb5cmT7XH7yivt/onuFelt5y+V9EvZtvMPeu//3Tn3E0kLvfcvOOf+KOlkSdvqv2Wj9/6y9m6TQAiIXrt3S3//9/YCfNQoS/TPPDPoqgB0RGVl8yCnvbCn6dyepjIzpQED7KN///DnbV3u04c35OgZ3tsb/ZISO3lRUSGNHWtvMG64wdrLkHja2j59zhybecP26d1r3Tpp7lwb0r1rl50E+PrXpVtukfr1C7o6BGHDBrsvPvigbRzQv7+1ac6caSeK0HMiGgh1BwIhIPq9+qo9sW/aJN15p/Qf/2FD3wD0HO9ta/W2gp22rmu6DXtTvXo1D3LaC3tycgh5EN0OHJCefNLCoSVLLMS8+moLh846i7/feFdbK730kq1SWbDA5h5efrkFQeedx///nlZVJT33nA3tfucdW1l+zTX2/4PtwuNfTY304ov2eLxggV130UX2eDx9OhtBBIVACEBEHDpkbWO//rX1iN9/v3TJJUFXBcS2UMjaMtsLdppeV1nZ+jacszOwHVnF078/y7MRvxYtsjciTz5pz1mjRtnZ6Jtusq3sET+2b7dVy3Pn2smqIUNshs3MmdLgwUFXB0n65BPpvvtslfmhQ9K4cTaj8vrrGRocb9assfvjww/ba5UhQ6Rbb7WPoqKgqwOBEICIeu896bbbpBUrbGn+L3/JC22gqdrao5vHU1vb+jaSkzsW7gwYIOXn2/weAKa0VHr6aQuH/vY3Oyt95ZV2lnryZGabxKqGAePFxbYzUW2tdOGFtvpk+nQeB6PVoUM21Lu4WPr4Y1uletNN9v9t9Oigq0NnVVba/bCkRHrzTXvdMnWqPc5OmcL9MZoQCAGIuKoqaxv7z/+0rVvvuccGNrIUGPGqsvLI4U7TeTxtPaVmZLQd7BxuHg9vWoGu++QTO3P92GO2Gu+44+ykxi23SAMHBl0dOmL/funRR221yYoV9vh4663Wys4sktjRMPuruNgC2+pq6dxzLRi64graiWLF8uUWAj36qO04WlRkK/NuuYXVedGKQAhAt/n0U3th/cEH0qWX2ou1wsKgqwKOzHtbRdCRVTw7dkgHD7Z9O7m5HVvFM2CAHUtoCgSjosJmm5SUSG+9ZWevp0+3s9kXXWRntxFdFi+2WTS//a1UXm4zaObMkb76VZsVhdi1a5dtP37ffTaQesAAez05e7Y0bFjQ1aGl8vLwqst335VSU21W16xZ0vnncwIr2hEIAehWdXXSr35l84WSkqSf/tResPHkgJ7m/ZHn8TS9XFHR9u3063fkdq2G63hTAsSezz4Lz7vYtUsaOjQ874KTGsGqqLCd44qL7WRTVpbNnJkzR5owIejqEGmhkPTaa/b/e/58u27qVPv/ffHFvJYM2tKlFgI9/ridGDvhBAuBbrrJXgMhNhAIAegR69bZ8u3XX7edXebNk0aODLoqxLq6OnvD1pFdtXbuPPw8nvz8jq3iyc+3M18A4l91tfTCC/aG5/XXbQXflCn2hmfqVB4LetLq1bZa5KGHLNgfNcpCgRtvlI45Jujq0BM2brQh4fPm2XP68OH2uvLWW+25GT3j0CFblVdSIi1caDvFNezcePbZrHSORQRCAHqM99ZT/O1vS2Vl0o9+JH3ve7yoxuEdOiStXGk96StW2AvCpmHP7t1tz+NJSzvyHJ6G6/r14ywjgPatWyc9+KB9bN1q84VuucVmYxx7bNDVxafaWgvkioulP/7R2viuvNKCoHPP5Y1noqqulv7wB2sXfOste76/+mr7uzjzTP4uuoP3tiKvpER66il7DX/yyRYCzZhhc7sQuwiEAPS4HTukb37T+o1PPll64AHpC18IuioEadcuC3xWrAiHPytWSJs3h49JTbXWjY5sn96rFy8KAUReba30yiv2xuill6yl5fzz7Y3R5Zfb2XJ0zdat9t937lz7vLDQVoLcdhuDvtHc8uW2cuyRR6xl6eSTbev6G26wuXzomn37rB2spMQG8Gdl2SYxs2ZJkybxOiteEAgBCMwLL9gZne3bpW99S/rJT6Ts7KCrQnfxXtq0KRz2NA2A9uwJH5edbe2Eo0bZlrOjRtnHsceymgxA9NiyxVqY5s2TNmyQ8vJsdsasWbREHy3vpTfesFUfzz9vQdvFF9trhKlTGeqN9pWVSU8+aavJliyRcnKsnXDOHAuJ0HHeS2+/bSHQM8/YLqqnnmqPa9ddZyfcEF8IhAAE6sAB6fvfl+6/3/rBS0rsbCtiV22ttHZt69U+K1fazl0N+vULhz1Nw5+CAlq4AMSOUMhmDJWUWJhRWyt98Yv2Burqqxku3559+2x493332TDvfv1sJdDXv04rHo5eQ2tTcbENH6+stPvinDnSV77CCr727NplK63mzZNWrbLgZ8YMa4sdPz7o6tCdCIQARIW33rIXz6tX21yGn/+cnuRoV1FhL+BbtnqtXm09/g2GDGm+0qch/GEIJIB4s2NH+E3V6tVS7972pmrWLGns2KCrix4ffmirgZ56yt60n3mmvWm/6iopIyPo6hAP9uwJh41r1thrjltvtbBx+PCgq4sOoZD0pz9ZmP2HP0g1NXZfbAizWbWfGAiEAESNigprG/vv/7al97/5jZ3RQbAOHGi7zWvduvBA56QkO5vbcrXPyJEsLwaQeLy3Ex0lJdKzz0pVVTYrb/Zs6dprraUl0ZSX2+5ExcXSokX2ZnPGDAuCCMvQXRpCj+JiW8HnvXTJJfZ3d8klidmOuHWrtbs+8IC9luvb19pdZ86UxowJujr0NAIhAFFnyRJbMr5kiXTFFdKvfy0NHhx0VfHNe9u5q2Wb14oV9sKhQVqadOKJrVf7jBjBWV0AaMvevdJjj1k4tGyZhUENg1knToz/wawrV9oqjYcfthMMJ51kb8ZnzOCEAXrW5s12PywpkbZtk4YNs5D2tttsQ4p4VlfXfCB+XZ30pS/Z49AVV/AaLpERCAGISrW11jb24x9bz/f//I89Ycf7C+fuFgrZ1u0tV/usWGGzHBrk5LQe6jxqlC2zTkkJrn4AiFXeS3/7m70h+93vbMXM2LH2huyGG6Rjjgm6wsipqbEWlOJi6c03bUOAq66yHaDOOovncgSrpsY2Nrn3XhtmnppqK9LnzJHOPju+/j43bLCVQA8+aIPwBwyQbr7ZVgMdf3zQ1SEaEAgBiGqrV9uL5bfesjMZc+fyBNYRNTXS55+3Xu2zcqW9CWmQn9/2YOchQ+LrBREARJODB21XpJISafFiGzx99dX2fBfLgcmmTeEVGNu3S0VFNrPl1lul/v2Drg5obdWq8Aq2/futZWrOHNulLFZXsDUEXiUl0muv2XUXX2yPL9Ons2MrmiMQAhD1QiEb0Pnd79qw4p/8RPr2t1mpIlm4s2pV24Oda2vDxxUWtl7tM2qUzWoCAARn8WJ74/bEE9KhQ/bYPHOmzfSIhcfohl3WioulF1+0lVCXXmpvqqdMScwZLYg95eW2cq+42IaeZ2fbyr05c6Rx44KurmPWrLHXyw89ZGMACgosjL31VmuPA9pCIAQgZmzZIt15pw0FPPVUe9KLlSfprtq3r+3Bzhs2hAc7JydLxx3XutVr5MjEHGAKALGkrEx6+mkLh957z2a2XXGFndX/0pdseH802bPH3njed5+tSM3PtyBr9mxbGQTEqoULLRj67W9tw5PTT7d2x6uvjr5ZO5WV0u9/b48bb75prwWnTbPHDQJZdASBEICY4r30zDPSXXfZi9HvfU/60Y+i7wm6M7y3JfZtDXbevj18XEZG24Odjz/e5i0BAGLbp5/aSY9HH7UTAscea2HLzTdLgwYFV5f30vvv2+yVp5+23dPOPttWUVx5Jc9BiC/79kmPPGKh56pVUr9+0i23SLffbifggrR8uYVAjz5qg+uHDw8/RrARC44GgRCAmLR3r/Sd71jP9wkn2JPiOecEXVXHhELS+vWt27xWrLAdWBr06tX2YOeiIs74AEAiqKyUnnvOnuP+/Gd77J8+3c7+X3xxzz0XlJbazKPiYmnpUik311rabr/ddg0D4pn3tvqmuNiGpdfW2v1vzhxp6tSeG2FQXh5eRfjuuzYLqGEV4XnnRd8qQsQGAiEAMe311215+vr19sL0v/4reoYAVlfbLJ+WrV4rV9qL/AYDBrQe6jxqlJ0FjtXBogCAyFq92lYNPfywzQcpLAzPBxk6tHt+5rJl9ib4scdsEPbYsfYm+IYbaEVGYtq61e6Hc+faKIOCAnsdOnNm963eW7IkPGfs4EFbJT5rloWy+fnd8zOROAiEAMS8sjLpn/5JuuceezIuLrYzqD3581eubN3mtWaNVFcXPm7YsLYHO/ft23O1AgBiW3W1DW9uuoPQlCn2BnHatK7vIFRdbauSioulv/zFZhldc40FQaefzokKQLJVQvPn2/3ktddsldAVV9j9ZPLkrt9PDh60GUYlJdKiRTYuoGEnwi9+kfshIodACEDc+OAD6bbbbPbCNddI//u/kd3mds+e1kOdV6yQNm4MH5OSYrN8Wq72OfFE27ECAIBIWb9eevBB+9iyRRo40GaIzJx59DNONmyQ7r9feuABW4F07LG28vaWW2JjtzMgKKtX233noYdspMHIkXbf+drXpGOO6fjtNMzoKimxHc/KyqRTTrEQ6IYbpD59uu93QOKKaCDknJsi6R5JyZLmee9/2uLr6ZIelXSqpD2SrvHer2/vNgmEAByN6mprG/u3f7Pl7L/4hXTjjR0/k+K9LQduudpn+XJp167wcZmZ9oTfstXruOPsbCoAAD2ltlZ65RVrZXnpJVudet559kbyiisOP+y5rk5asMBWObz0kj1XTptmOypdeCEzSYCjUVEh/d//2dD199+314rXX2+rhk499fDft3ev9PjjFgR9+qmdQLzuOrv/fuELrAZC94pYIOScS5b0maQLJW2W9KGk67z3y5scc4ekU7z3tzvnrpV0hff+mvZul0AIQGcsX25PpO++a4P/7ruv+Ta4dXXSunVtD3Y+dCh83DHHtD3YedgwXigDAKLPli02Z2jePFtB1K+fzRqZNcuevyQ7wfHgg7aqYd06W1k0c6Yd013ziIBEsmSJBa1PPGHDoCdNsmDommssKPLeWjJLSmz33KoqC39mzZKuvdYGtwM9IZKB0BmSfuy9v7j+8g8kyXv/n02hlkR+AAAgAElEQVSOWVB/zHvOuRRJ2yXl+3ZunEAIQGeFQnaW5gc/sCfe226Tduyw8Oezz+zJt8GgQW0Pdh4wgDMzAIDYEwpJf/qTDb99/nmppsZmjxQU2Iyg6mqbdXLHHdLll3d99hCA1g4csKHs995rJx379JGuvFJ65x3byr53b2nGDAuCxo4NulokokgGQldJmuK9n1l/+UZJp3nv72pyzKf1x2yuv/x5/TG7W9zWbEmzJWno0KGnbtiw4eh+KwBoYuNGOyvzyivS8OGthzqPGnV0Pd4AAMSSnTulRx6xVUM7dthsk9tvD68YAtC9GlYEFRdbIDtpkoVAV18tZWUFXR0SWSQDoaslXdwiEJrkvf9Gk2OW1R/TNBCa5L3fc7jbZYUQgEiprbWhzwAAJCLvWfUKBI37IaJJRwOhjkzK2CypsMnlAklbD3dMfctYb0l7O1YqAHQNYRAAIJHxJhQIHvdDxKKOBEIfShrhnBvunEuTdK2kF1oc84Kkr9V/fpWkN9qbHwQAAAAAAIDgdHTb+Usl/VK27fyD3vt/d879RNJC7/0LzrkMSY9JGi9bGXSt937tEW5zl6R4GSKUJ2n3EY8C0F24DwLB434IBIv7IBA87oeIFsO89/lHOqhDgRDa55xb2JH+PADdg/sgEDzuh0CwuA8CweN+iFjTkZYxAAAAAAAAxBECIQAAAAAAgARDIBQZc4MuAEhw3AeB4HE/BILFfRAIHvdDxBRmCAEAAAAAACQYVggBAAAAAAAkGAIhAAAAAACABEMg1AXOuSnOuVXOuTXOuX8Muh4g0TjnCp1zbzrnVjjnljnn/i7omoBE5JxLds4tcc7ND7oWIBE5545xzj3jnFtZ/5x4RtA1AYnEOfft+teinzrnfuucywi6JqAjCIQ6yTmXLOk3ki6RNFrSdc650cFWBSScWknf8d6PknS6pDu5HwKB+DtJK4IuAkhg90h61Xs/UtJYcX8Eeoxzboikb0qa6L0/SVKypGuDrQroGAKhzpskaY33fq33vlrSU5K+HHBNQELx3m/z3i+u//yQ7AXwkGCrAhKLc65A0lRJ84KuBUhEzrleks6R9IAkee+rvff7g60KSDgpkjKdcymSsiRtDbgeoEMIhDpviKRNTS5vFm9EgcA454okjZf0frCVAAnnl5K+JykUdCFAgjpW0i5JD9W3bs5zzmUHXRSQKLz3WyT9j6SNkrZJOuC9fy3YqoCOIRDqPNfGdb7HqwAg51yOpGclfct7fzDoeoBE4ZybJmmn935R0LUACSxF0gRJxd778ZLKJDHbEughzrk+sk6R4ZIGS8p2zs0ItiqgYwiEOm+zpMImlwvE0kCgxznnUmVh0BPe++eCrgdIMGdJusw5t17WOn2ec+7xYEsCEs5mSZu99w0rZJ+RBUQAesYFktZ573d572skPSfpzIBrAjqEQKjzPpQ0wjk33DmXJhsc9kLANQEJxTnnZDMTVnjv7w66HiDReO9/4L0v8N4XyZ4H3/Dec1YU6EHe++2SNjnnTqy/6nxJywMsCUg0GyWd7pzLqn9ter4Y7I4YkRJ0AbHKe1/rnLtL0gLZJPkHvffLAi4LSDRnSbpR0ifOuaX11/0/7/3LAdYEAEBP+4akJ+pPUq6VdEvA9QAJw3v/vnPuGUmLZTvgLpE0N9iqgI5x3gcz9iYvL88XFRUF8rMBAAAAAADi0aJFi3Z77/OPdFxgK4SKioq0cOHCoH48AAAAAABA3HHObejIccwQ6qoPPpBKS4OuAgAAAAAAoMMIhLqiulq64AIpL0+65BLp3nulTZuCrgoAAAAAAKBdBEJdkZws/eEP0pw50urV0p13SkOHSuPGSf/0T7Z6KBQKukoAAAAAAIBmIjJU2jlXKOlRSQMlhSTN9d7f0973TJw40cfVDCHvpZUrpfnzpRdflP76VwuDBg6Upk6Vpk+31UTZ2UFXCgAAAAAA4pRzbpH3fuIRj4tQIDRI0iDv/WLnXK6kRZIu994vP9z3xF0g1NKePdIrr1g49Oqr0sGDUnq6dN55Fg5Nny4VFARdJQAAAAAAiCM9Ggi18cOfl/Rr7/3rhzsm7gOhpqqrpbfftnDoxReltWvt+nHjwuHQqadKSXTwAQAAAACAzgssEHLOFUn6i6STvPcHD3dcQgVCTXkvrVgRDofeey/cWjZtWri1LCsr6EoBAAAAAECMCSQQcs7lSHpL0r97759r4+uzJc2WpKFDh566YcOGiP3smLV7d/PWskOHpIwM6fzzLRyaNk0aMiToKgEAAAAAQAzo8UDIOZcqab6kBd77u490fMKuEGpPdbX0l7+EVw+tW2fXjx8fbi2bMIHWMgAAAAAA0KaeHirtJD0iaa/3/lsd+R4CoSPwXlq+vHlrmffSoEHh1rLzz6e1DAAAAAAANOrpQOiLkt6W9Ils23lJ+n/e+5cP9z0EQkdp1y7p5ZctHFqwQCottdayCy4It5YNHhx0lQAAAAAAIECB7jLWEQRCXVBV1by1bP16u/7UU8Ph0IQJknOBlgkAAAAAAHoWgVCi8F5atiwcDv3tb3bd4MHNW8syM4OuFAAAAAAAdDMCoUS1c6e1ls2fH24ty8xs3lo2aFDQVQIAAAAAgG5AIARrLXvrrfDqoQ0b7PqJE8O7lo0bR2sZAAAAAABxgkAIzXkvffppOBx6/327rqDAVg1Nmyaddx6tZQAAAAAAxDACIbRv507ppZcsHHrtNamszLawb9paNnBg0FUCAAAAAICjQCCEjqusbN5atnGjXf+FL4Rby8aOpbUMAAAAAIAoRyCEzvFe+uSTcDj0wQd2XWFh89ayjIygKwUAAAAAAC0QCCEyduxo3lpWXm6tZRdeaCuHpk6ltQwAAAAAgChBIITIq6yU3nzTwqH586VNm+z6SZPCrWWnnEJrGQAAAAAAASEQQvfyXvr44+atZZK1ljWEQ5Mn01oGAAAAAEAPIhBCz9q+Pdxa9vrr1lqWnd28tWzAgKCrBAAAAAAgrhEIITgVFc1byzZvtjaypq1lJ59MaxkAAAAAABFGIITo4L20dGk4HPrwQ7t+6NDmrWXp6YGWCQAAAABAPCAQQnTatq15a1lFhZSTI110kW1pP3Wq1L9/0FUCAAAAABCTCIQQ/SoqpDfeCK8e2rLF2shOOy28euikk2gtAwAAAACggwiEEFu8l5YssWDoxRelhr+NoiJbOTR9unTuubSWAQAAAADQDgIhxLatW8OtZX/8Y7i17OKLLRy69FIpPz/oKgEAAAAAiCoEQogf5eXNW8u2brU2sjPOCK8eGjOG1jIAAAAAQMIjEEJ88l5avNjCoRdftM8lafjw5q1laWnB1gkAAAAAQAAIhJAYtmxp3lpWWSnl5jZvLcvLC7pKAAAAAAB6BIEQEk95ufSnP4Vby7Ztk5KSrLWsYdeyUaNoLQMAAAAAxC0CISS2UKh5a9mSJXb9sceGW8vOOYfWMgAAAABAXCEQApravDm8pf2f/iRVVUm9ejVvLevXL+gqAQAAAADoEgIh4HDKypq3lm3fbq1lZ54Zbi0bOZLWMgAAAABAzCEQAjoiFJIWLQq3li1datcfd1w4HDr7bCk1Ndg6AQAAAADoAAIhoDM2bQq3lr3xRri1bMoUC4cuuYTWMgAAAABA1CIQArqqrEx6/XULh156Sdqxw1rLzjorvHroxBNpLQMAAAAARA0CISCSQiFp4cJwa9lHH9n1xx8fDoe++EVaywAAAAAAgSIQArrTxo3NW8uqq6XevZu3lvXtG3SVAAAAAIAE0+OBkHPuQUnTJO303p90pOMJhBA3Skubt5bt3CklJ7duLQMAAAAAoJsFEQidI6lU0qMEQkhYoZD04Yfh1rKPP7brR4wIh0NnnUVrGQAAAACgWwTSMuacK5I0n0AIqLdhQ7i17M03rbXsmGOat5b16RN0lQAAAACAONHRQCipJ4oBEtawYdKdd0qvvirt3i09+6x0+eXSn/4k3XCDlJ8vTZ4s/fzn0mefBV0tAAAAACBB9OgKIefcbEmzJWno0KGnbtiwIWI/G4gpdXXNW8s++cSuP+GE5q1lKSnB1gkAAAAAiCm0jAGxZP365q1lNTXWSnbJJRYOTZlirWYAAAAAALSDljEglhQVSXfdJS1YIO3ZIz3zjHTZZdJrr0nXXSfl5Ulf+pJ0993S6tVBVwsAAAAAiHGR3GXst5ImS8qTtEPSP3vvHzjc8awQAjqgrk56/31bOTR/vvTpp3b9iSeGW8vOPJPWMgAAAACApIBaxo4GgRDQCevWhVvL/vxnay3r27d5a1nv3kFXCQAAAAAICIEQEO8OHrSWshdflF56yVrNUlKkc86xcGjaNOn444OuEgAAAACiU12dtGOHtGmTtHmz7QidnBx0VV1GIAQkkro66W9/C+9atny5XT9yZLi17IwzaC0DAAAAkBhCIWnXLgt7GgKfhs8bLm/ZItXWhr9n82ZpyJDgao4QAiEgka1dG5479NZb4daySy+1cOjii2ktAwAAABCbvLcOiZYBT9PLW7ZI1dXNvy8tTSookAoL7aPp54WF0ujRdkyMIxACYA4cCLeWvfxyuLXs3HPDq4eOPTboKgEAAADAwp59+9pe0dP088rK5t+XkmIBT8uQp+nl/HzJuWB+rx5EIASgtbo66b33wq1lK1bY9aNHh+cOnXFGXPTNAj2ipkaqqJDKy8P/Nv38aL7W9N/UVCk9PfyRkdHxy505lnZSAADQUw4cOHwLV8Pn5eXNvyc5WRo8uO1VPQ2XBwyQkpKC+Z2iDIEQgCP7/PNwOPSXv1j/bL9+zVvLevUKukrg6NTVHTl06Wpo0/B5057zo5GRIWVlSZmZzf/NyrKv1dZKVVV25quqqvXnDZc7+/NbSkrq3uDpaL43NTUhztwBABCXSkvbn9mzaZN06FDz73FOGjTo8G1cBQXSwIGcwDoKBEIAjs6BA9KCBeHWsr177Y1Z09ay4cODrhKxKhSyACNSwUx7X2vZK95RaWlthzRthTad+VrDvxkZkTt7FQq1HRi1FR519nJHj+3sf/eWnOt80BTp0CotjXAKAIAG5eWt27ZaBj7797f+vgEDDt/CVVhoYVBqas//PnGMQAhA59XWNm8tW7nSrh8zJhwOnXYarWWxznt7E99dwUzTY1r2eHdUcnI4aOnusIa/565p+HuKVDDV1e+NlLS0nmnb68hllsEDALpLZaUNYW5vSPPeva2/Lz+//TauIUPiYkhzrCEQAhA5a9aEw6G337bAKC+veWtZbm7QVcaPmprIBjPtfa0zzwHOHT5g6erqmZbXcbYIneG93Y+iIZiqqrKVXJGQktIzbXsd+V4CVACIHdXVFva0N7Nn167W39e3b/ttXAUF9pyAqEMgBKB77N/fvLVs3z570z55cnj1UFFR0FVGXtO5NN0RzDT9vK6uczV2ZfXM0QQ6tNEAR6etmVA9EUS1dTlSc6eSk3tu6PnhLjc8NvF4BCCR1dZKW7e2P7Nnx47WJwF7926/jWvIECk7O5jfCV1GIASg+9XWSu++G149tGqVXX/SSeFwaNKk7juT3DCXprMDgY/ma52dj9L0TUt3tjxlZPCmCMCR1dU1D4t6Kohq63Ik5k6lpEjHHCP16dP6o2/ftq9v+MjJ4XETQHSrq5O2b29/Zs+2ba1XoubkNA93WgY+BQWs7o9zBEIAet7q1c1by+rqrK946lT76NcvsqtpOjsnJDU18qtn2jqGtgoAOLxQyEKhzoZLFRW2IcK+fc0/9u61f/fvb79d73Bh0pGCJMIkAJEQCkk7d7Y/s2fr1tYrx7Oy2p/ZU1hoq3+Q0AiEAARr3z7p1VctHHrllbZ3HGgqKalrK2SOJqxhy0oAiH+hkG1t3DIwOtxHQ5DU2TCpI0ESYRKQGLy3mTztzezZssXm3TWVnt7+zJ7CQnsc4TEER0AgBCB61NZKCxfaWd3DhTWpqTy5AQCiQ0+GSR0NkgiTgOjgvd3n25vZs3mzve5tKjW1ddtWy8AnL4/7OCKCQAgAAADoaT0VJh1NkESYBHSM99aK2t7Mnk2brGW1qZQUG8LcXhtXfr6tiAd6QEcDIfomAAAAgEhJSrL5Hb17H/2um0cbJu3ebfP7jiZMOtogiTAJ8eTQofZn9mzeLJWWNv+epCRp8GALdcaOlaZNax34DBjA3EjEJAIhAAAAIBr0VJi0d+/Rh0lHGyIRJqGnlZW1P7Nn0ybp4MHm3+OcNHCghTpjxkgXX9x6hc+gQcyfRNziLxsAAACIdYRJiGcVFRbutBf47NvX+vv697dgZ8QI6bzzWrd0DRokpaX1/O8DRAkCIQAAACCRxVuY1LevlJ1NmBQrqqpsx632Zvbs3t36+/LyLNQZNkz64hdbt3ENGWK7dgE4LAIhAAAAAJ0TT2FSw3wlwqTIqamRtm5tf2bPjh2tv69Pn3DAM2lS6zauggLbqRZAlxAIAQAAAOh58RImNR3UnUhhUm2ttH17+zN7tm+3nbua6tUrHO5MmND2zlzZ2cH8TkCCIRACAAAAEFt6Ikzau7dzu7l1JkiKtjApFLIwp72ZPdu2SXV1zb8vOzsc7Jx0Uuut1wsKLBACEBUIhAAAAAAkjlgPk1oGSUcbJoVC0q5d7c/s2bLFVgA1lZkZDnXOP7/1zJ7CQvtvGi2hFoAjIhACAAAAgI6IxTApK8tW8zQEPps3S9XVzb8/PT0c7Jx9dusWrsJCC6IIe4C4QiAEAAAAAN0tqDCpvFwaONBCndNPb3tmT34+YQ+QgAiEAAAAACCadSVMAoDDSAq6AAAAAAAAAPQsAiEAAAAAAIAE47z3wfxg53ZJ2hDID4+8PEm7gy4CiAHcV4CO4b4CdAz3FeDIuJ8AHRNP95Vh3vv8Ix0UWCAUT5xzC733E4OuA4h23FeAjuG+AnQM9xXgyLifAB2TiPcVWsYAAAAAAAASDIEQAAAAAABAgiEQioy5QRcAxAjuK0DHcF8BOob7CnBk3E+Ajkm4+wozhAAAAAAAABIMK4QAAAAAAAASDIEQAAAAAABAgiEQ6gLn3BTn3Crn3Brn3D8GXQ8QrZxzDzrndjrnPg26FiBaOecKnXNvOudWOOeWOef+LuiagGjknMtwzn3gnPuo/r7yL0HXBEQz51yyc26Jc25+0LUA0co5t94594lzbqlzbmHQ9fQUZgh1knMuWdJnki6UtFnSh5Ku894vD7QwIAo5586RVCrpUe/9SUHXA0Qj59wgSYO894udc7mSFkm6nOcVoDnnnJOU7b0vdc6lSnpH0t957/8WcGlAVHLO/b2kiZJ6ee+nBV0PEI2cc+slTfTe7w66lp7ECqHOmyRpjfd+rfe+WtJTkr4ccE1AVPLe/0XS3qDrAKKZ936b935x/eeHJK2QNCTYqoDo401p/cXU+g/OcAJtcM4VSJoqaV7QtQCIPgRCnTdE0qYmlzeLF+4AgAhwzhVJGi/p/WArAaJTfQvMUkk7Jb3uvee+ArTtl5K+JykUdCFAlPOSXnPOLXLOzQ66mJ5CINR5ro3rODsFAOgS51yOpGclfct7fzDoeoBo5L2v896Pk1QgaZJzjnZkoAXn3DRJO733i4KuBYgBZ3nvJ0i6RNKd9SMv4h6BUOdtllTY5HKBpK0B1QIAiAP181CelfSE9/65oOsBop33fr+kP0uaEnApQDQ6S9Jl9bNRnpJ0nnPu8WBLAqKT935r/b87Jf1eNiIm7hEIdd6HkkY454Y759IkXSvphYBrAgDEqPpBuQ9IWuG9vzvoeoBo5ZzLd84dU/95pqQLJK0Mtiog+njvf+C9L/DeF8neq7zhvZ8RcFlA1HHOZddv6CHnXLakiyQlxO7IBEKd5L2vlXSXpAWywZ9Pe++XBVsVEJ2cc7+V9J6kE51zm51ztwVdExCFzpJ0o+wM7tL6j0uDLgqIQoMkvemc+1h2gu517z3baQMAOmuApHeccx9J+kDSS977VwOuqUcEtu18Xl6eLyoqCuRnAwAAAAAAxKNFixbt9t7nH+m4lJ4opi1FRUVauHBhUD8eAAAAAAAg7jjnNnTkOFrGAAAAAABAQvLeq7Jys3bvnq+gOqiCEtgKIQAAAAAAgJ7ifUgVFWtVWrpYpaVLdOjQYpWWLlZNzW5J0umnb1RGRuERbiV+EAgBAAAAAIC4EgrVqrx8pUpLF9cHP0tUWrpEdXWHJEnOpSo7e4z69btMubkTlJMzXmlp/QOuumcRCAEAAAAAgJhVV1epsrJPm4U/ZWUfKxSqlCQlJWUqJ2ecBgy4sTH8yc4eo6Sk9IArDxaBEAAAAAAAiAm1tYdUWvpRs/CnvHy5vK+VJCUn91Zu7gQNHnxHY/iTlXWinEsOuPLoQyAEAAAAAACiTk3NHh06tKRZ+FNRsVqSDX9OTe2v3NxT1a/ftMbwJyNjuJxzwRYeIwiEAAAAAABAYLz3qq7e2ir8qara2HhMevow5eZO0IABM5rM/BlE+NMFBEIAAAAAAKBH2Dbva1uFPzU1O+uPcMrMPEG9e5+lnJy76sOfcUpN7Rdo3fGIQAgAAAAAAERcKFSriopVLcKfpaqrOyBJci5FWVlj1K/fVOXkjFdu7gRlZ49VSkpOwJUnBgIhAAAAAADQJaFQlcrKPm0W/thOXxWSpKSkDGVnj9WAAdc3hj9ZWWOUnJwRcOWJi0AIAAAAAAB0WG1tqcrKPmoW/pSXL2uy01cv5eSM1+DBtzeGP5mZJyopiQgimvB/AwAAAAAAtKmmZq9KS5c0C38qKj5TeKevfOXkTFC/fpc2hj+201dSsIXjiAiEAAAAAACAqqq21Yc+4fCnqmpD49fT0wuVkzOhWdtXWtpgdvqKUQRCAAAAAAAkENvpa32r8KemZkfjMZmZI9Sr1+nKzb1DOTnj67d5zwuwakQagRAAAAAAAHHK+zqVl3/WLPwpLV2i2tr99UckKzt7tPr2nVK/xft45eSMVUpKr0DrRvcjEAIAAAAAIA6EQtUqK1vWIvz5SKFQuSTJuXTl5IxVfv41jeFPdvbJ7PSVoAiEAAAAAACIMXV1ZSot/bix3au0dInKyj6V9zWSpOTkXOXkjNegQbMaw5+srJFKSkoNuHJECwIhAAAAAACiWE3NPpWWLm228qe8fJWkkCQpNTVPOTnjVVDw943hT2bmcez0hXYRCAEAAAAAECWqqrartHRJs/CnsnJd49fT0wuUkzNB+flfbQx/0tML2OkLRy0igZBzrlDSo5IGyiLKud77eyJx2wAAAAAAxBvb6WtDk/DH2r6qq7c1HpOZebxyc7+gQYNmN4Y/aWn5AVaNeBKpFUK1kr7jvV/snMuVtMg597r3fnmEbh8AAAAAgJhkO32tbrXyp7Z2X/0RScrOHq0+fS5QTs6E+vBnrFJSegdaN+JbRAIh7/02SdvqPz/knFshaYgkAiEAAAAAQMKwnb6Wtwh/lioUKpMkOZemnJxTlJ9/tXJyxis3d0L9Tl+ZAVeORBPxGULOuSJJ4yW938bXZkuaLUlDhw6N9I8GAAAAAKDH1NWV1+/0FW77sp2+qiVJyck5yskZp0GDbmsMf7KyRrHTF6JCRAMh51yOpGclfct7f7Dl1733cyXNlaSJEyf6SP5sAAAAAAC6S03N/vqdvsLhT3n5SjXs9JWS0le5uRNUUPCtxvAnM/N4dvpC1IpYIOScS5WFQU9475+L1O0CAAAAANCTqqt3Ng55bgh/KivXNn49LW2wcnMnKD//qsbwJz29kJ2+EFMitcuYk/SApBXe+7sjcZsAAAAAAHQn772qqja1Cn+qq7c2HpORcaxycydo0KCZ9eHPeKWlDQiwaiAyIrVC6CxJN0r6xDm3tP66/+e9fzlCtw8AAAAAQKd5H1JFxZpW4U9t7d76I5KUlTVSffqcp5wc2+I9J2ecUlOPCbRuoLtEapexdySxNg4AAAAAELhQqEbl5SuahT+lpUtVV1cqyXb6ys4+Sfn5VzYJf05RcnJWwJUDPSfiu4wBAAAAANBT6uoqVFb2SYvw5xN5XyVJSkrKVk7OWA0ceHNj+JOdPVpJSWkBVw4Ei0AIAAAAABATamsPqrR0abPwp6xshaQ6SVJKSh/l5IxXQcE3GsOfrKwRci452MKBKEQgBAAAAACIOtXVu1RauqRZ+FNRsabx62lpg5STM155eZc3hj8ZGcPY6QvoIAIhAAAAAEBgbKevza3Cn6qqzY3HZGQMV07O+GZtX+npAwOsGoh9BEIAAAAAgB5hO3193ir8qanZXX+EU1bWSPXufY5ycyfUhz/jlJraJ9C6gXhEIAQAAAAAiLhQqFbl5StahD9LVFd3SJLkXKqys09Sv36XNQl/TlFycnbAlQOJgUAIAAAAANAldXWVKiv7pFn4U1b2sUKhSklSUlKmcnLGacCAGxvDn+zsMez0BQSIQAgAAAAA0GG1tYdUWrq0RfizTA07fSUn91Zu7gQNHnxHY/iTlXUCO30BUYZACAAAAADQpurq3Y2tXg3hT0XFaklekpSaOkC5uRPUr9+0xvAnI6OInb6AGEAgBAAAAAAJznuv6uqtjaFPw79VVRsbj0lPH6bc3An1bV/jlZMzQenpgwKsGkBXEAgBAAAAQALx3quycm2r8KemZmf9EU6ZmSeod++zlJNzV/3Kn/FKTe0baN0AIotACAAAAADiVChUq4qKVS3Cn6WqqzsgSXIuRVlZY9Sv31Tl5IxXbu4EZWePVUpKTsCVA+huBEIAAAAAEOO8r1NFxVqVly9XWdkylZUtV3n5cpWXr2i201d29ikaMOB65eRMUG7ueGVnn6SkpPSAqwcQBAIhAAAAAIgRoVCNKirW1Ac/y5v8u0reVzUel55eqKys0Ro8+EvKyRmn3NwJysw8UUlJvAUEYHg0AAAAAIAoEwpVqbx8dZPAx1b9VFR8Ju9rG4/LyBiurKzR6tv3YmVljVZ29mhlZY1USkqvAJYpyH4AAA5TSURBVKsHEAsIhAAAAAAgIHV1FSovX9VqxU9FxRpJdfVHJSkz81hlZY1WXt5lysoaUx/8nKjk5OwgywcQwwiEAAAAAKCb1dWVqbx8ZbPQp6xsmSor10ry9UclKytrhLKzx6h//6sbV/xkZp6g5OTMIMsHEIcIhAAAAAAgQmprD6q8fEWL+T7LVVm5vvEY51KVmXmCcnMnaMCAGcrOHq3s7DHKzByhpKS04IoHkFAIhAAAAADgKNXU7Gsz+Kmq2tR4jHPpysoaqV69ztDAgbfVt3mNVmbmcUpKSg2wegAgEAIAAACAw6qp2dNsG/eGf6urtzUek5SUqaysUTrmmHObDHYerczMY+VccoDVA8DhEQgBAAAASGjee9XU7GwV+pSVLVdNzc7G45KTc9rY0Wu0MjKGybmkAH8DADh6BEIAAAAAEoL3XtXV21oEP7b6p7Z2b+Nxycm9lZ09Wv36TW8MfbKzRys9vVDOuQB/AwCIHAIhAAAAAHHFe6+qqk1trvipqzvQeFxKSh9lZ49Rfv7VzYKftLRBBD8A4h6BEAAAAICY5H1IlZUbWoU+5eXLVVdX2nhcamp/ZWeP1oABNzQLflJT+xP8AEhYBEIAAAAAopr3daqoWNtG8LNCoVBF43FpaYOUlTVaAwfeouzsMcrKGq2srFFKS8sLsHoAiE4EQgAAAACiQihUo4qKz9sIflbK+6rG49LTC5WVNVqDBzfd1WuUUlP7BFg9AMSWiAVCzrkpku6RlCxpnvf+p5G6bQAAAADxIxSqVkXF6lbbuVdUfCbvaxqPy8goqt/V68L64GeMsrJGKiWlV4DVA0B8iEgg5JxLlvQbSRdK2izpQ+fcC9775ZG4fQAAAACxp66uUhUVq1rN9ykvXy2prv4op8zM45SVNVp5edObrPgZqeTk7CDLB4C4FqkVQpMkrfHer5Uk59xTkr4sKe4DocrKTZIk55IkOUlJ9Z8nNV7X8nLbxzDMDgAAALGprq5c5eUrm23jXl6+XBUVayWF6o9KVmbm8crOHq28vK80DnfOyjpRycmZQZYPAAkpUoHQEEmbmlzeLOm0CN12VPvww9HNdjDomsOFRm2HSF09pnVYFaljWn9PZI7pSuAW2f920fbfl0ARAAD0hNraQyovX9FqxU9l5XpJXpLkXKoyM09QTs549e9/Q5PgZ4SSktIDrR8AEBapQKitd6O+1UHOzZY0W5KGDh0aoR8drBEj7pX31fI+JClU/69vdTn8eUjet7wcmWNa/9yuHlN7mN+n7d+x/Vq7dgw6ommQFL2BW/h4x3U9cl34/0FkrmNFIwAkgpqa/SovX9EY+pSVLVN5+XJVVYXPATuXpqyskerV67T6Xb0s+MnMPF5JSakBVg8A6IhIBUKbJRU2uVwgaWvLg7z//+3d3W9bdx3H8c/3PPixTeP0YWnX0nXsgaZIPKjsZoILYGiwicebDTFpEtJumDTEBYJL/gHE9QS7QCAmpIGEYGJMYghNAvZEgbXpto490G1au6XdmtiJfewvFz5zkiZx7MTtceL3S4ocH59z/G3aX2N//Pv+jj8o6UFJOn78+IrAaCuanLwn6xJGwmJotVXCtLUCrkHtM9jA7UqEle7JZX+e5pLn91WOG+Q2X6U279SCQbiSodOobwsVRWOKoglFUUVxvHgbBCUCOQAD1Wi8u2K2z9zcKdXriy/lg6CgUumoxseXXtFrSoXCEQUBFy0GgK1qUP+DPy3pRjM7IukNSXdJ+uaAzg2kb4BC8T4Ig7B6wLg9ty0PAUdpW3OVgNWHYttmmMWKognFcWVFYHR5eNTe9sG+FQVBblPPDWDrcnc1GudXCX5OqtE419kvCMoql6dUqdzWCX3K5SkVCofVvoYMAGA7GUgg5O6Jmd0v6TG1Lzv/kLufHMS5AWDQCBiRpXZ4tXZw5J6o2XxfSXJBjcbMstskmVGj0b5Nkguq199StXoqfexi1+cNgnKXwGjlbKQPHo+isXQmE4Bh5+6q199adcZPkrzb2S8Mx1QuT2n37jvTy7i3g598/iDjHQBGyMDmeLr7o5IeHdT5AADYjtqBpHV90xXH45I+1Nd53ZtKkvfWDI8uD5dqtZc6j7datS5nDhRF4+vMQFo9UAqCIi1uwBXg7lpYOLsi9GkHxIvhcBRVVC4f096931g24yeXO8DYBAAMLhACAADZMQsVxxOK44m+j20259MQae3ZSEsDpfn5VzuPS80uNeU7LWu9zEZafHycBWkBSe4tzc+/3mnvWgx+ptVsXursF8d7VSpNad++u9PQ55jK5SnF8T6CHwDAmgiEAAAYcWFYUBjuVz6/v6/j3F3N5qWe2tsajQtaWHhDc3PPq9GYUbP5/jo17ey7vS2OKwrDMd4AY8txb6pWe2WVGT/TarWqnf1yuf0qlaY0OXnvksWdjyqX25th9QCArYpACAAAbIiZpVdEG1OhcLivY1utRElysaf2tiSZUbV6Ot1vRu4LXc4cKorGuy6wvVa4FIbFzf1AgHW0Wonm519eFvq0Z/6cXvbvOp8/qFJpSgcO3Lfsql5xXMmwegDAdkMgBAAArrogiJTL7VEut6fvY5vNWs/tbY3GjGq1M+njF9ReyHutmgobaG+rpC1uvKTColarrlrtpRXr+1SrL8i90dmvULhOpdKUJiZuWzbjJ4rGMqweADAqePUCAAC2lDAsKgyLyucP9HWce0vN5qUu7W3Lw6WFhdc1O3tCSXJh2Xotq9c01nd7WxRNKAx30OK2hTWb86rVXlwR/NRqL8k9SfcyFQrXq1ye0sTEHZ3ZPqXSRxRFOzKtHwAw2giEAADASDALFEW7FEW7JB3p69hWq5G2uK3f3tZoXNDc3MnOdvd6l5qiNCSq9H0ltyDIb/Ingl41m1VVq6dXCX5e1uKss0DF4g0ql49pz56vLwl+bqYdEQAwlAiEAAAA1hEEsXK5vX0v3uvuarVqPbW3tW/Pq1p9MX38oiTvUlOx7/a29vZdMgs3+RPZnpJkVtXq9IrFnefnX9EHfxdmkYrFm7Rjx8e0b9/dS4KfmwjpAABbCoEQAADAFWJmCsOSwrAk6WBfx7q3lCTv9dTeliQXND//imZnn1OjMaNWa67ruaNovOsMpLXCpTAsb4sWtyR5T3Nz06pWTy4LfhYWXu/sY5ZTqXSzdu78lCYn7+0EP8XiDQqCOMPqAQAYDAIhAACAIWQWKI4riuOKisXr+zq21aqnQdL67W3t9ZLOdrYvXfR4ZU1Rn7ORFm+DILfZH0nfGo2ZFW1ec3OnVK+/0dknCAoqlY5q165Pd0KfcnlKhcL1LBYOANjW+C0HAACwzQRBTrncNcrlrunrOHdXsznXU3tbksyoXn9b1ep0ev/iOjWVN9DeVklb3IKu567Xz192Gff2943G28uev1w+qkrlc2nwcywNfg7TQgcAGEkEQgAAAJDUbnGLoh3p1a8O9XWse3NFi1u3tZNqtTOdx1utareqOi1uy9vXSqrVXla1ekqNxjudvcNwTOXylHbvvmPZjJ98/tC6wRIAAKOEQAgAAACbZhYqjicUxxMqFj/c17Gt1kLP7W3t9ZJeU7M5q0LhiPbs+Von9CmXjymXO7At1jkCAOBKIxACAABApoIgr3x+Uvn8ZNalAAAwMpg3CwAAAAAAMGIIhAAAAAAAAEaMuXs2T2x2XtJrmTz54O2R9M66ewFgrAC9YawAvWGsAOtjnAC92U5j5bC7711vp8wCoe3EzJ5x9+NZ1wEMO8YK0BvGCtAbxgqwPsYJ0JtRHCu0jAEAAAAAAIwYAiEAAAAAAIARQyA0GA9mXQCwRTBWgN4wVoDeMFaA9TFOgN6M3FhhDSEAAAAAAIARwwwhAAAAAACAEUMgBAAAAAAAMGIIhDbBzG43sxfM7IyZ/SDreoBhZWYPmdk5M3s+61qAYWVmh8zsCTObNrOTZvZA1jUBw8jMCmb2lJn9Kx0rP8q6JmCYmVloZv80s99nXQswrMzsVTP7j5mdMLNnsq7namENoQ0ys1DSi5Juk3RW0tOS7nb3U5kWBgwhM/uMpFlJP3f3j2ZdDzCMzGy/pP3u/pyZ7ZT0rKSv8nsFWM7MTFLZ3WfNLJb0pKQH3P3vGZcGDCUz+56k45LG3P3OrOsBhpGZvSrpuLu/k3UtVxMzhDbuFkln3P2/7l6X9LCkr2RcEzCU3P2vkmayrgMYZu7+lrs/l35/SdK0pGuzrQoYPt42m96N0y8+4QRWYWYHJd0h6adZ1wJg+BAIbdy1kv635P5Z8cIdADAAZnadpE9I+ke2lQDDKW2BOSHpnKTH3Z2xAqzuJ5K+L6mVdSHAkHNJfzKzZ83svqyLuVoIhDbOVtnGp1MAgE0xsx2SHpH0XXd/P+t6gGHk7k13/7ikg5JuMTPakYHLmNmdks65+7NZ1wJsAbe6+yclfVHSd9IlL7Y9AqGNOyvp0JL7ByW9mVEtAIBtIF0P5RFJv3T332RdDzDs3P2ipL9Iuj3jUoBhdKukL6drozws6bNm9otsSwKGk7u/md6ek/RbtZeI2fYIhDbuaUk3mtkRM8tJukvS7zKuCQCwRaUL5f5M0rS7/zjreoBhZWZ7zWw8/b4o6fOSTmdbFTB83P2H7n7Q3a9T+73Kn939WxmXBQwdMyunF/SQmZUlfUHSSFwdmUBog9w9kXS/pMfUXvjz1+5+MtuqgOFkZr+S9DdJN5vZWTP7dtY1AUPoVkn3qP0J7on060tZFwUMof2SnjCzf6v9Ad3j7s7ltAEAG3WNpCfN7F+SnpL0B3f/Y8Y1XRVcdh4AAAAAAGDEMEMIAAAAAABgxBAIAQAAAAAAjBgCIQAAAAAAgBFDIAQAAAAAADBiCIQAAAAAAABGDIEQAAAAAADAiCEQAgAAAAAAGDH/B62u13suPmm6AAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(1, figsize=(20,5))\n", + "plt.subplot(311)\n", + "plt.plot(xs, ys, 'b')\n", + "\n", + "ys_fft = np.fft.rfft(ys)\n", + "ys_fft_abs = np.abs(ys_fft)\n", + "ys_fft_ang = np.angle(ys_fft)\n", + "plt.subplot(312)\n", + "plt.plot(ys_fft_abs, 'r')\n", + "plt.subplot(313)\n", + "plt.plot(ys_fft_ang, 'y')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/perception/misc/nonlinear-regression.ipynb b/perception/misc/nonlinear-regression.ipynb new file mode 100644 index 0000000..9d038db --- /dev/null +++ b/perception/misc/nonlinear-regression.ipynb @@ -0,0 +1,153 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline \n", + "import numpy as np\n", + "import numpy.linalg as linalg\n", + "import math\n", + "import scipy\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib\n", + "#ignore divide by 0 warnings\n", + "import warnings\n", + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "xs = np.random.random(8) * 20\n", + "a, b, c = -25 + np.random.random()*50, -1 + np.random.random()*2, -25 + np.random.random()*50\n", + "ys = [a*np.log(x) + b*np.sqrt(x) + c*np.sin(x) + np.random.normal()*5 for x in xs]\n", + "\n", + "# LIN REGRESSION\n", + "A = np.array([np.log(xs), np.sqrt(xs), np.sin(xs)])\n", + "a_, b_, c_ = linalg.lstsq(A.T,ys)[0]\n", + "# LIN REGRESSION\n", + "\n", + "xs_ = [x/10. for x in range(-1, 200)]\n", + "ys_ = [a_*np.log(x) + b_*np.sqrt(x) + c_*np.sin(x) for x in xs_]\n", + "ys_actual = [a*np.log(x) + b*np.sqrt(x) + c*np.sin(x) for x in xs_]\n", + "plt.figure(figsize=(20,5))\n", + "plt.plot(xs_, ys_, 'b', xs, ys, 'yp', xs_, ys_actual, 'r--')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABIkAAAEyCAYAAAB+h4BJAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzs3Xd0VNUWx/HvnUkISSgBAoReJFIFhIBAUOkqRVCpYkGpNsBengrP8gQFfU9QQcVCR0EQAQGlF0FCkQ6hhCQgRUggJCFt7vvj0gmkzWQm5PdZKwty595zdgYY5u7ZZx/DNE1ERERERERERCR/s7k7ABERERERERERcT8liUREREREREREREkiERERERERERFRkkhERERERERERFCSSEREREREREREUJJIRERERERERERQkkhERERERERERFCSSEREREREREREUJJIREREREREREQAL3cHcLnAwECzcuXK7g5DREREREREROSmsXHjxn9M0yyZ0XkelSSqXLkyYWFh7g5DREREREREROSmYRjGocycp+VmIiIiIiIiIiKiJJGIiIiIiIiIiChJJCIiIiIiIiIiKEkkIiIiIiIiIiIoSSQiIiIiIiIiIihJJCIiIiIiIiIiKEkkIiIiIiIiIiIoSSS5yDTTiIwcxerVgURGjsY009wdkoiIiIiIiIicpySR5IqEhHDCwkKIiBhOaupJIiKGsXFjIxISwt0dmoiIiIiIiIjgxCSRYRh2wzA2G4Yx7/z3VQzDWG8YRrhhGDMMwyjgrLkk79m8OZT4+K04HPEAOBzxnD37F5s3h+ZoXFUniYiIiIiIiDiHMyuJhgC7Lvt+JPCJaZrBQAzQ14lzSR7j718bcFx11IG/f51sj6nqJBERERERERHncUqSyDCM8kAH4Ovz3xtAK2Dm+VO+B7o4Yy7Jm4KC+mK3F7rimN1eiKCgJ7M95vWqkzZtaqbqIhEREREREZEsclYl0X+BV7hUKlICiDVNM/X899FAufQuNAxjgGEYYYZhhJ04ccJJ4YinCQzshGF4XXHMMLwIDOyU7TGvV53kcJxTdVEWaMmeiIiIiIiIAHhlfMqNGYbREThumuZGwzBaXDiczqlmetebpvkl8CVASEhIuudI3uflVZTmzWOcOmZQUF/i4sJISzt7xXGrssi8+PsLvY9C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qlULdZQiEkin2t2sxOVm1tpOp3jNj4HPgcE4M4ca5nuuAbDjFM8jIiIiIiIifnLWWfDZLMOQIW34+9+hQwcYNgxuvhk8ZSpAIiLhpqzdzZrke9gN2Oj7fiZwt6/LWXvggOoRiYiIiIiIhIbbDS++6MwqWroUbrtNCSIROVlZaxK9bIz5wRizFrgeGOjbPhvYAmwG3gMeKuN1RERERERE/MLaPNLTX2PJkljS04djbV6oQ/Kr4l5fjRohDExEyr0SaxIFk2oSiYiIiIhIIGVlpbJ+fU+ys1PxejNxuWKIjm5K8+afEh3dpOQTlHPh/vpEpHROtSZRmbubiYiIiIiIVBQpKYlkZq7F680EwOvN5NChNaSkJIY4Mv8I99cnIoGlJJGIiIiIiESMmJgWgPeErV5iYuJDEY7fhfvrE5HAUpJIREREREQiRlxcX9zuqgW2ud1ViYu7L0QR+Ve4vz4RCSwliUREREREJGLExnbFmIJtvYzxEBvbNUQR+Ve4vz4RCSw1PRQRERERkYjh8VQnKSkj1GEETLi/PhEJLM0kEhERERERiQDW5pGe/hpLlsSSnj4ca/MK3SYikctYa0Mdw3EJCQk2OTk51GGIiIiIiIiElaysVNav70l2dipebyYuVwxVqjQC4PDh9OPboqOb0rz5p0RHNwlxxCLiT8aYldbahJL200wiERERERGRMJeSkkhm5lq83kwAvN5MsrI2kJW1ocC2Q4fWkJKSGMpQRSSElCQSEREREREJczExLQDvKezpJSYmPtDhiEg5pSSRiIiIiIhImIuL64vbXbXANmOiMKZKgW1ud1Xi4u4LZmgiUo4oSSQiIiIiIhLmYmO7YkzB5tYuVxVcrqgC24zxEBvbNZihiUg54il5FxEREREREanIPJ7qJCVlhDoMESnnNJNIRERERERERESUJBIRERERERERESWJREREREREREQEJYlERERERERERAQliUREREREREREBCWJREREREREREQEJYlERERERERERAQliUREREREREREBCWJREREREREREQEMNbaUMdwnDFmL/BzqOPwk1hgX6iDkJDQ2Ec2jX/k0thHNo1/5NLYRzaNf+TS2Ee2ijj+51hra5e0U7lKEoUTY0yytTYh1HFI8GnsI5vGP3Jp7CObxj9yaewjm8Y/cmnsI1s4j7+Wm4mIiIiIiIiIiJJEIiIiIiIiIiKiJFEgvRvqACRkNPaRTeMfuTT2kU3jH7k09pFN4x+5NPaRLWzHXzWJREREREREREREM4lERERERERERERJIhERERERERERQUkivzPGdDbG/GiM2WyMeTLU8UhwGWPSjDHrjDGrjTHJoY5HAssYM9YYs8cY80O+bTWNMfOMMam+f88KZYwSGEWM/RBjzC+++3+1MaZLKGOUwDDGNDTGLDDGbDDGrDfGDPRt170fAYoZf93/Yc4YU8UYs9wYs8Y39s/7tjc2xnzvu/c/NcZUDnWs4n/FjP8Hxpit+e791qGOVQLDGOM2xqQYY2b5Hoftva8kkR8ZY9zAW8AfgebAHcaY5qGNSkLgamtta2ttQqgDkYD7AOh8wrYnga+ttU2Ar32PJfx8wMljDzDCd/+3ttbODnJMEhy5wF+ttc2A9sAA3//1uvcjQ1HjD7r/w10OcI219mKgNdDZGNMeeAVn7JsAGUDfEMYogVPU+AM8ke/eXx26ECXABgIb8j0O23tfSSL/uhTYbK3dYq09AnwCdA9xTCISINbaxcBvJ2zuDnzo+/5D4KagBiVBUcTYSwSw1u601q7yfX8Q5xfG+ujejwjFjL+EOes45HtYyfdlgWuAyb7tuvfDVDHjLxHAGNMAuAF43/fYEMb3vpJE/lUf2Jbv8Xb0i0OkscBcY8xKY8wDoQ5GQqKutXYnOB8mgDohjkeC62FjzFrfcjQtNwpzxphzgUuA79G9H3FOGH/Q/R/2fMtNVgN7gHnAT8B+a22ubxf97h/GThx/a+2xe3+o794fYYyJCmGIEjgjgUGA1/e4FmF87ytJ5F+mkG3KMEeWRGttG5wlhwOMMVeGOiARCZq3gfNxpqHvBIaHNhwJJGNMVWAK8Ki19n+hjkeCq5Dx1/0fAay1edba1kADnBUEzQrbLbhRSbCcOP7GmHjgKeAioB1QE/hbCEOUADDG3AjssdauzL+5kF3D5t5Xksi/tgMN8z1uAOwIUSwSAtbaHb5/9wDTcH6BkMiy2xhTD8D3754QxyNBYq3d7fsF0gu8h+7/sGWMqYSTIJhgrZ3q26x7P0IUNv66/yOLtXY/sBCnLlUNY4zH95R+948A+ca/s28JqrXW5gDj0L0fjhKBbsaYNJxyMtfgzCwK23tfSSL/WgE08VU6rwz0AmaGOCYJEmNMjDHmzGPfA9cDPxR/lIShmcA9vu/vAWaEMBYJomMJAp+b0f0flnx1CMYAG6y1r+d7Svd+BChq/HX/hz9jTG1jTA3f92cAnXBqUi0AbvXtpns/TBUx/hvz/XHA4NSk0b0fZqy1T1lrG1hrz8X5fD/fWnsnYXzvG2vDZlZUueBreToScANjrbVDQxySBIkx5jyc2UMAHuBjjX94M8ZMBK4CYoHdwGBgOjAJaASkA7dZa1XgOMwUMfZX4Sw1sUAa0P9YjRoJH8aYJOAbYB2/1yZ4Gqcuje79MFfM+N+B7v+wZoxphVOc1o3zh/ZJ1toXfL//fYKz1CgF6OObVSJhpJjxnw/Uxll+tBp4MF+BawkzxpirgMettTeG872vJJGIiIiIiIiIiGi5mYiIiIiIiIiIKEkkIiIiIiIiIiIoSSQiIiIiIiIiIihJJCIiIiIiIiIiKEkkIiIiIiIiIiIoSSQiIiIiIiIiIihJJCIiIiIiIiIiwP8HjPR5XYJDz+wAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]\n", + "num_sinusoids, freqs = 10, [2*math.pi/i for i in primes]\n", + "noise = 5\n", + "amps_, freqs_ = np.random.random(num_sinusoids)*10, np.copy(freqs)\n", + "np.random.shuffle(freqs_)\n", + "freqs_ = freqs_[:num_sinusoids]\n", + "\n", + "xs = np.array([x/5. for x in range(200)])\n", + "ys_, data = [], []\n", + "for x in xs:\n", + " y = 0\n", + " for a, f in zip(amps_, freqs_):\n", + " y += a*math.sin(f*x)\n", + " ys_.append(y)\n", + " data.append(y + np.random.normal()*noise)\n", + "\n", + "# LIN REGRESSION\n", + "A = np.array([np.sin(f * xs) for f in freqs]) # domain augmentation\n", + "amps = linalg.lstsq(A.T,data)[0] # obtaining the amps\n", + "# LIN REGRESSION\n", + "\n", + "ys = []\n", + "for x in xs:\n", + " y = 0\n", + " for a, f in zip(amps, freqs):\n", + " y += a*math.sin(f*x)\n", + " ys.append(y)\n", + "\n", + "plt.figure(figsize=(20,5))\n", + "plt.plot(xs, ys, 'b', xs, data, 'yp', xs, ys_, 'r--')\n", + "plt.savefig('regression.png')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/perception/misc/optical_flow.py b/perception/misc/optical_flow.py new file mode 100644 index 0000000..738a464 --- /dev/null +++ b/perception/misc/optical_flow.py @@ -0,0 +1,61 @@ +import numpy as np +import cv2 + +cap = cv2.VideoCapture(0) + +# params for ShiTomasi corner detection +feature_params = dict( maxCorners = 100, + qualityLevel = 0.3, + minDistance = 7, + blockSize = 7 ) + +# Parameters for lucas kanade optical flow +lk_params = dict( winSize = (15,15), + maxLevel = 2, + criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)) + +# Create some random colors +color = np.random.randint(0,255,(100,3)) + +# Take first frame and find corners in it +ret, old_frame = cap.read() +old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) +p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **feature_params) + +# Create a mask image for drawing purposes +mask = np.zeros_like(old_frame) + +while(1): + ret,frame = cap.read() + frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + # calculate optical flow + p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) + + # Select good points + + if p1 is None: + continue + + good_new = p1[st==1] + good_old = p0[st==1] + + # draw the tracks + for i,(new,old) in enumerate(zip(good_new,good_old)): + a,b = new.ravel() + c,d = old.ravel() + mask = cv2.line(mask, (a,b),(c,d), color[i].tolist(), 2) + frame = cv2.circle(frame,(a,b),5,color[i].tolist(),-1) + img = cv2.add(frame,mask) + + cv2.imshow('frame',img) + k = cv2.waitKey(30) & 0xff + if k == 27: + break + + # Now update the previous frame and previous points + old_gray = frame_gray.copy() + p0 = good_new.reshape(-1,1,2) + +cv2.destroyAllWindows() +cap.release() \ No newline at end of file diff --git a/perception/tasks/TaskPerceiver.py b/perception/tasks/TaskPerceiver.py new file mode 100644 index 0000000..40f5995 --- /dev/null +++ b/perception/tasks/TaskPerceiver.py @@ -0,0 +1,18 @@ +from typing import Any +import numpy as np +class TaskPerceiver: + + def __init__(self): + self.time = 0 + + def analyze(self, frame: np.ndarray, debug: bool) -> Any: + """Runs the algorithm and returns the result. + Args: + frame: The frame to analyze + debug: Whether or not to display intermediate images for debugging + + Returns: + the result of the algorithm + """ + raise NotImplementedError("Need to implement with child class.") + diff --git a/perception/tasks/cross/CrossPerceiver.py b/perception/tasks/cross/CrossPerceiver.py new file mode 100644 index 0000000..65c2424 --- /dev/null +++ b/perception/tasks/cross/CrossPerceiver.py @@ -0,0 +1,9 @@ +from collections import namedtuple +import numpy as np +import sys +sys.path.insert(0, '..') +from TaskPerceiver import TaskPerceiver + +class CrossPerceiver(TaskPerceiver): + named_tuple = namedtuple("CrossOutput", ["centerx", "centery"]) + named_tuple_types = {centerx: np.int16, centery: np.int16} diff --git a/perception/tasks/cross/cross_detection.py b/perception/tasks/cross/cross_detection.py new file mode 100644 index 0000000..0b49772 --- /dev/null +++ b/perception/tasks/cross/cross_detection.py @@ -0,0 +1,98 @@ +import numpy as np +import cv2 +import sys + +############################################################################# +# Compilation of ways to do cross detection for this year's challenge. +# Add more functions for alternative algorithms! +############################################################################# + +sys.path.insert(0, '../background_removal') +from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim +from combined_filter import combined_filter + +ret, frame = True, cv2.imread('../data/cross/cross.png') # https://i.imgur.com/rjv1Vcy.png + +# "hsv" = Apply hsv thresholding before trying to find the path marker +# "multidim" = Apply filter_out_highest_peak_multidim +# "combined" = Apply pca then multidim +thresholding = "combined" # Apply hsv thresholding before trying to find the path marker + +def find_cross(frame, draw_figs=True): + """ Returns the middle of a possible cross that has the largest contour area + + One of the ideas from: + https://stackoverflow.com/questions/14612192/detecting-a-cross-in-an-image-with-opencv + """ + + # Or any other colorspace transformation + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + ret, thresh = cv2.threshold(gray, 127, 255,0) + __, contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + contours.sort(key=lambda c: cv2.contourArea(c), reverse=True) + + possible_crosses = [] + + for i in range(len(contours)): + cnt = contours[i] + + hull = cv2.convexHull(cnt,returnPoints = False) + defects = cv2.convexityDefects(contours[i],hull) + + # Crosses have 4 "defects" (concave places) + if defects is not None and len(defects) == 4: + possible_crosses.append(defects) + + + if draw_figs: + img = frame.copy() + for defects in possible_crosses: + for i in range(defects.shape[0]): + s,e,f,d = defects[i,0] + # start = tuple(cnt[s][0]) + # end = tuple(cnt[e][0]) + far = tuple(cnt[f][0]) + # cv2.line(img,start,end,[0,255,0],2) + cv2.circle(img,far,5,[0,0,255],-1) + cv2.imshow('cross at contour number ' + str(i),img) + cv2.imshow('original', frame) + + + return possible_crosses[0] + +########################################### +# Main Body +########################################### + +if __name__ == "__main__": + ret_tries = 0 + while(1 and ret_tries < 50): + # ret,frame = cap.read() + + if ret == True: + # frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5) + + if thresholding == "multidim": + votes1, threshed = filter_out_highest_peak_multidim(frame) + threshed = cv2.morphologyEx(threshed, cv2.MORPH_OPEN, np.ones((5,5),np.uint8)) + elif thresholding == "combined": + threshed = combined_filter(frame) + else: + threshed = frame.copy() + + cross_pts = find_cross(frame, True) + input() # This test is only for one frame + + ret_tries = 0 + k = cv2.waitKey(60) & 0xff + if k == 27: # esc + if testing: + print("hsv thresholds:") + print(thresholds_used) + break + else: + ret_tries += 1 + + cv2.destroyAllWindows() + cap.release() diff --git a/perception/tasks/gate/GateCenter.py b/perception/tasks/gate/GateCenter.py new file mode 100644 index 0000000..19e3dac --- /dev/null +++ b/perception/tasks/gate/GateCenter.py @@ -0,0 +1,22 @@ +from GateSegmentationAlgo2 import GateSegmentationAlgo +from GatePerceiver import GatePerceiver +from typing import Tuple +import sys +import os +sys.path.append(os.path.dirname(__file__)) + +import numpy as np +import math +import cv2 as cv +import time +import cProfile +import statistics + +class GateCenter(GatePerceiver): + def __init__(self): + super() + self.gate_center = self.output_class(250, 250) + + + def analyze(self, frame): + \ No newline at end of file diff --git a/perception/tasks/gate/GatePerceiver.py b/perception/tasks/gate/GatePerceiver.py new file mode 100644 index 0000000..1820e94 --- /dev/null +++ b/perception/tasks/gate/GatePerceiver.py @@ -0,0 +1,9 @@ +from collections import namedtuple +import numpy as np +import sys +sys.path.insert(0, '..') +from TaskPerceiver import TaskPerceiver + +class GatePerceiver(TaskPerceiver): + output_class = namedtuple("GateOutput", ["centerx", "centery"]) + output_type = {'centerx': np.int16, 'centery': np.int16} diff --git a/perception/tasks/gate/GateSegmentationAlgo.py b/perception/tasks/gate/GateSegmentationAlgo.py new file mode 100644 index 0000000..191865c --- /dev/null +++ b/perception/tasks/gate/GateSegmentationAlgo.py @@ -0,0 +1,108 @@ +from GatePerceiver import GatePerceiver +from typing import Tuple +import sys +import os +sys.path.append(os.path.dirname(__file__)) + + +from segmentation.combinedFilter import init_combined_filter +import numpy as np +import cv2 as cv +import time +import cProfile + +class GateSegmentationAlgo(GatePerceiver): + __past_centers = [] + __ema = None + + def __init__(self, alpha): + super() + self.__alpha = alpha + + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze + debug: Whether or not tot display intermediate images for debugging + Reurns: + (x,y) coordinate with center of gate + """ + gate_center = self.output_class(250, 250) + filtered_frame = combined_filter(frame, display_figs=False) + filtered_frame_copies = [filtered_frame for _ in range(3)] + stacked_filter_frames = np.concatenate(filtered_frame_copies, axis = 2) + mask = cv.inRange(stacked_filter_frames, + np.array([100, 100, 100]), np.array([255, 255, 255])) + _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + if contours: + contours.sort(key=self.findStraightness, reverse=True) + cnts = contours[:2] + rects = [cv.minAreaRect(c) for c in cnts] + centers = [np.array(r[0]) for r in rects] + boxpts = [cv.boxPoints(r) for r in rects] + box = [np.int0(b) for b in boxpts] + for b in box: + cv.drawContours(stacked_filter_frames,[b],0,(0,0,255),5) + if len(centers) >= 2: + gate_center = (centers[0] + centers[1]) * 0.5 + if self.__ema is None: + self.__ema = gate_center + else: + self.__ema = self.__alpha*gate_center + (1 - self.__alpha)*self.__ema + gate_center = (int(self.__ema[0]), int(self.__ema[1])) + # if len(self.__past_centers) < 15: + # self.__past_centers += [gate_center] + # else: + # self.__past_centers.pop(0) + # self.__past_centers += [gate_center] + # gate_center = sum(self.__past_centers) / len(self.__past_centers) + # gate_center = (int(gate_center[0]), int(gate_center[1])) + cv.circle(stacked_filter_frames, gate_center, 10, (0,255,0), -1) + + if debug: + return (self.output_class(gate_center[0], gate_center[1]), stacked_filter_frames) + return self.output_class(gate_center[0], gate_center[1]) + + def findStraightness(self, contour): # output number = contour area/convex area, the bigger the straightest + hull = cv.convexHull(contour, False) + contour_area = cv.contourArea(contour) + hull_area = cv.contourArea(hull) + return 10 * contour_area - 5 * hull_area + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + gate_task = GateSegmentationAlgo(0.1) + # once = False + start_time = time.time() + frame_count = 0 + while ret_tries < 50: + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.4, fy=0.4) + + + ### FUNCTION CALL, can change this + center, filtered_frame = gate_task.analyze(frame, True) + # cProfile.run("gate_task.analyze(frame, True)") + # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, + # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, + # 2.0, (0, 165, 255), 3) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + # if not once: + # print(filtered_frame) + # once = True + ret_tries = 0 + k = cv.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 + frame_count += 1 + #print(frame_count / (time.time() - start_time)) diff --git a/perception/tasks/gate/GateSegmentationAlgo1.py b/perception/tasks/gate/GateSegmentationAlgo1.py new file mode 100644 index 0000000..e054d56 --- /dev/null +++ b/perception/tasks/gate/GateSegmentationAlgo1.py @@ -0,0 +1,132 @@ +from GatePerceiver import GatePerceiver +from typing import Tuple +import sys +import os +sys.path.append(os.path.dirname(__file__)) + + +from segmentation.combinedFilter import init_combined_filter +import numpy as np +import cv2 as cv +import time +import cProfile +import statistics + +class GateSegmentationAlgo(GatePerceiver): + center_x_locs, center_y_locs = [], [] + + def __init__(self, alpha): + super() + + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze + debug: Whether or not tot display intermediate images for debugging + Reurns: + (x,y) coordinate with center of gate + """ + gate_center = self.output_class(250, 250) + filtered_frame = combined_filter(frame, display_figs=False) + + max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) + lowerbound = max(0.84*max_brightness, 120) + upperbound = 255 + _,thresh = cv.threshold(filtered_frame,lowerbound, upperbound, cv.THRESH_BINARY) + debug_filter = cv.cvtColor(thresh, cv.COLOR_GRAY2BGR) + + cnt = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[-2] + + area_diff = [] + area_cnts = [] + + # remove all contours with zero area + cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] + + for i in range(len(cnt)): + area_cnt = cv.contourArea(cnt[i]) + area_cnts.append(area_cnt) + area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] + area_diff.append(abs((area_rect - area_cnt)/area_cnt)) + + if len(area_diff) >= 2: + largest_area_idx = [area_cnts.index(sorted(area_cnts, reverse=True)[i]) for i in range(min(3, len(cnt)))] + area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) + min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) + + (x1, y1, w1, h1) = cv.boundingRect(cnt[min_i1]) + (x2, y2, w2, h2) = cv.boundingRect(cnt[min_i2]) + cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) + cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) + + # drawing center dot + center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + gate_center = self.get_actual_center(center_x, center_y) + cv.circle(debug_filter, gate_center, 5, (0,0,255), -1) + + if debug: + return (self.output_class(gate_center[0], gate_center[1]), debug_filter) + return self.output_class(gate_center[0], gate_center[1]) + + def get_actual_center(self, center_x, center_y): + # get starting center location, averaging over the first 2510 frames + if len(self.center_x_locs) == 0: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + + elif len(self.center_x_locs) < 25: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + center_x = int(statistics.mean(self.center_x_locs)) + center_y = int(statistics.mean(self.center_y_locs)) + + # use new center location only when it is close to the previous valid location + else: + if abs(center_x - self.center_x_locs[-1]) > 10 or \ + abs(center_y - self.center_y_locs[-1]) > 10: + center_x, center_y = self.center_x_locs[-1], self.center_y_locs[-1] + else: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + + return (center_x, center_y) + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + gate_task = GateSegmentationAlgo(0.1) + # once = False + start_time = time.time() + frame_count = 0 + paused = False + speed = 1 + while ret_tries < 50: + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.3, fy=0.3) + + + ### FUNCTION CALL, can change this + center, filtered_frame = gate_task.analyze(frame, True) + # cProfile.run("gate_task.analyze(frame, True)") + # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, + # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, + # 2.0, (0, 165, 255), 3) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + # if not once: + # print(filtered_frame) + # once = True + ret_tries = 0 + k = cv.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 + frame_count += 1 + #print(frame_count / (time.time() - start_time)) diff --git a/perception/tasks/gate/GateSegmentationAlgo2.py b/perception/tasks/gate/GateSegmentationAlgo2.py new file mode 100644 index 0000000..e9f440b --- /dev/null +++ b/perception/tasks/gate/GateSegmentationAlgo2.py @@ -0,0 +1,192 @@ +from GatePerceiver import GatePerceiver +from typing import Tuple +import sys +import os +sys.path.append(os.path.dirname(__file__)) + + +from segmentation.combinedFilter import init_combined_filter +import numpy as np +import math +import cv2 as cv +import time +import cProfile +import statistics + +class GateSegmentationAlgo(GatePerceiver): + center_x_locs, center_y_locs = [], [] + + def __init__(self): + super() + self.gate_center = self.output_class(250, 250) + self.use_optical_flow = False + self.optical_flow_c = 0.05 + + + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze + debug: Whether or not tot display intermediate images for debugging + Reurns: + (x,y) coordinate with center of gate + """ + global prvs + filtered_frame = combined_filter(frame, display_figs=False) + + max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) + lowerbound = max(0.84*max_brightness, 120) + upperbound = 255 + _,thresh = cv.threshold(filtered_frame,lowerbound, upperbound, cv.THRESH_BINARY) + debug_filter = cv.cvtColor(thresh, cv.COLOR_GRAY2BGR) + + cnt = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[-2] + + area_diff = [] + area_cnts = [] + + # remove all contours with zero area + cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] + + for i in range(len(cnt)): + area_cnt = cv.contourArea(cnt[i]) + area_cnts.append(area_cnt) + area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] + area_diff.append(abs((area_rect - area_cnt)/area_cnt)) + + if len(area_diff) >= 2: + largest_area_idx = [area_cnts.index(sorted(area_cnts, reverse=True)[i]) for i in range(min(3, len(cnt)))] + area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) + min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) + + rect1 = cv.boundingRect(cnt[min_i1]) + rect2 = cv.boundingRect(cnt[min_i2]) + x1, y1, w1, h1 = rect1 + x2, y2, w2, h2 = rect2 + cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) + cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) + + # # drawing center dot + # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + # self.gate_center = self.get_actual_center(center_x, center_y) + # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + + # dense optical flow + # next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + # flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) + # mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) + # mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) + # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + # if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ + # (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): + # self.gate_center = self.get_actual_center(center_x, center_y) + # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + # self.use_optical_flow = False + # else: + # self.use_optical_flow = True + # self.gate_center = (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag) * math.cos(np.mean(ang))), \ + # int(self.gate_center[1] + self.optical_flow_c * np.mean(mag) * math.sin(np.mean(ang)))) + # cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) + self.gate_center = self.get_center(rect1, rect2, frame) + if self.use_optical_flow: + cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) + else: + cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + # ang = ang*180/np.pi + # print('mag:', np.mean(mag), '\tang:', np.mean(ang)) + # hsv[...,0] = ang + # hsv[...,2] = mag + # bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) + # prvs = next + if debug: + return (self.output_class(self.gate_center[0], self.gate_center[1]), debug_filter) + return self.output_class(self.gate_center[0], self.gate_center[1]) + + def center_without_optical_flow(self, center_x, center_y): + # get starting center location, averaging over the first 2510 frames + if len(self.center_x_locs) == 0: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + + elif len(self.center_x_locs) < 25: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + center_x = int(statistics.mean(self.center_x_locs)) + center_y = int(statistics.mean(self.center_y_locs)) + + # use new center location only when it is close to the previous valid location + else: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + self.center_x_locs.pop(0) + self.center_y_locs.pop(0) + x_temp_avg = int(statistics.mean(self.center_x_locs)) + y_temp_avg = int(statistics.mean(self.center_y_locs)) + if math.sqrt((center_x - x_temp_avg)**2 + (center_y - y_temp_avg)**2) > 10: + center_x, center_y = int(x_temp_avg), int(y_temp_avg) + + return (center_x, center_y) + + def dense_optical_flow(self, frame, prvs): + next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) + mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) + mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) + return next, mag, ang + + def get_center(self, rect1, rect2, rame): + global prvs + x1, y1, w1, h1 = rect1 + x2, y2, w2, h2 = rect2 + center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + prvs, mag, ang = self.dense_optical_flow(frame, prvs) + if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ + (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): + self.use_optical_flow = False + return self.center_without_optical_flow(center_x, center_y) + else: + self.use_optical_flow = True + return (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag * np.cos(ang))), \ + (int(self.gate_center[1] + self.optical_flow_c * np.mean(mag * np.sin(ang))))) + + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + # once = False + start_time = time.time() + frame_count = 0 + paused = False + speed = 1 + ret, frame1 = cap.read() + frame1 = cv.resize(frame1, None, fx=0.3, fy=0.3) + prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY) + hsv = np.zeros_like(frame1) + hsv[...,1] = 255 + gate_task = GateSegmentationAlgo() + while ret_tries < 50: + for _ in range(speed): + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.3, fy=0.3) + center, filtered_frame = gate_task.analyze(frame, True) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + ret_tries = 0 + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + if key == ord('i') and speed > 1: + speed -= 1 + if key == ord('o'): + speed += 1 + else: + ret_tries += 1 + frame_count += 1 diff --git a/perception/tasks/gate/archive/detectGate.py b/perception/tasks/gate/archive/detectGate.py new file mode 100644 index 0000000..cf309ff --- /dev/null +++ b/perception/tasks/gate/archive/detectGate.py @@ -0,0 +1,198 @@ +import numpy as np +import cv2 +import argparse +import sys +from PIL import Image +import time + +video_file = 'truncated_semi_final_run.mp4' +EPSILON = 40 +OVERLAP_EPS = 40 + +class Contour: + def __init__(self, _x, _y, _w, _h, _area): + self.x = _x + self.y = _y + self.w = _w + self.h = _h + self.area = _area + + def __str__(self): + return str(self.__dict__) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + +def imgDetect(file): + if isinstance(file, str): + frame = cv2.imread(file) + else: + frame = file + + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) + gray = cv2.cvtColor(hsv, cv2.COLOR_BGR2GRAY) + + lower = np.array([29,40,36], dtype='uint8') + upper = np.array([77,80,50], dtype='uint8') + mask = cv2.inRange(hsv, lower, upper) + #filtered = cv2.bitwise_and(frame, frame, mask=mask) + blur = cv2.GaussianBlur(gray, (3,3), 3) + thresh = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\ + cv2.THRESH_BINARY, 7, 2) + #thresh = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 21, 5) + canny = cv2.Canny(blur, 20, 70) + #img, contours, hierarchy = cv2.findContours(canny.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + img = frame.copy() + myContours = getContours(canny) + sortedContours = sorted(myContours, key=lambda x: x.area) + center = (0,0) + if len(sortedContours) > 1: + a,b = sortedContours[-1], sortedContours[-2] + x,y = (a.x+b.x+a.w//2+b.w//2)//2, (a.y+b.y+a.h//2+b.h//2)//2 + center = (x,y) + #cv2.circle(img, (x,y), radius=5, color=(0,255,0), thickness=2) + #cv2.putText(img, "Center Point", (x,y-10), 2, 0.5, (0,0,0)) + # for cnt in sortedContours[:-2]: + # cv2.rectangle(img, (cnt.x, cnt.y), (cnt.x+cnt.w, cnt.y+cnt.h), (0,0,255), 2) + for cnt in sortedContours[-2:]: + cv2.rectangle(img, (cnt.x, cnt.y), (cnt.x+cnt.w, cnt.y+cnt.h), (255,0,0), 2) + cv2.imshow('Canny', canny) + if isinstance(file, str): + cv2.imshow('Frame', frame) + cv2.imshow('HSV', hsv) + cv2.imshow('Thresh', thresh) + #cv2.imshow('Mask', mask) + cv2.imshow('Canny', canny) + cv2.imshow('Output', img) + cv2.waitKey(0) + return img, center + +def videoDetect(file): + #file = "D:/Documents/College Work/AUV/vision-testing" + file + print("File name is: " + file) + vid = cv2.VideoCapture(file) + frames = 0 + FPS = 30 + avgLength = 10 + saver = cv2.VideoWriter('gateDetectionVideo.avi', cv2.VideoWriter_fourcc(*'MJPG'), 30, (640,360)) + centers = [] + while vid.isOpened(): + start = time.time() + ret, frame = vid.read() + if frame is None: + continue + h, w, d = frame.shape + frame = cv2.resize(frame, (w//2, h//2)) + img, center = imgDetect(frame) + if center != (0,0): + centers.append(center) + if len(centers) > avgLength: + centers.pop(0) + x, y = int(np.mean(np.array(centers)[:,0])), int(np.mean(np.array(centers)[:,1])) + cv2.circle(img, (x,y), radius=5, color=(0,255,0), thickness=2) + frames += 1 + # print(frames) + # if frames == 500: + # cv2.imwrite('test.png', frame) + cv2.imshow('Frame', frame) + cv2.imshow('Output', img) + #saver.write(img) + end = time.time() + if (cv2.waitKey(1) & 0xFF) == ord('q') or frames > 900: + break + vid.release() + cv2.destroyAllWindows() + +def getContours(image): + start = time.time() + # blur = cv2.bilateralFilter(cropped, 9, 17, 17) + + # edge = cv2.Canny(blur, 10, 100) + # #cv2.imwrite('cannyContour.jpg', edge) + # edge = cv2.GaussianBlur(edge, (5,5), 0.6) + #cv2.imwrite('gaussianContour.jpg', edge) + #Image.fromarray(edge).show() + + img, contours, hier = cv2.findContours(image.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + rectContour = [] + contourImg = image.copy() + limited = image.copy() + combined = image.copy() + for cn in contours: + #area = cv2.contourArea(cn) + x,y,w,h = cv2.boundingRect(cn) + area = w*h + cv2.rectangle(contourImg, (x,y), (x+w, y+h), 1, 1) + if area > 100 and area < 10000 and h>w+10: + rectContour.append(Contour(x,y,w,h, cv2.contourArea(cn))) + cv2.rectangle(limited, (x,y), (x+w, y+h), 1, 1) + + #cv2.imshow('Contours', blur) + #cv2.imwrite('allContours.jpg', contourImg) + #contourImg = Image.fromarray(contourImg) + #contourImg.show() + #cv2.imwrite('filteredContours.jpg', limited) + #limited = Image.fromarray(limited) + #limited.show() + #combinedContours = combineTouchingContours(rectContour) + #combinedContours = self.combineContours(rectContour) + #combinedContours = self.combineContours(self.combineContours(rectContour)) + combinedContours = combineContours(combineContours(combineContours(rectContour))) + + for cn in combinedContours: + x,y,w,h = cn.x, cn.y, cn.w, cn.h + cv2.rectangle(combined, (x,y), (x+w, y+h), 1, 1) + #cv2.imshow('Combined Contours', invert) + cv2.imwrite('combinedContours.jpg', combined) + #combined = Image.fromarray(combined) + #combined.show() + + #print('Find Contours Time: ', time.time() - start) + # for i in range(len(rectContour)): + # print('Contour ', i, ': ', str(rectContour[i])) + + return combinedContours + +def combineContours(contours): + newContours = [] + for cnt in contours: + add = True + for other in newContours: + if cnt != other and Intersect(cnt, other): + merged = Merge(cnt, other) + if cnt in newContours: + newContours.remove(cnt) + newContours.remove(other) + newContours.append(merged) + add = False + if add: + newContours.append(cnt) + return newContours + +def Intersect(A, B): + left = max(A.x, B.x) + top = max(A.y, B.y) + right = min(A.x + A.w, B.x + B.w) + bottom = min(A.y + A.h, B.y + B.h) + return (left <= right or abs(left-right) <= OVERLAP_EPS) and ((abs(A.y-B.y) <= EPSILON or abs(A.y+A.h-B.y-B.h) <= EPSILON)) and abs(A.y-B.y) <= EPSILON*2 and abs(A.y+A.h-B.y-B.h) <= EPSILON*2 + + +def Merge(A, B): + left = min(A.x, B.x) + top = min(A.y, B.y) + right = max(A.x + A.w, B.x + B.w) + bottom = max(A.y + A.h, B.y + B.h) + return Contour(left, top, right - left, bottom - top, A.area+B.area) + + +ap = argparse.ArgumentParser() +ap.add_argument('file_name', type=str, help='File name of video or image') +ap.add_argument('--test', '-t', action='store_true') + +if __name__ == '__main__': + args = ap.parse_args() + if args.file_name.lower().endswith('.mp4'): + videoDetect(args.file_name) + else: + imgDetect(args.file_name) \ No newline at end of file diff --git a/perception/tasks/gate/archive/threshTest.py b/perception/tasks/gate/archive/threshTest.py new file mode 100644 index 0000000..0bd6c71 --- /dev/null +++ b/perception/tasks/gate/archive/threshTest.py @@ -0,0 +1,209 @@ +from __future__ import print_function +import cv2 as cv +import argparse +import numpy as np +#expectations +#contours closest to the last ones +#should know when we passed through the gate +""" +IMPORTANT!!!! RUN THIS WITH $ python3 threshTest.py GOPR1142.mp4 +""" +max_value = 255 +max_value_H = 360//2 +low_H = 0 +low_S = 98 +low_V = 0 +high_H = max_value_H +high_S = max_value +high_V = max_value +window_capture_name = 'Video Capture' +window_detection_name = 'Object Detection' +low_H_name = 'Low H' +low_S_name = 'Low S' +low_V_name = 'Low V' +high_H_name = 'High H' +high_S_name = 'High S' +high_V_name = 'High V' +pauseWhenFound = 0 +old_gray = None +p0 = None +heur_thresh = 200 + +# params for ShiTomasi corner detection +feature_params = dict( maxCorners = 100, + qualityLevel = 0.3, + minDistance = 7, + blockSize = 7 ) +# Parameters for lucas kanade optical flow +lk_params = dict( winSize = (15,15), + maxLevel = 2, + criteria = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03)) +# Create some random colors +color = np.random.randint(0,255,(100,3)) + +def trueFalsePause(val): + global pauseWhenFound + pauseWhenFound = val + cv.setTrackbarPos('pausing', window_capture_name, pauseWhenFound) +def on_low_H_thresh_trackbar(val): + global low_H + global high_H + low_H = val + low_H = min(high_H-1, low_H) + cv.setTrackbarPos(low_H_name, window_detection_name, low_H) +def on_high_H_thresh_trackbar(val): + global low_H + global high_H + high_H = val + high_H = max(high_H, low_H+1) + cv.setTrackbarPos(high_H_name, window_detection_name, high_H) +def on_low_S_thresh_trackbar(val): + global low_S + global high_S + low_S = val + low_S = min(high_S-1, low_S) + cv.setTrackbarPos(low_S_name, window_detection_name, low_S) +def on_high_S_thresh_trackbar(val): + global low_S + global high_S + high_S = val + high_S = max(high_S, low_S+1) + cv.setTrackbarPos(high_S_name, window_detection_name, high_S) +def on_low_V_thresh_trackbar(val): + global low_V + global high_V + low_V = val + low_V = min(high_V-1, low_V) + cv.setTrackbarPos(low_V_name, window_detection_name, low_V) +def on_high_V_thresh_trackbar(val): + global low_V + global high_V + high_V = val + high_V = max(high_V, low_V+1) + cv.setTrackbarPos(high_V_name, window_detection_name, high_V) + +def drawRects(frame, contours): + tempPts = [] + for cnt in contours: + rect = cv.minAreaRect(cnt['cont']) + boxpts = cv.boxPoints(rect) + box = np.int0(boxpts) + cv.drawContours(frame,[box],0,(0,0,255),1) + cv.drawContours(frame, [cnt['cont']],0,(0,255,0),1) + cv.drawContours(frame, [cv.convexHull(cnt['cont'])],0,(255,0,0),1) + tempPts.append(rect[0]) + cv.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) + if len(tempPts) > 1 and allLarger(heur_thresh): + global paused + if pauseWhenFound: + paused = True + avgPt = getAvgPt(midPt(tempPts[0], tempPts[1])) + cv.circle(frame, (avgPt[0], avgPt[1]), 10, (0,0,255), -1) + +def midPt(pt1, pt2): + return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) + +def getAvgPt(pt): + points.append(pt) + exes = list(map(lambda x: x[0], points)) + whys = list(map(lambda y: y[1], points)) + + if len(points) > 50: + del points[:10] + return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) + +def heuristic(contour): + rect = cv.minAreaRect(contour) + area = rect[1][0] * rect[1][1] + diff = cv.contourArea(cv.convexHull(contour)) - cv.contourArea(contour) + cent = rect[0] + dist = 0 + if len(likelyGate) > 1 and allLarger(heur_thresh): + cen0 = cv.minAreaRect(likelyGate[0]['cont'])[0] + dis0 = np.linalg.norm(np.array(cent) - np.array(cen0)) + cen1 = cv.minAreaRect(likelyGate[1]['cont'])[0] + dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) + dist = min([dis0, dis1]) + heur = area - 3 * diff - 20 * dist #only factor in dist with all heurs larger than 60 + #print(heur) + return heur + +def allLarger(thresh): + for cnt in likelyGate: + if cnt['heur'] < thresh: + return False + return True + +parser = argparse.ArgumentParser(description='Code for Thresholding Operations using inRange tutorial.') +parser.add_argument('camera', help='Camera devide number.', default=0, type=str) +args = parser.parse_args() +cap = cv.VideoCapture(args.camera) + +cv.namedWindow(window_capture_name) +cv.namedWindow(window_detection_name) + +cv.createTrackbar(low_H_name, window_detection_name , low_H, max_value_H, on_low_H_thresh_trackbar) +cv.createTrackbar(high_H_name, window_detection_name , high_H, max_value_H, on_high_H_thresh_trackbar) +cv.createTrackbar(low_S_name, window_detection_name , low_S, max_value, on_low_S_thresh_trackbar) +cv.createTrackbar(high_S_name, window_detection_name , high_S, max_value, on_high_S_thresh_trackbar) +cv.createTrackbar(low_V_name, window_detection_name , low_V, max_value, on_low_V_thresh_trackbar) +cv.createTrackbar(high_V_name, window_detection_name , high_V, max_value, on_high_V_thresh_trackbar) +cv.createTrackbar('pausing', window_capture_name, pauseWhenFound, 1, trueFalsePause) + +#cv.createTrackbar('low_canny', 'canny', low_canny, 500, lcanny) +paused = False + +likelyGate = [] +points = [] +while True: + if not paused: + ret, frame = cap.read() #reads the frame + else: + frame = untampered + if ret: + if not paused: + frame = cv.resize(frame, (0,0), fx=0.4, fy=0.4)#resizes frame so that it fits on screen + blur = cv.GaussianBlur(frame, (5, 5), 0) + frame_HSV = cv.cvtColor(blur, cv.COLOR_BGR2HSV) + #frame_gray = cv.cvtColor(blur, cv.COLOR_BGR2GRAY) + #canny = cv.Canny(frame_gray, 200, 3, True) + frame_threshold = cv.inRange(frame_HSV, (low_H, low_S, low_V), (high_H, high_S, high_V)) #low_S ideal = 98 Sets threshold in hsv + + frame_threshold = cv.bitwise_not(frame_threshold) + res = cv.bitwise_and(frame,frame, mask= frame_threshold) + res2, contours, hierarchy = cv.findContours(frame_threshold, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + + contours.sort(key=heuristic, reverse=True) #sorts the list of contours by a heuristic function, based on area, distance from previous contours + if len(contours) > 1:#two largest heuristics are assumed to be the two gate posts + heur0 = heuristic(contours[0]) + heur1 = heuristic(contours[1]) + likelyGate = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] + + """ + ### This is very crude adaptive thresholding + totArea = 0 + for cnt in contours: + totArea += cv.contourArea(cnt) + if totArea > 19000: + low_S -= .5 + if low_S < 250: + low_S += .2 + ### + """ + + untampered = np.copy(frame) + #cv.putText(frame, str(low_S)+' '+str(totArea), (100, 100), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) + if contours: + #likelyGate.append(contours[0]) + #findLikelyGate(likelyGate, contours) + drawRects(frame, likelyGate) + cv.imshow(window_capture_name, frame) + cv.imshow(window_detection_name, frame_threshold) + + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + +#generalized problem, giving center of object contrasting with water \ No newline at end of file diff --git a/perception/tasks/gate/gateDetectionVideo.avi b/perception/tasks/gate/gateDetectionVideo.avi new file mode 100644 index 0000000000000000000000000000000000000000..b67ca42f26fcca7c8f12cc79a4b2e6b61847e02a GIT binary patch literal 5686 zcmWIYbaT@aV_)ek_?v|WsHWvXb6mkz-S1JhQMeD zjE2By2#kinXb6mkz-S1JhQMeDjEE2bjSsK@2OjcMa&v$v np.ndarray: + """ Assumes frame is grayscale + + Args: + frame: The frame to analyze + num_contours: The number of contours to return, sorted + by area from largest to smallest. + Returns: + frame/threshed: thresholded image or original image depending + on if num_contours specified. + """ + + frame = np.array(frame, np.uint8) + + img, contours, hierarchy = cv2.findContours( + frame, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE + ) + if contours is not None: + contours.sort(key=lambda c: cv2.contourArea(c), reverse=True) + contours = contours[:num_contours] + + threshed = np.zeros(frame.shape, np.uint8) + cv2.fillPoly(threshed, contours, 255) + return threshed + else: + return frame + + +def find_path_marker(frame, draw_figs=False, thresh=0.3): + """ Assumes frame is grayscale + Returns angle of bottom line and top line relative to 0 radians + This function doesn't guarantee that the angles are distinct + Returns None if no good lines are found """ + + def line_length(line): + x0, y0, x1, y1 = line[0] + return (x0 - x1) ** 2 + (y0 - y1) ** 2 + + frame = thresh_by_contour_size(frame, num_contours=2) + + # gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + edges = cv2.Canny(frame, 100, 150) + + # Find Hough lines + # Source: https://stackoverflow.com/questions/45322630/how-to-detect-lines-in-opencv + + rho = 1 # distance resolution in pixels of the Hough grid + theta = np.pi / 180 # angular resolution in radians of the Hough grid + threshold = 5 # minimum number of votes (intersections in Hough grid cell) + min_line_length = 20 # minimum number of pixels making up a line + max_line_gap = 2 # maximum gap in pixels between connectable line segments + + # Run Hough on edge detected image + # Output "lines" is an array containing endpoints of detected line segments + lines = cv2.HoughLinesP( + edges, rho, theta, threshold, np.array([]), min_line_length, max_line_gap + ) + # lines[0] looks like [[x1, y1, x2, y2]] + # where (x1, y1) is the first end point and (x2, y2) is the second end point + + if lines is not None: + lines = lines.tolist() + lines.sort(key=line_length, reverse=True) + lines = lines[:10] + + # if line's y0 is below the average y0, it is a part of the top line, opposite for bottom line + avgy = sum([l[0][1] + l[0][3] for l in lines]) // (len(lines) * 2) + bot_lines = [l for l in lines if min(l[0][1], l[0][3]) > avgy] + top_lines = [l for l in lines if max(l[0][1], l[0][3]) < avgy] + + if len(bot_lines) > 0 and len(top_lines) > 0: + bot_angle = sum( + [np.arctan2(l[0][1] - l[0][3], l[0][0] - l[0][2]) for l in bot_lines] + ) / len(bot_lines) + top_angle = sum( + [np.arctan2(l[0][1] - l[0][3], l[0][0] - l[0][2]) for l in top_lines] + ) / len(top_lines) + + if bot_angle - top_angle > np.pi: + diff = (np.pi * 5 / 4 - (bot_angle - top_angle)) / 2 + else: + diff = (np.pi * 3 / 4 - (bot_angle - top_angle)) / 2 + + # abort if the detected marker segments are not close to 135 degrees apart at all + if diff > thresh: + if draw_figs: + cv2.imshow('lines bottom', frame) + cv2.imshow('lines top', frame) + cv2.imshow('frame with path marker angles', frame) + return None + + # # otherwise, make the two segments 135 degrees apart + # # Oh this might increase the weights given to bad data. meh + # bot_angle += diff + # top_angle -= diff + + if draw_figs: + line_image_bot = frame.copy() + line_image_top = frame.copy() + for l in bot_lines: + cv2.line(line_image_bot, tuple(l[0][0:2]), tuple(l[0][2:4]), 255, 5) + for l in top_lines: + cv2.line(line_image_top, tuple(l[0][0:2]), tuple(l[0][2:4]), 255, 5) + + cv2.imshow('lines bottom', line_image_bot) + cv2.imshow('lines top', line_image_top) + + cv2.imshow( + 'frame with path marker angles', + draw_marker_angles(frame, (bot_angle, top_angle)), + ) + + return bot_angle, top_angle + else: + if draw_figs: + cv2.imshow('lines bottom', frame) + cv2.imshow('lines top', frame) + cv2.imshow('frame with path marker angles', frame) + + return None + + +def path_marker_get_new_heading(cap, is_approaching, draw_figs=False): + """ Returns the next heading for the sub based on the path marker. + (heading is positive for a counterclockwise turn) + Takes an average of 10 frames. + @param cap a VideoCapture device, for example an .mp4 or a camera stream + @param is_approaching True: sub still wants to orient itself as it approaches + the path marker. Returns the angle for the bottom leg of the + path marker. + False: sub wants to orient itself towards wherever the path marker + points towards. Returns angle for the top leg of the path marker. + """ + angles = [] + test_angles = np.empty((10, 2)) # temporary. for testing porpoises + + # function aborts if the 10 most recent camera frames were invalid + ret_tries = 0 + frames_used = 0 + + while frames_used < 10 and ret_tries < 10: + ret, frame = cap.read() + if ret: + ret_tries = 0 + frames_used += 1 + frame = cv2.resize(frame, None, fx=0.5, fy=0.5) + + threshed = combined_filter(frame, False) + new_angles = find_path_marker(threshed, draw_figs) + + if new_angles is not None: + bot_angle, top_angle = new_angles + + if is_approaching: + angles.append(np.pi / 2 - bot_angle) + test_angles[len(angles) - 1] = (bot_angle, top_angle) + else: + # top_angle is always negative so compare it to + # -np.pi/2 + angles.append(-np.pi / 2 - top_angle) + test_angles[len(angles) - 1] = (bot_angle, top_angle) + if draw_figs: + cv2.waitKey(50) + else: + ret_tries += 1 + + if ( + ret_tries >= 10 or len(angles) < 3 + ): # does not return an answer if it was really unsure + return None + else: + if draw_figs: + cv2.imshow( + 'averaged angles', + draw_marker_angles( + frame, test_angles[: len(angles)].sum(axis=0) / len(angles) + ), + ) + return sum(angles) / len(angles) + + +def draw_marker_angles(frame, marker_angles, right=False): + """ Draws lines with the same angles as those in marker_angles off to + the side of the frame """ + + line_image = frame.copy() + bot_angle, top_angle = marker_angles + + h, w = frame.shape[:2] + if right: + x, y = w * 0.25, h * 0.5 + else: + x, y = w * 0.75, h * 0.5 + r = 20 + pt_mid = (int(x), int(y)) + pt_bot = (int(x + r * np.cos(bot_angle)), int(y + r * np.sin(bot_angle))) + pt_top = (int(x + r * np.cos(top_angle)), int(y + r * np.sin(top_angle))) + if frame.shape[2] == 1: + line_image = cv2.line(line_image, pt_mid, pt_bot, 255, 5) + line_image = cv2.line(line_image, pt_mid, pt_top, 255, 5) + else: + line_image = cv2.line(line_image, pt_mid, pt_bot, (255, 255, 255), 5) + line_image = cv2.line(line_image, pt_mid, pt_top, (255, 255, 255), 5) + + return line_image + + +########################################### +# Main Body +########################################### + +if __name__ == "__main__": + marker_angles = None + + # # For testing purposes + # # Thresholding is really bad if this starts at 650 + # for _ in range(600): + # cap.read() + + combined_filter = init_combined_filter() + + num_fails = 0 + while num_fails < 5: + # Instead of passing in cap, we can alternatively pass in the last 10 frames as a list + # but idk which is the better design choice + new_heading = path_marker_get_new_heading( + cap, is_approaching=True, draw_figs=True + ) + print('new heading:', new_heading) + + if new_heading is None: + num_fails += 1 + else: + num_fails = 0 + + k = cv2.waitKey(60) & 0xFF + if k == 27: # esc + break + + cv2.destroyAllWindows() + cap.release() diff --git a/perception/tasks/path_marker/play_slots_detection.py b/perception/tasks/path_marker/play_slots_detection.py new file mode 100644 index 0000000..0ebb4b5 --- /dev/null +++ b/perception/tasks/path_marker/play_slots_detection.py @@ -0,0 +1,234 @@ +import numpy as np +import cv2 +import sys + +#### TODO: maybe look into pattern matching + +# Data fron the new course footage dropbox folder +cap = cv2.VideoCapture('../data/course_footage/play_slots_GOPR1142.mp4') + +detecting = True # Use hsv thresholding and rectangle detection. Always True +tracking = False # Use trackers to try to accomodate for failure +tracker_num = 5 # The tracker to use +testing = False # Show hsv sliders and threshold image. + +detection_interval = 10 # If tracking +fail_thresh = 5 # Number of detection failures before rectangle disappears from output +close_thresh = 30 # Pixel threshold used to reject dissimilar consecutive detection results +slots_size_thresh = 50 # Minimum area of detected slots slot +slots_dimension_thresh = 0.3 # Slot result's maximum % deviation from expected width height ratio + +# HSV threshold values. Can be changed during runtime if testing +# Use python3 play_slots_detection test to open testing mode +h_low = 96 +s_low = 82 +v_low = 131 +h_hi = 190 +s_hi = 180 +v_hi = 228 + +def nothing(x): + """Helper method for the trackbar""" + pass + +def test_hsv_thresholds(frame, has_input=False,h_low=None,s_low=None,v_low=None,h_hi=None,s_hi=None,v_hi=None): + hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) + + if has_input: + cv2.setTrackbarPos('h_low', 'contours', h_low) + cv2.setTrackbarPos('s_low', 'contours', s_low) + cv2.setTrackbarPos('v_low', 'contours', v_low) + cv2.setTrackbarPos('h_high', 'contours', h_hi) + cv2.setTrackbarPos('s_high', 'contours', s_hi) + cv2.setTrackbarPos('v_high', 'contours', v_hi) + + h_low = cv2.getTrackbarPos('h_low','contours') + s_low = cv2.getTrackbarPos('s_low','contours') + v_low = cv2.getTrackbarPos('v_low','contours') + h_hi = cv2.getTrackbarPos('h_high','contours') + s_hi = cv2.getTrackbarPos('s_high','contours') + v_hi = cv2.getTrackbarPos('v_high','contours') + + mask = cv2.inRange(hsv, np.array([h_low,s_low,v_low]), np.array([h_hi,s_hi,v_hi])) + res = cv2.bitwise_and(frame,frame, mask= mask) + + cv2.imshow('contours', res) + + return h_low, s_low, v_low, h_hi, s_hi, v_hi + +def hsv_threshold(frame, _h_low, s_low, v_low, h_hi, s_hi, v_hi, tries=0): + global h_low + h_low = _h_low + hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) + mask = cv2.inRange(hsv, np.array([h_low,s_low,v_low]), np.array([h_hi,s_hi,v_hi])) + res = cv2.bitwise_and(frame,frame, mask= mask) + + # Threshold depend on whether the sub is close to or far from the target + if tries < 3: + if np.count_nonzero(res) > res.shape[0]*res.shape[1] * 0.02: + # narrow the threshold and retry + h_low += 1 + res = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi, tries+1) + if np.count_nonzero(res) < res.shape[0]*res.shape[1] * 0.005: + # widen the threshold and retry + h_low -= 1 + res = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi, tries+1) + return res + +def filter_for_rectangles(contours): + rects = [] + for c in contours: + peri = cv2.arcLength(c, True) + approx = cv2.approxPolyDP(c, 0.1 * peri, True) + if len(approx) == 4 or len(approx) == 8: + rects.append(c) + return rects + +def find_red_slots_hole(frame, size_thresh, dimension_thresh): + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + edges = cv2.Canny(gray, 100, 150) + + im, contours, hierarchy = cv2.findContours(edges,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE) + + def get_area(rect): + return rect[1][0] * rect[1][1] + def wh_ratio(rect): + return max(rect[1])/min(rect[1]) + def dim_ratio(rect, reference): + return abs(reference-wh_ratio(rect))/reference + def is_open(h): + return h[2] < 0 + + # contours = filter_for_rectangles(contours) # Makes it worse right now lol + + # Take the first few contours and find the one that fits the dimensions the best + # The play slots rectangle is square + contours = [cv2.minAreaRect(c) for c in contours] + contours = [c for c in contours if get_area(c) > size_thresh] + + if len(contours) > 0: + contours = [c for c,h in zip(contours, hierarchy[0]) if dim_ratio(c,1/1) 0: + return contours[0] + else: + return None + +def close_to(rect1, rect2, threshold): + ### returns whether rect1 is close to rect2 based on threshold + if rect1 is None: + return True + dx, dy = rect1[0][0]-rect2[0][0], rect1[0][1]-rect2[0][1]; + return dx**2 + dy**2 < threshold * threshold + +def init_tracker(tracker_num): + # tracker_types: ['BOOSTING', 'MIL','KCF', 'TLD', 'MEDIANFLOW', 'GOTURN', 'MOSSE', 'CSRT'] + if tracker_num == 1: + tracker = cv2.TrackerBoosting_create() + elif tracker_num == 2: + tracker = cv2.TrackerMIL_create() + elif tracker_num == 3: + tracker = cv2.TrackerKCF_create() + elif tracker_num == 4: + tracker = cv2.TrackerTLD_create() + elif tracker_num == 5: + tracker = cv2.TrackerMedianFlow_create() + elif tracker_num == 6: + tracker = cv2.TrackerGOTURN_create() + elif tracker_num == 7: + tracker = cv2.TrackerMOSSE_create() + elif tracker_num == 8: + tracker = cv2.TrackerCSRT_create() + else: + print("Invalid tracker number") + exit() + return tracker + +########################################### +# Main Body +########################################### + +if __name__ == "__main__": + if len(sys.argv) > 0: + if "test" in sys.argv: + testing = True + cv2.namedWindow('contours') + cv2.createTrackbar('h_low','contours',h_low,255,nothing) + cv2.createTrackbar('s_low','contours',s_low,255,nothing) + cv2.createTrackbar('v_low','contours',v_low,255,nothing) + cv2.createTrackbar('h_high','contours',h_hi,255,nothing) + cv2.createTrackbar('s_high','contours',s_hi,255,nothing) + cv2.createTrackbar('v_high','contours',v_hi,255,nothing) + + tracker = init_tracker(tracker_num) + slots_hole = None + num_failures = 0 + time_since_detection = 0 + + # # For testing purposes + # for _ in range(500): + # cap.read() + + while(1): + ret ,frame = cap.read() + + if ret == True: + frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5) + + if testing: + h_low, s_low, v_low, h_hi, s_hi, v_hi = test_hsv_thresholds(frame,True,h_low,s_low,v_low,h_hi,s_hi,v_hi) + + if time_since_detection >= detection_interval: + detecting = True + + if detecting and tracking: + hsv_thresh = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi) + new_slots_hole = find_red_slots_hole(hsv_thresh, slots_size_thresh, slots_dimension_thresh) + if new_slots_hole is not None and close_to(slots_hole, new_slots_hole, close_thresh): + num_failures = 0 + slots_hole = new_slots_hole + tracker = init_tracker(tracker_num)# does this have to happen every time? + tracker.init(frame, slots_hole[0]+slots_hole[1]) + detecting = False + time_since_detection = 0 + else: + num_failures += 1 + elif detecting and not tracking: + hsv_thresh = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi) + new_slots_hole = find_red_slots_hole(hsv_thresh, slots_size_thresh, slots_dimension_thresh) + if new_slots_hole is not None and close_to(slots_hole, new_slots_hole, close_thresh): + num_failures = 0 + slots_hole = new_slots_hole + else: + num_failures += 1 + elif tracking: + ret, bounding_box = tracker.update(frame) + if ret: + num_failures = 0 + slots_hole = (bounding_box[:2], bounding_box[2:4], slots_hole[2]) + else: + num_failures += 1 + if num_failures > fail_thresh: + slots_hole = None + detecting = True + + # draw slots hole onto original image + slots_img = frame.copy() + if slots_hole != None: + box = np.int0(cv2.boxPoints(slots_hole)) + slots_img = cv2.drawContours(slots_img, [box], 0, (0,255,0), 2) + cv2.imshow("slots hole", slots_img) + + + time_since_detection += 1 + k = cv2.waitKey(60) & 0xff + if k == 27: + if testing: + print("hsv thresholds:") + print(h_low, s_low, v_low, h_hi, s_hi, v_hi) + break + + cv2.destroyAllWindows() + cap.release() diff --git a/perception/tasks/sanity_test.py b/perception/tasks/sanity_test.py new file mode 100644 index 0000000..e3d07a3 --- /dev/null +++ b/perception/tasks/sanity_test.py @@ -0,0 +1,65 @@ +import multiprocessing +import pytest + +def sanity_test(algorithm, test_imgs): + """ + Runs a sanity test on the algorithm that checks for run time and general exceptions. + + Args: + algorithm: object that extends TaskPerceiver + + Example usage: + ##### In Algorithm1.py ##### + class Algorithm1(TaskPerceiver): + def analyze(self, frame, debug): + pass + + ##### In test_Algorithm1.py ##### + from sanity_test import sanity_test + import pytest + + # Some function that returns test images. This is scoped to this file/module. + def get_test_imgs(): + return [None, None, None] + + @pytest.mark.parametrize("algorithm", [Algorithm1()]) + @pytest.mark.parametrize("test_imgs", [get_test_imgs()]) + def test_sanity(algorithm, test_imgs): + sanity_test(algorithm, test_imgs) + """ + + MAX_RUNTIME = 3 # Per call to analyze() in seconds + NUM_RUNS = 3 # Number of calls to analyze() + + if len(test_imgs) < NUM_RUNS: + pytest.fail("Received less than {} test images".format(NUM_RUNS)) + + # Run analyze() NUM_RUNS times and be ready to stop it if it takes too long + pconn, cconn = multiprocessing.Pipe() + p = multiprocessing.Process(target=lambda: run_algorithm(algorithm, test_imgs, NUM_RUNS, cconn)) + p.start() + + # Wait for MAX_RUNTIME * NUM_RUNS seconds or until the process finishes + p.join(MAX_RUNTIME * NUM_RUNS) + + # If thread is still active, it took too long to finish + if p.is_alive(): + p.terminate() + p.join() + pytest.fail("analyze() took over {} seconds with {} iterations." + .format(MAX_RUNTIME * NUM_RUNS, NUM_RUNS)) + # Check for exceptions + if pconn.poll(): + error = pconn.recv() + pytest.fail("analyze() encountered exception '{}' with test image {}".format(error[0], error[1])) + +def run_algorithm(algorithm, test_imgs, num_runs, cconn): + """ Wrapper function to run the algorithm on a separate thread. """ + for i in range(num_runs): + try: + algorithm.analyze(test_imgs[i], True) + except Exception as e: + # Send the error message and the image number back to the main thread + cconn.send((e, i)) + break + \ No newline at end of file diff --git a/perception/tasks/segmentation/GateTaskExample.py.orig b/perception/tasks/segmentation/GateTaskExample.py.orig new file mode 100644 index 0000000..5563092 --- /dev/null +++ b/perception/tasks/segmentation/GateTaskExample.py.orig @@ -0,0 +1,86 @@ +from TaskPerceiver import TaskPerceiver +from typing import Tuple +from sys import argv as args +from combinedFilter import init_combined_filter +import numpy as np +import cv2 as cv +#from segmentation.aggregateRescaling import init_aggregate_rescaling + +class GateTask(TaskPerceiver): + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze + debug: Whether or not tot display intermediate images for debugging + + Returns: + (x,y) coordinate with center of gate + """ +<<<<<<< HEAD + filtered_frame_copies = [filtered_frame for _ in range[10]] + np.stack(filtered_frame_copies, axis = -1) + mask = cv.inRange(filtered_frame, np.array[190], ) + + filtered_frame = combined_filter(frame, display_figs=False) + if debug: + return ((250, 250), filtered_frame) +======= + filtered_frame = combined_filter(frame, display_figs=False) + filtered_frame_copies = [filtered_frame for _ in range(3)] + stacked_filter_frames = np.concatenate(filtered_frame_copies, axis = 2) + mask = cv.inRange(stacked_filter_frames, + np.array([100, 100, 100]), np.array([255, 255, 255])) + _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + if contours: + cnt = max(contours, key=self.findStraightness)#lambda x: cv.minAreaRect(x)[1][0] * cv.minAreaRect(x)[1][1]) + # sorted_straight = sorted(contours, key=self.findStraightness) + # sorted_size = sorted(contours, key=cv.contourArea) + #todo: use these sorted lists and weights to each value to give two best values + rect = cv.minAreaRect(cnt) + boxpts = cv.boxPoints(rect) + box = np.int0(boxpts) + cv.drawContours(stacked_filter_frames,[box],0,(0,0,255),5) + for corner in boxpts: + cv.circle(stacked_filter_frames, (corner[0], corner[1]), 10, (0,0,255), -1) + + if debug: + return ((250, 250), stacked_filter_frames) +>>>>>>> origin/gate-task-example + return (250, 250) + + def findStraightness(self, contour): # output number = contour area/convex area, the bigger the straightest + hull = cv.convexHull(contour, False) + contour_area = cv.contourArea(contour) + hull_area = cv.contourArea(hull) + return 10 * contour_area + 3 * (hull_area - contour_area) + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(args[1]) + ret_tries = True + gate_task = GateTask() + once = False + while 1 and ret_tries < 50: + ret, frame = cap.read() + if ret: + frame = cv.resize(frame, None, fx=0.4, fy=0.4) + + + ### FUNCTION CALL, can change this + (x, y), filtered_frame = gate_task.analyze(frame, True) + cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, + (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, + 2.0, (0, 165, 255), 3) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + if not once: + print(filtered_frame) + once = True + ret_tries = 0 + k = cv.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 diff --git a/perception/tasks/segmentation/aggregateRescaling.py b/perception/tasks/segmentation/aggregateRescaling.py new file mode 100644 index 0000000..3e2d9ea --- /dev/null +++ b/perception/tasks/segmentation/aggregateRescaling.py @@ -0,0 +1,80 @@ +import cv2 as cv +from sys import argv as args +import numpy as np +import numpy.linalg as LA + +# Jenny -> unsigned ints fixed the problem +# Damas -> flip weight vector every frame + +# man/min of past ten frames; average or total +def init_aggregate_rescaling(show_frame=True): + only_once = False + weights = [] + max_min = {'max': 90, 'min': -20} + + def aggregate_rescaling(frame): # you only pca once + nonlocal only_once + nonlocal weights + nonlocal max_min + + frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + + r, c, d = frame.shape + A = np.reshape(frame, (r * c, d)) + + if not only_once: + + A_dot = A - A.mean(axis=0)[np.newaxis, :] + + _, eigv = LA.eigh(A_dot.T @ A_dot) + weights = eigv[:, 0] + + red = np.reshape(A_dot @ weights, (r, c)) + only_once = True + else: + red = np.reshape(A @ weights, (r, c)) + + if np.min(red) < max_min['min']: + max_min['min'] = np.min(red) + if np.max(red) > max_min['max']: + max_min['max'] = np.max(red) + + red -= max_min['min'] + red *= 255.0 / (max_min['max'] - max_min['min']) + """ + if False:#not paused: + print(np.min(red), np.max(red), max_min['min'], max_min['max']) + """ + red = red.astype(np.uint8) + red = np.expand_dims(red, axis=2) + red = np.concatenate((red, red, red), axis=2) + + if show_frame: + cv.imshow('frame', frame_gray) + cv.imshow('One Time PCA plus all time aggregate rescaling', red) + return red + + return aggregate_rescaling + + +paused = False +speed = 1 +if __name__ == '__main__': + cap = cv.VideoCapture(args[1]) + agg_res = init_aggregate_rescaling() + while True: + if not paused: + for _ in range(speed): + ret, frame = cap.read() + if ret: + frame = cv.resize(frame, (0, 0), fx=0.5, fy=0.5) + agg_res(frame) + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + if key == ord('i') and speed > 1: + speed -= 1 + if key == ord('o'): + speed += 1 diff --git a/perception/tasks/segmentation/combinedFilter.py b/perception/tasks/segmentation/combinedFilter.py new file mode 100644 index 0000000..6338a05 --- /dev/null +++ b/perception/tasks/segmentation/combinedFilter.py @@ -0,0 +1,58 @@ +import cv2 +import numpy as np + +import sys +import os +from sys import argv as args +sys.path.append(os.path.dirname(__file__)) +from aggregateRescaling import init_aggregate_rescaling +from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim + +if __name__ == "__main__": + if args[1] == '0': + cap = cv2.VideoCapture(0) + else: + cap = cv2.VideoCapture(args[1]) + +# Returns a grayscale image +def init_combined_filter(): + aggregate_rescaling = init_aggregate_rescaling(False) + + def combined_filter(frame, custom_weights=None, display_figs=False, print_weights=False): + pca_frame = aggregate_rescaling(frame) # this resizes the frame within its body + + __, other_frame = filter_out_highest_peak_multidim( + np.dstack([pca_frame[:,:,0], frame]), + custom_weights=custom_weights, + print_weights=print_weights) + + other_frame = other_frame[:,:,:1] + + if display_figs: + cv2.imshow('original', frame) + cv2.imshow('Aggregate Rescaling via PCA', pca_frame) + cv2.imshow('Peak Removal Thresholding after PCA', other_frame) + return other_frame + return combined_filter + +if __name__ == "__main__": + ret = True + ret_tries = 0 + + # for i in range(3000): + # cap.read() + + combined_filter = init_combined_filter() + + while 1 and ret_tries < 50: + ret, frame = cap.read() + if ret: + frame = cv2.resize(frame, None, fx=0.4, fy=0.4) + filtered_frame = combined_filter(frame, display_figs=True) + + ret_tries = 0 + k = cv2.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 diff --git a/perception/tasks/segmentation/peak_removal_adaptive_thresholding.py b/perception/tasks/segmentation/peak_removal_adaptive_thresholding.py new file mode 100644 index 0000000..892dc02 --- /dev/null +++ b/perception/tasks/segmentation/peak_removal_adaptive_thresholding.py @@ -0,0 +1,570 @@ +import cv2 +import numpy as np +import matplotlib.pyplot as plt +from scipy.signal import find_peaks, peak_widths +from sys import argv as args +######################################################################## +# An attempt at an adaptive thresholding algorithm based on the frequency +# of pixel values ("peaks" if looking at a histogram of # pixels vs pixel value of a frame) +# +# *1. *** best of the three *** filter_out_highest_peak_multidim +# pools together how "peak-like" each pixel is in all of the color channels +# of the frame to make a final decision on what is the background +# 2. init_filter_out_highest_peak +# gets rid of large peaks in many different color channels individually +# 3. remove_blotchy_chunks +# places a mask over areas that have lots of edges, which in many cases +# is equivalent to places with lots of noise +######################################################################## + +if __name__ == "__main__": + testing = False + + cap = cv2.VideoCapture('../data/course_footage/GOPR1142.MP4') + # No thresholds + h_low = 0 + s_low = 0 + v_low = 0 + h_hi = 255 + s_hi = 255 + v_hi = 255 + + # cap = cv2.VideoCapture('../data/course_footage/path_marker_GOPR1142.mp4') + # # Path marker default + # h_low = 31 + # s_low = 28 + # v_low = 179 + # h_hi = 79 + # s_hi = 88 + # v_hi = 218 + + # cap = cv2.VideoCapture('../data/course_footage/play_slots_GOPR1142.MP4') + # # Play slots default + # h_low = 96 + # s_low = 82 + # v_low = 131 + # h_hi = 190 + # s_hi = 180 + # v_hi = 228 + + # cap = cv2.VideoCapture(0) + + # thresholds_used = [h_low, s_low, v_low, h_hi, s_hi, v_hi] + +def init_test_hsv_thresholds(thresholds): + # Keep track of previous threhold values to see if the user is using the trackbar + # is there a function that detects whether the mouse button is down? + prev_h_low, prev_s_low, prev_v_low, prev_h_hi, prev_s_hi, prev_v_hi = thresholds + + def nothing(x): + """Helper method for the trackbar""" + pass + + cv2.namedWindow('ideal thresholding') + cv2.createTrackbar('h_low','ideal thresholding',h_low,255,nothing) + cv2.createTrackbar('s_low','ideal thresholding',s_low,255,nothing) + cv2.createTrackbar('v_low','ideal thresholding',v_low,255,nothing) + cv2.createTrackbar('h_high','ideal thresholding',h_hi,255,nothing) + cv2.createTrackbar('s_high','ideal thresholding',s_hi,255,nothing) + cv2.createTrackbar('v_high','ideal thresholding',v_hi,255,nothing) + + def test_hsv_thresholds(frame, thresholds): + nonlocal prev_h_low, prev_s_low, prev_v_low, prev_h_hi, prev_s_hi, prev_v_hi + + h_low_track = cv2.getTrackbarPos('h_low','ideal thresholding') + s_low_track = cv2.getTrackbarPos('s_low','ideal thresholding') + v_low_track = cv2.getTrackbarPos('v_low','ideal thresholding') + h_hi_track = cv2.getTrackbarPos('h_high','ideal thresholding') + s_hi_track = cv2.getTrackbarPos('s_high','ideal thresholding') + v_hi_track = cv2.getTrackbarPos('v_high','ideal thresholding') + + if h_low_track!=prev_h_low or s_low_track!=prev_s_low or v_low_track!=prev_v_low \ + or h_hi_track!=prev_h_hi or s_hi_track!=prev_s_hi or v_hi_track!=prev_v_hi: + # If user is adjusting the trackbars, use the user input + thresholds_used = [h_low_track, s_low_track, v_low_track, h_hi_track, s_hi_track, v_hi_track] + else: + # Otherwise, copy program data to trackbars + thresholds_used = thresholds + cv2.setTrackbarPos('h_low', 'ideal thresholding', thresholds_used[0]) + cv2.setTrackbarPos('s_low', 'ideal thresholding', thresholds_used[1]) + cv2.setTrackbarPos('v_low', 'ideal thresholding', thresholds_used[2]) + cv2.setTrackbarPos('h_high', 'ideal thresholding', thresholds_used[3]) + cv2.setTrackbarPos('s_high', 'ideal thresholding', thresholds_used[4]) + cv2.setTrackbarPos('v_high', 'ideal thresholding', thresholds_used[5]) + + hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) + mask = cv2.inRange(hsv, np.array(thresholds_used[:3]), np.array(thresholds_used[3:])) + res = cv2.bitwise_and(frame,frame, mask= mask) + + cv2.imshow('ideal thresholding', res) + + prev_h_low, prev_s_low, prev_v_low, prev_h_hi, prev_s_hi, prev_v_hi = thresholds_used + return thresholds_used + + return test_hsv_thresholds + +def hsv_threshold(frame, thresh_used): + hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) + mask = cv2.inRange(hsv, np.array(thresh_used[:3]), np.array(thresh_used[3:])) + res = cv2.bitwise_and(frame,frame, mask= mask) + return thresh_used, res + +def disp_hist(frame, title, labels, colors): + frame0 = frame[:,:,0].flatten() + frame0 = frame0[frame0 > 0] + + frame1 = frame[:,:,1].flatten() + frame1 = frame1[frame1 > 0] + + frame2 = frame[:,:,2].flatten() + frame2 = frame2[frame2 > 0] + + plt.figure(hash(title)) + plt.clf() + ax = plt.gca() + ax.set_xlim([0, 255]) + + plt.hist(frame0, alpha=0.5, label=labels[0], color=colors[0]) + plt.hist(frame1, alpha=0.5, label=labels[1], color=colors[1]) + plt.hist(frame2, alpha=0.5, label=labels[2], color=colors[2]) + plt.title(title) + plt.legend() + plt.draw() + +def find_peak_ranges(frame, display_plots=False, title=None, labels=None, colors=None): + """ Finds a returns the widest peak's x-range in all three channels of frame + Result is formatted to fit cv2.inRange() -> ((low1, low2, low3), (hi1, hi2, hi3)) + Shape of frame must have 3 dimensions (pass in np.expand_dims(frame, 2) if erroring) """ + + # TODO: Maybe use a different combination of peak characteristics to more accurately + # select the entire peak (only the tip is selected right now) + peak_width_height = 0.95 # How far down the peak that the algorithm draws + # the horizontal width line + + def find_highest_peak(channel, display_plots=False): + """ Finds and returns the x-range of the highest peak in the + given channel of frame """ + + f = frame[:, :, channel].flatten() + + # Some semi-hardcoded values :) + num_bins = max(int((np.amax(f)-np.amin(f)) / 4), 10) + # num_bins = 30 + + hist, bins = np.histogram(f, bins=num_bins) + + hist[0] = 0 # get rid of stuff that was thresholded to 0 + hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak + bins = np.hstack([bins, [bins[bins.shape[0]-1] + 1]]) + + peaks, properties = find_peaks(hist, height=0.1) + if len(peaks) > 0: + i = np.argmax(properties['peak_heights']) + widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] + # i = np.argmax(widths) + largest_peak = (int((bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]//2), + int((bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]//2)) # beginning and end of the peak + + if display_plots: + ax = plt.gca() + print(max(f)) + ax.set_xlim([0, max(255, max(f))]) + #Plot values in this channel + plt.plot(bins[1:],hist, label=labels[channel], color=colors[channel]) + # Plot peaks + plt.plot(bins[peaks+1], hist[peaks], "x") + # Plot peak widths + plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) + else: + largest_peak = (0, 0) + + return largest_peak + + if display_plots: + fig = plt.figure(hash(title)) + plt.clf() + + background = (np.empty(frame.shape[2]),np.empty(frame.shape[2])) + for channel in range(frame.shape[2]): + low, high = find_highest_peak(channel, display_plots) + background[0][channel] = low + background[1][channel] = high + + if display_plots: + plt.title(title) + plt.legend() + plt.draw() + + return background + +def plot_peaks(frame, title, labels, colors): + # Shh this is just a helper function that makes the code more readable + # Not to be used in practice. + # NOTE: you need to call plt.pause(0.001) afterwards to render the plot + find_peak_ranges(frame, True, title, labels, colors) + +def init_filter_out_highest_peak(filters, return_colorspace="any", input_colorspace="bgr"): + """ Takes in an hsv image! Returns an hsv image""" + # low pass filter + # vk* = vk*lambda + v*(1-lambda) + # lambda = 0.9-0.4 + + prev_hsv_threshes = [[] for i in range(len(filters))] + hsv_labels = (('H','S','V'), ("red","purple","gray")) + bgr_labels = (('B','G','R'), ("blue","green","red")) + + # Figure out how the procedure to convert among hsv and bgr. + # Format of stuff in fitler_fns: + # [<'c' convert or 'f' filter>, ] + filter_fns = [] + curr_color = input_colorspace + for f in filters: + if f != curr_color: + filter_fns.append(['c',f]) + curr_color = f + filter_fns.append(['f',f]) + if return_colorspace != "any" and return_colorspace != curr_color: + filter_fns.append(['c', return_colorspace]) + + def filter_out_highest_peak(frame, cache, display_plots=False, title=None, labels=None, colors=None): + + background_thresh = find_peak_ranges(frame, display_plots, title, labels, colors) + raw_thresh = background_thresh + # multiply everything in cache by (1-lpf_lambda) + if len(cache) > 0: + # cache = np.array(cache) * (1-lpf_lambda) + # calculate average + for i in range(2): + for j in range(3): + background_thresh[i][j] = (background_thresh[i][j] + sum([c[i][j] for c in cache])) // (len(cache) + 1) + + background_mask = cv2.bitwise_not(cv2.bitwise_or( + cv2.inRange(frame[:, :, 0], background_thresh[0][0], background_thresh[1][0]), + cv2.inRange(frame[:, :, 1], background_thresh[0][1], background_thresh[1][1]), + cv2.inRange(frame[:, :, 2], background_thresh[0][2], background_thresh[1][2]) + )) + no_background = cv2.bitwise_and(frame,frame, mask=background_mask) + + return background_thresh, raw_thresh, no_background + + def combine_threshes(th1, th2): + return ([min(th1[0][0], th2[0][0]), min(th1[0][1], th2[0][1]), min(th1[0][2], th2[0][2])], + [max(th1[1][0], th2[1][0]), max(th1[1][1], th2[1][1]), max(th1[1][2], th2[1][2])]) + + def bgr_thresh2hsv_thresh(th): + th = cv2.cvtColor(np.array([[th[0]], [th[1]]], np.uint8), cv2.COLOR_BGR2HSV).tolist() + return ([min(th[0][0][0], th[1][0][0]), min(th[0][0][1], th[1][0][1]), min(th[0][0][2], th[1][0][2])], + [max(th[0][0][0], th[1][0][0]), max(th[0][0][1], th[1][0][1]), max(th[0][0][2], th[1][0][2])]) + + + def do_filter(frame, display_plots=False): + nonlocal prev_hsv_threshes + if len(prev_hsv_threshes[0]) == lpf_cache_size: + for x in prev_hsv_threshes: + x.pop(0) + + filter_index = 0 + for f in filter_fns: + if f[0] == 'c': + if f[1] == "hsv": + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) + else: + # f[1] == "bgr" + frame = cv2.cvtColor(frame, cv2.COLOR_HSV2BGR) + else: + if f[1] == "hsv": + thresh, raw_thresh, frame = filter_out_highest_peak(frame, prev_hsv_threshes[filter_index]) + prev_hsv_threshes[filter_index].append(raw_thresh) + else: + # f[1] == "bgr" + thresh, raw_thresh, frame = filter_out_highest_peak(frame, prev_hsv_threshes[filter_index]) + thresh = bgr_thresh2hsv_thresh(thresh) + prev_hsv_threshes[filter_index].append(raw_thresh) + filter_index += 1 + + # Doesn't do anything :c + # frame = cv2.fastNlMeansDenoising(frame) + + # # # Post processing + # # Performs badly if there is a lot of noise or if there is no noise at all around targets + # frame = remove_blotchy_chunks(frame, iterations=1, display_imgs=True) + # cv2.imshow('after antiblotchy', frame) + + # kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5, 5)) + # frame = cv2.morphologyEx(frame, cv2.MORPH_OPEN, kernel) + # cv2.imshow('after open', frame) + + return frame + + return do_filter + +def keep_highest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, title=None, label='1', color='blue'): + """ Returns a mask for the frame that keeps the num_peaks highest peaks in the histogram of + pixel values. + Only works for grayscale/1-channel images (to speed this up) + Shape of frame must have 3 dimensions (pass in np.expand_dims(frame, 2) if erroring) """ + # Some semi-thresholded values :) + num_bins = max(int((np.amax(frame)-np.amin(frame)) / 4), 10) + hist, bins = np.histogram(frame, bins=num_bins) + hist[0] = 0 # get rid of stuff that was thresholded to 0 + hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak + bins = np.hstack([bins, [bins[bins.shape[0]-1] + 1]]) + + peaks, properties = find_peaks(hist, prominence=100) + widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] + + if len(peaks) > 0: + i = len(peaks) - 1 + mask = cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, + (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2) + + # To support keeping multiple peaks + for j in range(num_peaks - 1): + i = len(peaks) - 2 - j + if i >= 0: + mask = cv2.bitwise_or(cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i], + (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]), mask) + # frame = cv2.bitwise_and(frame, frame, mask=mask) + else: + mask = np.ones(frame.shape, np.uint8) + + if display_plots: + fig = plt.figure(hash(title)) + plt.clf() + + ax = plt.gca() + ax.set_xlim([0, 255]) + #Plot values in this channel + plt.plot(bins[1:],hist, label=label, color=color) + # Plot peaks + plt.plot(bins[peaks+1], hist[peaks], "x") + # Plot peak widths + plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) + + plt.title(title) + plt.legend() + plt.draw() + + return mask + +def delete_lowest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, title=None, label='1', color='blue'): + """ Returns a mask for the frame that deletes the num_peaks lowest-valued peaks in the histogram of + pixel values. + Only works for grayscale/1-channel images (to speed this up) """ + + # Some semi-thresholded values :) + num_bins = max(int((np.amax(frame)-np.amin(frame)) / 4), 10) + hist, bins = np.histogram(frame, bins=num_bins) + hist[0] = 0 # get rid of stuff that was thresholded to 0 + + peaks, properties = find_peaks(hist, prominence=100) + widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] + + if len(peaks) > 0: + i = 0 + mask = cv2.bitwise_not(cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, + (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2)) + + # To support deleting multiple peaks + for j in range(num_peaks - 1): + i = j + 1 + if len(peaks) > i: + mask = cv2.bitwise_and(cv2.bitwise_not(cv2.inRange( + frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, + (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2)), mask) + else: + mask = np.ones(frame.shape, np.uint8) + # frame = cv2.bitwise_and(frame, frame, mask=mask) + + if display_plots: + fig = plt.figure(hash(title)) + plt.clf() + + ax = plt.gca() + ax.set_xlim([0, 255]) + #Plot values in this channel + plt.plot(bins[1:],hist, label=label, color=color) + # Plot peaks + plt.plot(bins[peaks+1], hist[peaks], "x") + # Plot peak widths + plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) + + plt.title(title) + plt.legend() + plt.draw() + + return mask + +def remove_blotchy_chunks(frame, kernel_size=201, iterations=1, display_imgs=False): + """ Works best when object isn't surrounded by blotchy stuff """ + edges = cv2.Canny(frame, 100, 150) + + blurred = edges.copy() + for _ in range(iterations): + blurred = cv2.GaussianBlur(edges, (kernel_size, kernel_size), -1) + + ret, mask = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) + + result = cv2.bitwise_and(frame, frame, mask=mask) + + if display_imgs: + cv2.imshow('original', frame) + cv2.imshow('edges', edges) + cv2.imshow('blurred', blurred) + cv2.imshow('mask', mask) + cv2.imshow('blotchy result', result) + + return result + +def filter_out_highest_peak_multidim(frame, res=69, percentile=10, custom_weights=None, print_weights=False): + """ Estimates the "peak-ness" of each pixel in frame across color channels + and thresholds out pixels that were "peak-like" in many colorspaces. + frame is a stack of color channels (np.dstack()) and this will consider all channels + in the final calculation + + @param res Resolution. Higher number is lower resolution + @param percentile Threshold for pixels to keep in the overall_votes array + List of colorspaces that can be converted to from BGR: + https://docs.opencv.org/3.4/d8/d01/group__imgproc__color__conversions.html#gga4e0972be5de079fed4e3a10e24ef5ef0a2a80354788f54d0bed59fa44ea2fb98e + - HSV, GRAY, Lab, XYZ, YCrCb, Luv, HLS, YUV + "Theoretically", the more colorspaces you consider, the better? But noise is added """ + + from math import log + def get_peak_votes(frame): + """ Takes in a single-channel frame and returns an array that contains + the number of other pixels with the same value at every pixel """ + dist = np.bincount(frame.flatten()) + + # Set the recommended weight to (the spread of the pixel values from 0-255) + # recommended_weight = abs(np.subtract(*np.percentile(np.nonzero(dist), [75, 25])) - (255//2)) + recommended_weight = abs((np.max(np.nonzero(dist)) - np.min(np.nonzero(dist)))) + # Stretch out extremes + # recommended_weight = pow(2, recommended_weight//8) + + if res == 1: + vote_arr = dist[frame] + else: + dist = np.array([np.mean(dist[i*res:i*res+res]) for i in range(len(dist) // res + 1)]) + vote_arr = dist[frame // res] + + return recommended_weight, vote_arr + + overall_votes = np.zeros(frame.shape[:2], np.uint8) + overall_mask = np.zeros(frame.shape[:2], np.uint8) + + if print_weights: + print('------------------------', custom_weights) + for ch in range(frame.shape[2]): + weight, vote_arr = get_peak_votes(frame[:,:,ch]) + if custom_weights is not None: + weight = custom_weights[ch] + if print_weights: + print(weight) + overall_votes = overall_votes + vote_arr * weight + + # Sometimes returns no pixels + thresh = np.mean(overall_votes) - 2 * np.std(overall_votes) + + # thresh = np.percentile(overall_votes, percentile) + + # Only keep pixels that were very un-peak-like in every colorspace + overall_mask[overall_votes <= thresh] = 255 + + return overall_votes, cv2.bitwise_and(frame, frame, mask=overall_mask) + +def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): + """ Attempts to use kmeans to segment the frame into num_group features + (not including the background), denoted by a very large value in votes. + votes is an output of the filter_out_highest_peak_multidim() function + Output: frame_shape x num_groups 3D matrix. Get a group mapped to the + frame by doing groups[:,:,group_num] """ + votes = np.float32(votes).flatten() + + # Make kmeans only consider the non-background pixels + background = np.zeros(votes.shape) + background[votes>=np.percentile(votes, percentile)] = 1 + cluster_data = votes[background==0] + cluster_indexes = np.array(range(len(votes)))[background==0] + + # Do kmeans + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) + flags = cv2.KMEANS_RANDOM_CENTERS + compactness,labels,centers = cv2.kmeans(cluster_data,num_groups,None,criteria,10,flags) + + # Reconstruct the original votes array with background's label = -1 + label_arr = np.empty(votes.shape) + label_arr[background==1] = -1 + for i in range(num_groups): + label_arr[cluster_indexes[labels.flatten()==i]] = i + + unique_labels, label_counts = np.unique(label_arr, return_counts=True) + label_order = list(range(np.int0(np.amax(unique_labels)) + 2)) # something is erroring here + if len(label_counts) < num_groups + 1: + # add in a slot for the background if no background is found + label_counts = np.insert(label_counts, 0, 0) + + label_order.sort(key=lambda x: label_counts[x]) + + groups = np.empty((frame_shape[0], frame_shape[1], num_groups + 1)) + for i, l in enumerate(label_order): + group = np.zeros(votes.shape) + group[label_arr.flatten()==l - 1] = 255 + groups[:,:,i] = np.reshape(group, frame_shape[:2]) + + # for i in range(len(unique_labels)): + # cv2.imshow(str(i) + " label", groups[:,:,i]) + + return groups + +########################################### +# Main Body +########################################### + +if __name__ == "__main__": + # For testing porpoises + cap = cv2.VideoCapture(args[1]) + ret, frame = cap.read() + out = cv2.VideoWriter('out.avi',cv2.VideoWriter_fourcc('M','J','P','G'), 30.0, (int(frame.shape[1]*0.4), int(frame.shape[0]*0.4))) + + if testing: + test_hsv_thresholds = init_test_hsv_thresholds(thresholds_used) + + filter_peaks = init_filter_out_highest_peak(['hsv,', 'bgr', 'hsv'], 'hsv') + + ret_tries = 0 + + while (1 and ret_tries < 50): + ret, frame = cap.read() + + if ret: + frame = cv2.resize(frame, None, fx=0.4, fy=0.4) + + votes, multi_filter1 = filter_out_highest_peak_multidim(np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)]), custom_weights=[1, 1, 1, 1, 1, 1]) + votes, multi_filter2 = filter_out_highest_peak_multidim(np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)])) + multi_filter1 = multi_filter1[:, :, :3] + multi_filter2 = multi_filter2[:, :, :3] + + # kmeans_groups = k_means_segmentation(votes, frame.shape) + + cv2.imshow('original', frame) + # cv2.imshow('multi_filter bgr', multi_filter1) + cv2.imshow('multi_filter bgr recommended', multi_filter2) + + # for i in range(kmeans_groups.shape[2]): + # cv2.imshow('Kmeans group ' + str(i), kmeans_groups[:,:,i]) + + # For testing porpoises + # out.write(filtered2) + + # Update all of the plt charts + plt.pause(0.001) + + ret_tries = 0 + k = cv2.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 + cv2.destroyAllWindows() + cap.release() + out.release() \ No newline at end of file diff --git a/perception/tasks/spinny/spinny_wheel_detection.py b/perception/tasks/spinny/spinny_wheel_detection.py new file mode 100644 index 0000000..8f0f72f --- /dev/null +++ b/perception/tasks/spinny/spinny_wheel_detection.py @@ -0,0 +1,112 @@ +import numpy as np +import cv2 +import argparse +import sys +import time + +#CHANGE PARAMETER IN CALL TO "THRESH" FUNCTION TO CHANGE COLOR +file_name = "GOPR1145.MP4" #video file from dropbox +vid = cv2.VideoCapture(file_name) +frames = 0 +avgLength = 10 +centers = [] + +def thresh(frame, color='red'): + + blur = cv2.GaussianBlur(frame, (5, 5), 0) + hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV) + + if color == 'red': + lower = np.uint8([29,77,36]) + upper = np.uint8([130,250,255]) + mask = cv2.inRange(hsv,lower,upper) + mask = cv2.bitwise_not(mask) + elif color == 'blue': + lower = np.uint8([86,141,0]) + upper = np.uint8([106,220,168]) + mask = cv2.inRange(hsv,lower,upper) + else: + lower = np.uint8([66,208,157]) + upper = np.uint8([86,255,209]) + mask = cv2.inRange(hsv,lower,upper) + + return mask + +def heuristic(contour): + rect = cv2.minAreaRect(contour) + area = rect[1][0] * rect[1][1] + diff = cv2.contourArea(cv2.convexHull(contour)) - cv2.contourArea(contour) + cent = rect[0] + dist = 0 + if len(likelySection) > 1 and allLarger(60): + cen0 = cv2.minAreaRect(likelySection[0]['cont'])[0] + dis0 = np.linalg.norm(np.array(cent) - np.array(cen0)) + cen1 = cv2.minAreaRect(likelySection[1]['cont'])[0] + dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) + dist = min([dis0, dis1]) + heur = area - 3 * diff - 20 * dist + return heur + +def allLarger(thresh): + for cnt in likelySection: + if cnt['heur'] < thresh: + return False + return True + +def drawRects(frame, contours): + tempPts = [] + for cnt in contours: + rect = cv2.minAreaRect(cnt['cont']) + boxpts = cv2.boxPoints(rect) + box = np.int0(boxpts) + cv2.drawContours(frame,[box],0,(0,0,255),1) + cv2.drawContours(frame, [cnt['cont']],0,(0,255,0),1) + cv2.drawContours(frame, [cv2.convexHull(cnt['cont'])],0,(255,0,0),1) + tempPts.append(rect[0]) + #cv2.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) + if len(tempPts) > 1 and allLarger(60): + cv2.circle(frame, (int(tempPts[0][0]), int(tempPts[0][1])), 10, (0,0,255), -1) + cv2.circle(frame, (int(tempPts[1][0]), int(tempPts[1][1])), 10, (0,0,255), -1) +def midPt(pt1, pt2): + return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) + +def getAvgPt(pt): + points.append(pt) + exes = list(map(lambda x: x[0], points)) + whys = list(map(lambda y: y[1], points)) + + if len(points) > 50: + del points[:10] + return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) + +likelySection = [] +points = [] +while vid.isOpened(): + start = time.time() + ret, frame = vid.read() + if(ret == False): + continue + + threshed = thresh(frame,'other') + res, contours, hierarchy = cv2.findContours(threshed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + contours.sort(key=heuristic, reverse = True) + if len(contours) > 1: + c1 = contours[0] + c2 = contours[1] + heur0 = heuristic(c1) + heur1 = heuristic(c2) + likelySection = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] + untampered = np.copy(frame) + if contours: + drawRects(frame, likelySection) + cv2.imshow("Frame", frame) + cv2.imshow('Res', res) + + if (cv2.waitKey(1) & 0xFF) == ord('q') or frames > 900: + break + +vid.release() +cv2.destroyAllWindows() + + diff --git a/perception/tasks/spinny/threshslider.py b/perception/tasks/spinny/threshslider.py new file mode 100644 index 0000000..e8bf654 --- /dev/null +++ b/perception/tasks/spinny/threshslider.py @@ -0,0 +1,185 @@ +from __future__ import print_function +import cv2 as cv +import argparse +import numpy as np +#expectations +#contours closest to the last ones +#should know when we passed through the gate +""" +IMPORTANT!!!! RUN THIS WITH $ python3 threshTest.py GOPR1142.mp4 +""" +max_value = 255 +max_value_H = 360//2 +low_H = 86#49#29#0 +low_S = 141#77#0 +low_V = 0#36#0 For Small sector, increasing lower V bound reduces +high_H = 106#130#max_value_H +high_S = 217#250#max_value +high_V = 168#max_value +window_capture_name = 'Video Capture' +window_detection_name = 'Object Detection' +low_H_name = 'Low H' +low_S_name = 'Low S' +low_V_name = 'Low V' +high_H_name = 'High H' +high_S_name = 'High S' +high_V_name = 'High V' +def on_low_H_thresh_trackbar(val): + global low_H + global high_H + low_H = val + low_H = min(high_H-1, low_H) + cv.setTrackbarPos(low_H_name, window_detection_name, low_H) +def on_high_H_thresh_trackbar(val): + global low_H + global high_H + high_H = val + high_H = max(high_H, low_H+1) + cv.setTrackbarPos(high_H_name, window_detection_name, high_H) +def on_low_S_thresh_trackbar(val): + global low_S + global high_S + low_S = val + low_S = min(high_S-1, low_S) + cv.setTrackbarPos(low_S_name, window_detection_name, low_S) +def on_high_S_thresh_trackbar(val): + global low_S + global high_S + high_S = val + high_S = max(high_S, low_S+1) + cv.setTrackbarPos(high_S_name, window_detection_name, high_S) +def on_low_V_thresh_trackbar(val): + global low_V + global high_V + low_V = val + low_V = min(high_V-1, low_V) + cv.setTrackbarPos(low_V_name, window_detection_name, low_V) +def on_high_V_thresh_trackbar(val): + global low_V + global high_V + high_V = val + high_V = max(high_V, low_V+1) + cv.setTrackbarPos(high_V_name, window_detection_name, high_V) + +def drawRects(frame, contours): + tempPts = [] + for cnt in contours: + rect = cv.minAreaRect(cnt['cont']) + boxpts = cv.boxPoints(rect) + box = np.int0(boxpts) + cv.drawContours(frame,[box],0,(0,0,255),1) + cv.drawContours(frame, [cnt['cont']],0,(0,255,0),1) + cv.drawContours(frame, [cv.convexHull(cnt['cont'])],0,(255,0,0),1) + tempPts.append(rect[0]) + cv.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) + if len(tempPts) > 1 and allLarger(60): + #global paused + paused = True + avgPt = getAvgPt(midPt(tempPts[0], tempPts[1])) + cv.circle(frame, (avgPt[0], avgPt[1]), 10, (0,0,255), -1) + +def midPt(pt1, pt2): + return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) + +def getAvgPt(pt): + points.append(pt) + exes = list(map(lambda x: x[0], points)) + whys = list(map(lambda y: y[1], points)) + + if len(points) > 50: + del points[:10] + return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) + +""" +def findLikelyGate(rectList, contours): + if not (rectList and contours): + return + try: + closest = min(contours, key=lambda x: abs(cv.minAreaRect(x)[0][1] - cv.minAreaRect(rectList[0])[0][1]) + abs(1/(cv.minAreaRect(x)[0][0] - cv.minAreaRect(rectList[0])[0][0]))) + rectList.append(closest) + except: + return +""" + +def heuristic(contour): + rect = cv.minAreaRect(contour) + area = rect[1][0] * rect[1][1] + diff = cv.contourArea(cv.convexHull(contour)) - cv.contourArea(contour) + cent = rect[0] + dist = 0 + if len(likelyGate) > 1 and allLarger(60): + cen0 = cv.minAreaRect(likelyGate[0]['cont'])[0] + dis0 = np.linalg.norm(np.array(cent) - np.array(cen0)) + cen1 = cv.minAreaRect(likelyGate[1]['cont'])[0] + dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) + dist = min([dis0, dis1]) + heur = area - 3 * diff - 20 * dist #only factor in dist with all heurs larger than 60 + #print(heur) + return heur + +def allLarger(thresh): + for cnt in likelyGate: + if cnt['heur'] < thresh: + return False + return True + +parser = argparse.ArgumentParser(description='Code for Thresholding Operations using inRange tutorial.') +parser.add_argument('camera', help='Camera devide number.', default=0, type=str) +args = parser.parse_args() +cap = cv.VideoCapture(args.camera) + +cv.namedWindow(window_capture_name) +cv.namedWindow(window_detection_name) + +cv.createTrackbar(low_H_name, window_detection_name , low_H, max_value_H, on_low_H_thresh_trackbar) +cv.createTrackbar(high_H_name, window_detection_name , high_H, max_value_H, on_high_H_thresh_trackbar) +cv.createTrackbar(low_S_name, window_detection_name , low_S, max_value, on_low_S_thresh_trackbar) +cv.createTrackbar(high_S_name, window_detection_name , high_S, max_value, on_high_S_thresh_trackbar) +cv.createTrackbar(low_V_name, window_detection_name , low_V, max_value, on_low_V_thresh_trackbar) +cv.createTrackbar(high_V_name, window_detection_name , high_V, max_value, on_high_V_thresh_trackbar) + +#cv.createTrackbar('low_canny', 'canny', low_canny, 500, lcanny) +paused = False + +likelyGate = [] +points = [] +while True: + if not paused: + ret, frame = cap.read() + else: + frame = untampered + if ret: + if not paused: + frame = cv.resize(frame, (0,0), fx=0.5, fy=0.5) + blur = cv.GaussianBlur(frame, (5, 5), 0) + frame_HSV = cv.cvtColor(blur, cv.COLOR_BGR2HSV) + #frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + #canny = cv.Canny(frame_gray, 0) + frame_threshold = cv.inRange(frame_HSV, (low_H, low_S, low_V), (high_H, high_S, high_V)) #low_S ideal = 98 + + #frame_threshold = cv.bitwise_not(frame_threshold) + res = cv.bitwise_and(frame,frame, mask= frame_threshold) + res2, contours, hierarchy = cv.findContours(frame_threshold, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + + contours.sort(key=heuristic, reverse=True) + if len(contours) > 1: + heur0 = heuristic(contours[0]) + heur1 = heuristic(contours[1]) + + likelyGate = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] + untampered = np.copy(frame) + if contours: + #likelyGate.append(contours[0]) + #findLikelyGate(likelyGate, contours) + drawRects(frame, likelyGate) + cv.imshow(window_capture_name, frame) + cv.imshow(window_detection_name, frame_threshold) + #cv.imshow('canny', canny) + + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + +#generalized problem, giving center of object contrasting with water diff --git a/perception/vis/FrameWrapper.py b/perception/vis/FrameWrapper.py new file mode 100644 index 0000000..4eb6e01 --- /dev/null +++ b/perception/vis/FrameWrapper.py @@ -0,0 +1,103 @@ +import cv2 +import sys + +class FrameWrapper(): + """ + A standard interface for getting frames from images, videos, and the webcam. + TODO: ZED camera interfacing + + Example usage: + filenames = ['relative/path/img.png', './video.mp4', 'webcam'] + frames = FrameWrapper(filenames) + + # Shows img.png, then all frames in video.mp4, then frames from webcam forever + for frame in frames: + cv2.imshow('Next frame', frame) + """ + + # Keywords used to identify file types + WEBCAM = ['webcam'] + VIDEO_EXTS = ['mp4', 'avi'] + IMG_EXTS = ['jpg', 'png'] + + VIDEO_TRIES = 200 + WEBCAM_TRIES = 10 + + def __init__(self, filenames, resize=1): + self.filenames = filenames # Get this list of relative paths to files from vis + # There aren't any checks for resize==1 to improve speed b/c this expects resize != 1 + self.resize = resize + + def __iter__(self): + self.index = -1 + self.next_data = ('', None) + self.next_data_obj() + return self + + def __next__(self): + if not self.has_next: + raise StopIteration + + while self.index < len(self.filenames): + if self.next_data[0] == "v": # Video + # Try to get a frame out at most VIDEO_TRIES times. + # If it still fails, we're probably at the end of the video file + for i in range(self.VIDEO_TRIES): + ret, frame = self.next_data[1].read() + if ret: + return cv2.resize(frame, None, fx=self.resize, fy=self.resize) + else: + print("WARNING: Failed to get frame from video {}. Try {}." \ + .format(self.filenames[self.index], i), file=sys.stderr) + self.next_data_obj() + elif self.next_data[0] == "i": # Image + img = self.next_data[1] + self.next_data_obj() + if img is not None: + return cv2.resize(img, None, fx=self.resize, fy=self.resize) + else: + print("WARNING: Failed to get image {}." \ + .format(self.filenames[self.index-1]), file=sys.stderr) + else: # Webcam + # Try to get a frame out at most WEBCAM_TRIES times. + for i in range(self.WEBCAM_TRIES): + ret, frame = self.next_data[1].read() + if ret: + return cv2.resize(frame, None, fx=self.resize, fy=self.resize) + else: + print("WARNING: Failed to get frame from webcam. Try {}." \ + .format(i), file=sys.stderr) + self.next_data_obj() + + raise StopIteration + + def next_data_obj(self): + """ + Helper function for getting the next object (video, image, webcam) when + the previous one is exhausted. + """ + # Close the webcam if it was open and we don't want it anymore + if self.next_data[0] in self.WEBCAM: + self.next_data[1].release() + + # Stop if we don't have any more data + if self.index >= len(self.filenames) - 1: + self.index += 1 + self.has_next = False + return + + # Prepare the next data object + self.index += 1 + filename = self.filenames[self.index] + extension = filename[filename.rindex('.') + 1:].lower() if '.' in filename else None + + if filename in self.WEBCAM: + self.next_data = ('w', cv2.VideoCapture(0)) + elif extension in self.VIDEO_EXTS: + self.next_data = ('v', cv2.VideoCapture(filename)) + elif extension in self.IMG_EXTS: + self.next_data = ('i', cv2.imread(filename)) + else: + print("Unknown file format:", extension) + + self.has_next = True diff --git a/perception/vis/TaskPerceiver.py b/perception/vis/TaskPerceiver.py new file mode 100644 index 0000000..e8b8609 --- /dev/null +++ b/perception/vis/TaskPerceiver.py @@ -0,0 +1,31 @@ +from typing import Any, Dict, Tuple +import numpy as np + +class TaskPerceiver: + + def __init__(self, **kwargs): + """Initializes the TaskPerceiver. + Args: + kwargs: Each keyworded argument is of the form + var_name = (range, default_val), where range is the range of values + for the slider which controls this variable, and default_val is + the initial value of the slider. + """ + self.time = 0 + self.variables = kwargs + + def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]) -> Any: + """Runs the algorithm and returns the result. + Args: + frame: The frame to analyze + debug: Whether or not to display intermediate images for debugging + slider_vals: A list of names of the variables which the user should be + able to control from the Visualizer, mapped to current slider + value for that variable + Returns: + the result of the algorithm + """ + raise NotImplementedError("Need to implement with child class.") + + def var_info(self) -> Dict[str, Tuple[Tuple[int, int], int]]: + return self.variables \ No newline at end of file diff --git a/perception/vis/TestTasks/GateSegmentationAlgo.py b/perception/vis/TestTasks/GateSegmentationAlgo.py new file mode 100644 index 0000000..1c791d5 --- /dev/null +++ b/perception/vis/TestTasks/GateSegmentationAlgo.py @@ -0,0 +1,120 @@ +from TaskPerceiver import TaskPerceiver +from typing import Tuple +import sys +import os +from pathlib import Path +from collections import namedtuple +sys.path.append(str(Path(__file__).parents[2]) + '/tasks') + +from segmentation.combinedFilter import init_combined_filter +import numpy as np +import cv2 as cv +import time +import cProfile + +class GateSegmentationAlgo(TaskPerceiver): + __past_centers = [] + __ema = None + output_class = namedtuple("GateOutput", ["centerx", "centery"]) + output_type = {'centerx': np.int16, 'centery': np.int16} + + def __init__(self, alpha=0.1): + super() + self.__alpha = alpha + self.combined_filter = init_combined_filter() + + def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: + """Takes in the background removed image and returns the center between + the two gate posts. + Args: + frame: The background removed frame to analyze. + debug: Whether or not to display intermediate images for debugging. + Returns: + (x,y) coordinate with center of gate + """ + gate_center = self.output_class(250, 250) + filtered_frame = self.combined_filter(frame, display_figs=False) + filtered_frame_copies = [filtered_frame for _ in range(3)] + stacked_filter_frames = np.concatenate(filtered_frame_copies, axis=2) + mask = cv.inRange( + stacked_filter_frames, np.array([100, 100, 100]), np.array([255, 255, 255]) + ) + _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + if contours: + contours.sort(key=self.findStraightness, reverse=True) + cnts = contours[:2] + rects = [cv.minAreaRect(c) for c in cnts] + centers = [np.array(r[0]) for r in rects] + boxpts = [cv.boxPoints(r) for r in rects] + box = [np.int0(b) for b in boxpts] + for b in box: + cv.drawContours(stacked_filter_frames, [b], 0, (0, 0, 255), 5) + if len(centers) >= 2: + gate_center = (centers[0] + centers[1]) * 0.5 + if self.__ema is None: + self.__ema = gate_center + else: + self.__ema = ( + self.__alpha * gate_center + (1 - self.__alpha) * self.__ema + ) + gate_center = (int(self.__ema[0]), int(self.__ema[1])) + # if len(self.__past_centers) < 15: + # self.__past_centers += [gate_center] + # else: + # self.__past_centers.pop(0) + # self.__past_centers += [gate_center] + # gate_center = sum(self.__past_centers) / len(self.__past_centers) + # gate_center = (int(gate_center[0]), int(gate_center[1])) + cv.circle(stacked_filter_frames, gate_center, 10, (0, 255, 0), -1) + + if debug: + return ( + self.output_class(gate_center[0], gate_center[1]), + [stacked_filter_frames], + ) + return self.output_class(gate_center[0], gate_center[1]) + + def findStraightness( + self, contour + ): # output number = contour area/convex area, the bigger the straightest + hull = cv.convexHull(contour, False) + contour_area = cv.contourArea(contour) + hull_area = cv.contourArea(hull) + return 10 * contour_area - 5 * hull_area + + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + combined_filter = init_combined_filter() + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + gate_task = GateSegmentationAlgo(0.1) + # once = False + start_time = time.time() + frame_count = 0 + while ret_tries < 50: + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.4, fy=0.4) + + ### FUNCTION CALL, can change this + center, filtered_frame = gate_task.analyze(frame, True) + # cProfile.run("gate_task.analyze(frame, True)") + # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, + # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, + # 2.0, (0, 165, 255), 3) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + # if not once: + # print(filtered_frame) + # once = True + ret_tries = 0 + k = cv.waitKey(60) & 0xFF + if k == 27: + break + else: + ret_tries += 1 + frame_count += 1 + # print(frame_count / (time.time() - start_time)) diff --git a/perception/vis/TestTasks/TestAlgo.py b/perception/vis/TestTasks/TestAlgo.py new file mode 100644 index 0000000..15ca31f --- /dev/null +++ b/perception/vis/TestTasks/TestAlgo.py @@ -0,0 +1,16 @@ +from TaskPerceiver import TaskPerceiver +from typing import Dict +import sys +import os +import numpy as np +import cv2 as cv + +class TestAlgo(TaskPerceiver): + def __init__(self): + super().__init__(canny_low=((0, 255), 100), canny_high=((0, 255), 200)) + + def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]): + + return frame, [frame, cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), + cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), + cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0)] \ No newline at end of file diff --git a/perception/vis/algo_stats b/perception/vis/algo_stats new file mode 100644 index 0000000000000000000000000000000000000000..edd4cf5661573c93313bc05cc1650ad0680e24f1 GIT binary patch literal 5441 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 literal 0 HcmV?d00001 diff --git a/perception/vis/vis.py b/perception/vis/vis.py new file mode 100644 index 0000000..81f08e8 --- /dev/null +++ b/perception/vis/vis.py @@ -0,0 +1,58 @@ +import argparse +from FrameWrapper import FrameWrapper +import cv2 as cv +from window_builder import Visualizer +import cProfile as cp +import pstats + +# Parse arguments +parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') +parser.add_argument( + '--data', default='webcam', type=str +) +parser.add_argument('--algorithm', type=str) +parser.add_argument('--save_video', action='store_true') +args = parser.parse_args() + +# Get algorithm module +exec("from TestTasks.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) + +# Initialize image source +data_sources = [args.data] +data = FrameWrapper(data_sources, 0.25) + +algorithm = Algorithm() +window_builder = Visualizer(algorithm.var_info()) +video_frames = [] +# Main Loop +def main(): + for frame in data: + + state, debug_frames = algorithm.analyze( + frame, debug=True, slider_vals=window_builder.update_vars() + ) + to_show = window_builder.display(debug_frames) + cv.imshow('Debug Frames', to_show) + if args.save_video: + video_frames.append(to_show) + + key_pressed = cv.waitKey(60) & 0xFF + if key_pressed == 112: + cv.waitKey(0) # pause + if key_pressed == 113: + break # quit + + +cp.run('main()', 'algo_stats') +cv.destroyAllWindows() +p = pstats.Stats('algo_stats') +p.print_stats('analyze') + +if args.save_video: + height, width, _ = video_frames[0].shape + out = cv.VideoWriter('deb_cap.avi', cv.VideoWriter_fourcc(*'XVID'), 60, (height, width)) + for img in video_frames: + height2, width2, _ = img.shape + if (height2, width2) == (height, width): + out.write(img) + out.release() \ No newline at end of file diff --git a/perception/vis/window_builder.py b/perception/vis/window_builder.py new file mode 100644 index 0000000..3e054cb --- /dev/null +++ b/perception/vis/window_builder.py @@ -0,0 +1,56 @@ +import numpy as np +import cv2 as cv +import math +from typing import Dict, Tuple, List + +def nothing(x): + pass + +class Visualizer: + def __init__(self, vars: Dict[str, Tuple[Tuple[int, int], int]]): + self.variables = vars.keys() + cv.namedWindow('Debug Frames') + for name, info in vars.items(): + range, default_val = info + low_range, high_range = range + cv.createTrackbar(name, 'Debug Frames', low_range, high_range, nothing) + cv.setTrackbarPos(name, 'Debug Frames', default_val) + + def three_stack(self, frames: List[np.ndarray]) -> List[np.ndarray]: + newLst = [] + for frame in frames: + if len(frame.shape) == 2 or frame.shape[2] == 1: + frame = np.stack((frame, frame, frame), axis=2) + newLst.append((frame)) + return newLst + + def display(self, frames: List[np.ndarray]) -> np.ndarray: + num_frames = len(frames) + assert (num_frames > 0 and num_frames <= 9), 'Invalid number of frames!' + frames = self.three_stack(frames) + + columns = math.ceil(num_frames/math.sqrt(num_frames)) + rows = math.ceil(num_frames/columns) + frame_num = 0 + to_show = 0 + for j in range(rows): + this_row = frames[frame_num] + for i in range(columns * j + 1, columns * (j + 1)): + frame_num += 1 + if frame_num < num_frames: + to_add = frames[frame_num] + this_row = np.hstack((this_row, to_add)) + else: + this_row = np.hstack((this_row, np.zeros(frames[0].shape, dtype=np.uint8))) + if type(to_show) != int: + to_show = np.vstack((to_show, this_row)) + else: + to_show = this_row + frame_num += 1 + return to_show + + def update_vars(self) -> Dict[str, int]: + variable_values = {} + for var in self.variables: + variable_values[var] = cv.getTrackbarPos(var, 'Debug Frames') + return variable_values \ No newline at end of file From 740b20f84225afd044c6c27570c5231c80044ad3 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sat, 5 Dec 2020 22:40:40 -0800 Subject: [PATCH 08/19] deleted outdated files --- misc/camera_chessboard_calibration.py | 178 ------ misc/combinedFilTest.py | 60 -- misc/combined_filter.py | 55 -- misc/featureGray2_higher_order_fns.py | 93 --- misc/hydrophones.ipynb | 195 ------ misc/nonlinear-regression.ipynb | 153 ----- misc/optical_flow.py | 61 -- tasks/.DS_Store | Bin 6148 -> 0 bytes tasks/.vscode/launch.json | 22 - tasks/TaskPerceiver.py | 18 - tasks/cross/CrossPerceiver.py | 9 - tasks/cross/cross_detection.py | 98 --- tasks/gate/GatePerceiver.py | 9 - tasks/gate/GateSegmentationAlgo.py | 108 ---- tasks/gate/GateSegmentationAlgo1.py | 132 ---- tasks/gate/GateSegmentationAlgo2.py | 192 ------ tasks/gate/archive/detectGate.py | 198 ------ tasks/gate/archive/threshTest.py | 209 ------- tasks/gate/gateDetectionVideo.avi | Bin 5686 -> 0 bytes tasks/path_marker/PathMarkerPerceiver.py | 9 - tasks/path_marker/path_marker_detection.py | 256 -------- tasks/path_marker/play_slots_detection.py | 234 ------- tasks/sanity_test.py | 65 -- tasks/segmentation/GateTaskExample.py.orig | 86 --- tasks/segmentation/aggregateRescaling.py | 80 --- tasks/segmentation/combinedFilter.py | 58 -- .../peak_removal_adaptive_thresholding.py | 570 ------------------ tasks/spinny/spinny_wheel_detection.py | 112 ---- tasks/spinny/threshslider.py | 185 ------ vis/FrameWrapper.py | 103 ---- vis/TaskPerceiver.py | 31 - vis/TestTasks/GateSegmentationAlgo.py | 120 ---- vis/TestTasks/TestAlgo.py | 30 - vis/vis.py | 67 -- 34 files changed, 3796 deletions(-) delete mode 100644 misc/camera_chessboard_calibration.py delete mode 100644 misc/combinedFilTest.py delete mode 100644 misc/combined_filter.py delete mode 100644 misc/featureGray2_higher_order_fns.py delete mode 100644 misc/hydrophones.ipynb delete mode 100644 misc/nonlinear-regression.ipynb delete mode 100644 misc/optical_flow.py delete mode 100644 tasks/.DS_Store delete mode 100644 tasks/.vscode/launch.json delete mode 100644 tasks/TaskPerceiver.py delete mode 100644 tasks/cross/CrossPerceiver.py delete mode 100644 tasks/cross/cross_detection.py delete mode 100644 tasks/gate/GatePerceiver.py delete mode 100644 tasks/gate/GateSegmentationAlgo.py delete mode 100644 tasks/gate/GateSegmentationAlgo1.py delete mode 100644 tasks/gate/GateSegmentationAlgo2.py delete mode 100644 tasks/gate/archive/detectGate.py delete mode 100644 tasks/gate/archive/threshTest.py delete mode 100644 tasks/gate/gateDetectionVideo.avi delete mode 100644 tasks/path_marker/PathMarkerPerceiver.py delete mode 100644 tasks/path_marker/path_marker_detection.py delete mode 100644 tasks/path_marker/play_slots_detection.py delete mode 100644 tasks/sanity_test.py delete mode 100644 tasks/segmentation/GateTaskExample.py.orig delete mode 100644 tasks/segmentation/aggregateRescaling.py delete mode 100644 tasks/segmentation/combinedFilter.py delete mode 100644 tasks/segmentation/peak_removal_adaptive_thresholding.py delete mode 100644 tasks/spinny/spinny_wheel_detection.py delete mode 100644 tasks/spinny/threshslider.py delete mode 100644 vis/FrameWrapper.py delete mode 100644 vis/TaskPerceiver.py delete mode 100644 vis/TestTasks/GateSegmentationAlgo.py delete mode 100644 vis/TestTasks/TestAlgo.py delete mode 100644 vis/vis.py diff --git a/misc/camera_chessboard_calibration.py b/misc/camera_chessboard_calibration.py deleted file mode 100644 index 3df18fc..0000000 --- a/misc/camera_chessboard_calibration.py +++ /dev/null @@ -1,178 +0,0 @@ -import numpy as np -import cv2 -import glob, os -import random, string - -################################################################## -# Measures characteristics of the camera so that -# later images can be undistorted with -# cv2.undistort(), camera matrix, and distortion values. -# Undistorted lines appear straighter and aid in feature detection. -# Requires a certain number of image samples of a chessboard in a variety -# of positions and angles. -# - add more diverse images if undistort_test errors or the result is swirly -# -# It's possible to use a circle grid instead of a chessboard: -# https://docs.opencv.org/3.4/d4/d94/tutorial_camera_calibration.html -# -# Most of the code is from: -# https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_calib3d/py_calibration/py_calibration.html -################################################################## - -################################################################## -# Helper functions -################################################################## - -def isFromWebcam(cap): - return isinstance(cap, cv2.VideoCapture) -def isFromFileSystem(cap): - return isinstance(cap, list) -def get_frame(cap, index=0): - """ Returns ret, frame just like a regular cv2.VideoCapture does""" - if isFromFileSystem(cap): - if len(cap) == 0 or index >= len(cap): - return (False, None) - else: - return (True, cv2.imread(cap[index])) - elif isFromWebcam(cap): - return cap.read() - else: - return (True, cap) - -################################################################## -# Test 1: OpenCV's tutorial dataset -# source: https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_calib3d/py_calibration/py_calibration.html -################################################################## -# images = glob.glob('../data/opencv_tutorial_calibration_images/*.jpg') -# test_img = get_frame(cv2.imread('../data/opencv_tutorial_calibration_images/left02.jpg')) -# checker_rows, checker_cols = 6, 7 - -################################################################## -# Test 2: Munich Visual Odometry dataset -# source: https://vision.in.tum.de/data/datasets/mono-dataset -################################################################## -# images = glob.glob('../data/calib_narrow_checkerboard1/images/*.jpg') -# test_img = get_frame(cv2.imread('../data/sequence_47/images/00001.jpg')) -# checker_rows, checker_cols = 5, 8 - -################################################################## -# Test 3: Pictures taken by oneself -################################################################## -# # Files from my computer 1 -# images = glob.glob('iphone_chessboard_imgs/*.JPG') -# test_img = get_frame(cv2.imread('iphone_chessboard_imgs/IMG_3413.JPG')) -# checker_rows, checker_cols = 7, 7 -# # Files from my computer 2 -# images = glob.glob('*.png') -# test_img = get_frame(cv2.imread('GIKTZTK9HV.png')) -# checker_rows, checker_cols = 7, 7 -# Use webcam -images = cv2.VideoCapture(0) -test_img = get_frame(cv2.VideoCapture(0)) -checker_rows, checker_cols = 7, 7 - -def undistort_test(mtx, dist, test_img): - print('camera matrix:') - print(np.array2string(mtx, separator=', ')) - print('distortion matrix:') - print(np.array2string(dist, separator=', ')) - - ret, test_img = get_frame(test_img) - h, w = test_img.shape[:2] - newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h)) - - # undistort - dst = cv2.undistort(test_img, mtx, dist, None, newcameramtx) - - # crop the image - x,y,w,h = roi - dst = dst[y:y+h, x:x+w] - - scaling = 500/max(test_img.shape) - cv2.imshow('original', cv2.resize(test_img, None, fx=scaling, fy=scaling)) - if dst.shape[:2] != (0, 0): - cv2.imshow('undistorted', cv2.resize(dst, None, fx=scaling, fy=scaling)) - else: - print('Error: No valid undistort_test result') - cv2.waitKey(500) - print('Hit enter to quit') - input() - -def is_recording(): - print("Do you want to save the images taken in this session? (y/n)") - user = input() - if 'y' in user or 't' in user: - return True - if 'n' in user or 'f' in user: - return False - else: - return is_recording() - -######################################################################## -# Start script -######################################################################## - -if isFromWebcam(images): - print("Is recording") - recording = is_recording() -else: - print("Not recording") - recording = False - -# subpix function termination criteria -criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) -# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0) -objp = np.zeros((checker_rows*checker_cols,3), np.float32) -objp[:,:2] = np.mgrid[0:checker_cols,0:checker_rows].T.reshape(-1,2) -# Arrays to store object points and image points from all the images. -objpoints = [] # 3d point in real world space -imgpoints = [] # 2d points in image plane. - -# Gather data points -count = 0 -ret_frame = True -gray = None -while count < 40 and ret_frame: - ret_frame, img = get_frame(images, count) - if ret_frame: - gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) - - # Find the chess board corners - ret, corners = cv2.findChessboardCorners(gray, (checker_cols, checker_rows),None) - - # If found, add object points, image points (after refining them) - if ret == True: - objpoints.append(objp) - - corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria) - imgpoints.append(corners2) - - # Draw and display the corners - img_chess = np.copy(img) - cv2.drawChessboardCorners(img_chess, (checker_cols, checker_rows), corners2,ret) - - cv2.imshow('chessboard', img_chess) - if recording: - name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) - cv2.imwrite(name + '.png', img) - print(count) - cv2.waitKey(500) - else: - cv2.imshow('chessboard', img) - print(count, 'no chessboard found. skipped', images[count].split('/').pop() if isinstance(images, list) else '') - if isinstance(images, list): - os.remove('iphone_chessboard_imgs/'+images[count].split('/').pop()) - print(' -removed') - cv2.waitKey(500) - count += 1 - -if gray is not None: - # Get camera matrix and dist - ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None) - - # Test it out - undistort_test(mtx, dist, test_img) -else: - print("No images found. If you want to capture from your webcam, uncomment the 'Use webcam' block at the top") - -cv2.destroyAllWindows() diff --git a/misc/combinedFilTest.py b/misc/combinedFilTest.py deleted file mode 100644 index 0084045..0000000 --- a/misc/combinedFilTest.py +++ /dev/null @@ -1,60 +0,0 @@ -import cv2 -import numpy as np - -from sys import argv as args -from aggregateRescaling import init_aggregate_rescaling -from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim - -if __name__ == "__main__": - if args[1] == '0': - cap = cv2.VideoCapture(0) - else: - cap = cv2.VideoCapture(args[1]) - -# Returns a grayscale image -def init_combined_filter(): - aggregate_rescaling = init_aggregate_rescaling() - - def combined_filter( - frame, custom_weights=None, display_figs=False, print_weights=False - ): - pca_frame = aggregate_rescaling(frame) # this resizes the frame within its body - - __, other_frame = filter_out_highest_peak_multidim( - np.dstack([pca_frame[:, :, 0], frame]), - custom_weights=custom_weights, - print_weights=print_weights, - ) - - other_frame = other_frame[:, :, :1] - - if display_figs: - cv2.imshow('original', frame) - cv2.imshow('Aggregate Rescaling via PCA', pca_frame) - cv2.imshow('Peak Removal Thresholding after PCA', other_frame) - return other_frame - - return combined_filter - - -if __name__ == "__main__": - ret = True - ret_tries = 0 - - # for i in range(3000): - # cap.read() - - combined_filter = init_combined_filter() - - while 1 and ret_tries < 50: - ret, frame = cap.read() - if ret: - frame = cv2.resize(frame, None, fx=0.4, fy=0.4) - filtered_frame = combined_filter(frame, display_figs=True) - - ret_tries = 0 - k = cv2.waitKey(60) & 0xFF - if k == 27: - break - else: - ret_tries += 1 diff --git a/misc/combined_filter.py b/misc/combined_filter.py deleted file mode 100644 index 84bec1f..0000000 --- a/misc/combined_filter.py +++ /dev/null @@ -1,55 +0,0 @@ -import cv2 -import numpy as np - -import sys -sys.path.insert(0, '../background_removal') -from featureGray2_higher_order_fns import init_aggregate_rescaling -from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim -#from workshop import draw_rect - -#format: [video feed] -if __name__ == "__main__": - cap = cv2.VideoCapture('../data/course_footage/GOPR1142.mp4') - -# Returns a grayscale image -def init_combined_filter(): - aggregate_rescaling = init_aggregate_rescaling() - - def combined_filter(frame, custom_weights=None, display_figs=False, print_weights=False): - pca_frame = aggregate_rescaling(frame) # this resizes the frame within its body - - __, other_frame = filter_out_highest_peak_multidim( - np.dstack([pca_frame[:,:,0], frame]), - custom_weights=custom_weights, - print_weights=print_weights) - - other_frame = other_frame[:,:,:1] - - if display_figs: - cv2.imshow('original', frame) - cv2.imshow('pca thing', pca_frame) - cv2.imshow('other filter thing', other_frame) - return other_frame - return combined_filter - -if __name__ == "__main__": - ret = True - ret_tries = 0 - - # for i in range(3000): - # cap.read() - - combined_filter = init_combined_filter() - - while 1 and ret_tries < 50: - ret, frame = cap.read() - if ret: - frame = cv2.resize(frame, None, fx=0.4, fy=0.4) - filtered_frame = combined_filter(frame, display_figs=True) - - ret_tries = 0 - k = cv2.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 \ No newline at end of file diff --git a/misc/featureGray2_higher_order_fns.py b/misc/featureGray2_higher_order_fns.py deleted file mode 100644 index f7e4e47..0000000 --- a/misc/featureGray2_higher_order_fns.py +++ /dev/null @@ -1,93 +0,0 @@ -import cv2 as cv -from sys import argv as args -import numpy as np -import numpy.linalg as LA - -#Jenny -> unsigned ints fixed the problem -#Damas -> flip weight vector every frame -if __name__ == "__main__": - cap = cv.VideoCapture('../data/course_footage/path_marker_GOPR1142.mp4') -paused = False -speed = 1 -#man/min of past ten frames; average or total -def init_aggregate_rescaling(only_once=False, weights=[], max_min={'max': 90, 'min': -20}): #you only pca once - def aggregate_rescaling(frame, display_fig=False): - nonlocal only_once, weights, max_min - #frame = cv.cvtColor(frame, cv.COLOR_BGR2HSV) - frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - - #kernel = np.ones((5,5),np.float32)/25 - #frame = cv.filter2D(frame,-1,kernel) - - r, c, d = frame.shape - A = np.reshape(frame, (r * c, d)) - - if not only_once: - - A_dot = A - A.mean(axis=0)[np.newaxis, :] - - _, eigv = LA.eigh(A_dot.T @ A_dot) - weights = eigv[:, 0] - #if (weights<0).sum() > 0: - #if np.mean(weights) < 0: - # weights *= -1 - - red = np.reshape(A_dot @ weights, (r, c)) - only_once = True - else: - red = np.reshape(A @ weights, (r, c)) - #red /= np.max(np.abs(red),axis=0) #this looks real cool - Damas - """ - if len(max_min['max']) == 10: - max_min['max'] = max_min['max'][1:] + [np.max(red)] - max_min['min'] = max_min['min'][1:] + [np.min(red)] - else: - max_min['max'].append(np.max(red)) - max_min['min'].append(np.min(red)) - """ - - if np.min(red) < max_min['min']: - max_min['min'] = np.min(red) - if np.max(red) > max_min['max']: - max_min['max'] = np.max(red) - - #print(np.min(red), np.max(red), 'all time Domas', max_min['min'], max_min['max']) - - red -= max_min['min'] - red *= (255.0/(max_min['max'] - max_min['min'])) - - #red -= np.min(max_min['min']) - #red *= (255.0/np.abs(np.max(max_min['max']))) - - #red -= np.min(red) - #red *= (255.0/np.abs(np.max(red))) - - red = red.astype(np.uint8) - red = np.expand_dims(red, axis = 2) - red = np.concatenate((red, red, red), axis = 2) - - if display_fig: - cv.imshow('One Time PCA plus all time aggregate rescaling', red) - cv.imshow('frame', frame_gray) - return red - - return aggregate_rescaling - -if __name__ == "__main__": - aggregate_rescaling = init_aggregate_rescaling() - while True: - if not paused: - for _ in range(speed): - ret, frame = cap.read() - if ret: - aggregate_rescaling(frame, True) - #break - key = cv.waitKey(30) - if key == ord('q') or key == 27: - break - if key == ord('p'): - paused = not paused - if key == ord('i') and speed > 1: - speed -= 1 - if key == ord('o'): - speed += 1 \ No newline at end of file diff --git a/misc/hydrophones.ipynb b/misc/hydrophones.ipynb deleted file mode 100644 index e9e2e9a..0000000 --- a/misc/hydrophones.ipynb +++ /dev/null @@ -1,195 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline \n", - "import numpy as np\n", - "import numpy.linalg as LA\n", - "import math\n", - "import scipy\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib\n", - "#ignore divide by 0 warnings\n", - "import warnings\n", - "warnings.filterwarnings('ignore')" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[24.64692931 63.9232561 ] x, y pos\n" - ] - } - ], - "source": [ - "# pingers = (x, y, frequency)\n", - "# pingers = [(np.random.random()*100, np.random.random()*100, f) for f in range(2,5)]\n", - "pingers = np.asarray([(0, 0, 2), (100, 0, 3), (50, 100, 4)])\n", - "pinger_amp = 10\n", - "# assuming cylindrical spreading: https://dosits.org/science/advanced-topics/cylindrical-vs-spherical-spreading/\n", - "# amp_detected = amp_source / distance(m)\n", - "sound_speed = 1481 # (m/s) https://en.wikipedia.org/wiki/Speed_of_sound#Water\n", - "mic_sample_rate = 1 # Hz\n", - "robot = np.random.random(2)*100 # x, y position\n", - "print(robot, 'x, y pos')\n", - "\n", - "def simulate(duration=5):\n", - " num_samples=duration*mic_sample_rate\n", - " # get 'duration' seconds of samples\n", - " xs = range(num_samples)\n", - " ys = []\n", - " for x in xs:\n", - " y = 0\n", - " for pinger in pingers:\n", - " delta = pinger[0:2] - robot\n", - " dist = np.sqrt(np.dot(delta, delta))\n", - " if int(x / mic_sample_rate - dist / sound_speed) % pinger[2] == 0:\n", - " y += pinger_amp / dist\n", - " ys.append(y)\n", - " return xs, ys\n", - "\n", - "xs, ys = simulate(10)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "4\n", - "2 0\n", - "2 1\n", - "2 2\n", - "3 0\n", - "3 1\n", - "4 0\n", - "4 1\n" - ] - }, - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "max_period = max(pingers, key=lambda x:x[2])[2]\n", - "print(max_period)\n", - "feature_len = max_period * mic_sample_rate - 1\n", - "features = np.zeros((len(pingers), feature_len))\n", - "\n", - "for pinger in range(len(pingers)):\n", - " period = pingers[pinger][2]\n", - "# features[pinger, 0] = 1\n", - " for i in range(max_period // period + 1):\n", - " print(period, i)\n", - " features[pinger, i * mic_sample_rate] = 1\n", - "\n", - "A = features\n", - "xs_ = xs[:feature_len]\n", - "ys_ = ys[:feature_len]\n", - "amps = LA.lstsq(A.T,ys_)[0]\n", - "\n", - "\n", - "plt.figure(1, figsize=(20,5))\n", - "plt.subplot(311)\n", - "plt.plot(xs, ys, 'b')\n", - "plt.subplot(312)\n", - "plt.plot(xs_, amps @ features , 'r')\n", - "# plt.subplot(313)\n", - "# plt.plot(xs, ys_fft_ang, 'y')\n", - "# plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 91, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(1, figsize=(20,5))\n", - "plt.subplot(311)\n", - "plt.plot(xs, ys, 'b')\n", - "\n", - "ys_fft = np.fft.rfft(ys)\n", - "ys_fft_abs = np.abs(ys_fft)\n", - "ys_fft_ang = np.angle(ys_fft)\n", - "plt.subplot(312)\n", - "plt.plot(ys_fft_abs, 'r')\n", - "plt.subplot(313)\n", - "plt.plot(ys_fft_ang, 'y')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/misc/nonlinear-regression.ipynb b/misc/nonlinear-regression.ipynb deleted file mode 100644 index 9d038db..0000000 --- a/misc/nonlinear-regression.ipynb +++ /dev/null @@ -1,153 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline \n", - "import numpy as np\n", - "import numpy.linalg as linalg\n", - "import math\n", - "import scipy\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib\n", - "#ignore divide by 0 warnings\n", - "import warnings\n", - "warnings.filterwarnings('ignore')" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "xs = np.random.random(8) * 20\n", - "a, b, c = -25 + np.random.random()*50, -1 + np.random.random()*2, -25 + np.random.random()*50\n", - "ys = [a*np.log(x) + b*np.sqrt(x) + c*np.sin(x) + np.random.normal()*5 for x in xs]\n", - "\n", - "# LIN REGRESSION\n", - "A = np.array([np.log(xs), np.sqrt(xs), np.sin(xs)])\n", - "a_, b_, c_ = linalg.lstsq(A.T,ys)[0]\n", - "# LIN REGRESSION\n", - "\n", - "xs_ = [x/10. for x in range(-1, 200)]\n", - "ys_ = [a_*np.log(x) + b_*np.sqrt(x) + c_*np.sin(x) for x in xs_]\n", - "ys_actual = [a*np.log(x) + b*np.sqrt(x) + c*np.sin(x) for x in xs_]\n", - "plt.figure(figsize=(20,5))\n", - "plt.plot(xs_, ys_, 'b', xs, ys, 'yp', xs_, ys_actual, 'r--')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABIkAAAEyCAYAAAB+h4BJAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzs3Xd0VNUWx/HvnUkISSgBAoReJFIFhIBAUOkqRVCpYkGpNsBengrP8gQFfU9QQcVCR0EQAQGlF0FCkQ6hhCQgRUggJCFt7vvj0gmkzWQm5PdZKwty595zdgYY5u7ZZx/DNE1ERERERERERCR/s7k7ABERERERERERcT8liUREREREREREREkiERERERERERFRkkhERERERERERFCSSEREREREREREUJJIRERERERERERQkkhERERERERERFCSSEREREREREREUJJIREREREREREQAL3cHcLnAwECzcuXK7g5DREREREREROSmsXHjxn9M0yyZ0XkelSSqXLkyYWFh7g5DREREREREROSmYRjGocycp+VmIiIiIiIiIiKiJJGIiIiIiIiIiChJJCIiIiIiIiIiKEkkIiIiIiIiIiIoSSQiIiIiIiIiIihJJCIiIiIiIiIiKEkkIiIiIiIiIiIoSSS5yDTTiIwcxerVgURGjsY009wdkoiIiIiIiIicpySR5IqEhHDCwkKIiBhOaupJIiKGsXFjIxISwt0dmoiIiIiIiIjgxCSRYRh2wzA2G4Yx7/z3VQzDWG8YRrhhGDMMwyjgrLkk79m8OZT4+K04HPEAOBzxnD37F5s3h+ZoXFUniYiIiIiIiDiHMyuJhgC7Lvt+JPCJaZrBQAzQ14lzSR7j718bcFx11IG/f51sj6nqJBERERERERHncUqSyDCM8kAH4Ovz3xtAK2Dm+VO+B7o4Yy7Jm4KC+mK3F7rimN1eiKCgJ7M95vWqkzZtaqbqIhEREREREZEsclYl0X+BV7hUKlICiDVNM/X899FAufQuNAxjgGEYYYZhhJ04ccJJ4YinCQzshGF4XXHMMLwIDOyU7TGvV53kcJxTdVEWaMmeiIiIiIiIAHhlfMqNGYbREThumuZGwzBaXDiczqlmetebpvkl8CVASEhIuudI3uflVZTmzWOcOmZQUF/i4sJISzt7xXGrssi8+PsLvY9C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qlULdZQiEkin2t2sxOVm1tpOp3jNj4HPgcE4M4ca5nuuAbDjFM8jIiIiIiIifnLWWfDZLMOQIW34+9+hQwcYNgxuvhk8ZSpAIiLhpqzdzZrke9gN2Oj7fiZwt6/LWXvggOoRiYiIiIiIhIbbDS++6MwqWroUbrtNCSIROVlZaxK9bIz5wRizFrgeGOjbPhvYAmwG3gMeKuN1RERERERE/MLaPNLTX2PJkljS04djbV6oQ/Kr4l5fjRohDExEyr0SaxIFk2oSiYiIiIhIIGVlpbJ+fU+ys1PxejNxuWKIjm5K8+afEh3dpOQTlHPh/vpEpHROtSZRmbubiYiIiIiIVBQpKYlkZq7F680EwOvN5NChNaSkJIY4Mv8I99cnIoGlJJGIiIiIiESMmJgWgPeErV5iYuJDEY7fhfvrE5HAUpJIREREREQiRlxcX9zuqgW2ud1ViYu7L0QR+Ve4vz4RCSwliUREREREJGLExnbFmIJtvYzxEBvbNUQR+Ve4vz4RCSw1PRQRERERkYjh8VQnKSkj1GEETLi/PhEJLM0kEhERERERiQDW5pGe/hpLlsSSnj4ca/MK3SYikctYa0Mdw3EJCQk2OTk51GGIiIiIiIiElaysVNav70l2dipebyYuVwxVqjQC4PDh9OPboqOb0rz5p0RHNwlxxCLiT8aYldbahJL200wiERERERGRMJeSkkhm5lq83kwAvN5MsrI2kJW1ocC2Q4fWkJKSGMpQRSSElCQSEREREREJczExLQDvKezpJSYmPtDhiEg5pSSRiIiIiIhImIuL64vbXbXANmOiMKZKgW1ud1Xi4u4LZmgiUo4oSSQiIiIiIhLmYmO7YkzB5tYuVxVcrqgC24zxEBvbNZihiUg54il5FxEREREREanIPJ7qJCVlhDoMESnnNJNIRERERERERESUJBIRERERERERESWJREREREREREQEJYlERERERERERAQliUREREREREREBCWJREREREREREQEJYlERERERERERAQliUREREREREREBCWJREREREREREQEMNbaUMdwnDFmL/BzqOPwk1hgX6iDkJDQ2Ec2jX/k0thHNo1/5NLYRzaNf+TS2Ee2ijj+51hra5e0U7lKEoUTY0yytTYh1HFI8GnsI5vGP3Jp7CObxj9yaewjm8Y/cmnsI1s4j7+Wm4mIiIiIiIiIiJJEIiIiIiIiIiKiJFEgvRvqACRkNPaRTeMfuTT2kU3jH7k09pFN4x+5NPaRLWzHXzWJREREREREREREM4lERERERERERERJIhERERERERERQUkivzPGdDbG/GiM2WyMeTLU8UhwGWPSjDHrjDGrjTHJoY5HAssYM9YYs8cY80O+bTWNMfOMMam+f88KZYwSGEWM/RBjzC+++3+1MaZLKGOUwDDGNDTGLDDGbDDGrDfGDPRt170fAYoZf93/Yc4YU8UYs9wYs8Y39s/7tjc2xnzvu/c/NcZUDnWs4n/FjP8Hxpit+e791qGOVQLDGOM2xqQYY2b5Hoftva8kkR8ZY9zAW8AfgebAHcaY5qGNSkLgamtta2ttQqgDkYD7AOh8wrYnga+ttU2Ar32PJfx8wMljDzDCd/+3ttbODnJMEhy5wF+ttc2A9sAA3//1uvcjQ1HjD7r/w10OcI219mKgNdDZGNMeeAVn7JsAGUDfEMYogVPU+AM8ke/eXx26ECXABgIb8j0O23tfSSL/uhTYbK3dYq09AnwCdA9xTCISINbaxcBvJ2zuDnzo+/5D4KagBiVBUcTYSwSw1u601q7yfX8Q5xfG+ujejwjFjL+EOes45HtYyfdlgWuAyb7tuvfDVDHjLxHAGNMAuAF43/fYEMb3vpJE/lUf2Jbv8Xb0i0OkscBcY8xKY8wDoQ5GQqKutXYnOB8mgDohjkeC62FjzFrfcjQtNwpzxphzgUuA79G9H3FOGH/Q/R/2fMtNVgN7gHnAT8B+a22ubxf97h/GThx/a+2xe3+o794fYYyJCmGIEjgjgUGA1/e4FmF87ytJ5F+mkG3KMEeWRGttG5wlhwOMMVeGOiARCZq3gfNxpqHvBIaHNhwJJGNMVWAK8Ki19n+hjkeCq5Dx1/0fAay1edba1kADnBUEzQrbLbhRSbCcOP7GmHjgKeAioB1QE/hbCEOUADDG3AjssdauzL+5kF3D5t5Xksi/tgMN8z1uAOwIUSwSAtbaHb5/9wDTcH6BkMiy2xhTD8D3754QxyNBYq3d7fsF0gu8h+7/sGWMqYSTIJhgrZ3q26x7P0IUNv66/yOLtXY/sBCnLlUNY4zH95R+948A+ca/s28JqrXW5gDj0L0fjhKBbsaYNJxyMtfgzCwK23tfSSL/WgE08VU6rwz0AmaGOCYJEmNMjDHmzGPfA9cDPxR/lIShmcA9vu/vAWaEMBYJomMJAp+b0f0flnx1CMYAG6y1r+d7Svd+BChq/HX/hz9jTG1jTA3f92cAnXBqUi0AbvXtpns/TBUx/hvz/XHA4NSk0b0fZqy1T1lrG1hrz8X5fD/fWnsnYXzvG2vDZlZUueBreToScANjrbVDQxySBIkx5jyc2UMAHuBjjX94M8ZMBK4CYoHdwGBgOjAJaASkA7dZa1XgOMwUMfZX4Sw1sUAa0P9YjRoJH8aYJOAbYB2/1yZ4Gqcuje79MFfM+N+B7v+wZoxphVOc1o3zh/ZJ1toXfL//fYKz1CgF6OObVSJhpJjxnw/Uxll+tBp4MF+BawkzxpirgMettTeG872vJJGIiIiIiIiIiGi5mYiIiIiIiIiIKEkkIiIiIiIiIiIoSSQiIiIiIiIiIihJJCIiIiIiIiIiKEkkIiIiIiIiIiIoSSQiIiIiIiIiIihJJCIiIiIiIiIiwP8HjPR5XYJDz+wAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]\n", - "num_sinusoids, freqs = 10, [2*math.pi/i for i in primes]\n", - "noise = 5\n", - "amps_, freqs_ = np.random.random(num_sinusoids)*10, np.copy(freqs)\n", - "np.random.shuffle(freqs_)\n", - "freqs_ = freqs_[:num_sinusoids]\n", - "\n", - "xs = np.array([x/5. for x in range(200)])\n", - "ys_, data = [], []\n", - "for x in xs:\n", - " y = 0\n", - " for a, f in zip(amps_, freqs_):\n", - " y += a*math.sin(f*x)\n", - " ys_.append(y)\n", - " data.append(y + np.random.normal()*noise)\n", - "\n", - "# LIN REGRESSION\n", - "A = np.array([np.sin(f * xs) for f in freqs]) # domain augmentation\n", - "amps = linalg.lstsq(A.T,data)[0] # obtaining the amps\n", - "# LIN REGRESSION\n", - "\n", - "ys = []\n", - "for x in xs:\n", - " y = 0\n", - " for a, f in zip(amps, freqs):\n", - " y += a*math.sin(f*x)\n", - " ys.append(y)\n", - "\n", - "plt.figure(figsize=(20,5))\n", - "plt.plot(xs, ys, 'b', xs, data, 'yp', xs, ys_, 'r--')\n", - "plt.savefig('regression.png')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/misc/optical_flow.py b/misc/optical_flow.py deleted file mode 100644 index 738a464..0000000 --- a/misc/optical_flow.py +++ /dev/null @@ -1,61 +0,0 @@ -import numpy as np -import cv2 - -cap = cv2.VideoCapture(0) - -# params for ShiTomasi corner detection -feature_params = dict( maxCorners = 100, - qualityLevel = 0.3, - minDistance = 7, - blockSize = 7 ) - -# Parameters for lucas kanade optical flow -lk_params = dict( winSize = (15,15), - maxLevel = 2, - criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)) - -# Create some random colors -color = np.random.randint(0,255,(100,3)) - -# Take first frame and find corners in it -ret, old_frame = cap.read() -old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) -p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **feature_params) - -# Create a mask image for drawing purposes -mask = np.zeros_like(old_frame) - -while(1): - ret,frame = cap.read() - frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - - # calculate optical flow - p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) - - # Select good points - - if p1 is None: - continue - - good_new = p1[st==1] - good_old = p0[st==1] - - # draw the tracks - for i,(new,old) in enumerate(zip(good_new,good_old)): - a,b = new.ravel() - c,d = old.ravel() - mask = cv2.line(mask, (a,b),(c,d), color[i].tolist(), 2) - frame = cv2.circle(frame,(a,b),5,color[i].tolist(),-1) - img = cv2.add(frame,mask) - - cv2.imshow('frame',img) - k = cv2.waitKey(30) & 0xff - if k == 27: - break - - # Now update the previous frame and previous points - old_gray = frame_gray.copy() - p0 = good_new.reshape(-1,1,2) - -cv2.destroyAllWindows() -cap.release() \ No newline at end of file diff --git a/tasks/.DS_Store b/tasks/.DS_Store deleted file mode 100644 index b0e5925f4e4460187a9977a7d06f768da65d8987..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHL!A=4(5Pd~77>wbj$31#7(W{pY!Gkvwy(28E5Mh_FXu@qT{)4~Z_xS<(rfq;N zka#l2%#ikVyYr@<*Ufed0MmXMTmUTq4Hm)LA*(GW_oZxD&G!h;*cb_VNH9c-?uNH^ z{6z(1?V4PzE~Z#;?N;m8y&308f1LN}A&x>*3eZ-uyD$nNrzBbdHyvIS2^z(eoad5!kQ-;g<3nl`QXgy#H_Z+-n!ncw#rA^6v zwe4+*oCJIsPfZn21yq3_E5JQltkHC+wJM+rr~*p``-e} zKozJeu;(sYvj1Q2KL1xqdZh}e0{=<@Q}5h$+DysstsBY7UK_DIu!xCY>2OJ5!*0d& fm91F*+p)&}NE*c0W9g7RH2n~;GH9g={Hg-q{>6oO diff --git a/tasks/.vscode/launch.json b/tasks/.vscode/launch.json deleted file mode 100644 index 7e74bec..0000000 --- a/tasks/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - }, - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - } - ] -} \ No newline at end of file diff --git a/tasks/TaskPerceiver.py b/tasks/TaskPerceiver.py deleted file mode 100644 index 40f5995..0000000 --- a/tasks/TaskPerceiver.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Any -import numpy as np -class TaskPerceiver: - - def __init__(self): - self.time = 0 - - def analyze(self, frame: np.ndarray, debug: bool) -> Any: - """Runs the algorithm and returns the result. - Args: - frame: The frame to analyze - debug: Whether or not to display intermediate images for debugging - - Returns: - the result of the algorithm - """ - raise NotImplementedError("Need to implement with child class.") - diff --git a/tasks/cross/CrossPerceiver.py b/tasks/cross/CrossPerceiver.py deleted file mode 100644 index 65c2424..0000000 --- a/tasks/cross/CrossPerceiver.py +++ /dev/null @@ -1,9 +0,0 @@ -from collections import namedtuple -import numpy as np -import sys -sys.path.insert(0, '..') -from TaskPerceiver import TaskPerceiver - -class CrossPerceiver(TaskPerceiver): - named_tuple = namedtuple("CrossOutput", ["centerx", "centery"]) - named_tuple_types = {centerx: np.int16, centery: np.int16} diff --git a/tasks/cross/cross_detection.py b/tasks/cross/cross_detection.py deleted file mode 100644 index 0b49772..0000000 --- a/tasks/cross/cross_detection.py +++ /dev/null @@ -1,98 +0,0 @@ -import numpy as np -import cv2 -import sys - -############################################################################# -# Compilation of ways to do cross detection for this year's challenge. -# Add more functions for alternative algorithms! -############################################################################# - -sys.path.insert(0, '../background_removal') -from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim -from combined_filter import combined_filter - -ret, frame = True, cv2.imread('../data/cross/cross.png') # https://i.imgur.com/rjv1Vcy.png - -# "hsv" = Apply hsv thresholding before trying to find the path marker -# "multidim" = Apply filter_out_highest_peak_multidim -# "combined" = Apply pca then multidim -thresholding = "combined" # Apply hsv thresholding before trying to find the path marker - -def find_cross(frame, draw_figs=True): - """ Returns the middle of a possible cross that has the largest contour area - - One of the ideas from: - https://stackoverflow.com/questions/14612192/detecting-a-cross-in-an-image-with-opencv - """ - - # Or any other colorspace transformation - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - - ret, thresh = cv2.threshold(gray, 127, 255,0) - __, contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - contours.sort(key=lambda c: cv2.contourArea(c), reverse=True) - - possible_crosses = [] - - for i in range(len(contours)): - cnt = contours[i] - - hull = cv2.convexHull(cnt,returnPoints = False) - defects = cv2.convexityDefects(contours[i],hull) - - # Crosses have 4 "defects" (concave places) - if defects is not None and len(defects) == 4: - possible_crosses.append(defects) - - - if draw_figs: - img = frame.copy() - for defects in possible_crosses: - for i in range(defects.shape[0]): - s,e,f,d = defects[i,0] - # start = tuple(cnt[s][0]) - # end = tuple(cnt[e][0]) - far = tuple(cnt[f][0]) - # cv2.line(img,start,end,[0,255,0],2) - cv2.circle(img,far,5,[0,0,255],-1) - cv2.imshow('cross at contour number ' + str(i),img) - cv2.imshow('original', frame) - - - return possible_crosses[0] - -########################################### -# Main Body -########################################### - -if __name__ == "__main__": - ret_tries = 0 - while(1 and ret_tries < 50): - # ret,frame = cap.read() - - if ret == True: - # frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5) - - if thresholding == "multidim": - votes1, threshed = filter_out_highest_peak_multidim(frame) - threshed = cv2.morphologyEx(threshed, cv2.MORPH_OPEN, np.ones((5,5),np.uint8)) - elif thresholding == "combined": - threshed = combined_filter(frame) - else: - threshed = frame.copy() - - cross_pts = find_cross(frame, True) - input() # This test is only for one frame - - ret_tries = 0 - k = cv2.waitKey(60) & 0xff - if k == 27: # esc - if testing: - print("hsv thresholds:") - print(thresholds_used) - break - else: - ret_tries += 1 - - cv2.destroyAllWindows() - cap.release() diff --git a/tasks/gate/GatePerceiver.py b/tasks/gate/GatePerceiver.py deleted file mode 100644 index 1820e94..0000000 --- a/tasks/gate/GatePerceiver.py +++ /dev/null @@ -1,9 +0,0 @@ -from collections import namedtuple -import numpy as np -import sys -sys.path.insert(0, '..') -from TaskPerceiver import TaskPerceiver - -class GatePerceiver(TaskPerceiver): - output_class = namedtuple("GateOutput", ["centerx", "centery"]) - output_type = {'centerx': np.int16, 'centery': np.int16} diff --git a/tasks/gate/GateSegmentationAlgo.py b/tasks/gate/GateSegmentationAlgo.py deleted file mode 100644 index 191865c..0000000 --- a/tasks/gate/GateSegmentationAlgo.py +++ /dev/null @@ -1,108 +0,0 @@ -from GatePerceiver import GatePerceiver -from typing import Tuple -import sys -import os -sys.path.append(os.path.dirname(__file__)) - - -from segmentation.combinedFilter import init_combined_filter -import numpy as np -import cv2 as cv -import time -import cProfile - -class GateSegmentationAlgo(GatePerceiver): - __past_centers = [] - __ema = None - - def __init__(self, alpha): - super() - self.__alpha = alpha - - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze - debug: Whether or not tot display intermediate images for debugging - Reurns: - (x,y) coordinate with center of gate - """ - gate_center = self.output_class(250, 250) - filtered_frame = combined_filter(frame, display_figs=False) - filtered_frame_copies = [filtered_frame for _ in range(3)] - stacked_filter_frames = np.concatenate(filtered_frame_copies, axis = 2) - mask = cv.inRange(stacked_filter_frames, - np.array([100, 100, 100]), np.array([255, 255, 255])) - _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - if contours: - contours.sort(key=self.findStraightness, reverse=True) - cnts = contours[:2] - rects = [cv.minAreaRect(c) for c in cnts] - centers = [np.array(r[0]) for r in rects] - boxpts = [cv.boxPoints(r) for r in rects] - box = [np.int0(b) for b in boxpts] - for b in box: - cv.drawContours(stacked_filter_frames,[b],0,(0,0,255),5) - if len(centers) >= 2: - gate_center = (centers[0] + centers[1]) * 0.5 - if self.__ema is None: - self.__ema = gate_center - else: - self.__ema = self.__alpha*gate_center + (1 - self.__alpha)*self.__ema - gate_center = (int(self.__ema[0]), int(self.__ema[1])) - # if len(self.__past_centers) < 15: - # self.__past_centers += [gate_center] - # else: - # self.__past_centers.pop(0) - # self.__past_centers += [gate_center] - # gate_center = sum(self.__past_centers) / len(self.__past_centers) - # gate_center = (int(gate_center[0]), int(gate_center[1])) - cv.circle(stacked_filter_frames, gate_center, 10, (0,255,0), -1) - - if debug: - return (self.output_class(gate_center[0], gate_center[1]), stacked_filter_frames) - return self.output_class(gate_center[0], gate_center[1]) - - def findStraightness(self, contour): # output number = contour area/convex area, the bigger the straightest - hull = cv.convexHull(contour, False) - contour_area = cv.contourArea(contour) - hull_area = cv.contourArea(hull) - return 10 * contour_area - 5 * hull_area - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - gate_task = GateSegmentationAlgo(0.1) - # once = False - start_time = time.time() - frame_count = 0 - while ret_tries < 50: - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.4, fy=0.4) - - - ### FUNCTION CALL, can change this - center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 - frame_count += 1 - #print(frame_count / (time.time() - start_time)) diff --git a/tasks/gate/GateSegmentationAlgo1.py b/tasks/gate/GateSegmentationAlgo1.py deleted file mode 100644 index e054d56..0000000 --- a/tasks/gate/GateSegmentationAlgo1.py +++ /dev/null @@ -1,132 +0,0 @@ -from GatePerceiver import GatePerceiver -from typing import Tuple -import sys -import os -sys.path.append(os.path.dirname(__file__)) - - -from segmentation.combinedFilter import init_combined_filter -import numpy as np -import cv2 as cv -import time -import cProfile -import statistics - -class GateSegmentationAlgo(GatePerceiver): - center_x_locs, center_y_locs = [], [] - - def __init__(self, alpha): - super() - - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze - debug: Whether or not tot display intermediate images for debugging - Reurns: - (x,y) coordinate with center of gate - """ - gate_center = self.output_class(250, 250) - filtered_frame = combined_filter(frame, display_figs=False) - - max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) - lowerbound = max(0.84*max_brightness, 120) - upperbound = 255 - _,thresh = cv.threshold(filtered_frame,lowerbound, upperbound, cv.THRESH_BINARY) - debug_filter = cv.cvtColor(thresh, cv.COLOR_GRAY2BGR) - - cnt = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[-2] - - area_diff = [] - area_cnts = [] - - # remove all contours with zero area - cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] - - for i in range(len(cnt)): - area_cnt = cv.contourArea(cnt[i]) - area_cnts.append(area_cnt) - area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] - area_diff.append(abs((area_rect - area_cnt)/area_cnt)) - - if len(area_diff) >= 2: - largest_area_idx = [area_cnts.index(sorted(area_cnts, reverse=True)[i]) for i in range(min(3, len(cnt)))] - area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) - min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) - - (x1, y1, w1, h1) = cv.boundingRect(cnt[min_i1]) - (x2, y2, w2, h2) = cv.boundingRect(cnt[min_i2]) - cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) - cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) - - # drawing center dot - center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - gate_center = self.get_actual_center(center_x, center_y) - cv.circle(debug_filter, gate_center, 5, (0,0,255), -1) - - if debug: - return (self.output_class(gate_center[0], gate_center[1]), debug_filter) - return self.output_class(gate_center[0], gate_center[1]) - - def get_actual_center(self, center_x, center_y): - # get starting center location, averaging over the first 2510 frames - if len(self.center_x_locs) == 0: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - - elif len(self.center_x_locs) < 25: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - center_x = int(statistics.mean(self.center_x_locs)) - center_y = int(statistics.mean(self.center_y_locs)) - - # use new center location only when it is close to the previous valid location - else: - if abs(center_x - self.center_x_locs[-1]) > 10 or \ - abs(center_y - self.center_y_locs[-1]) > 10: - center_x, center_y = self.center_x_locs[-1], self.center_y_locs[-1] - else: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - - return (center_x, center_y) - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - gate_task = GateSegmentationAlgo(0.1) - # once = False - start_time = time.time() - frame_count = 0 - paused = False - speed = 1 - while ret_tries < 50: - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.3, fy=0.3) - - - ### FUNCTION CALL, can change this - center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 - frame_count += 1 - #print(frame_count / (time.time() - start_time)) diff --git a/tasks/gate/GateSegmentationAlgo2.py b/tasks/gate/GateSegmentationAlgo2.py deleted file mode 100644 index e9f440b..0000000 --- a/tasks/gate/GateSegmentationAlgo2.py +++ /dev/null @@ -1,192 +0,0 @@ -from GatePerceiver import GatePerceiver -from typing import Tuple -import sys -import os -sys.path.append(os.path.dirname(__file__)) - - -from segmentation.combinedFilter import init_combined_filter -import numpy as np -import math -import cv2 as cv -import time -import cProfile -import statistics - -class GateSegmentationAlgo(GatePerceiver): - center_x_locs, center_y_locs = [], [] - - def __init__(self): - super() - self.gate_center = self.output_class(250, 250) - self.use_optical_flow = False - self.optical_flow_c = 0.05 - - - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze - debug: Whether or not tot display intermediate images for debugging - Reurns: - (x,y) coordinate with center of gate - """ - global prvs - filtered_frame = combined_filter(frame, display_figs=False) - - max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) - lowerbound = max(0.84*max_brightness, 120) - upperbound = 255 - _,thresh = cv.threshold(filtered_frame,lowerbound, upperbound, cv.THRESH_BINARY) - debug_filter = cv.cvtColor(thresh, cv.COLOR_GRAY2BGR) - - cnt = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[-2] - - area_diff = [] - area_cnts = [] - - # remove all contours with zero area - cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] - - for i in range(len(cnt)): - area_cnt = cv.contourArea(cnt[i]) - area_cnts.append(area_cnt) - area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] - area_diff.append(abs((area_rect - area_cnt)/area_cnt)) - - if len(area_diff) >= 2: - largest_area_idx = [area_cnts.index(sorted(area_cnts, reverse=True)[i]) for i in range(min(3, len(cnt)))] - area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) - min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) - - rect1 = cv.boundingRect(cnt[min_i1]) - rect2 = cv.boundingRect(cnt[min_i2]) - x1, y1, w1, h1 = rect1 - x2, y2, w2, h2 = rect2 - cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) - cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) - - # # drawing center dot - # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - # self.gate_center = self.get_actual_center(center_x, center_y) - # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) - - # dense optical flow - # next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - # flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) - # mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) - # mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) - # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - # if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ - # (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): - # self.gate_center = self.get_actual_center(center_x, center_y) - # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) - # self.use_optical_flow = False - # else: - # self.use_optical_flow = True - # self.gate_center = (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag) * math.cos(np.mean(ang))), \ - # int(self.gate_center[1] + self.optical_flow_c * np.mean(mag) * math.sin(np.mean(ang)))) - # cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) - self.gate_center = self.get_center(rect1, rect2, frame) - if self.use_optical_flow: - cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) - else: - cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) - # ang = ang*180/np.pi - # print('mag:', np.mean(mag), '\tang:', np.mean(ang)) - # hsv[...,0] = ang - # hsv[...,2] = mag - # bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) - # prvs = next - if debug: - return (self.output_class(self.gate_center[0], self.gate_center[1]), debug_filter) - return self.output_class(self.gate_center[0], self.gate_center[1]) - - def center_without_optical_flow(self, center_x, center_y): - # get starting center location, averaging over the first 2510 frames - if len(self.center_x_locs) == 0: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - - elif len(self.center_x_locs) < 25: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - center_x = int(statistics.mean(self.center_x_locs)) - center_y = int(statistics.mean(self.center_y_locs)) - - # use new center location only when it is close to the previous valid location - else: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - self.center_x_locs.pop(0) - self.center_y_locs.pop(0) - x_temp_avg = int(statistics.mean(self.center_x_locs)) - y_temp_avg = int(statistics.mean(self.center_y_locs)) - if math.sqrt((center_x - x_temp_avg)**2 + (center_y - y_temp_avg)**2) > 10: - center_x, center_y = int(x_temp_avg), int(y_temp_avg) - - return (center_x, center_y) - - def dense_optical_flow(self, frame, prvs): - next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) - mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) - mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) - return next, mag, ang - - def get_center(self, rect1, rect2, rame): - global prvs - x1, y1, w1, h1 = rect1 - x2, y2, w2, h2 = rect2 - center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - prvs, mag, ang = self.dense_optical_flow(frame, prvs) - if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ - (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): - self.use_optical_flow = False - return self.center_without_optical_flow(center_x, center_y) - else: - self.use_optical_flow = True - return (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag * np.cos(ang))), \ - (int(self.gate_center[1] + self.optical_flow_c * np.mean(mag * np.sin(ang))))) - - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - # once = False - start_time = time.time() - frame_count = 0 - paused = False - speed = 1 - ret, frame1 = cap.read() - frame1 = cv.resize(frame1, None, fx=0.3, fy=0.3) - prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY) - hsv = np.zeros_like(frame1) - hsv[...,1] = 255 - gate_task = GateSegmentationAlgo() - while ret_tries < 50: - for _ in range(speed): - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.3, fy=0.3) - center, filtered_frame = gate_task.analyze(frame, True) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - ret_tries = 0 - key = cv.waitKey(30) - if key == ord('q') or key == 27: - break - if key == ord('p'): - paused = not paused - if key == ord('i') and speed > 1: - speed -= 1 - if key == ord('o'): - speed += 1 - else: - ret_tries += 1 - frame_count += 1 diff --git a/tasks/gate/archive/detectGate.py b/tasks/gate/archive/detectGate.py deleted file mode 100644 index cf309ff..0000000 --- a/tasks/gate/archive/detectGate.py +++ /dev/null @@ -1,198 +0,0 @@ -import numpy as np -import cv2 -import argparse -import sys -from PIL import Image -import time - -video_file = 'truncated_semi_final_run.mp4' -EPSILON = 40 -OVERLAP_EPS = 40 - -class Contour: - def __init__(self, _x, _y, _w, _h, _area): - self.x = _x - self.y = _y - self.w = _w - self.h = _h - self.area = _area - - def __str__(self): - return str(self.__dict__) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - -def imgDetect(file): - if isinstance(file, str): - frame = cv2.imread(file) - else: - frame = file - - hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) - gray = cv2.cvtColor(hsv, cv2.COLOR_BGR2GRAY) - - lower = np.array([29,40,36], dtype='uint8') - upper = np.array([77,80,50], dtype='uint8') - mask = cv2.inRange(hsv, lower, upper) - #filtered = cv2.bitwise_and(frame, frame, mask=mask) - blur = cv2.GaussianBlur(gray, (3,3), 3) - thresh = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\ - cv2.THRESH_BINARY, 7, 2) - #thresh = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 21, 5) - canny = cv2.Canny(blur, 20, 70) - #img, contours, hierarchy = cv2.findContours(canny.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - img = frame.copy() - myContours = getContours(canny) - sortedContours = sorted(myContours, key=lambda x: x.area) - center = (0,0) - if len(sortedContours) > 1: - a,b = sortedContours[-1], sortedContours[-2] - x,y = (a.x+b.x+a.w//2+b.w//2)//2, (a.y+b.y+a.h//2+b.h//2)//2 - center = (x,y) - #cv2.circle(img, (x,y), radius=5, color=(0,255,0), thickness=2) - #cv2.putText(img, "Center Point", (x,y-10), 2, 0.5, (0,0,0)) - # for cnt in sortedContours[:-2]: - # cv2.rectangle(img, (cnt.x, cnt.y), (cnt.x+cnt.w, cnt.y+cnt.h), (0,0,255), 2) - for cnt in sortedContours[-2:]: - cv2.rectangle(img, (cnt.x, cnt.y), (cnt.x+cnt.w, cnt.y+cnt.h), (255,0,0), 2) - cv2.imshow('Canny', canny) - if isinstance(file, str): - cv2.imshow('Frame', frame) - cv2.imshow('HSV', hsv) - cv2.imshow('Thresh', thresh) - #cv2.imshow('Mask', mask) - cv2.imshow('Canny', canny) - cv2.imshow('Output', img) - cv2.waitKey(0) - return img, center - -def videoDetect(file): - #file = "D:/Documents/College Work/AUV/vision-testing" + file - print("File name is: " + file) - vid = cv2.VideoCapture(file) - frames = 0 - FPS = 30 - avgLength = 10 - saver = cv2.VideoWriter('gateDetectionVideo.avi', cv2.VideoWriter_fourcc(*'MJPG'), 30, (640,360)) - centers = [] - while vid.isOpened(): - start = time.time() - ret, frame = vid.read() - if frame is None: - continue - h, w, d = frame.shape - frame = cv2.resize(frame, (w//2, h//2)) - img, center = imgDetect(frame) - if center != (0,0): - centers.append(center) - if len(centers) > avgLength: - centers.pop(0) - x, y = int(np.mean(np.array(centers)[:,0])), int(np.mean(np.array(centers)[:,1])) - cv2.circle(img, (x,y), radius=5, color=(0,255,0), thickness=2) - frames += 1 - # print(frames) - # if frames == 500: - # cv2.imwrite('test.png', frame) - cv2.imshow('Frame', frame) - cv2.imshow('Output', img) - #saver.write(img) - end = time.time() - if (cv2.waitKey(1) & 0xFF) == ord('q') or frames > 900: - break - vid.release() - cv2.destroyAllWindows() - -def getContours(image): - start = time.time() - # blur = cv2.bilateralFilter(cropped, 9, 17, 17) - - # edge = cv2.Canny(blur, 10, 100) - # #cv2.imwrite('cannyContour.jpg', edge) - # edge = cv2.GaussianBlur(edge, (5,5), 0.6) - #cv2.imwrite('gaussianContour.jpg', edge) - #Image.fromarray(edge).show() - - img, contours, hier = cv2.findContours(image.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - rectContour = [] - contourImg = image.copy() - limited = image.copy() - combined = image.copy() - for cn in contours: - #area = cv2.contourArea(cn) - x,y,w,h = cv2.boundingRect(cn) - area = w*h - cv2.rectangle(contourImg, (x,y), (x+w, y+h), 1, 1) - if area > 100 and area < 10000 and h>w+10: - rectContour.append(Contour(x,y,w,h, cv2.contourArea(cn))) - cv2.rectangle(limited, (x,y), (x+w, y+h), 1, 1) - - #cv2.imshow('Contours', blur) - #cv2.imwrite('allContours.jpg', contourImg) - #contourImg = Image.fromarray(contourImg) - #contourImg.show() - #cv2.imwrite('filteredContours.jpg', limited) - #limited = Image.fromarray(limited) - #limited.show() - #combinedContours = combineTouchingContours(rectContour) - #combinedContours = self.combineContours(rectContour) - #combinedContours = self.combineContours(self.combineContours(rectContour)) - combinedContours = combineContours(combineContours(combineContours(rectContour))) - - for cn in combinedContours: - x,y,w,h = cn.x, cn.y, cn.w, cn.h - cv2.rectangle(combined, (x,y), (x+w, y+h), 1, 1) - #cv2.imshow('Combined Contours', invert) - cv2.imwrite('combinedContours.jpg', combined) - #combined = Image.fromarray(combined) - #combined.show() - - #print('Find Contours Time: ', time.time() - start) - # for i in range(len(rectContour)): - # print('Contour ', i, ': ', str(rectContour[i])) - - return combinedContours - -def combineContours(contours): - newContours = [] - for cnt in contours: - add = True - for other in newContours: - if cnt != other and Intersect(cnt, other): - merged = Merge(cnt, other) - if cnt in newContours: - newContours.remove(cnt) - newContours.remove(other) - newContours.append(merged) - add = False - if add: - newContours.append(cnt) - return newContours - -def Intersect(A, B): - left = max(A.x, B.x) - top = max(A.y, B.y) - right = min(A.x + A.w, B.x + B.w) - bottom = min(A.y + A.h, B.y + B.h) - return (left <= right or abs(left-right) <= OVERLAP_EPS) and ((abs(A.y-B.y) <= EPSILON or abs(A.y+A.h-B.y-B.h) <= EPSILON)) and abs(A.y-B.y) <= EPSILON*2 and abs(A.y+A.h-B.y-B.h) <= EPSILON*2 - - -def Merge(A, B): - left = min(A.x, B.x) - top = min(A.y, B.y) - right = max(A.x + A.w, B.x + B.w) - bottom = max(A.y + A.h, B.y + B.h) - return Contour(left, top, right - left, bottom - top, A.area+B.area) - - -ap = argparse.ArgumentParser() -ap.add_argument('file_name', type=str, help='File name of video or image') -ap.add_argument('--test', '-t', action='store_true') - -if __name__ == '__main__': - args = ap.parse_args() - if args.file_name.lower().endswith('.mp4'): - videoDetect(args.file_name) - else: - imgDetect(args.file_name) \ No newline at end of file diff --git a/tasks/gate/archive/threshTest.py b/tasks/gate/archive/threshTest.py deleted file mode 100644 index 0bd6c71..0000000 --- a/tasks/gate/archive/threshTest.py +++ /dev/null @@ -1,209 +0,0 @@ -from __future__ import print_function -import cv2 as cv -import argparse -import numpy as np -#expectations -#contours closest to the last ones -#should know when we passed through the gate -""" -IMPORTANT!!!! RUN THIS WITH $ python3 threshTest.py GOPR1142.mp4 -""" -max_value = 255 -max_value_H = 360//2 -low_H = 0 -low_S = 98 -low_V = 0 -high_H = max_value_H -high_S = max_value -high_V = max_value -window_capture_name = 'Video Capture' -window_detection_name = 'Object Detection' -low_H_name = 'Low H' -low_S_name = 'Low S' -low_V_name = 'Low V' -high_H_name = 'High H' -high_S_name = 'High S' -high_V_name = 'High V' -pauseWhenFound = 0 -old_gray = None -p0 = None -heur_thresh = 200 - -# params for ShiTomasi corner detection -feature_params = dict( maxCorners = 100, - qualityLevel = 0.3, - minDistance = 7, - blockSize = 7 ) -# Parameters for lucas kanade optical flow -lk_params = dict( winSize = (15,15), - maxLevel = 2, - criteria = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03)) -# Create some random colors -color = np.random.randint(0,255,(100,3)) - -def trueFalsePause(val): - global pauseWhenFound - pauseWhenFound = val - cv.setTrackbarPos('pausing', window_capture_name, pauseWhenFound) -def on_low_H_thresh_trackbar(val): - global low_H - global high_H - low_H = val - low_H = min(high_H-1, low_H) - cv.setTrackbarPos(low_H_name, window_detection_name, low_H) -def on_high_H_thresh_trackbar(val): - global low_H - global high_H - high_H = val - high_H = max(high_H, low_H+1) - cv.setTrackbarPos(high_H_name, window_detection_name, high_H) -def on_low_S_thresh_trackbar(val): - global low_S - global high_S - low_S = val - low_S = min(high_S-1, low_S) - cv.setTrackbarPos(low_S_name, window_detection_name, low_S) -def on_high_S_thresh_trackbar(val): - global low_S - global high_S - high_S = val - high_S = max(high_S, low_S+1) - cv.setTrackbarPos(high_S_name, window_detection_name, high_S) -def on_low_V_thresh_trackbar(val): - global low_V - global high_V - low_V = val - low_V = min(high_V-1, low_V) - cv.setTrackbarPos(low_V_name, window_detection_name, low_V) -def on_high_V_thresh_trackbar(val): - global low_V - global high_V - high_V = val - high_V = max(high_V, low_V+1) - cv.setTrackbarPos(high_V_name, window_detection_name, high_V) - -def drawRects(frame, contours): - tempPts = [] - for cnt in contours: - rect = cv.minAreaRect(cnt['cont']) - boxpts = cv.boxPoints(rect) - box = np.int0(boxpts) - cv.drawContours(frame,[box],0,(0,0,255),1) - cv.drawContours(frame, [cnt['cont']],0,(0,255,0),1) - cv.drawContours(frame, [cv.convexHull(cnt['cont'])],0,(255,0,0),1) - tempPts.append(rect[0]) - cv.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) - if len(tempPts) > 1 and allLarger(heur_thresh): - global paused - if pauseWhenFound: - paused = True - avgPt = getAvgPt(midPt(tempPts[0], tempPts[1])) - cv.circle(frame, (avgPt[0], avgPt[1]), 10, (0,0,255), -1) - -def midPt(pt1, pt2): - return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) - -def getAvgPt(pt): - points.append(pt) - exes = list(map(lambda x: x[0], points)) - whys = list(map(lambda y: y[1], points)) - - if len(points) > 50: - del points[:10] - return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) - -def heuristic(contour): - rect = cv.minAreaRect(contour) - area = rect[1][0] * rect[1][1] - diff = cv.contourArea(cv.convexHull(contour)) - cv.contourArea(contour) - cent = rect[0] - dist = 0 - if len(likelyGate) > 1 and allLarger(heur_thresh): - cen0 = cv.minAreaRect(likelyGate[0]['cont'])[0] - dis0 = np.linalg.norm(np.array(cent) - np.array(cen0)) - cen1 = cv.minAreaRect(likelyGate[1]['cont'])[0] - dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) - dist = min([dis0, dis1]) - heur = area - 3 * diff - 20 * dist #only factor in dist with all heurs larger than 60 - #print(heur) - return heur - -def allLarger(thresh): - for cnt in likelyGate: - if cnt['heur'] < thresh: - return False - return True - -parser = argparse.ArgumentParser(description='Code for Thresholding Operations using inRange tutorial.') -parser.add_argument('camera', help='Camera devide number.', default=0, type=str) -args = parser.parse_args() -cap = cv.VideoCapture(args.camera) - -cv.namedWindow(window_capture_name) -cv.namedWindow(window_detection_name) - -cv.createTrackbar(low_H_name, window_detection_name , low_H, max_value_H, on_low_H_thresh_trackbar) -cv.createTrackbar(high_H_name, window_detection_name , high_H, max_value_H, on_high_H_thresh_trackbar) -cv.createTrackbar(low_S_name, window_detection_name , low_S, max_value, on_low_S_thresh_trackbar) -cv.createTrackbar(high_S_name, window_detection_name , high_S, max_value, on_high_S_thresh_trackbar) -cv.createTrackbar(low_V_name, window_detection_name , low_V, max_value, on_low_V_thresh_trackbar) -cv.createTrackbar(high_V_name, window_detection_name , high_V, max_value, on_high_V_thresh_trackbar) -cv.createTrackbar('pausing', window_capture_name, pauseWhenFound, 1, trueFalsePause) - -#cv.createTrackbar('low_canny', 'canny', low_canny, 500, lcanny) -paused = False - -likelyGate = [] -points = [] -while True: - if not paused: - ret, frame = cap.read() #reads the frame - else: - frame = untampered - if ret: - if not paused: - frame = cv.resize(frame, (0,0), fx=0.4, fy=0.4)#resizes frame so that it fits on screen - blur = cv.GaussianBlur(frame, (5, 5), 0) - frame_HSV = cv.cvtColor(blur, cv.COLOR_BGR2HSV) - #frame_gray = cv.cvtColor(blur, cv.COLOR_BGR2GRAY) - #canny = cv.Canny(frame_gray, 200, 3, True) - frame_threshold = cv.inRange(frame_HSV, (low_H, low_S, low_V), (high_H, high_S, high_V)) #low_S ideal = 98 Sets threshold in hsv - - frame_threshold = cv.bitwise_not(frame_threshold) - res = cv.bitwise_and(frame,frame, mask= frame_threshold) - res2, contours, hierarchy = cv.findContours(frame_threshold, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - - contours.sort(key=heuristic, reverse=True) #sorts the list of contours by a heuristic function, based on area, distance from previous contours - if len(contours) > 1:#two largest heuristics are assumed to be the two gate posts - heur0 = heuristic(contours[0]) - heur1 = heuristic(contours[1]) - likelyGate = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] - - """ - ### This is very crude adaptive thresholding - totArea = 0 - for cnt in contours: - totArea += cv.contourArea(cnt) - if totArea > 19000: - low_S -= .5 - if low_S < 250: - low_S += .2 - ### - """ - - untampered = np.copy(frame) - #cv.putText(frame, str(low_S)+' '+str(totArea), (100, 100), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) - if contours: - #likelyGate.append(contours[0]) - #findLikelyGate(likelyGate, contours) - drawRects(frame, likelyGate) - cv.imshow(window_capture_name, frame) - cv.imshow(window_detection_name, frame_threshold) - - key = cv.waitKey(30) - if key == ord('q') or key == 27: - break - if key == ord('p'): - paused = not paused - -#generalized problem, giving center of object contrasting with water \ No newline at end of file diff --git a/tasks/gate/gateDetectionVideo.avi b/tasks/gate/gateDetectionVideo.avi deleted file mode 100644 index b67ca42f26fcca7c8f12cc79a4b2e6b61847e02a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5686 zcmWIYbaT@aV_)ek_?v|WsHWvXb6mkz-S1JhQMeD zjE2By2#kinXb6mkz-S1JhQMeDjEE2bjSsK@2OjcMa&v$v np.ndarray: - """ Assumes frame is grayscale - - Args: - frame: The frame to analyze - num_contours: The number of contours to return, sorted - by area from largest to smallest. - Returns: - frame/threshed: thresholded image or original image depending - on if num_contours specified. - """ - - frame = np.array(frame, np.uint8) - - img, contours, hierarchy = cv2.findContours( - frame, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE - ) - if contours is not None: - contours.sort(key=lambda c: cv2.contourArea(c), reverse=True) - contours = contours[:num_contours] - - threshed = np.zeros(frame.shape, np.uint8) - cv2.fillPoly(threshed, contours, 255) - return threshed - else: - return frame - - -def find_path_marker(frame, draw_figs=False, thresh=0.3): - """ Assumes frame is grayscale - Returns angle of bottom line and top line relative to 0 radians - This function doesn't guarantee that the angles are distinct - Returns None if no good lines are found """ - - def line_length(line): - x0, y0, x1, y1 = line[0] - return (x0 - x1) ** 2 + (y0 - y1) ** 2 - - frame = thresh_by_contour_size(frame, num_contours=2) - - # gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - edges = cv2.Canny(frame, 100, 150) - - # Find Hough lines - # Source: https://stackoverflow.com/questions/45322630/how-to-detect-lines-in-opencv - - rho = 1 # distance resolution in pixels of the Hough grid - theta = np.pi / 180 # angular resolution in radians of the Hough grid - threshold = 5 # minimum number of votes (intersections in Hough grid cell) - min_line_length = 20 # minimum number of pixels making up a line - max_line_gap = 2 # maximum gap in pixels between connectable line segments - - # Run Hough on edge detected image - # Output "lines" is an array containing endpoints of detected line segments - lines = cv2.HoughLinesP( - edges, rho, theta, threshold, np.array([]), min_line_length, max_line_gap - ) - # lines[0] looks like [[x1, y1, x2, y2]] - # where (x1, y1) is the first end point and (x2, y2) is the second end point - - if lines is not None: - lines = lines.tolist() - lines.sort(key=line_length, reverse=True) - lines = lines[:10] - - # if line's y0 is below the average y0, it is a part of the top line, opposite for bottom line - avgy = sum([l[0][1] + l[0][3] for l in lines]) // (len(lines) * 2) - bot_lines = [l for l in lines if min(l[0][1], l[0][3]) > avgy] - top_lines = [l for l in lines if max(l[0][1], l[0][3]) < avgy] - - if len(bot_lines) > 0 and len(top_lines) > 0: - bot_angle = sum( - [np.arctan2(l[0][1] - l[0][3], l[0][0] - l[0][2]) for l in bot_lines] - ) / len(bot_lines) - top_angle = sum( - [np.arctan2(l[0][1] - l[0][3], l[0][0] - l[0][2]) for l in top_lines] - ) / len(top_lines) - - if bot_angle - top_angle > np.pi: - diff = (np.pi * 5 / 4 - (bot_angle - top_angle)) / 2 - else: - diff = (np.pi * 3 / 4 - (bot_angle - top_angle)) / 2 - - # abort if the detected marker segments are not close to 135 degrees apart at all - if diff > thresh: - if draw_figs: - cv2.imshow('lines bottom', frame) - cv2.imshow('lines top', frame) - cv2.imshow('frame with path marker angles', frame) - return None - - # # otherwise, make the two segments 135 degrees apart - # # Oh this might increase the weights given to bad data. meh - # bot_angle += diff - # top_angle -= diff - - if draw_figs: - line_image_bot = frame.copy() - line_image_top = frame.copy() - for l in bot_lines: - cv2.line(line_image_bot, tuple(l[0][0:2]), tuple(l[0][2:4]), 255, 5) - for l in top_lines: - cv2.line(line_image_top, tuple(l[0][0:2]), tuple(l[0][2:4]), 255, 5) - - cv2.imshow('lines bottom', line_image_bot) - cv2.imshow('lines top', line_image_top) - - cv2.imshow( - 'frame with path marker angles', - draw_marker_angles(frame, (bot_angle, top_angle)), - ) - - return bot_angle, top_angle - else: - if draw_figs: - cv2.imshow('lines bottom', frame) - cv2.imshow('lines top', frame) - cv2.imshow('frame with path marker angles', frame) - - return None - - -def path_marker_get_new_heading(cap, is_approaching, draw_figs=False): - """ Returns the next heading for the sub based on the path marker. - (heading is positive for a counterclockwise turn) - Takes an average of 10 frames. - @param cap a VideoCapture device, for example an .mp4 or a camera stream - @param is_approaching True: sub still wants to orient itself as it approaches - the path marker. Returns the angle for the bottom leg of the - path marker. - False: sub wants to orient itself towards wherever the path marker - points towards. Returns angle for the top leg of the path marker. - """ - angles = [] - test_angles = np.empty((10, 2)) # temporary. for testing porpoises - - # function aborts if the 10 most recent camera frames were invalid - ret_tries = 0 - frames_used = 0 - - while frames_used < 10 and ret_tries < 10: - ret, frame = cap.read() - if ret: - ret_tries = 0 - frames_used += 1 - frame = cv2.resize(frame, None, fx=0.5, fy=0.5) - - threshed = combined_filter(frame, False) - new_angles = find_path_marker(threshed, draw_figs) - - if new_angles is not None: - bot_angle, top_angle = new_angles - - if is_approaching: - angles.append(np.pi / 2 - bot_angle) - test_angles[len(angles) - 1] = (bot_angle, top_angle) - else: - # top_angle is always negative so compare it to - # -np.pi/2 - angles.append(-np.pi / 2 - top_angle) - test_angles[len(angles) - 1] = (bot_angle, top_angle) - if draw_figs: - cv2.waitKey(50) - else: - ret_tries += 1 - - if ( - ret_tries >= 10 or len(angles) < 3 - ): # does not return an answer if it was really unsure - return None - else: - if draw_figs: - cv2.imshow( - 'averaged angles', - draw_marker_angles( - frame, test_angles[: len(angles)].sum(axis=0) / len(angles) - ), - ) - return sum(angles) / len(angles) - - -def draw_marker_angles(frame, marker_angles, right=False): - """ Draws lines with the same angles as those in marker_angles off to - the side of the frame """ - - line_image = frame.copy() - bot_angle, top_angle = marker_angles - - h, w = frame.shape[:2] - if right: - x, y = w * 0.25, h * 0.5 - else: - x, y = w * 0.75, h * 0.5 - r = 20 - pt_mid = (int(x), int(y)) - pt_bot = (int(x + r * np.cos(bot_angle)), int(y + r * np.sin(bot_angle))) - pt_top = (int(x + r * np.cos(top_angle)), int(y + r * np.sin(top_angle))) - if frame.shape[2] == 1: - line_image = cv2.line(line_image, pt_mid, pt_bot, 255, 5) - line_image = cv2.line(line_image, pt_mid, pt_top, 255, 5) - else: - line_image = cv2.line(line_image, pt_mid, pt_bot, (255, 255, 255), 5) - line_image = cv2.line(line_image, pt_mid, pt_top, (255, 255, 255), 5) - - return line_image - - -########################################### -# Main Body -########################################### - -if __name__ == "__main__": - marker_angles = None - - # # For testing purposes - # # Thresholding is really bad if this starts at 650 - # for _ in range(600): - # cap.read() - - combined_filter = init_combined_filter() - - num_fails = 0 - while num_fails < 5: - # Instead of passing in cap, we can alternatively pass in the last 10 frames as a list - # but idk which is the better design choice - new_heading = path_marker_get_new_heading( - cap, is_approaching=True, draw_figs=True - ) - print('new heading:', new_heading) - - if new_heading is None: - num_fails += 1 - else: - num_fails = 0 - - k = cv2.waitKey(60) & 0xFF - if k == 27: # esc - break - - cv2.destroyAllWindows() - cap.release() diff --git a/tasks/path_marker/play_slots_detection.py b/tasks/path_marker/play_slots_detection.py deleted file mode 100644 index 0ebb4b5..0000000 --- a/tasks/path_marker/play_slots_detection.py +++ /dev/null @@ -1,234 +0,0 @@ -import numpy as np -import cv2 -import sys - -#### TODO: maybe look into pattern matching - -# Data fron the new course footage dropbox folder -cap = cv2.VideoCapture('../data/course_footage/play_slots_GOPR1142.mp4') - -detecting = True # Use hsv thresholding and rectangle detection. Always True -tracking = False # Use trackers to try to accomodate for failure -tracker_num = 5 # The tracker to use -testing = False # Show hsv sliders and threshold image. - -detection_interval = 10 # If tracking -fail_thresh = 5 # Number of detection failures before rectangle disappears from output -close_thresh = 30 # Pixel threshold used to reject dissimilar consecutive detection results -slots_size_thresh = 50 # Minimum area of detected slots slot -slots_dimension_thresh = 0.3 # Slot result's maximum % deviation from expected width height ratio - -# HSV threshold values. Can be changed during runtime if testing -# Use python3 play_slots_detection test to open testing mode -h_low = 96 -s_low = 82 -v_low = 131 -h_hi = 190 -s_hi = 180 -v_hi = 228 - -def nothing(x): - """Helper method for the trackbar""" - pass - -def test_hsv_thresholds(frame, has_input=False,h_low=None,s_low=None,v_low=None,h_hi=None,s_hi=None,v_hi=None): - hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) - - if has_input: - cv2.setTrackbarPos('h_low', 'contours', h_low) - cv2.setTrackbarPos('s_low', 'contours', s_low) - cv2.setTrackbarPos('v_low', 'contours', v_low) - cv2.setTrackbarPos('h_high', 'contours', h_hi) - cv2.setTrackbarPos('s_high', 'contours', s_hi) - cv2.setTrackbarPos('v_high', 'contours', v_hi) - - h_low = cv2.getTrackbarPos('h_low','contours') - s_low = cv2.getTrackbarPos('s_low','contours') - v_low = cv2.getTrackbarPos('v_low','contours') - h_hi = cv2.getTrackbarPos('h_high','contours') - s_hi = cv2.getTrackbarPos('s_high','contours') - v_hi = cv2.getTrackbarPos('v_high','contours') - - mask = cv2.inRange(hsv, np.array([h_low,s_low,v_low]), np.array([h_hi,s_hi,v_hi])) - res = cv2.bitwise_and(frame,frame, mask= mask) - - cv2.imshow('contours', res) - - return h_low, s_low, v_low, h_hi, s_hi, v_hi - -def hsv_threshold(frame, _h_low, s_low, v_low, h_hi, s_hi, v_hi, tries=0): - global h_low - h_low = _h_low - hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) - mask = cv2.inRange(hsv, np.array([h_low,s_low,v_low]), np.array([h_hi,s_hi,v_hi])) - res = cv2.bitwise_and(frame,frame, mask= mask) - - # Threshold depend on whether the sub is close to or far from the target - if tries < 3: - if np.count_nonzero(res) > res.shape[0]*res.shape[1] * 0.02: - # narrow the threshold and retry - h_low += 1 - res = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi, tries+1) - if np.count_nonzero(res) < res.shape[0]*res.shape[1] * 0.005: - # widen the threshold and retry - h_low -= 1 - res = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi, tries+1) - return res - -def filter_for_rectangles(contours): - rects = [] - for c in contours: - peri = cv2.arcLength(c, True) - approx = cv2.approxPolyDP(c, 0.1 * peri, True) - if len(approx) == 4 or len(approx) == 8: - rects.append(c) - return rects - -def find_red_slots_hole(frame, size_thresh, dimension_thresh): - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - edges = cv2.Canny(gray, 100, 150) - - im, contours, hierarchy = cv2.findContours(edges,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE) - - def get_area(rect): - return rect[1][0] * rect[1][1] - def wh_ratio(rect): - return max(rect[1])/min(rect[1]) - def dim_ratio(rect, reference): - return abs(reference-wh_ratio(rect))/reference - def is_open(h): - return h[2] < 0 - - # contours = filter_for_rectangles(contours) # Makes it worse right now lol - - # Take the first few contours and find the one that fits the dimensions the best - # The play slots rectangle is square - contours = [cv2.minAreaRect(c) for c in contours] - contours = [c for c in contours if get_area(c) > size_thresh] - - if len(contours) > 0: - contours = [c for c,h in zip(contours, hierarchy[0]) if dim_ratio(c,1/1) 0: - return contours[0] - else: - return None - -def close_to(rect1, rect2, threshold): - ### returns whether rect1 is close to rect2 based on threshold - if rect1 is None: - return True - dx, dy = rect1[0][0]-rect2[0][0], rect1[0][1]-rect2[0][1]; - return dx**2 + dy**2 < threshold * threshold - -def init_tracker(tracker_num): - # tracker_types: ['BOOSTING', 'MIL','KCF', 'TLD', 'MEDIANFLOW', 'GOTURN', 'MOSSE', 'CSRT'] - if tracker_num == 1: - tracker = cv2.TrackerBoosting_create() - elif tracker_num == 2: - tracker = cv2.TrackerMIL_create() - elif tracker_num == 3: - tracker = cv2.TrackerKCF_create() - elif tracker_num == 4: - tracker = cv2.TrackerTLD_create() - elif tracker_num == 5: - tracker = cv2.TrackerMedianFlow_create() - elif tracker_num == 6: - tracker = cv2.TrackerGOTURN_create() - elif tracker_num == 7: - tracker = cv2.TrackerMOSSE_create() - elif tracker_num == 8: - tracker = cv2.TrackerCSRT_create() - else: - print("Invalid tracker number") - exit() - return tracker - -########################################### -# Main Body -########################################### - -if __name__ == "__main__": - if len(sys.argv) > 0: - if "test" in sys.argv: - testing = True - cv2.namedWindow('contours') - cv2.createTrackbar('h_low','contours',h_low,255,nothing) - cv2.createTrackbar('s_low','contours',s_low,255,nothing) - cv2.createTrackbar('v_low','contours',v_low,255,nothing) - cv2.createTrackbar('h_high','contours',h_hi,255,nothing) - cv2.createTrackbar('s_high','contours',s_hi,255,nothing) - cv2.createTrackbar('v_high','contours',v_hi,255,nothing) - - tracker = init_tracker(tracker_num) - slots_hole = None - num_failures = 0 - time_since_detection = 0 - - # # For testing purposes - # for _ in range(500): - # cap.read() - - while(1): - ret ,frame = cap.read() - - if ret == True: - frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5) - - if testing: - h_low, s_low, v_low, h_hi, s_hi, v_hi = test_hsv_thresholds(frame,True,h_low,s_low,v_low,h_hi,s_hi,v_hi) - - if time_since_detection >= detection_interval: - detecting = True - - if detecting and tracking: - hsv_thresh = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi) - new_slots_hole = find_red_slots_hole(hsv_thresh, slots_size_thresh, slots_dimension_thresh) - if new_slots_hole is not None and close_to(slots_hole, new_slots_hole, close_thresh): - num_failures = 0 - slots_hole = new_slots_hole - tracker = init_tracker(tracker_num)# does this have to happen every time? - tracker.init(frame, slots_hole[0]+slots_hole[1]) - detecting = False - time_since_detection = 0 - else: - num_failures += 1 - elif detecting and not tracking: - hsv_thresh = hsv_threshold(frame, h_low, s_low, v_low, h_hi, s_hi, v_hi) - new_slots_hole = find_red_slots_hole(hsv_thresh, slots_size_thresh, slots_dimension_thresh) - if new_slots_hole is not None and close_to(slots_hole, new_slots_hole, close_thresh): - num_failures = 0 - slots_hole = new_slots_hole - else: - num_failures += 1 - elif tracking: - ret, bounding_box = tracker.update(frame) - if ret: - num_failures = 0 - slots_hole = (bounding_box[:2], bounding_box[2:4], slots_hole[2]) - else: - num_failures += 1 - if num_failures > fail_thresh: - slots_hole = None - detecting = True - - # draw slots hole onto original image - slots_img = frame.copy() - if slots_hole != None: - box = np.int0(cv2.boxPoints(slots_hole)) - slots_img = cv2.drawContours(slots_img, [box], 0, (0,255,0), 2) - cv2.imshow("slots hole", slots_img) - - - time_since_detection += 1 - k = cv2.waitKey(60) & 0xff - if k == 27: - if testing: - print("hsv thresholds:") - print(h_low, s_low, v_low, h_hi, s_hi, v_hi) - break - - cv2.destroyAllWindows() - cap.release() diff --git a/tasks/sanity_test.py b/tasks/sanity_test.py deleted file mode 100644 index e3d07a3..0000000 --- a/tasks/sanity_test.py +++ /dev/null @@ -1,65 +0,0 @@ -import multiprocessing -import pytest - -def sanity_test(algorithm, test_imgs): - """ - Runs a sanity test on the algorithm that checks for run time and general exceptions. - - Args: - algorithm: object that extends TaskPerceiver - - Example usage: - ##### In Algorithm1.py ##### - class Algorithm1(TaskPerceiver): - def analyze(self, frame, debug): - pass - - ##### In test_Algorithm1.py ##### - from sanity_test import sanity_test - import pytest - - # Some function that returns test images. This is scoped to this file/module. - def get_test_imgs(): - return [None, None, None] - - @pytest.mark.parametrize("algorithm", [Algorithm1()]) - @pytest.mark.parametrize("test_imgs", [get_test_imgs()]) - def test_sanity(algorithm, test_imgs): - sanity_test(algorithm, test_imgs) - """ - - MAX_RUNTIME = 3 # Per call to analyze() in seconds - NUM_RUNS = 3 # Number of calls to analyze() - - if len(test_imgs) < NUM_RUNS: - pytest.fail("Received less than {} test images".format(NUM_RUNS)) - - # Run analyze() NUM_RUNS times and be ready to stop it if it takes too long - pconn, cconn = multiprocessing.Pipe() - p = multiprocessing.Process(target=lambda: run_algorithm(algorithm, test_imgs, NUM_RUNS, cconn)) - p.start() - - # Wait for MAX_RUNTIME * NUM_RUNS seconds or until the process finishes - p.join(MAX_RUNTIME * NUM_RUNS) - - # If thread is still active, it took too long to finish - if p.is_alive(): - p.terminate() - p.join() - pytest.fail("analyze() took over {} seconds with {} iterations." - .format(MAX_RUNTIME * NUM_RUNS, NUM_RUNS)) - # Check for exceptions - if pconn.poll(): - error = pconn.recv() - pytest.fail("analyze() encountered exception '{}' with test image {}".format(error[0], error[1])) - -def run_algorithm(algorithm, test_imgs, num_runs, cconn): - """ Wrapper function to run the algorithm on a separate thread. """ - for i in range(num_runs): - try: - algorithm.analyze(test_imgs[i], True) - except Exception as e: - # Send the error message and the image number back to the main thread - cconn.send((e, i)) - break - \ No newline at end of file diff --git a/tasks/segmentation/GateTaskExample.py.orig b/tasks/segmentation/GateTaskExample.py.orig deleted file mode 100644 index 5563092..0000000 --- a/tasks/segmentation/GateTaskExample.py.orig +++ /dev/null @@ -1,86 +0,0 @@ -from TaskPerceiver import TaskPerceiver -from typing import Tuple -from sys import argv as args -from combinedFilter import init_combined_filter -import numpy as np -import cv2 as cv -#from segmentation.aggregateRescaling import init_aggregate_rescaling - -class GateTask(TaskPerceiver): - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze - debug: Whether or not tot display intermediate images for debugging - - Returns: - (x,y) coordinate with center of gate - """ -<<<<<<< HEAD - filtered_frame_copies = [filtered_frame for _ in range[10]] - np.stack(filtered_frame_copies, axis = -1) - mask = cv.inRange(filtered_frame, np.array[190], ) - - filtered_frame = combined_filter(frame, display_figs=False) - if debug: - return ((250, 250), filtered_frame) -======= - filtered_frame = combined_filter(frame, display_figs=False) - filtered_frame_copies = [filtered_frame for _ in range(3)] - stacked_filter_frames = np.concatenate(filtered_frame_copies, axis = 2) - mask = cv.inRange(stacked_filter_frames, - np.array([100, 100, 100]), np.array([255, 255, 255])) - _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - if contours: - cnt = max(contours, key=self.findStraightness)#lambda x: cv.minAreaRect(x)[1][0] * cv.minAreaRect(x)[1][1]) - # sorted_straight = sorted(contours, key=self.findStraightness) - # sorted_size = sorted(contours, key=cv.contourArea) - #todo: use these sorted lists and weights to each value to give two best values - rect = cv.minAreaRect(cnt) - boxpts = cv.boxPoints(rect) - box = np.int0(boxpts) - cv.drawContours(stacked_filter_frames,[box],0,(0,0,255),5) - for corner in boxpts: - cv.circle(stacked_filter_frames, (corner[0], corner[1]), 10, (0,0,255), -1) - - if debug: - return ((250, 250), stacked_filter_frames) ->>>>>>> origin/gate-task-example - return (250, 250) - - def findStraightness(self, contour): # output number = contour area/convex area, the bigger the straightest - hull = cv.convexHull(contour, False) - contour_area = cv.contourArea(contour) - hull_area = cv.contourArea(hull) - return 10 * contour_area + 3 * (hull_area - contour_area) - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(args[1]) - ret_tries = True - gate_task = GateTask() - once = False - while 1 and ret_tries < 50: - ret, frame = cap.read() - if ret: - frame = cv.resize(frame, None, fx=0.4, fy=0.4) - - - ### FUNCTION CALL, can change this - (x, y), filtered_frame = gate_task.analyze(frame, True) - cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - if not once: - print(filtered_frame) - once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 diff --git a/tasks/segmentation/aggregateRescaling.py b/tasks/segmentation/aggregateRescaling.py deleted file mode 100644 index 3e2d9ea..0000000 --- a/tasks/segmentation/aggregateRescaling.py +++ /dev/null @@ -1,80 +0,0 @@ -import cv2 as cv -from sys import argv as args -import numpy as np -import numpy.linalg as LA - -# Jenny -> unsigned ints fixed the problem -# Damas -> flip weight vector every frame - -# man/min of past ten frames; average or total -def init_aggregate_rescaling(show_frame=True): - only_once = False - weights = [] - max_min = {'max': 90, 'min': -20} - - def aggregate_rescaling(frame): # you only pca once - nonlocal only_once - nonlocal weights - nonlocal max_min - - frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - - r, c, d = frame.shape - A = np.reshape(frame, (r * c, d)) - - if not only_once: - - A_dot = A - A.mean(axis=0)[np.newaxis, :] - - _, eigv = LA.eigh(A_dot.T @ A_dot) - weights = eigv[:, 0] - - red = np.reshape(A_dot @ weights, (r, c)) - only_once = True - else: - red = np.reshape(A @ weights, (r, c)) - - if np.min(red) < max_min['min']: - max_min['min'] = np.min(red) - if np.max(red) > max_min['max']: - max_min['max'] = np.max(red) - - red -= max_min['min'] - red *= 255.0 / (max_min['max'] - max_min['min']) - """ - if False:#not paused: - print(np.min(red), np.max(red), max_min['min'], max_min['max']) - """ - red = red.astype(np.uint8) - red = np.expand_dims(red, axis=2) - red = np.concatenate((red, red, red), axis=2) - - if show_frame: - cv.imshow('frame', frame_gray) - cv.imshow('One Time PCA plus all time aggregate rescaling', red) - return red - - return aggregate_rescaling - - -paused = False -speed = 1 -if __name__ == '__main__': - cap = cv.VideoCapture(args[1]) - agg_res = init_aggregate_rescaling() - while True: - if not paused: - for _ in range(speed): - ret, frame = cap.read() - if ret: - frame = cv.resize(frame, (0, 0), fx=0.5, fy=0.5) - agg_res(frame) - key = cv.waitKey(30) - if key == ord('q') or key == 27: - break - if key == ord('p'): - paused = not paused - if key == ord('i') and speed > 1: - speed -= 1 - if key == ord('o'): - speed += 1 diff --git a/tasks/segmentation/combinedFilter.py b/tasks/segmentation/combinedFilter.py deleted file mode 100644 index 6338a05..0000000 --- a/tasks/segmentation/combinedFilter.py +++ /dev/null @@ -1,58 +0,0 @@ -import cv2 -import numpy as np - -import sys -import os -from sys import argv as args -sys.path.append(os.path.dirname(__file__)) -from aggregateRescaling import init_aggregate_rescaling -from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim - -if __name__ == "__main__": - if args[1] == '0': - cap = cv2.VideoCapture(0) - else: - cap = cv2.VideoCapture(args[1]) - -# Returns a grayscale image -def init_combined_filter(): - aggregate_rescaling = init_aggregate_rescaling(False) - - def combined_filter(frame, custom_weights=None, display_figs=False, print_weights=False): - pca_frame = aggregate_rescaling(frame) # this resizes the frame within its body - - __, other_frame = filter_out_highest_peak_multidim( - np.dstack([pca_frame[:,:,0], frame]), - custom_weights=custom_weights, - print_weights=print_weights) - - other_frame = other_frame[:,:,:1] - - if display_figs: - cv2.imshow('original', frame) - cv2.imshow('Aggregate Rescaling via PCA', pca_frame) - cv2.imshow('Peak Removal Thresholding after PCA', other_frame) - return other_frame - return combined_filter - -if __name__ == "__main__": - ret = True - ret_tries = 0 - - # for i in range(3000): - # cap.read() - - combined_filter = init_combined_filter() - - while 1 and ret_tries < 50: - ret, frame = cap.read() - if ret: - frame = cv2.resize(frame, None, fx=0.4, fy=0.4) - filtered_frame = combined_filter(frame, display_figs=True) - - ret_tries = 0 - k = cv2.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 diff --git a/tasks/segmentation/peak_removal_adaptive_thresholding.py b/tasks/segmentation/peak_removal_adaptive_thresholding.py deleted file mode 100644 index 892dc02..0000000 --- a/tasks/segmentation/peak_removal_adaptive_thresholding.py +++ /dev/null @@ -1,570 +0,0 @@ -import cv2 -import numpy as np -import matplotlib.pyplot as plt -from scipy.signal import find_peaks, peak_widths -from sys import argv as args -######################################################################## -# An attempt at an adaptive thresholding algorithm based on the frequency -# of pixel values ("peaks" if looking at a histogram of # pixels vs pixel value of a frame) -# -# *1. *** best of the three *** filter_out_highest_peak_multidim -# pools together how "peak-like" each pixel is in all of the color channels -# of the frame to make a final decision on what is the background -# 2. init_filter_out_highest_peak -# gets rid of large peaks in many different color channels individually -# 3. remove_blotchy_chunks -# places a mask over areas that have lots of edges, which in many cases -# is equivalent to places with lots of noise -######################################################################## - -if __name__ == "__main__": - testing = False - - cap = cv2.VideoCapture('../data/course_footage/GOPR1142.MP4') - # No thresholds - h_low = 0 - s_low = 0 - v_low = 0 - h_hi = 255 - s_hi = 255 - v_hi = 255 - - # cap = cv2.VideoCapture('../data/course_footage/path_marker_GOPR1142.mp4') - # # Path marker default - # h_low = 31 - # s_low = 28 - # v_low = 179 - # h_hi = 79 - # s_hi = 88 - # v_hi = 218 - - # cap = cv2.VideoCapture('../data/course_footage/play_slots_GOPR1142.MP4') - # # Play slots default - # h_low = 96 - # s_low = 82 - # v_low = 131 - # h_hi = 190 - # s_hi = 180 - # v_hi = 228 - - # cap = cv2.VideoCapture(0) - - # thresholds_used = [h_low, s_low, v_low, h_hi, s_hi, v_hi] - -def init_test_hsv_thresholds(thresholds): - # Keep track of previous threhold values to see if the user is using the trackbar - # is there a function that detects whether the mouse button is down? - prev_h_low, prev_s_low, prev_v_low, prev_h_hi, prev_s_hi, prev_v_hi = thresholds - - def nothing(x): - """Helper method for the trackbar""" - pass - - cv2.namedWindow('ideal thresholding') - cv2.createTrackbar('h_low','ideal thresholding',h_low,255,nothing) - cv2.createTrackbar('s_low','ideal thresholding',s_low,255,nothing) - cv2.createTrackbar('v_low','ideal thresholding',v_low,255,nothing) - cv2.createTrackbar('h_high','ideal thresholding',h_hi,255,nothing) - cv2.createTrackbar('s_high','ideal thresholding',s_hi,255,nothing) - cv2.createTrackbar('v_high','ideal thresholding',v_hi,255,nothing) - - def test_hsv_thresholds(frame, thresholds): - nonlocal prev_h_low, prev_s_low, prev_v_low, prev_h_hi, prev_s_hi, prev_v_hi - - h_low_track = cv2.getTrackbarPos('h_low','ideal thresholding') - s_low_track = cv2.getTrackbarPos('s_low','ideal thresholding') - v_low_track = cv2.getTrackbarPos('v_low','ideal thresholding') - h_hi_track = cv2.getTrackbarPos('h_high','ideal thresholding') - s_hi_track = cv2.getTrackbarPos('s_high','ideal thresholding') - v_hi_track = cv2.getTrackbarPos('v_high','ideal thresholding') - - if h_low_track!=prev_h_low or s_low_track!=prev_s_low or v_low_track!=prev_v_low \ - or h_hi_track!=prev_h_hi or s_hi_track!=prev_s_hi or v_hi_track!=prev_v_hi: - # If user is adjusting the trackbars, use the user input - thresholds_used = [h_low_track, s_low_track, v_low_track, h_hi_track, s_hi_track, v_hi_track] - else: - # Otherwise, copy program data to trackbars - thresholds_used = thresholds - cv2.setTrackbarPos('h_low', 'ideal thresholding', thresholds_used[0]) - cv2.setTrackbarPos('s_low', 'ideal thresholding', thresholds_used[1]) - cv2.setTrackbarPos('v_low', 'ideal thresholding', thresholds_used[2]) - cv2.setTrackbarPos('h_high', 'ideal thresholding', thresholds_used[3]) - cv2.setTrackbarPos('s_high', 'ideal thresholding', thresholds_used[4]) - cv2.setTrackbarPos('v_high', 'ideal thresholding', thresholds_used[5]) - - hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) - mask = cv2.inRange(hsv, np.array(thresholds_used[:3]), np.array(thresholds_used[3:])) - res = cv2.bitwise_and(frame,frame, mask= mask) - - cv2.imshow('ideal thresholding', res) - - prev_h_low, prev_s_low, prev_v_low, prev_h_hi, prev_s_hi, prev_v_hi = thresholds_used - return thresholds_used - - return test_hsv_thresholds - -def hsv_threshold(frame, thresh_used): - hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) - mask = cv2.inRange(hsv, np.array(thresh_used[:3]), np.array(thresh_used[3:])) - res = cv2.bitwise_and(frame,frame, mask= mask) - return thresh_used, res - -def disp_hist(frame, title, labels, colors): - frame0 = frame[:,:,0].flatten() - frame0 = frame0[frame0 > 0] - - frame1 = frame[:,:,1].flatten() - frame1 = frame1[frame1 > 0] - - frame2 = frame[:,:,2].flatten() - frame2 = frame2[frame2 > 0] - - plt.figure(hash(title)) - plt.clf() - ax = plt.gca() - ax.set_xlim([0, 255]) - - plt.hist(frame0, alpha=0.5, label=labels[0], color=colors[0]) - plt.hist(frame1, alpha=0.5, label=labels[1], color=colors[1]) - plt.hist(frame2, alpha=0.5, label=labels[2], color=colors[2]) - plt.title(title) - plt.legend() - plt.draw() - -def find_peak_ranges(frame, display_plots=False, title=None, labels=None, colors=None): - """ Finds a returns the widest peak's x-range in all three channels of frame - Result is formatted to fit cv2.inRange() -> ((low1, low2, low3), (hi1, hi2, hi3)) - Shape of frame must have 3 dimensions (pass in np.expand_dims(frame, 2) if erroring) """ - - # TODO: Maybe use a different combination of peak characteristics to more accurately - # select the entire peak (only the tip is selected right now) - peak_width_height = 0.95 # How far down the peak that the algorithm draws - # the horizontal width line - - def find_highest_peak(channel, display_plots=False): - """ Finds and returns the x-range of the highest peak in the - given channel of frame """ - - f = frame[:, :, channel].flatten() - - # Some semi-hardcoded values :) - num_bins = max(int((np.amax(f)-np.amin(f)) / 4), 10) - # num_bins = 30 - - hist, bins = np.histogram(f, bins=num_bins) - - hist[0] = 0 # get rid of stuff that was thresholded to 0 - hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak - bins = np.hstack([bins, [bins[bins.shape[0]-1] + 1]]) - - peaks, properties = find_peaks(hist, height=0.1) - if len(peaks) > 0: - i = np.argmax(properties['peak_heights']) - widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] - # i = np.argmax(widths) - largest_peak = (int((bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]//2), - int((bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]//2)) # beginning and end of the peak - - if display_plots: - ax = plt.gca() - print(max(f)) - ax.set_xlim([0, max(255, max(f))]) - #Plot values in this channel - plt.plot(bins[1:],hist, label=labels[channel], color=colors[channel]) - # Plot peaks - plt.plot(bins[peaks+1], hist[peaks], "x") - # Plot peak widths - plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) - else: - largest_peak = (0, 0) - - return largest_peak - - if display_plots: - fig = plt.figure(hash(title)) - plt.clf() - - background = (np.empty(frame.shape[2]),np.empty(frame.shape[2])) - for channel in range(frame.shape[2]): - low, high = find_highest_peak(channel, display_plots) - background[0][channel] = low - background[1][channel] = high - - if display_plots: - plt.title(title) - plt.legend() - plt.draw() - - return background - -def plot_peaks(frame, title, labels, colors): - # Shh this is just a helper function that makes the code more readable - # Not to be used in practice. - # NOTE: you need to call plt.pause(0.001) afterwards to render the plot - find_peak_ranges(frame, True, title, labels, colors) - -def init_filter_out_highest_peak(filters, return_colorspace="any", input_colorspace="bgr"): - """ Takes in an hsv image! Returns an hsv image""" - # low pass filter - # vk* = vk*lambda + v*(1-lambda) - # lambda = 0.9-0.4 - - prev_hsv_threshes = [[] for i in range(len(filters))] - hsv_labels = (('H','S','V'), ("red","purple","gray")) - bgr_labels = (('B','G','R'), ("blue","green","red")) - - # Figure out how the procedure to convert among hsv and bgr. - # Format of stuff in fitler_fns: - # [<'c' convert or 'f' filter>, ] - filter_fns = [] - curr_color = input_colorspace - for f in filters: - if f != curr_color: - filter_fns.append(['c',f]) - curr_color = f - filter_fns.append(['f',f]) - if return_colorspace != "any" and return_colorspace != curr_color: - filter_fns.append(['c', return_colorspace]) - - def filter_out_highest_peak(frame, cache, display_plots=False, title=None, labels=None, colors=None): - - background_thresh = find_peak_ranges(frame, display_plots, title, labels, colors) - raw_thresh = background_thresh - # multiply everything in cache by (1-lpf_lambda) - if len(cache) > 0: - # cache = np.array(cache) * (1-lpf_lambda) - # calculate average - for i in range(2): - for j in range(3): - background_thresh[i][j] = (background_thresh[i][j] + sum([c[i][j] for c in cache])) // (len(cache) + 1) - - background_mask = cv2.bitwise_not(cv2.bitwise_or( - cv2.inRange(frame[:, :, 0], background_thresh[0][0], background_thresh[1][0]), - cv2.inRange(frame[:, :, 1], background_thresh[0][1], background_thresh[1][1]), - cv2.inRange(frame[:, :, 2], background_thresh[0][2], background_thresh[1][2]) - )) - no_background = cv2.bitwise_and(frame,frame, mask=background_mask) - - return background_thresh, raw_thresh, no_background - - def combine_threshes(th1, th2): - return ([min(th1[0][0], th2[0][0]), min(th1[0][1], th2[0][1]), min(th1[0][2], th2[0][2])], - [max(th1[1][0], th2[1][0]), max(th1[1][1], th2[1][1]), max(th1[1][2], th2[1][2])]) - - def bgr_thresh2hsv_thresh(th): - th = cv2.cvtColor(np.array([[th[0]], [th[1]]], np.uint8), cv2.COLOR_BGR2HSV).tolist() - return ([min(th[0][0][0], th[1][0][0]), min(th[0][0][1], th[1][0][1]), min(th[0][0][2], th[1][0][2])], - [max(th[0][0][0], th[1][0][0]), max(th[0][0][1], th[1][0][1]), max(th[0][0][2], th[1][0][2])]) - - - def do_filter(frame, display_plots=False): - nonlocal prev_hsv_threshes - if len(prev_hsv_threshes[0]) == lpf_cache_size: - for x in prev_hsv_threshes: - x.pop(0) - - filter_index = 0 - for f in filter_fns: - if f[0] == 'c': - if f[1] == "hsv": - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) - else: - # f[1] == "bgr" - frame = cv2.cvtColor(frame, cv2.COLOR_HSV2BGR) - else: - if f[1] == "hsv": - thresh, raw_thresh, frame = filter_out_highest_peak(frame, prev_hsv_threshes[filter_index]) - prev_hsv_threshes[filter_index].append(raw_thresh) - else: - # f[1] == "bgr" - thresh, raw_thresh, frame = filter_out_highest_peak(frame, prev_hsv_threshes[filter_index]) - thresh = bgr_thresh2hsv_thresh(thresh) - prev_hsv_threshes[filter_index].append(raw_thresh) - filter_index += 1 - - # Doesn't do anything :c - # frame = cv2.fastNlMeansDenoising(frame) - - # # # Post processing - # # Performs badly if there is a lot of noise or if there is no noise at all around targets - # frame = remove_blotchy_chunks(frame, iterations=1, display_imgs=True) - # cv2.imshow('after antiblotchy', frame) - - # kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5, 5)) - # frame = cv2.morphologyEx(frame, cv2.MORPH_OPEN, kernel) - # cv2.imshow('after open', frame) - - return frame - - return do_filter - -def keep_highest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, title=None, label='1', color='blue'): - """ Returns a mask for the frame that keeps the num_peaks highest peaks in the histogram of - pixel values. - Only works for grayscale/1-channel images (to speed this up) - Shape of frame must have 3 dimensions (pass in np.expand_dims(frame, 2) if erroring) """ - # Some semi-thresholded values :) - num_bins = max(int((np.amax(frame)-np.amin(frame)) / 4), 10) - hist, bins = np.histogram(frame, bins=num_bins) - hist[0] = 0 # get rid of stuff that was thresholded to 0 - hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak - bins = np.hstack([bins, [bins[bins.shape[0]-1] + 1]]) - - peaks, properties = find_peaks(hist, prominence=100) - widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] - - if len(peaks) > 0: - i = len(peaks) - 1 - mask = cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2) - - # To support keeping multiple peaks - for j in range(num_peaks - 1): - i = len(peaks) - 2 - j - if i >= 0: - mask = cv2.bitwise_or(cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i], - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]), mask) - # frame = cv2.bitwise_and(frame, frame, mask=mask) - else: - mask = np.ones(frame.shape, np.uint8) - - if display_plots: - fig = plt.figure(hash(title)) - plt.clf() - - ax = plt.gca() - ax.set_xlim([0, 255]) - #Plot values in this channel - plt.plot(bins[1:],hist, label=label, color=color) - # Plot peaks - plt.plot(bins[peaks+1], hist[peaks], "x") - # Plot peak widths - plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) - - plt.title(title) - plt.legend() - plt.draw() - - return mask - -def delete_lowest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, title=None, label='1', color='blue'): - """ Returns a mask for the frame that deletes the num_peaks lowest-valued peaks in the histogram of - pixel values. - Only works for grayscale/1-channel images (to speed this up) """ - - # Some semi-thresholded values :) - num_bins = max(int((np.amax(frame)-np.amin(frame)) / 4), 10) - hist, bins = np.histogram(frame, bins=num_bins) - hist[0] = 0 # get rid of stuff that was thresholded to 0 - - peaks, properties = find_peaks(hist, prominence=100) - widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] - - if len(peaks) > 0: - i = 0 - mask = cv2.bitwise_not(cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2)) - - # To support deleting multiple peaks - for j in range(num_peaks - 1): - i = j + 1 - if len(peaks) > i: - mask = cv2.bitwise_and(cv2.bitwise_not(cv2.inRange( - frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2)), mask) - else: - mask = np.ones(frame.shape, np.uint8) - # frame = cv2.bitwise_and(frame, frame, mask=mask) - - if display_plots: - fig = plt.figure(hash(title)) - plt.clf() - - ax = plt.gca() - ax.set_xlim([0, 255]) - #Plot values in this channel - plt.plot(bins[1:],hist, label=label, color=color) - # Plot peaks - plt.plot(bins[peaks+1], hist[peaks], "x") - # Plot peak widths - plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) - - plt.title(title) - plt.legend() - plt.draw() - - return mask - -def remove_blotchy_chunks(frame, kernel_size=201, iterations=1, display_imgs=False): - """ Works best when object isn't surrounded by blotchy stuff """ - edges = cv2.Canny(frame, 100, 150) - - blurred = edges.copy() - for _ in range(iterations): - blurred = cv2.GaussianBlur(edges, (kernel_size, kernel_size), -1) - - ret, mask = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) - - result = cv2.bitwise_and(frame, frame, mask=mask) - - if display_imgs: - cv2.imshow('original', frame) - cv2.imshow('edges', edges) - cv2.imshow('blurred', blurred) - cv2.imshow('mask', mask) - cv2.imshow('blotchy result', result) - - return result - -def filter_out_highest_peak_multidim(frame, res=69, percentile=10, custom_weights=None, print_weights=False): - """ Estimates the "peak-ness" of each pixel in frame across color channels - and thresholds out pixels that were "peak-like" in many colorspaces. - frame is a stack of color channels (np.dstack()) and this will consider all channels - in the final calculation - - @param res Resolution. Higher number is lower resolution - @param percentile Threshold for pixels to keep in the overall_votes array - List of colorspaces that can be converted to from BGR: - https://docs.opencv.org/3.4/d8/d01/group__imgproc__color__conversions.html#gga4e0972be5de079fed4e3a10e24ef5ef0a2a80354788f54d0bed59fa44ea2fb98e - - HSV, GRAY, Lab, XYZ, YCrCb, Luv, HLS, YUV - "Theoretically", the more colorspaces you consider, the better? But noise is added """ - - from math import log - def get_peak_votes(frame): - """ Takes in a single-channel frame and returns an array that contains - the number of other pixels with the same value at every pixel """ - dist = np.bincount(frame.flatten()) - - # Set the recommended weight to (the spread of the pixel values from 0-255) - # recommended_weight = abs(np.subtract(*np.percentile(np.nonzero(dist), [75, 25])) - (255//2)) - recommended_weight = abs((np.max(np.nonzero(dist)) - np.min(np.nonzero(dist)))) - # Stretch out extremes - # recommended_weight = pow(2, recommended_weight//8) - - if res == 1: - vote_arr = dist[frame] - else: - dist = np.array([np.mean(dist[i*res:i*res+res]) for i in range(len(dist) // res + 1)]) - vote_arr = dist[frame // res] - - return recommended_weight, vote_arr - - overall_votes = np.zeros(frame.shape[:2], np.uint8) - overall_mask = np.zeros(frame.shape[:2], np.uint8) - - if print_weights: - print('------------------------', custom_weights) - for ch in range(frame.shape[2]): - weight, vote_arr = get_peak_votes(frame[:,:,ch]) - if custom_weights is not None: - weight = custom_weights[ch] - if print_weights: - print(weight) - overall_votes = overall_votes + vote_arr * weight - - # Sometimes returns no pixels - thresh = np.mean(overall_votes) - 2 * np.std(overall_votes) - - # thresh = np.percentile(overall_votes, percentile) - - # Only keep pixels that were very un-peak-like in every colorspace - overall_mask[overall_votes <= thresh] = 255 - - return overall_votes, cv2.bitwise_and(frame, frame, mask=overall_mask) - -def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): - """ Attempts to use kmeans to segment the frame into num_group features - (not including the background), denoted by a very large value in votes. - votes is an output of the filter_out_highest_peak_multidim() function - Output: frame_shape x num_groups 3D matrix. Get a group mapped to the - frame by doing groups[:,:,group_num] """ - votes = np.float32(votes).flatten() - - # Make kmeans only consider the non-background pixels - background = np.zeros(votes.shape) - background[votes>=np.percentile(votes, percentile)] = 1 - cluster_data = votes[background==0] - cluster_indexes = np.array(range(len(votes)))[background==0] - - # Do kmeans - criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) - flags = cv2.KMEANS_RANDOM_CENTERS - compactness,labels,centers = cv2.kmeans(cluster_data,num_groups,None,criteria,10,flags) - - # Reconstruct the original votes array with background's label = -1 - label_arr = np.empty(votes.shape) - label_arr[background==1] = -1 - for i in range(num_groups): - label_arr[cluster_indexes[labels.flatten()==i]] = i - - unique_labels, label_counts = np.unique(label_arr, return_counts=True) - label_order = list(range(np.int0(np.amax(unique_labels)) + 2)) # something is erroring here - if len(label_counts) < num_groups + 1: - # add in a slot for the background if no background is found - label_counts = np.insert(label_counts, 0, 0) - - label_order.sort(key=lambda x: label_counts[x]) - - groups = np.empty((frame_shape[0], frame_shape[1], num_groups + 1)) - for i, l in enumerate(label_order): - group = np.zeros(votes.shape) - group[label_arr.flatten()==l - 1] = 255 - groups[:,:,i] = np.reshape(group, frame_shape[:2]) - - # for i in range(len(unique_labels)): - # cv2.imshow(str(i) + " label", groups[:,:,i]) - - return groups - -########################################### -# Main Body -########################################### - -if __name__ == "__main__": - # For testing porpoises - cap = cv2.VideoCapture(args[1]) - ret, frame = cap.read() - out = cv2.VideoWriter('out.avi',cv2.VideoWriter_fourcc('M','J','P','G'), 30.0, (int(frame.shape[1]*0.4), int(frame.shape[0]*0.4))) - - if testing: - test_hsv_thresholds = init_test_hsv_thresholds(thresholds_used) - - filter_peaks = init_filter_out_highest_peak(['hsv,', 'bgr', 'hsv'], 'hsv') - - ret_tries = 0 - - while (1 and ret_tries < 50): - ret, frame = cap.read() - - if ret: - frame = cv2.resize(frame, None, fx=0.4, fy=0.4) - - votes, multi_filter1 = filter_out_highest_peak_multidim(np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)]), custom_weights=[1, 1, 1, 1, 1, 1]) - votes, multi_filter2 = filter_out_highest_peak_multidim(np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)])) - multi_filter1 = multi_filter1[:, :, :3] - multi_filter2 = multi_filter2[:, :, :3] - - # kmeans_groups = k_means_segmentation(votes, frame.shape) - - cv2.imshow('original', frame) - # cv2.imshow('multi_filter bgr', multi_filter1) - cv2.imshow('multi_filter bgr recommended', multi_filter2) - - # for i in range(kmeans_groups.shape[2]): - # cv2.imshow('Kmeans group ' + str(i), kmeans_groups[:,:,i]) - - # For testing porpoises - # out.write(filtered2) - - # Update all of the plt charts - plt.pause(0.001) - - ret_tries = 0 - k = cv2.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 - cv2.destroyAllWindows() - cap.release() - out.release() \ No newline at end of file diff --git a/tasks/spinny/spinny_wheel_detection.py b/tasks/spinny/spinny_wheel_detection.py deleted file mode 100644 index 8f0f72f..0000000 --- a/tasks/spinny/spinny_wheel_detection.py +++ /dev/null @@ -1,112 +0,0 @@ -import numpy as np -import cv2 -import argparse -import sys -import time - -#CHANGE PARAMETER IN CALL TO "THRESH" FUNCTION TO CHANGE COLOR -file_name = "GOPR1145.MP4" #video file from dropbox -vid = cv2.VideoCapture(file_name) -frames = 0 -avgLength = 10 -centers = [] - -def thresh(frame, color='red'): - - blur = cv2.GaussianBlur(frame, (5, 5), 0) - hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV) - - if color == 'red': - lower = np.uint8([29,77,36]) - upper = np.uint8([130,250,255]) - mask = cv2.inRange(hsv,lower,upper) - mask = cv2.bitwise_not(mask) - elif color == 'blue': - lower = np.uint8([86,141,0]) - upper = np.uint8([106,220,168]) - mask = cv2.inRange(hsv,lower,upper) - else: - lower = np.uint8([66,208,157]) - upper = np.uint8([86,255,209]) - mask = cv2.inRange(hsv,lower,upper) - - return mask - -def heuristic(contour): - rect = cv2.minAreaRect(contour) - area = rect[1][0] * rect[1][1] - diff = cv2.contourArea(cv2.convexHull(contour)) - cv2.contourArea(contour) - cent = rect[0] - dist = 0 - if len(likelySection) > 1 and allLarger(60): - cen0 = cv2.minAreaRect(likelySection[0]['cont'])[0] - dis0 = np.linalg.norm(np.array(cent) - np.array(cen0)) - cen1 = cv2.minAreaRect(likelySection[1]['cont'])[0] - dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) - dist = min([dis0, dis1]) - heur = area - 3 * diff - 20 * dist - return heur - -def allLarger(thresh): - for cnt in likelySection: - if cnt['heur'] < thresh: - return False - return True - -def drawRects(frame, contours): - tempPts = [] - for cnt in contours: - rect = cv2.minAreaRect(cnt['cont']) - boxpts = cv2.boxPoints(rect) - box = np.int0(boxpts) - cv2.drawContours(frame,[box],0,(0,0,255),1) - cv2.drawContours(frame, [cnt['cont']],0,(0,255,0),1) - cv2.drawContours(frame, [cv2.convexHull(cnt['cont'])],0,(255,0,0),1) - tempPts.append(rect[0]) - #cv2.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) - if len(tempPts) > 1 and allLarger(60): - cv2.circle(frame, (int(tempPts[0][0]), int(tempPts[0][1])), 10, (0,0,255), -1) - cv2.circle(frame, (int(tempPts[1][0]), int(tempPts[1][1])), 10, (0,0,255), -1) -def midPt(pt1, pt2): - return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) - -def getAvgPt(pt): - points.append(pt) - exes = list(map(lambda x: x[0], points)) - whys = list(map(lambda y: y[1], points)) - - if len(points) > 50: - del points[:10] - return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) - -likelySection = [] -points = [] -while vid.isOpened(): - start = time.time() - ret, frame = vid.read() - if(ret == False): - continue - - threshed = thresh(frame,'other') - res, contours, hierarchy = cv2.findContours(threshed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - contours.sort(key=heuristic, reverse = True) - if len(contours) > 1: - c1 = contours[0] - c2 = contours[1] - heur0 = heuristic(c1) - heur1 = heuristic(c2) - likelySection = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] - untampered = np.copy(frame) - if contours: - drawRects(frame, likelySection) - cv2.imshow("Frame", frame) - cv2.imshow('Res', res) - - if (cv2.waitKey(1) & 0xFF) == ord('q') or frames > 900: - break - -vid.release() -cv2.destroyAllWindows() - - diff --git a/tasks/spinny/threshslider.py b/tasks/spinny/threshslider.py deleted file mode 100644 index e8bf654..0000000 --- a/tasks/spinny/threshslider.py +++ /dev/null @@ -1,185 +0,0 @@ -from __future__ import print_function -import cv2 as cv -import argparse -import numpy as np -#expectations -#contours closest to the last ones -#should know when we passed through the gate -""" -IMPORTANT!!!! RUN THIS WITH $ python3 threshTest.py GOPR1142.mp4 -""" -max_value = 255 -max_value_H = 360//2 -low_H = 86#49#29#0 -low_S = 141#77#0 -low_V = 0#36#0 For Small sector, increasing lower V bound reduces -high_H = 106#130#max_value_H -high_S = 217#250#max_value -high_V = 168#max_value -window_capture_name = 'Video Capture' -window_detection_name = 'Object Detection' -low_H_name = 'Low H' -low_S_name = 'Low S' -low_V_name = 'Low V' -high_H_name = 'High H' -high_S_name = 'High S' -high_V_name = 'High V' -def on_low_H_thresh_trackbar(val): - global low_H - global high_H - low_H = val - low_H = min(high_H-1, low_H) - cv.setTrackbarPos(low_H_name, window_detection_name, low_H) -def on_high_H_thresh_trackbar(val): - global low_H - global high_H - high_H = val - high_H = max(high_H, low_H+1) - cv.setTrackbarPos(high_H_name, window_detection_name, high_H) -def on_low_S_thresh_trackbar(val): - global low_S - global high_S - low_S = val - low_S = min(high_S-1, low_S) - cv.setTrackbarPos(low_S_name, window_detection_name, low_S) -def on_high_S_thresh_trackbar(val): - global low_S - global high_S - high_S = val - high_S = max(high_S, low_S+1) - cv.setTrackbarPos(high_S_name, window_detection_name, high_S) -def on_low_V_thresh_trackbar(val): - global low_V - global high_V - low_V = val - low_V = min(high_V-1, low_V) - cv.setTrackbarPos(low_V_name, window_detection_name, low_V) -def on_high_V_thresh_trackbar(val): - global low_V - global high_V - high_V = val - high_V = max(high_V, low_V+1) - cv.setTrackbarPos(high_V_name, window_detection_name, high_V) - -def drawRects(frame, contours): - tempPts = [] - for cnt in contours: - rect = cv.minAreaRect(cnt['cont']) - boxpts = cv.boxPoints(rect) - box = np.int0(boxpts) - cv.drawContours(frame,[box],0,(0,0,255),1) - cv.drawContours(frame, [cnt['cont']],0,(0,255,0),1) - cv.drawContours(frame, [cv.convexHull(cnt['cont'])],0,(255,0,0),1) - tempPts.append(rect[0]) - cv.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) - if len(tempPts) > 1 and allLarger(60): - #global paused - paused = True - avgPt = getAvgPt(midPt(tempPts[0], tempPts[1])) - cv.circle(frame, (avgPt[0], avgPt[1]), 10, (0,0,255), -1) - -def midPt(pt1, pt2): - return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) - -def getAvgPt(pt): - points.append(pt) - exes = list(map(lambda x: x[0], points)) - whys = list(map(lambda y: y[1], points)) - - if len(points) > 50: - del points[:10] - return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) - -""" -def findLikelyGate(rectList, contours): - if not (rectList and contours): - return - try: - closest = min(contours, key=lambda x: abs(cv.minAreaRect(x)[0][1] - cv.minAreaRect(rectList[0])[0][1]) + abs(1/(cv.minAreaRect(x)[0][0] - cv.minAreaRect(rectList[0])[0][0]))) - rectList.append(closest) - except: - return -""" - -def heuristic(contour): - rect = cv.minAreaRect(contour) - area = rect[1][0] * rect[1][1] - diff = cv.contourArea(cv.convexHull(contour)) - cv.contourArea(contour) - cent = rect[0] - dist = 0 - if len(likelyGate) > 1 and allLarger(60): - cen0 = cv.minAreaRect(likelyGate[0]['cont'])[0] - dis0 = np.linalg.norm(np.array(cent) - np.array(cen0)) - cen1 = cv.minAreaRect(likelyGate[1]['cont'])[0] - dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) - dist = min([dis0, dis1]) - heur = area - 3 * diff - 20 * dist #only factor in dist with all heurs larger than 60 - #print(heur) - return heur - -def allLarger(thresh): - for cnt in likelyGate: - if cnt['heur'] < thresh: - return False - return True - -parser = argparse.ArgumentParser(description='Code for Thresholding Operations using inRange tutorial.') -parser.add_argument('camera', help='Camera devide number.', default=0, type=str) -args = parser.parse_args() -cap = cv.VideoCapture(args.camera) - -cv.namedWindow(window_capture_name) -cv.namedWindow(window_detection_name) - -cv.createTrackbar(low_H_name, window_detection_name , low_H, max_value_H, on_low_H_thresh_trackbar) -cv.createTrackbar(high_H_name, window_detection_name , high_H, max_value_H, on_high_H_thresh_trackbar) -cv.createTrackbar(low_S_name, window_detection_name , low_S, max_value, on_low_S_thresh_trackbar) -cv.createTrackbar(high_S_name, window_detection_name , high_S, max_value, on_high_S_thresh_trackbar) -cv.createTrackbar(low_V_name, window_detection_name , low_V, max_value, on_low_V_thresh_trackbar) -cv.createTrackbar(high_V_name, window_detection_name , high_V, max_value, on_high_V_thresh_trackbar) - -#cv.createTrackbar('low_canny', 'canny', low_canny, 500, lcanny) -paused = False - -likelyGate = [] -points = [] -while True: - if not paused: - ret, frame = cap.read() - else: - frame = untampered - if ret: - if not paused: - frame = cv.resize(frame, (0,0), fx=0.5, fy=0.5) - blur = cv.GaussianBlur(frame, (5, 5), 0) - frame_HSV = cv.cvtColor(blur, cv.COLOR_BGR2HSV) - #frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - #canny = cv.Canny(frame_gray, 0) - frame_threshold = cv.inRange(frame_HSV, (low_H, low_S, low_V), (high_H, high_S, high_V)) #low_S ideal = 98 - - #frame_threshold = cv.bitwise_not(frame_threshold) - res = cv.bitwise_and(frame,frame, mask= frame_threshold) - res2, contours, hierarchy = cv.findContours(frame_threshold, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - - contours.sort(key=heuristic, reverse=True) - if len(contours) > 1: - heur0 = heuristic(contours[0]) - heur1 = heuristic(contours[1]) - - likelyGate = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] - untampered = np.copy(frame) - if contours: - #likelyGate.append(contours[0]) - #findLikelyGate(likelyGate, contours) - drawRects(frame, likelyGate) - cv.imshow(window_capture_name, frame) - cv.imshow(window_detection_name, frame_threshold) - #cv.imshow('canny', canny) - - key = cv.waitKey(30) - if key == ord('q') or key == 27: - break - if key == ord('p'): - paused = not paused - -#generalized problem, giving center of object contrasting with water diff --git a/vis/FrameWrapper.py b/vis/FrameWrapper.py deleted file mode 100644 index 4eb6e01..0000000 --- a/vis/FrameWrapper.py +++ /dev/null @@ -1,103 +0,0 @@ -import cv2 -import sys - -class FrameWrapper(): - """ - A standard interface for getting frames from images, videos, and the webcam. - TODO: ZED camera interfacing - - Example usage: - filenames = ['relative/path/img.png', './video.mp4', 'webcam'] - frames = FrameWrapper(filenames) - - # Shows img.png, then all frames in video.mp4, then frames from webcam forever - for frame in frames: - cv2.imshow('Next frame', frame) - """ - - # Keywords used to identify file types - WEBCAM = ['webcam'] - VIDEO_EXTS = ['mp4', 'avi'] - IMG_EXTS = ['jpg', 'png'] - - VIDEO_TRIES = 200 - WEBCAM_TRIES = 10 - - def __init__(self, filenames, resize=1): - self.filenames = filenames # Get this list of relative paths to files from vis - # There aren't any checks for resize==1 to improve speed b/c this expects resize != 1 - self.resize = resize - - def __iter__(self): - self.index = -1 - self.next_data = ('', None) - self.next_data_obj() - return self - - def __next__(self): - if not self.has_next: - raise StopIteration - - while self.index < len(self.filenames): - if self.next_data[0] == "v": # Video - # Try to get a frame out at most VIDEO_TRIES times. - # If it still fails, we're probably at the end of the video file - for i in range(self.VIDEO_TRIES): - ret, frame = self.next_data[1].read() - if ret: - return cv2.resize(frame, None, fx=self.resize, fy=self.resize) - else: - print("WARNING: Failed to get frame from video {}. Try {}." \ - .format(self.filenames[self.index], i), file=sys.stderr) - self.next_data_obj() - elif self.next_data[0] == "i": # Image - img = self.next_data[1] - self.next_data_obj() - if img is not None: - return cv2.resize(img, None, fx=self.resize, fy=self.resize) - else: - print("WARNING: Failed to get image {}." \ - .format(self.filenames[self.index-1]), file=sys.stderr) - else: # Webcam - # Try to get a frame out at most WEBCAM_TRIES times. - for i in range(self.WEBCAM_TRIES): - ret, frame = self.next_data[1].read() - if ret: - return cv2.resize(frame, None, fx=self.resize, fy=self.resize) - else: - print("WARNING: Failed to get frame from webcam. Try {}." \ - .format(i), file=sys.stderr) - self.next_data_obj() - - raise StopIteration - - def next_data_obj(self): - """ - Helper function for getting the next object (video, image, webcam) when - the previous one is exhausted. - """ - # Close the webcam if it was open and we don't want it anymore - if self.next_data[0] in self.WEBCAM: - self.next_data[1].release() - - # Stop if we don't have any more data - if self.index >= len(self.filenames) - 1: - self.index += 1 - self.has_next = False - return - - # Prepare the next data object - self.index += 1 - filename = self.filenames[self.index] - extension = filename[filename.rindex('.') + 1:].lower() if '.' in filename else None - - if filename in self.WEBCAM: - self.next_data = ('w', cv2.VideoCapture(0)) - elif extension in self.VIDEO_EXTS: - self.next_data = ('v', cv2.VideoCapture(filename)) - elif extension in self.IMG_EXTS: - self.next_data = ('i', cv2.imread(filename)) - else: - print("Unknown file format:", extension) - - self.has_next = True diff --git a/vis/TaskPerceiver.py b/vis/TaskPerceiver.py deleted file mode 100644 index e8b8609..0000000 --- a/vis/TaskPerceiver.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Any, Dict, Tuple -import numpy as np - -class TaskPerceiver: - - def __init__(self, **kwargs): - """Initializes the TaskPerceiver. - Args: - kwargs: Each keyworded argument is of the form - var_name = (range, default_val), where range is the range of values - for the slider which controls this variable, and default_val is - the initial value of the slider. - """ - self.time = 0 - self.variables = kwargs - - def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]) -> Any: - """Runs the algorithm and returns the result. - Args: - frame: The frame to analyze - debug: Whether or not to display intermediate images for debugging - slider_vals: A list of names of the variables which the user should be - able to control from the Visualizer, mapped to current slider - value for that variable - Returns: - the result of the algorithm - """ - raise NotImplementedError("Need to implement with child class.") - - def var_info(self) -> Dict[str, Tuple[Tuple[int, int], int]]: - return self.variables \ No newline at end of file diff --git a/vis/TestTasks/GateSegmentationAlgo.py b/vis/TestTasks/GateSegmentationAlgo.py deleted file mode 100644 index 1c791d5..0000000 --- a/vis/TestTasks/GateSegmentationAlgo.py +++ /dev/null @@ -1,120 +0,0 @@ -from TaskPerceiver import TaskPerceiver -from typing import Tuple -import sys -import os -from pathlib import Path -from collections import namedtuple -sys.path.append(str(Path(__file__).parents[2]) + '/tasks') - -from segmentation.combinedFilter import init_combined_filter -import numpy as np -import cv2 as cv -import time -import cProfile - -class GateSegmentationAlgo(TaskPerceiver): - __past_centers = [] - __ema = None - output_class = namedtuple("GateOutput", ["centerx", "centery"]) - output_type = {'centerx': np.int16, 'centery': np.int16} - - def __init__(self, alpha=0.1): - super() - self.__alpha = alpha - self.combined_filter = init_combined_filter() - - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze. - debug: Whether or not to display intermediate images for debugging. - Returns: - (x,y) coordinate with center of gate - """ - gate_center = self.output_class(250, 250) - filtered_frame = self.combined_filter(frame, display_figs=False) - filtered_frame_copies = [filtered_frame for _ in range(3)] - stacked_filter_frames = np.concatenate(filtered_frame_copies, axis=2) - mask = cv.inRange( - stacked_filter_frames, np.array([100, 100, 100]), np.array([255, 255, 255]) - ) - _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - if contours: - contours.sort(key=self.findStraightness, reverse=True) - cnts = contours[:2] - rects = [cv.minAreaRect(c) for c in cnts] - centers = [np.array(r[0]) for r in rects] - boxpts = [cv.boxPoints(r) for r in rects] - box = [np.int0(b) for b in boxpts] - for b in box: - cv.drawContours(stacked_filter_frames, [b], 0, (0, 0, 255), 5) - if len(centers) >= 2: - gate_center = (centers[0] + centers[1]) * 0.5 - if self.__ema is None: - self.__ema = gate_center - else: - self.__ema = ( - self.__alpha * gate_center + (1 - self.__alpha) * self.__ema - ) - gate_center = (int(self.__ema[0]), int(self.__ema[1])) - # if len(self.__past_centers) < 15: - # self.__past_centers += [gate_center] - # else: - # self.__past_centers.pop(0) - # self.__past_centers += [gate_center] - # gate_center = sum(self.__past_centers) / len(self.__past_centers) - # gate_center = (int(gate_center[0]), int(gate_center[1])) - cv.circle(stacked_filter_frames, gate_center, 10, (0, 255, 0), -1) - - if debug: - return ( - self.output_class(gate_center[0], gate_center[1]), - [stacked_filter_frames], - ) - return self.output_class(gate_center[0], gate_center[1]) - - def findStraightness( - self, contour - ): # output number = contour area/convex area, the bigger the straightest - hull = cv.convexHull(contour, False) - contour_area = cv.contourArea(contour) - hull_area = cv.contourArea(hull) - return 10 * contour_area - 5 * hull_area - - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - gate_task = GateSegmentationAlgo(0.1) - # once = False - start_time = time.time() - frame_count = 0 - while ret_tries < 50: - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.4, fy=0.4) - - ### FUNCTION CALL, can change this - center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xFF - if k == 27: - break - else: - ret_tries += 1 - frame_count += 1 - # print(frame_count / (time.time() - start_time)) diff --git a/vis/TestTasks/TestAlgo.py b/vis/TestTasks/TestAlgo.py deleted file mode 100644 index e46e507..0000000 --- a/vis/TestTasks/TestAlgo.py +++ /dev/null @@ -1,30 +0,0 @@ -from TaskPerceiver import TaskPerceiver -from typing import Dict -import numpy as np -import cv2 as cv -import matplotlib.pyplot as plt - -class TestAlgo(TaskPerceiver): - def __init__(self): - super().__init__(canny_low=((0, 255), 100), canny_high=((0, 255), 200)) - - def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]): - fig = plt.figure() - x1 = np.linspace(0.0, 5.0) - x2 = np.linspace(0.0, 2.0) - - y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) - y2 = np.cos(2 * np.pi * x2) - - line1, = plt.plot(x1, y1, 'ko-') - line1.set_ydata(np.cos(2 * np.pi * (x1 + slider_vals['canny_low'] * 3.14 / 2)) * np.exp(-x1)) - fig.canvas.draw() - img = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, - sep='') - img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) - img = cv.cvtColor(img, cv.COLOR_RGB2BGR) - img = cv.resize(img, (frame.shape[1], frame.shape[0])) - - return frame, [frame, cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), - cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), - cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0), img] \ No newline at end of file diff --git a/vis/vis.py b/vis/vis.py deleted file mode 100644 index 3b3665f..0000000 --- a/vis/vis.py +++ /dev/null @@ -1,67 +0,0 @@ -import argparse -import os - -from FrameWrapper import FrameWrapper -import cv2 as cv -from window_builder import Visualizer -import cProfile as cp -import pstats - -# Parse arguments -parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') -parser.add_argument( - '--data', default='webcam', type=str -) -parser.add_argument('--algorithm', type=str) -parser.add_argument('--save_video', action='store_true') -args = parser.parse_args() - -# Get algorithm module -exec("from TestTasks.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) - -# Initialize image source -# detects args.data, get a list of all file directory when given a directory -# change data_source to a list of all files in the directory -if os.path.isfile(args.data): - data_sources = [args.data] -elif os.path.isdir(args.data): - data_sources = os.listdir(args.data) -data = FrameWrapper(data_sources, 0.25) - -algorithm = Algorithm() -window_builder = Visualizer(algorithm.var_info()) -video_frames = [] - - -# Main Loop -def main(): - for frame in data: - - state, debug_frames = algorithm.analyze( - frame, debug=True, slider_vals=window_builder.update_vars() - ) - to_show = window_builder.display(debug_frames) - cv.imshow('Debug Frames', to_show) - if args.save_video: - video_frames.append(to_show) - - key_pressed = cv.waitKey(60) & 0xFF - if key_pressed == 112: - cv.waitKey(0) # pause - if key_pressed == 113: - break # quit - - -cp.run('main()', 'algo_stats') -cv.destroyAllWindows() -p = pstats.Stats('algo_stats') -p.print_stats('analyze') - -if args.save_video: - height, width, _ = video_frames[0].shape - out = cv.VideoWriter('deb_cap.avi', cv.VideoWriter_fourcc(*'XVID'), 60, (height, width)) - for img in video_frames: - height2, width2, _ = img.shape - if (height2, width2) == (height, width): - out.write(img) - out.release() From 73a9b4b870a0f5c680e9e8dcb6c87a8a3a1f214c Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sat, 5 Dec 2020 23:37:11 -0800 Subject: [PATCH 09/19] Updated optical flow & separated classes --- perception/tasks/gate/GateCenter.py | 109 +++++++++++++++++- .../tasks/gate/GateSegmentationAlgo2.py | 104 ++--------------- setup.py | 17 +++ 3 files changed, 134 insertions(+), 96 deletions(-) create mode 100644 setup.py diff --git a/perception/tasks/gate/GateCenter.py b/perception/tasks/gate/GateCenter.py index 19e3dac..2a96a05 100644 --- a/perception/tasks/gate/GateCenter.py +++ b/perception/tasks/gate/GateCenter.py @@ -13,10 +13,115 @@ import statistics class GateCenter(GatePerceiver): + center_x_locs, center_y_locs = [], [] + def __init__(self): super() self.gate_center = self.output_class(250, 250) + self.use_optical_flow = False + self.optical_flow_c = 0.1 + self.gate = GateSegmentationAlgo() + + def analyze(self, frame, debug): + global prvs + rect1, rect2, debug_filter = self.gate.analyze(frame, True) + if rect1 and rect2: + self.gate_center = self.get_center(rect1, rect2, frame) + if self.use_optical_flow: + cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) + else: + cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + + if debug: + return (self.output_class(self.gate_center[0], self.gate_center[1]), debug_filter) + return self.output_class(self.gate_center[0], self.gate_center[1]) + + def center_without_optical_flow(self, center_x, center_y): + # get starting center location, averaging over the first 2510 frames + if len(self.center_x_locs) == 0: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + + elif len(self.center_x_locs) < 25: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + center_x = int(statistics.mean(self.center_x_locs)) + center_y = int(statistics.mean(self.center_y_locs)) + + # use new center location only when it is close to the previous valid location + else: + self.center_x_locs.append(center_x) + self.center_y_locs.append(center_y) + self.center_x_locs.pop(0) + self.center_y_locs.pop(0) + x_temp_avg = int(statistics.mean(self.center_x_locs)) + y_temp_avg = int(statistics.mean(self.center_y_locs)) + if math.sqrt((center_x - x_temp_avg)**2 + (center_y - y_temp_avg)**2) > 10: + center_x, center_y = int(x_temp_avg), int(y_temp_avg) + + return (center_x, center_y) + def dense_optical_flow(self, frame, prvs): + next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) + mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) + mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) + # hsv[...,0] = ang*180/np.pi + # hsv[...,2] = mag + # bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) + # cv.imshow('bgr', bgr) + return next, mag, ang - def analyze(self, frame): - \ No newline at end of file + def get_center(self, rect1, rect2, frame): + global prvs + x1, y1, w1, h1 = rect1 + x2, y2, w2, h2 = rect2 + center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 + prvs, mag, ang = self.dense_optical_flow(frame, prvs) + # print(np.mean(mag)) + if len(self.center_x_locs) < 25 or (np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ + (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50))): + self.use_optical_flow = False + return self.center_without_optical_flow(center_x, center_y) + else: + self.use_optical_flow = True + return (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag * np.cos(ang))), \ + (int(self.gate_center[1] + self.optical_flow_c * np.mean(mag * np.sin(ang))))) + +# this part is temporary and will be covered by other files in the future +if __name__ == '__main__': + cap = cv.VideoCapture(sys.argv[1]) + ret_tries = 0 + start_time = time.time() + frame_count = 0 + paused = False + speed = 1 + ret, frame1 = cap.read() + frame1 = cv.resize(frame1, None, fx=0.3, fy=0.3) + prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY) + hsv = np.zeros_like(frame1) + hsv[...,1] = 255 + gate_center = GateCenter() + while ret_tries < 50: + for _ in range(speed): + ret, frame = cap.read() + if frame_count == 1000: + break + if ret: + frame = cv.resize(frame, None, fx=0.3, fy=0.3) + center, filtered_frame = gate_center.analyze(frame, True) + cv.imshow('original', frame) + cv.imshow('filtered_frame', filtered_frame) + ret_tries = 0 + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + if key == ord('i') and speed > 1: + speed -= 1 + if key == ord('o'): + speed += 1 + else: + ret_tries += 1 + frame_count += 1 \ No newline at end of file diff --git a/perception/tasks/gate/GateSegmentationAlgo2.py b/perception/tasks/gate/GateSegmentationAlgo2.py index e9f440b..fbd0da3 100644 --- a/perception/tasks/gate/GateSegmentationAlgo2.py +++ b/perception/tasks/gate/GateSegmentationAlgo2.py @@ -19,10 +19,10 @@ class GateSegmentationAlgo(GatePerceiver): def __init__(self): super() self.gate_center = self.output_class(250, 250) - self.use_optical_flow = False - self.optical_flow_c = 0.05 + # self.use_optical_flow = False + # self.optical_flow_c = 0.05 + self.combined_filter = init_combined_filter() - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: """Takes in the background removed image and returns the center between the two gate posts. @@ -32,8 +32,9 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: Reurns: (x,y) coordinate with center of gate """ - global prvs - filtered_frame = combined_filter(frame, display_figs=False) + rect1, rect2 = None, None + + filtered_frame = self.combined_filter(frame, display_figs=False) max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) lowerbound = max(0.84*max_brightness, 120) @@ -66,106 +67,21 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: x2, y2, w2, h2 = rect2 cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) - - # # drawing center dot - # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - # self.gate_center = self.get_actual_center(center_x, center_y) - # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) - - # dense optical flow - # next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - # flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) - # mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) - # mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) - # center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - # if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ - # (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): - # self.gate_center = self.get_actual_center(center_x, center_y) - # cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) - # self.use_optical_flow = False - # else: - # self.use_optical_flow = True - # self.gate_center = (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag) * math.cos(np.mean(ang))), \ - # int(self.gate_center[1] + self.optical_flow_c * np.mean(mag) * math.sin(np.mean(ang)))) - # cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) - self.gate_center = self.get_center(rect1, rect2, frame) - if self.use_optical_flow: - cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) - else: - cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) - # ang = ang*180/np.pi - # print('mag:', np.mean(mag), '\tang:', np.mean(ang)) - # hsv[...,0] = ang - # hsv[...,2] = mag - # bgr = cv.cvtColor(hsv,cv.COLOR_HSV2BGR) - # prvs = next - if debug: - return (self.output_class(self.gate_center[0], self.gate_center[1]), debug_filter) - return self.output_class(self.gate_center[0], self.gate_center[1]) - def center_without_optical_flow(self, center_x, center_y): - # get starting center location, averaging over the first 2510 frames - if len(self.center_x_locs) == 0: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - - elif len(self.center_x_locs) < 25: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - center_x = int(statistics.mean(self.center_x_locs)) - center_y = int(statistics.mean(self.center_y_locs)) + if debug: + return (rect1, rect2, debug_filter) + return (rect1, rect2) - # use new center location only when it is close to the previous valid location - else: - self.center_x_locs.append(center_x) - self.center_y_locs.append(center_y) - self.center_x_locs.pop(0) - self.center_y_locs.pop(0) - x_temp_avg = int(statistics.mean(self.center_x_locs)) - y_temp_avg = int(statistics.mean(self.center_y_locs)) - if math.sqrt((center_x - x_temp_avg)**2 + (center_y - y_temp_avg)**2) > 10: - center_x, center_y = int(x_temp_avg), int(y_temp_avg) - - return (center_x, center_y) - - def dense_optical_flow(self, frame, prvs): - next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) - mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) - mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) - return next, mag, ang - def get_center(self, rect1, rect2, rame): - global prvs - x1, y1, w1, h1 = rect1 - x2, y2, w2, h2 = rect2 - center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - prvs, mag, ang = self.dense_optical_flow(frame, prvs) - if np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ - (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50)): - self.use_optical_flow = False - return self.center_without_optical_flow(center_x, center_y) - else: - self.use_optical_flow = True - return (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag * np.cos(ang))), \ - (int(self.gate_center[1] + self.optical_flow_c * np.mean(mag * np.sin(ang))))) - # this part is temporary and will be covered by other files in the future if __name__ == '__main__': - combined_filter = init_combined_filter() cap = cv.VideoCapture(sys.argv[1]) ret_tries = 0 - # once = False start_time = time.time() frame_count = 0 paused = False speed = 1 - ret, frame1 = cap.read() - frame1 = cv.resize(frame1, None, fx=0.3, fy=0.3) - prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY) - hsv = np.zeros_like(frame1) - hsv[...,1] = 255 gate_task = GateSegmentationAlgo() while ret_tries < 50: for _ in range(speed): @@ -174,7 +90,7 @@ def get_center(self, rect1, rect2, rame): break if ret: frame = cv.resize(frame, None, fx=0.3, fy=0.3) - center, filtered_frame = gate_task.analyze(frame, True) + rect1, rect2, filtered_frame = gate_task.analyze(frame, True) cv.imshow('original', frame) cv.imshow('filtered_frame', filtered_frame) ret_tries = 0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1587e1a --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +print(setuptools.find_packages()) + +setuptools.setup( + name="perception", + version="0.0.1", + author="Underwater Robotics at Berkeley", + description="Perception algorithms for our autonomous submarine", + long_description=long_description, + long_description_content_type="text/markdown", + packages=setuptools.find_packages(), + python_requires='>=3.7', +) From 1de4b8c82718cb4b5b9ec8d1f1c7c82790350f45 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sat, 5 Dec 2020 23:40:05 -0800 Subject: [PATCH 10/19] Removed unnecessary code --- perception/tasks/gate/GateSegmentationAlgo2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/perception/tasks/gate/GateSegmentationAlgo2.py b/perception/tasks/gate/GateSegmentationAlgo2.py index fbd0da3..9f86374 100644 --- a/perception/tasks/gate/GateSegmentationAlgo2.py +++ b/perception/tasks/gate/GateSegmentationAlgo2.py @@ -18,9 +18,6 @@ class GateSegmentationAlgo(GatePerceiver): def __init__(self): super() - self.gate_center = self.output_class(250, 250) - # self.use_optical_flow = False - # self.optical_flow_c = 0.05 self.combined_filter = init_combined_filter() def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: From a5b282eabbf77e875f6211c0c13cbb17b412be76 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sun, 6 Dec 2020 17:51:04 -0800 Subject: [PATCH 11/19] updated with vis --- perception/tasks/gate/GateCenter.py | 41 ++++++++++-------- .../tasks/gate/GateSegmentationAlgo2.py | 6 +-- perception/vis/algo_stats | Bin 5441 -> 24802 bytes perception/vis/vis.py | 3 +- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/perception/tasks/gate/GateCenter.py b/perception/tasks/gate/GateCenter.py index 2a96a05..4e2be44 100644 --- a/perception/tasks/gate/GateCenter.py +++ b/perception/tasks/gate/GateCenter.py @@ -1,6 +1,7 @@ -from GateSegmentationAlgo2 import GateSegmentationAlgo -from GatePerceiver import GatePerceiver +from .GateSegmentationAlgo2 import GateSegmentationAlgo +from TaskPerceiver import TaskPerceiver from typing import Tuple +from collections import namedtuple import sys import os sys.path.append(os.path.dirname(__file__)) @@ -12,28 +13,35 @@ import cProfile import statistics -class GateCenter(GatePerceiver): +class GateCenter(TaskPerceiver): center_x_locs, center_y_locs = [], [] + output_class = namedtuple("GateOutput", ["centerx", "centery"]) + output_type = {'centerx': np.int16, 'centery': np.int16} def __init__(self): - super() + super().__init__(optical_flow_c=((0, 100), 10)) self.gate_center = self.output_class(250, 250) self.use_optical_flow = False self.optical_flow_c = 0.1 self.gate = GateSegmentationAlgo() + self.prvs = None - def analyze(self, frame, debug): - global prvs + def analyze(self, frame, debug, slider_vals): + self.optical_flow_c = slider_vals['optical_flow_c']/100 rect1, rect2, debug_filter = self.gate.analyze(frame, True) - if rect1 and rect2: - self.gate_center = self.get_center(rect1, rect2, frame) - if self.use_optical_flow: - cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) - else: - cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) + if self.prvs is None: + # frame = cv.resize(frame, None, fx=0.3, fy=0.3) + self.prvs = cv.cvtColor(frame,cv.COLOR_BGR2GRAY) + else: + if rect1 and rect2: + self.gate_center = self.get_center(rect1, rect2, frame) + if self.use_optical_flow: + cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) + else: + cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) if debug: - return (self.output_class(self.gate_center[0], self.gate_center[1]), debug_filter) + return (self.output_class(self.gate_center[0], self.gate_center[1]), [frame, debug_filter]) return self.output_class(self.gate_center[0], self.gate_center[1]) def center_without_optical_flow(self, center_x, center_y): @@ -61,9 +69,9 @@ def center_without_optical_flow(self, center_x, center_y): return (center_x, center_y) - def dense_optical_flow(self, frame, prvs): + def dense_optical_flow(self, frame): next = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - flow = cv.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) + flow = cv.calcOpticalFlowFarneback(self.prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0) mag, ang = cv.cartToPolar(flow[...,0], flow[...,1]) mag = cv.normalize(mag,None,0,255,cv.NORM_MINMAX) # hsv[...,0] = ang*180/np.pi @@ -73,11 +81,10 @@ def dense_optical_flow(self, frame, prvs): return next, mag, ang def get_center(self, rect1, rect2, frame): - global prvs x1, y1, w1, h1 = rect1 x2, y2, w2, h2 = rect2 center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 - prvs, mag, ang = self.dense_optical_flow(frame, prvs) + self.prvs, mag, ang = self.dense_optical_flow(frame) # print(np.mean(mag)) if len(self.center_x_locs) < 25 or (np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50))): diff --git a/perception/tasks/gate/GateSegmentationAlgo2.py b/perception/tasks/gate/GateSegmentationAlgo2.py index 9f86374..a6cffc1 100644 --- a/perception/tasks/gate/GateSegmentationAlgo2.py +++ b/perception/tasks/gate/GateSegmentationAlgo2.py @@ -1,11 +1,11 @@ -from GatePerceiver import GatePerceiver +from TaskPerceiver import TaskPerceiver from typing import Tuple import sys import os sys.path.append(os.path.dirname(__file__)) -from segmentation.combinedFilter import init_combined_filter +from ..segmentation.combinedFilter import init_combined_filter import numpy as np import math import cv2 as cv @@ -13,7 +13,7 @@ import cProfile import statistics -class GateSegmentationAlgo(GatePerceiver): +class GateSegmentationAlgo(TaskPerceiver): center_x_locs, center_y_locs = [], [] def __init__(self): diff --git a/perception/vis/algo_stats b/perception/vis/algo_stats index edd4cf5661573c93313bc05cc1650ad0680e24f1..0dfff929be6331d4845aaa0d16daf3975e50a63d 100644 GIT binary patch literal 24802 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 WcN4X%kyXHXj>cL0xfo2lcK;8#NqA%c literal 5441 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 diff --git a/perception/vis/vis.py b/perception/vis/vis.py index 81f08e8..19e8317 100644 --- a/perception/vis/vis.py +++ b/perception/vis/vis.py @@ -4,6 +4,7 @@ from window_builder import Visualizer import cProfile as cp import pstats +import perception # Parse arguments parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') @@ -15,7 +16,7 @@ args = parser.parse_args() # Get algorithm module -exec("from TestTasks.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) +exec("from perception.tasks.gate.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) # Initialize image source data_sources = [args.data] From 98551a4866566e91f93ca8f7626973cc2ed225a8 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sun, 6 Dec 2020 17:52:52 -0800 Subject: [PATCH 12/19] added init --- perception/tasks/__init__.py | 0 perception/vis/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 perception/tasks/__init__.py create mode 100644 perception/vis/__init__.py diff --git a/perception/tasks/__init__.py b/perception/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/perception/vis/__init__.py b/perception/vis/__init__.py new file mode 100644 index 0000000..e69de29 From 543d0542d967fa33839dc0c40ade4776b912a5c5 Mon Sep 17 00:00:00 2001 From: Yu Liu Date: Sun, 6 Dec 2020 17:53:58 -0800 Subject: [PATCH 13/19] added init --- perception/tasks/gate/__init__.py | 0 perception/tasks/segmentation/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 perception/tasks/gate/__init__.py create mode 100644 perception/tasks/segmentation/__init__.py diff --git a/perception/tasks/gate/__init__.py b/perception/tasks/gate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/perception/tasks/segmentation/__init__.py b/perception/tasks/segmentation/__init__.py new file mode 100644 index 0000000..e69de29 From 41373d94f8df99d1236b2b00ee6c00fe25aa539e Mon Sep 17 00:00:00 2001 From: axquaris Date: Mon, 4 Jan 2021 00:08:38 -0800 Subject: [PATCH 14/19] Refactor Vis + Gate Algos --- .gitattributes | 1 + .gitignore | 2 + perception/tasks/TaskPerceiver.py | 20 ++- perception/tasks/cross/__init__.py | 0 .../gate/{GateCenter.py => GateCenterAlgo.py} | 64 +++------- perception/tasks/gate/GatePerceiver.py | 2 +- perception/tasks/gate/GateSegmentationAlgo.py | 108 ---------------- ...ationAlgo2.py => GateSegmentationAlgoA.py} | 58 +++------ ...ationAlgo1.py => GateSegmentationAlgoB.py} | 68 +++------- .../gate/GateSegmentationAlgoC.py} | 71 +++-------- perception/tasks/path_marker/__init__.py | 0 perception/tasks/roulette/__init__.py | 0 .../spinny_wheel_detection.py | 0 .../{spinny => roulette}/threshslider.py | 0 .../segmentation/GateTaskExample.py.orig | 86 ------------- perception/tasks/segmentation/kmeans.py | 99 +++++++++++++++ perception/vis/FrameWrapper.py | 2 +- perception/vis/TaskPerceiver.py | 31 ----- .../vis/{window_builder.py => Visualizer.py} | 0 perception/vis/algo_stats | Bin 5441 -> 0 bytes perception/vis/vis.py | 118 ++++++++++-------- requirements.txt | 6 - 22 files changed, 248 insertions(+), 488 deletions(-) create mode 100644 .gitattributes create mode 100644 perception/tasks/cross/__init__.py rename perception/tasks/gate/{GateCenter.py => GateCenterAlgo.py} (65%) delete mode 100644 perception/tasks/gate/GateSegmentationAlgo.py rename perception/tasks/gate/{GateSegmentationAlgo2.py => GateSegmentationAlgoA.py} (63%) rename perception/tasks/gate/{GateSegmentationAlgo1.py => GateSegmentationAlgoB.py} (63%) rename perception/{vis/TestTasks/GateSegmentationAlgo.py => tasks/gate/GateSegmentationAlgoC.py} (56%) create mode 100644 perception/tasks/path_marker/__init__.py create mode 100644 perception/tasks/roulette/__init__.py rename perception/tasks/{spinny => roulette}/spinny_wheel_detection.py (100%) rename perception/tasks/{spinny => roulette}/threshslider.py (100%) delete mode 100644 perception/tasks/segmentation/GateTaskExample.py.orig create mode 100644 perception/tasks/segmentation/kmeans.py delete mode 100644 perception/vis/TaskPerceiver.py rename perception/vis/{window_builder.py => Visualizer.py} (100%) delete mode 100644 perception/vis/algo_stats diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..486a232 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.zip filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index f42b50c..ae9b556 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ __pycache__/ # IDE files .idea .vs_code/ + +data/ diff --git a/perception/tasks/TaskPerceiver.py b/perception/tasks/TaskPerceiver.py index 40f5995..a9cab96 100644 --- a/perception/tasks/TaskPerceiver.py +++ b/perception/tasks/TaskPerceiver.py @@ -1,18 +1,28 @@ -from typing import Any +from typing import Any, Dict, Tuple import numpy as np + class TaskPerceiver: - def __init__(self): + def __init__(self, **kwargs): + """Initializes the TaskPerceiver. + Args: + kwargs: Each keyworded argument is of the form + var_name = (range, default_val), where range is the range of values + for the slider which controls this variable, and default_val is + the initial value of the slider. + """ self.time = 0 + self.variables = kwargs - def analyze(self, frame: np.ndarray, debug: bool) -> Any: + def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]) -> Any: """Runs the algorithm and returns the result. Args: frame: The frame to analyze debug: Whether or not to display intermediate images for debugging - + slider_vals: A list of names of the variables which the user should be + able to control from the Visualizer, mapped to current slider + value for that variable Returns: the result of the algorithm """ raise NotImplementedError("Need to implement with child class.") - diff --git a/perception/tasks/cross/__init__.py b/perception/tasks/cross/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/perception/tasks/gate/GateCenter.py b/perception/tasks/gate/GateCenterAlgo.py similarity index 65% rename from perception/tasks/gate/GateCenter.py rename to perception/tasks/gate/GateCenterAlgo.py index 4e2be44..fe4d661 100644 --- a/perception/tasks/gate/GateCenter.py +++ b/perception/tasks/gate/GateCenterAlgo.py @@ -1,5 +1,5 @@ -from .GateSegmentationAlgo2 import GateSegmentationAlgo -from TaskPerceiver import TaskPerceiver +from perception.tasks.gate.GateSegmentationAlgoA import GateSegmentationAlgoA +from perception.tasks.TaskPerceiver import TaskPerceiver from typing import Tuple from collections import namedtuple import sys @@ -13,7 +13,7 @@ import cProfile import statistics -class GateCenter(TaskPerceiver): +class GateCenterAlgo(TaskPerceiver): center_x_locs, center_y_locs = [], [] output_class = namedtuple("GateOutput", ["centerx", "centery"]) output_type = {'centerx': np.int16, 'centery': np.int16} @@ -23,29 +23,32 @@ def __init__(self): self.gate_center = self.output_class(250, 250) self.use_optical_flow = False self.optical_flow_c = 0.1 - self.gate = GateSegmentationAlgo() + self.gate = GateSegmentationAlgoA() self.prvs = None def analyze(self, frame, debug, slider_vals): self.optical_flow_c = slider_vals['optical_flow_c']/100 - rect1, rect2, debug_filter = self.gate.analyze(frame, True) + rect, debug_filters = self.gate.analyze(frame, True) + debug_filter = debug_filters[-1] + debug_filters = debug_filters[:-1] + if self.prvs is None: # frame = cv.resize(frame, None, fx=0.3, fy=0.3) - self.prvs = cv.cvtColor(frame,cv.COLOR_BGR2GRAY) + self.prvs = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) else: - if rect1 and rect2: - self.gate_center = self.get_center(rect1, rect2, frame) + if rect[0] and rect[1]: + self.gate_center = self.get_center(rect[0], rect[1], frame) if self.use_optical_flow: cv.circle(debug_filter, self.gate_center, 5, (3,186,252), -1) else: cv.circle(debug_filter, self.gate_center, 5, (0,0,255), -1) if debug: - return (self.output_class(self.gate_center[0], self.gate_center[1]), [frame, debug_filter]) - return self.output_class(self.gate_center[0], self.gate_center[1]) + return (self.gate_center[0], self.gate_center[1]), list(debug_filters) + [debug_filter] + return (self.gate_center[0], self.gate_center[1]) def center_without_optical_flow(self, center_x, center_y): - # get starting center location, averaging over the first 2510 frames + # get starting center location, averaging over the first 2510 frames if len(self.center_x_locs) == 0: self.center_x_locs.append(center_x) self.center_y_locs.append(center_y) @@ -95,40 +98,7 @@ def get_center(self, rect1, rect2, frame): return (int(self.gate_center[0] + self.optical_flow_c * np.mean(mag * np.cos(ang))), \ (int(self.gate_center[1] + self.optical_flow_c * np.mean(mag * np.sin(ang))))) -# this part is temporary and will be covered by other files in the future + if __name__ == '__main__': - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - start_time = time.time() - frame_count = 0 - paused = False - speed = 1 - ret, frame1 = cap.read() - frame1 = cv.resize(frame1, None, fx=0.3, fy=0.3) - prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY) - hsv = np.zeros_like(frame1) - hsv[...,1] = 255 - gate_center = GateCenter() - while ret_tries < 50: - for _ in range(speed): - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.3, fy=0.3) - center, filtered_frame = gate_center.analyze(frame, True) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - ret_tries = 0 - key = cv.waitKey(30) - if key == ord('q') or key == 27: - break - if key == ord('p'): - paused = not paused - if key == ord('i') and speed > 1: - speed -= 1 - if key == ord('o'): - speed += 1 - else: - ret_tries += 1 - frame_count += 1 \ No newline at end of file + from perception.vis.vis import run + run(['..\..\..\data\GOPR1142.MP4'], GateCenterAlgo(), False) \ No newline at end of file diff --git a/perception/tasks/gate/GatePerceiver.py b/perception/tasks/gate/GatePerceiver.py index 1820e94..ef275d5 100644 --- a/perception/tasks/gate/GatePerceiver.py +++ b/perception/tasks/gate/GatePerceiver.py @@ -2,7 +2,7 @@ import numpy as np import sys sys.path.insert(0, '..') -from TaskPerceiver import TaskPerceiver +from perception.tasks.TaskPerceiver import TaskPerceiver class GatePerceiver(TaskPerceiver): output_class = namedtuple("GateOutput", ["centerx", "centery"]) diff --git a/perception/tasks/gate/GateSegmentationAlgo.py b/perception/tasks/gate/GateSegmentationAlgo.py deleted file mode 100644 index 191865c..0000000 --- a/perception/tasks/gate/GateSegmentationAlgo.py +++ /dev/null @@ -1,108 +0,0 @@ -from GatePerceiver import GatePerceiver -from typing import Tuple -import sys -import os -sys.path.append(os.path.dirname(__file__)) - - -from segmentation.combinedFilter import init_combined_filter -import numpy as np -import cv2 as cv -import time -import cProfile - -class GateSegmentationAlgo(GatePerceiver): - __past_centers = [] - __ema = None - - def __init__(self, alpha): - super() - self.__alpha = alpha - - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze - debug: Whether or not tot display intermediate images for debugging - Reurns: - (x,y) coordinate with center of gate - """ - gate_center = self.output_class(250, 250) - filtered_frame = combined_filter(frame, display_figs=False) - filtered_frame_copies = [filtered_frame for _ in range(3)] - stacked_filter_frames = np.concatenate(filtered_frame_copies, axis = 2) - mask = cv.inRange(stacked_filter_frames, - np.array([100, 100, 100]), np.array([255, 255, 255])) - _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - if contours: - contours.sort(key=self.findStraightness, reverse=True) - cnts = contours[:2] - rects = [cv.minAreaRect(c) for c in cnts] - centers = [np.array(r[0]) for r in rects] - boxpts = [cv.boxPoints(r) for r in rects] - box = [np.int0(b) for b in boxpts] - for b in box: - cv.drawContours(stacked_filter_frames,[b],0,(0,0,255),5) - if len(centers) >= 2: - gate_center = (centers[0] + centers[1]) * 0.5 - if self.__ema is None: - self.__ema = gate_center - else: - self.__ema = self.__alpha*gate_center + (1 - self.__alpha)*self.__ema - gate_center = (int(self.__ema[0]), int(self.__ema[1])) - # if len(self.__past_centers) < 15: - # self.__past_centers += [gate_center] - # else: - # self.__past_centers.pop(0) - # self.__past_centers += [gate_center] - # gate_center = sum(self.__past_centers) / len(self.__past_centers) - # gate_center = (int(gate_center[0]), int(gate_center[1])) - cv.circle(stacked_filter_frames, gate_center, 10, (0,255,0), -1) - - if debug: - return (self.output_class(gate_center[0], gate_center[1]), stacked_filter_frames) - return self.output_class(gate_center[0], gate_center[1]) - - def findStraightness(self, contour): # output number = contour area/convex area, the bigger the straightest - hull = cv.convexHull(contour, False) - contour_area = cv.contourArea(contour) - hull_area = cv.contourArea(hull) - return 10 * contour_area - 5 * hull_area - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - gate_task = GateSegmentationAlgo(0.1) - # once = False - start_time = time.time() - frame_count = 0 - while ret_tries < 50: - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.4, fy=0.4) - - - ### FUNCTION CALL, can change this - center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 - frame_count += 1 - #print(frame_count / (time.time() - start_time)) diff --git a/perception/tasks/gate/GateSegmentationAlgo2.py b/perception/tasks/gate/GateSegmentationAlgoA.py similarity index 63% rename from perception/tasks/gate/GateSegmentationAlgo2.py rename to perception/tasks/gate/GateSegmentationAlgoA.py index a6cffc1..78c097b 100644 --- a/perception/tasks/gate/GateSegmentationAlgo2.py +++ b/perception/tasks/gate/GateSegmentationAlgoA.py @@ -1,11 +1,11 @@ -from TaskPerceiver import TaskPerceiver +from perception.tasks.TaskPerceiver import TaskPerceiver from typing import Tuple import sys import os sys.path.append(os.path.dirname(__file__)) -from ..segmentation.combinedFilter import init_combined_filter +from perception.tasks.segmentation.combinedFilter import init_combined_filter import numpy as np import math import cv2 as cv @@ -13,11 +13,11 @@ import cProfile import statistics -class GateSegmentationAlgo(TaskPerceiver): +class GateSegmentationAlgoA(TaskPerceiver): center_x_locs, center_y_locs = [], [] def __init__(self): - super() + super().__init__() self.combined_filter = init_combined_filter() def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: @@ -26,13 +26,13 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: Args: frame: The background removed frame to analyze debug: Whether or not tot display intermediate images for debugging - Reurns: - (x,y) coordinate with center of gate - """ + Reurns: + (x,y) coordinate with center of gate + """ rect1, rect2 = None, None filtered_frame = self.combined_filter(frame, display_figs=False) - + max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) lowerbound = max(0.84*max_brightness, 120) upperbound = 255 @@ -46,18 +46,18 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: # remove all contours with zero area cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] - + for i in range(len(cnt)): area_cnt = cv.contourArea(cnt[i]) area_cnts.append(area_cnt) area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] area_diff.append(abs((area_rect - area_cnt)/area_cnt)) - + if len(area_diff) >= 2: largest_area_idx = [area_cnts.index(sorted(area_cnts, reverse=True)[i]) for i in range(min(3, len(cnt)))] area_diff_copy = sorted([area_diff[i] for i in largest_area_idx]) min_i1, min_i2 = area_diff.index(area_diff_copy[0]), area_diff.index(area_diff_copy[1]) - + rect1 = cv.boundingRect(cnt[min_i1]) rect2 = cv.boundingRect(cnt[min_i2]) x1, y1, w1, h1 = rect1 @@ -66,40 +66,10 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) if debug: - return (rect1, rect2, debug_filter) + return (rect1, rect2), (frame, debug_filter) return (rect1, rect2) - -# this part is temporary and will be covered by other files in the future if __name__ == '__main__': - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - start_time = time.time() - frame_count = 0 - paused = False - speed = 1 - gate_task = GateSegmentationAlgo() - while ret_tries < 50: - for _ in range(speed): - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.3, fy=0.3) - rect1, rect2, filtered_frame = gate_task.analyze(frame, True) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - ret_tries = 0 - key = cv.waitKey(30) - if key == ord('q') or key == 27: - break - if key == ord('p'): - paused = not paused - if key == ord('i') and speed > 1: - speed -= 1 - if key == ord('o'): - speed += 1 - else: - ret_tries += 1 - frame_count += 1 + from perception.vis.vis import run + run(['..\..\..\data\GOPR1142.MP4'], GateSegmentationAlgoA(), False) diff --git a/perception/tasks/gate/GateSegmentationAlgo1.py b/perception/tasks/gate/GateSegmentationAlgoB.py similarity index 63% rename from perception/tasks/gate/GateSegmentationAlgo1.py rename to perception/tasks/gate/GateSegmentationAlgoB.py index e054d56..0c416fd 100644 --- a/perception/tasks/gate/GateSegmentationAlgo1.py +++ b/perception/tasks/gate/GateSegmentationAlgoB.py @@ -1,22 +1,22 @@ -from GatePerceiver import GatePerceiver from typing import Tuple import sys import os sys.path.append(os.path.dirname(__file__)) - -from segmentation.combinedFilter import init_combined_filter +from perception.tasks.TaskPerceiver import TaskPerceiver +from perception.tasks.segmentation.combinedFilter import init_combined_filter import numpy as np import cv2 as cv import time import cProfile import statistics -class GateSegmentationAlgo(GatePerceiver): +class GateSegmentationAlgoB(TaskPerceiver): center_x_locs, center_y_locs = [], [] - def __init__(self, alpha): - super() + def __init__(self): + super().__init__() + self.combined_filter = init_combined_filter() def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: """Takes in the background removed image and returns the center between @@ -27,8 +27,8 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: Reurns: (x,y) coordinate with center of gate """ - gate_center = self.output_class(250, 250) - filtered_frame = combined_filter(frame, display_figs=False) + gate_center = (250, 250) + filtered_frame = self.combined_filter(frame, display_figs=False) max_brightness = max([b for b in filtered_frame[:, :, 0][0]]) lowerbound = max(0.84*max_brightness, 120) @@ -41,7 +41,7 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: area_diff = [] area_cnts = [] - # remove all contours with zero area + # remove all contours with zero area cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] for i in range(len(cnt)): @@ -60,14 +60,14 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: cv.rectangle(debug_filter, (x1, y1), (x1+w1, y1+h1), (0,255,0), 2) cv.rectangle(debug_filter, (x2, y2), (x2+w2, y2+h2), (0,255,0), 2) - # drawing center dot + # drawing center dot center_x, center_y = (x1+x2)//2, ((y1+h1//2)+(y2+h2//2))//2 gate_center = self.get_actual_center(center_x, center_y) cv.circle(debug_filter, gate_center, 5, (0,0,255), -1) if debug: - return (self.output_class(gate_center[0], gate_center[1]), debug_filter) - return self.output_class(gate_center[0], gate_center[1]) + return (gate_center[0], gate_center[1]), (frame, debug_filter) + return (gate_center[0], gate_center[1]) def get_actual_center(self, center_x, center_y): # get starting center location, averaging over the first 2510 frames @@ -80,8 +80,8 @@ def get_actual_center(self, center_x, center_y): self.center_y_locs.append(center_y) center_x = int(statistics.mean(self.center_x_locs)) center_y = int(statistics.mean(self.center_y_locs)) - - # use new center location only when it is close to the previous valid location + + # use new center location only when it is close to the previous valid location else: if abs(center_x - self.center_x_locs[-1]) > 10 or \ abs(center_y - self.center_y_locs[-1]) > 10: @@ -92,41 +92,7 @@ def get_actual_center(self, center_x, center_y): return (center_x, center_y) -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - gate_task = GateSegmentationAlgo(0.1) - # once = False - start_time = time.time() - frame_count = 0 - paused = False - speed = 1 - while ret_tries < 50: - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.3, fy=0.3) - - ### FUNCTION CALL, can change this - center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 - frame_count += 1 - #print(frame_count / (time.time() - start_time)) +if __name__ == '__main__': + from perception.vis.vis import run + run(['..\..\..\data\GOPR1142.MP4'], GateSegmentationAlgoB(), False) diff --git a/perception/vis/TestTasks/GateSegmentationAlgo.py b/perception/tasks/gate/GateSegmentationAlgoC.py similarity index 56% rename from perception/vis/TestTasks/GateSegmentationAlgo.py rename to perception/tasks/gate/GateSegmentationAlgoC.py index 1c791d5..3166728 100644 --- a/perception/vis/TestTasks/GateSegmentationAlgo.py +++ b/perception/tasks/gate/GateSegmentationAlgoC.py @@ -1,4 +1,4 @@ -from TaskPerceiver import TaskPerceiver +from perception.tasks.TaskPerceiver import TaskPerceiver from typing import Tuple import sys import os @@ -6,32 +6,32 @@ from collections import namedtuple sys.path.append(str(Path(__file__).parents[2]) + '/tasks') -from segmentation.combinedFilter import init_combined_filter +from perception.tasks.segmentation.combinedFilter import init_combined_filter import numpy as np import cv2 as cv import time import cProfile -class GateSegmentationAlgo(TaskPerceiver): +class GateSegmentationAlgoC(TaskPerceiver): __past_centers = [] __ema = None output_class = namedtuple("GateOutput", ["centerx", "centery"]) output_type = {'centerx': np.int16, 'centery': np.int16} def __init__(self, alpha=0.1): - super() + super().__init__() self.__alpha = alpha self.combined_filter = init_combined_filter() def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze. - debug: Whether or not to display intermediate images for debugging. - Returns: - (x,y) coordinate with center of gate - """ + the two gate posts. + Args: + frame: The background removed frame to analyze. + debug: Whether or not to display intermediate images for debugging. + Returns: + (x,y) coordinate with center of gate + """ gate_center = self.output_class(250, 250) filtered_frame = self.combined_filter(frame, display_figs=False) filtered_frame_copies = [filtered_frame for _ in range(3)] @@ -39,7 +39,7 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: mask = cv.inRange( stacked_filter_frames, np.array([100, 100, 100]), np.array([255, 255, 255]) ) - _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + contours, _ = cv.findContours(mask, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE) if contours: contours.sort(key=self.findStraightness, reverse=True) cnts = contours[:2] @@ -68,53 +68,16 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: cv.circle(stacked_filter_frames, gate_center, 10, (0, 255, 0), -1) if debug: - return ( - self.output_class(gate_center[0], gate_center[1]), - [stacked_filter_frames], - ) - return self.output_class(gate_center[0], gate_center[1]) + return (gate_center[0], gate_center[1]), (frame, stacked_filter_frames) + return (gate_center[0], gate_center[1]) - def findStraightness( - self, contour - ): # output number = contour area/convex area, the bigger the straightest + def findStraightness(self, contour): # output number = contour area/convex area, the bigger the straightest hull = cv.convexHull(contour, False) contour_area = cv.contourArea(contour) hull_area = cv.contourArea(hull) return 10 * contour_area - 5 * hull_area -# this part is temporary and will be covered by other files in the future if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(sys.argv[1]) - ret_tries = 0 - gate_task = GateSegmentationAlgo(0.1) - # once = False - start_time = time.time() - frame_count = 0 - while ret_tries < 50: - ret, frame = cap.read() - if frame_count == 1000: - break - if ret: - frame = cv.resize(frame, None, fx=0.4, fy=0.4) - - ### FUNCTION CALL, can change this - center, filtered_frame = gate_task.analyze(frame, True) - # cProfile.run("gate_task.analyze(frame, True)") - # cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - # (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - # 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - # if not once: - # print(filtered_frame) - # once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xFF - if k == 27: - break - else: - ret_tries += 1 - frame_count += 1 - # print(frame_count / (time.time() - start_time)) + from perception.vis.vis import run + run(['..\..\..\data\GOPR1142.MP4'], GateSegmentationAlgoC(), False) diff --git a/perception/tasks/path_marker/__init__.py b/perception/tasks/path_marker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/perception/tasks/roulette/__init__.py b/perception/tasks/roulette/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/perception/tasks/spinny/spinny_wheel_detection.py b/perception/tasks/roulette/spinny_wheel_detection.py similarity index 100% rename from perception/tasks/spinny/spinny_wheel_detection.py rename to perception/tasks/roulette/spinny_wheel_detection.py diff --git a/perception/tasks/spinny/threshslider.py b/perception/tasks/roulette/threshslider.py similarity index 100% rename from perception/tasks/spinny/threshslider.py rename to perception/tasks/roulette/threshslider.py diff --git a/perception/tasks/segmentation/GateTaskExample.py.orig b/perception/tasks/segmentation/GateTaskExample.py.orig deleted file mode 100644 index 5563092..0000000 --- a/perception/tasks/segmentation/GateTaskExample.py.orig +++ /dev/null @@ -1,86 +0,0 @@ -from TaskPerceiver import TaskPerceiver -from typing import Tuple -from sys import argv as args -from combinedFilter import init_combined_filter -import numpy as np -import cv2 as cv -#from segmentation.aggregateRescaling import init_aggregate_rescaling - -class GateTask(TaskPerceiver): - def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: - """Takes in the background removed image and returns the center between - the two gate posts. - Args: - frame: The background removed frame to analyze - debug: Whether or not tot display intermediate images for debugging - - Returns: - (x,y) coordinate with center of gate - """ -<<<<<<< HEAD - filtered_frame_copies = [filtered_frame for _ in range[10]] - np.stack(filtered_frame_copies, axis = -1) - mask = cv.inRange(filtered_frame, np.array[190], ) - - filtered_frame = combined_filter(frame, display_figs=False) - if debug: - return ((250, 250), filtered_frame) -======= - filtered_frame = combined_filter(frame, display_figs=False) - filtered_frame_copies = [filtered_frame for _ in range(3)] - stacked_filter_frames = np.concatenate(filtered_frame_copies, axis = 2) - mask = cv.inRange(stacked_filter_frames, - np.array([100, 100, 100]), np.array([255, 255, 255])) - _, contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) - if contours: - cnt = max(contours, key=self.findStraightness)#lambda x: cv.minAreaRect(x)[1][0] * cv.minAreaRect(x)[1][1]) - # sorted_straight = sorted(contours, key=self.findStraightness) - # sorted_size = sorted(contours, key=cv.contourArea) - #todo: use these sorted lists and weights to each value to give two best values - rect = cv.minAreaRect(cnt) - boxpts = cv.boxPoints(rect) - box = np.int0(boxpts) - cv.drawContours(stacked_filter_frames,[box],0,(0,0,255),5) - for corner in boxpts: - cv.circle(stacked_filter_frames, (corner[0], corner[1]), 10, (0,0,255), -1) - - if debug: - return ((250, 250), stacked_filter_frames) ->>>>>>> origin/gate-task-example - return (250, 250) - - def findStraightness(self, contour): # output number = contour area/convex area, the bigger the straightest - hull = cv.convexHull(contour, False) - contour_area = cv.contourArea(contour) - hull_area = cv.contourArea(hull) - return 10 * contour_area + 3 * (hull_area - contour_area) - -# this part is temporary and will be covered by other files in the future -if __name__ == '__main__': - combined_filter = init_combined_filter() - cap = cv.VideoCapture(args[1]) - ret_tries = True - gate_task = GateTask() - once = False - while 1 and ret_tries < 50: - ret, frame = cap.read() - if ret: - frame = cv.resize(frame, None, fx=0.4, fy=0.4) - - - ### FUNCTION CALL, can change this - (x, y), filtered_frame = gate_task.analyze(frame, True) - cv.putText(frame, "x: %.2f" % x + " y: %.2f" % y, - (20, frame.shape[0] - 20), cv.FONT_HERSHEY_SIMPLEX, - 2.0, (0, 165, 255), 3) - cv.imshow('original', frame) - cv.imshow('filtered_frame', filtered_frame) - if not once: - print(filtered_frame) - once = True - ret_tries = 0 - k = cv.waitKey(60) & 0xff - if k == 27: - break - else: - ret_tries += 1 diff --git a/perception/tasks/segmentation/kmeans.py b/perception/tasks/segmentation/kmeans.py new file mode 100644 index 0000000..f555779 --- /dev/null +++ b/perception/tasks/segmentation/kmeans.py @@ -0,0 +1,99 @@ +import cv2 +import numpy as np +import matplotlib.pyplot as plt +from scipy.signal import find_peaks, peak_widths +from sys import argv as args + +######################################################################## +# An attempt at an adaptive thresholding algorithm based on the frequency +# of pixel values ("peaks" if looking at a histogram of # pixels vs pixel value of a frame) +# +# *1. *** best of the three *** filter_out_highest_peak_multidim +# pools together how "peak-like" each pixel is in all of the color channels +# of the frame to make a final decision on what is the background +# 2. init_filter_out_highest_peak +# gets rid of large peaks in many different color channels individually +# 3. remove_blotchy_chunks +# places a mask over areas that have lots of edges, which in many cases +# is equivalent to places with lots of noise +######################################################################## + +def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): + """ Attempts to use kmeans to segment the frame into num_group features + (not including the background), denoted by a very large value in votes. + votes is an output of the filter_out_highest_peak_multidim() function + Output: frame_shape x num_groups 3D matrix. Get a group mapped to the + frame by doing groups[:,:,group_num] """ + votes = np.float32(votes).flatten() + + # Make kmeans only consider the non-background pixels + background = np.zeros(votes.shape) + background[votes >= np.percentile(votes, percentile)] = 1 + cluster_data = votes[background == 0] + cluster_indexes = np.array(range(len(votes)))[background == 0] + + # Do kmeans + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) + flags = cv2.KMEANS_RANDOM_CENTERS + compactness, labels, centers = cv2.kmeans(cluster_data, num_groups, None, criteria, 10, flags) + + # Reconstruct the original votes array with background's label = -1 + label_arr = np.empty(votes.shape) + label_arr[background == 1] = -1 + for i in range(num_groups): + label_arr[cluster_indexes[labels.flatten() == i]] = i + + unique_labels, label_counts = np.unique(label_arr, return_counts=True) + label_order = list(range(np.int0(np.amax(unique_labels)) + 2)) # something is erroring here + if len(label_counts) < num_groups + 1: + # add in a slot for the background if no background is found + label_counts = np.insert(label_counts, 0, 0) + + label_order.sort(key=lambda x: label_counts[x]) + + groups = np.empty((frame_shape[0], frame_shape[1], num_groups + 1)) + for i, l in enumerate(label_order): + group = np.zeros(votes.shape) + group[label_arr.flatten() == l - 1] = 255 + groups[:, :, i] = np.reshape(group, frame_shape[:2]) + + # for i in range(len(unique_labels)): + # cv2.imshow(str(i) + " label", groups[:,:,i]) + + return groups + + +########################################### +# Main Body +########################################### + +if __name__ == "__main__": + # For testing porpoises + cap = cv2.VideoCapture(args[1]) + ret, frame = cap.read() + out = cv2.VideoWriter('out.avi', cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'), 30.0, + (int(frame.shape[1] * 0.4), int(frame.shape[0] * 0.4))) + + + ret_tries = 0 + + while (1 and ret_tries < 50): + ret, frame = cap.read() + + if ret: + frame = cv2.resize(frame, None, fx=0.4, fy=0.4) + + + + cv2.imshow('original', frame) + plt.pause(0.001) + + ret_tries = 0 + k = cv2.waitKey(60) & 0xff + if k == 27: + break + else: + ret_tries += 1 + cv2.destroyAllWindows() + cap.release() + out.release() \ No newline at end of file diff --git a/perception/vis/FrameWrapper.py b/perception/vis/FrameWrapper.py index 4eb6e01..fc6d602 100644 --- a/perception/vis/FrameWrapper.py +++ b/perception/vis/FrameWrapper.py @@ -24,7 +24,7 @@ class FrameWrapper(): WEBCAM_TRIES = 10 def __init__(self, filenames, resize=1): - self.filenames = filenames # Get this list of relative paths to files from vis + self.filenames = filenames # Get this list of relative paths to files from vis # There aren't any checks for resize==1 to improve speed b/c this expects resize != 1 self.resize = resize diff --git a/perception/vis/TaskPerceiver.py b/perception/vis/TaskPerceiver.py deleted file mode 100644 index e8b8609..0000000 --- a/perception/vis/TaskPerceiver.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Any, Dict, Tuple -import numpy as np - -class TaskPerceiver: - - def __init__(self, **kwargs): - """Initializes the TaskPerceiver. - Args: - kwargs: Each keyworded argument is of the form - var_name = (range, default_val), where range is the range of values - for the slider which controls this variable, and default_val is - the initial value of the slider. - """ - self.time = 0 - self.variables = kwargs - - def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]) -> Any: - """Runs the algorithm and returns the result. - Args: - frame: The frame to analyze - debug: Whether or not to display intermediate images for debugging - slider_vals: A list of names of the variables which the user should be - able to control from the Visualizer, mapped to current slider - value for that variable - Returns: - the result of the algorithm - """ - raise NotImplementedError("Need to implement with child class.") - - def var_info(self) -> Dict[str, Tuple[Tuple[int, int], int]]: - return self.variables \ No newline at end of file diff --git a/perception/vis/window_builder.py b/perception/vis/Visualizer.py similarity index 100% rename from perception/vis/window_builder.py rename to perception/vis/Visualizer.py diff --git a/perception/vis/algo_stats b/perception/vis/algo_stats deleted file mode 100644 index edd4cf5661573c93313bc05cc1650ad0680e24f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5441 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 diff --git a/perception/vis/vis.py b/perception/vis/vis.py index 81f08e8..a5e30e9 100644 --- a/perception/vis/vis.py +++ b/perception/vis/vis.py @@ -1,58 +1,68 @@ import argparse -from FrameWrapper import FrameWrapper +from perception.vis.FrameWrapper import FrameWrapper import cv2 as cv -from window_builder import Visualizer -import cProfile as cp -import pstats - -# Parse arguments -parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') -parser.add_argument( - '--data', default='webcam', type=str -) -parser.add_argument('--algorithm', type=str) -parser.add_argument('--save_video', action='store_true') -args = parser.parse_args() - -# Get algorithm module -exec("from TestTasks.{} import {} as Algorithm".format(args.algorithm, args.algorithm)) - -# Initialize image source -data_sources = [args.data] -data = FrameWrapper(data_sources, 0.25) - -algorithm = Algorithm() -window_builder = Visualizer(algorithm.var_info()) -video_frames = [] -# Main Loop -def main(): +from perception.vis.Visualizer import Visualizer +import cProfile + + +def run(data_sources, algorithm, save_video=False): + out = None + window_builder = Visualizer(algorithm.variables) + data = FrameWrapper(data_sources, 0.25) + frame_count = 0 + paused = False + speed = 1 + for frame in data: + if frame_count % speed == 0 and not paused: + if algorithm.variables: + state, debug_frames = algorithm.analyze(frame, debug=True, slider_vals=window_builder.update_vars()) + else: + state, debug_frames = algorithm.analyze(frame, debug=True) + + to_show = window_builder.display(debug_frames) + cv.imshow('Debug Frames', to_show) + if save_video: + if out is None: + height, width, _ = to_show.shape + # TODO: get codec to work + out = cv.VideoWriter('rec.mp4', cv.VideoWriter_fourcc(*'mp4v'), 60, (height, width)) + if out: + out.write(to_show) + + key = cv.waitKey(30) + if key == ord('q') or key == 27: + break + if key == ord('p'): + paused = not paused + if key == ord('i') and speed > 1: + speed -= 1 + print(f'speed {speed}') + if key == ord('o'): + speed += 1 + print(f'speed {speed}') + frame_count += 1 + + cv.destroyAllWindows() + if out: + out.release() + + +if __name__ == '__main__': + # Parse arguments + parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') + parser.add_argument('--data', default='webcam', type=str) + parser.add_argument('--algorithm', type=str) + parser.add_argument('--save_video', action='store_true') + parser.add_argument('--profile', action='store_true') + args = parser.parse_args() + + # Import Algorithm + exec(f"from {args.algorithm} import {args.algorithm.split('.')[-1]} as Algorithm") + algorithm = Algorithm() + data_sources = [args.data] - state, debug_frames = algorithm.analyze( - frame, debug=True, slider_vals=window_builder.update_vars() - ) - to_show = window_builder.display(debug_frames) - cv.imshow('Debug Frames', to_show) - if args.save_video: - video_frames.append(to_show) - - key_pressed = cv.waitKey(60) & 0xFF - if key_pressed == 112: - cv.waitKey(0) # pause - if key_pressed == 113: - break # quit - - -cp.run('main()', 'algo_stats') -cv.destroyAllWindows() -p = pstats.Stats('algo_stats') -p.print_stats('analyze') - -if args.save_video: - height, width, _ = video_frames[0].shape - out = cv.VideoWriter('deb_cap.avi', cv.VideoWriter_fourcc(*'XVID'), 60, (height, width)) - for img in video_frames: - height2, width2, _ = img.shape - if (height2, width2) == (height, width): - out.write(img) - out.release() \ No newline at end of file + if args.profile: + stats = cProfile.run('run(data_sources, algorithm, args.save_video)') + else: + run(data_sources, algorithm, args.save_video) diff --git a/requirements.txt b/requirements.txt index 2f6349f..e69de29 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +0,0 @@ -opencv-python==3.4.3.18 -torch==1.2.0 -torchvision==0.6.0 -scipy -numpy -matplotlib From 0b41e4ca48ba0352e1d8f95f5dc36e683b78cff8 Mon Sep 17 00:00:00 2001 From: axquaris Date: Tue, 5 Jan 2021 15:43:18 -0800 Subject: [PATCH 15/19] vis updates merged --- perception/tasks/TaskPerceiver.py | 1 - perception/vis/TestAlgo.py | 24 +++++++++------- perception/vis/Visualizer.py | 10 +++++++ perception/vis/vis.py | 48 +++++++++++++++++++++++-------- requirements.txt | 1 + 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/perception/tasks/TaskPerceiver.py b/perception/tasks/TaskPerceiver.py index eec40b7..f99f153 100644 --- a/perception/tasks/TaskPerceiver.py +++ b/perception/tasks/TaskPerceiver.py @@ -11,7 +11,6 @@ def __init__(self, **kwargs): for the slider which controls this variable, and default_val is the initial value of the slider. """ - self.time = 0 self.kwargs = kwargs def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]) -> Any: diff --git a/perception/vis/TestAlgo.py b/perception/vis/TestAlgo.py index 3410aad..3d2ebac 100644 --- a/perception/vis/TestAlgo.py +++ b/perception/vis/TestAlgo.py @@ -7,19 +7,23 @@ class TestAlgo(TaskPerceiver): def __init__(self): super().__init__(canny_low=((0, 255), 100), canny_high=((0, 255), 200)) + self.t = .1 def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]): fig = plt.figure() - x1 = np.linspace(0.0, 5.0) - x2 = np.linspace(0.0, 2.0) - - y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) - y2 = np.cos(2 * np.pi * x2) - - line1, = plt.plot(x1, y1, 'ko-') - line1.set_ydata(np.cos(2 * np.pi * (x1 + slider_vals['canny_low'] * 3.14 / 2)) * np.exp(-x1)) + x = np.linspace(0.0, 5.0) + y = np.cos(2 * np.pi * (x + slider_vals['canny_low'] * 3.14 / 2)) * np.exp(-x * self.t) + plt.plot(x, y, 'ko-') fig.canvas.draw() - return frame, [frame, cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), + self.t *= 1.01 + return frame, [frame, + cv.cvtColor(frame, cv.COLOR_BGR2GRAY), + cv.flip(cv.cvtColor(frame, cv.COLOR_BGR2GRAY), cv.ROTATE_180), cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), - cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0), fig] \ No newline at end of file + cv.flip(cv.Canny(frame, slider_vals['canny_low'], slider_vals['canny_high']), 0), + fig] + +if __name__ == '__main__': + from perception.vis.vis import run + run(['webcam'], TestAlgo(), True) \ No newline at end of file diff --git a/perception/vis/Visualizer.py b/perception/vis/Visualizer.py index 664db0e..65c7bb2 100644 --- a/perception/vis/Visualizer.py +++ b/perception/vis/Visualizer.py @@ -2,6 +2,7 @@ import cv2 as cv import math from typing import Dict, Tuple, List +from matplotlib.pyplot import Figure def nothing(x): pass @@ -40,6 +41,15 @@ def reshape(self, frames: List[np.ndarray]) -> List[np.ndarray]: def display(self, frames: List[np.ndarray]) -> np.ndarray: num_frames = len(frames) assert (num_frames > 0 and num_frames <= 9), 'Invalid number of frames!' + + for i, frame in enumerate(frames): + if isinstance(frame, Figure): + img = np.fromstring(frame.canvas.tostring_rgb(), dtype=np.uint8, + sep='') + img = img.reshape(frame.canvas.get_width_height()[::-1] + (3,)) + img = cv.cvtColor(img, cv.COLOR_RGB2BGR) + frames[i] = img + frames = self.reshape(self.three_stack(frames)) columns = math.ceil(num_frames/math.sqrt(num_frames)) diff --git a/perception/vis/vis.py b/perception/vis/vis.py index 520e65d..2a2c0d4 100644 --- a/perception/vis/vis.py +++ b/perception/vis/vis.py @@ -1,8 +1,15 @@ import argparse +import os + +from perception import ALGOS from perception.vis.FrameWrapper import FrameWrapper import cv2 as cv from perception.vis.Visualizer import Visualizer import cProfile +import pstats +import imageio +from matplotlib.pyplot import Figure +import numpy as np def run(data_sources, algorithm, save_video=False): @@ -24,11 +31,13 @@ def run(data_sources, algorithm, save_video=False): cv.imshow('Debug Frames', to_show) if save_video: if out is None: - height, width, _ = to_show.shape + # height, width, _ = to_show.shape # TODO: get codec to work - out = cv.VideoWriter('rec.mp4', cv.VideoWriter_fourcc(*'mp4v'), 60, (height, width)) + # out = cv.VideoWriter('rec.mp4', cv.VideoWriter_fourcc(*'mp4v'), 60, (height, width)) + out = imageio.get_writer('vis_rec.mp4') if out: - out.write(to_show) + out_img = cv.cvtColor(to_show, cv.COLOR_BGR2RGB) + out.append_data(out_img) key = cv.waitKey(30) if key == ord('q') or key == 27: @@ -45,24 +54,39 @@ def run(data_sources, algorithm, save_video=False): cv.destroyAllWindows() if out: - out.release() + out.close() + + +def profile(*args, stats='all'): + with cProfile.Profile() as pr: + run(*args) + if stats == 'all': + pr.print_stats() + else: + pr.print_stats(stats) if __name__ == '__main__': # Parse arguments parser = argparse.ArgumentParser(description='Visualizes perception algorithms.') parser.add_argument('--data', default='webcam', type=str) - parser.add_argument('--algorithm', type=str) + parser.add_argument('--algorithm', type=str, required=True) + parser.add_argument('--profile', default=None, type=str) parser.add_argument('--save_video', action='store_true') - parser.add_argument('--profile', action='store_true') args = parser.parse_args() - # Import Algorithm - exec(f"from {args.algorithm} import {args.algorithm.split('.')[-1]} as Algorithm") - algorithm = Algorithm() - data_sources = [args.data] + # Get algorithm class and init + algorithm = ALGOS[args.algorithm]() - if args.profile: - stats = cProfile.run('run(data_sources, algorithm, args.save_video)') + # Initialize image source + # detects args.data, get a list of all file directory when given a directory + # change data_source to a list of all files in the directory + if os.path.isdir(args.data): + data_sources = os.listdir(args.data) else: + data_sources = [args.data] + + if args.cProfiler is not None: run(data_sources, algorithm, args.save_video) + else: + profile(data_sources, algorithm, args.save_video, stats=args.profile) diff --git a/requirements.txt b/requirements.txt index af6d83d..a4fd5c0 100755 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ scipy numpy matplotlib imageio +imageio-ffmpeg \ No newline at end of file From 3573a218121d447b2d0db25df7988565204763fd Mon Sep 17 00:00:00 2001 From: axquaris Date: Tue, 5 Jan 2021 16:12:38 -0800 Subject: [PATCH 16/19] tidied up formatting and imports --- perception/slots/__init__.py | 0 .../play_slots_detection.py | 2 + perception/tasks/TaskPerceiver.py | 12 +- perception/tasks/cross/cross_detection.py | 25 +-- perception/tasks/gate/GateCenterAlgo.py | 8 +- .../tasks/gate/GateSegmentationAlgoA.py | 15 +- .../tasks/gate/GateSegmentationAlgoB.py | 3 +- .../tasks/gate/GateSegmentationAlgoC.py | 9 +- perception/tasks/gate/archive/detectGate.py | 4 +- perception/tasks/gate/archive/threshTest.py | 3 + .../path_marker/path_marker_detection.py | 4 +- .../tasks/roulette/spinny_wheel_detection.py | 113 ++++++----- perception/tasks/roulette/threshslider.py | 106 ++++++---- perception/tasks/sanity_test.py | 2 + .../tasks/segmentation/aggregateRescaling.py | 8 +- .../tasks/segmentation/combinedFilter.py | 4 +- perception/tasks/segmentation/kmeans.py | 6 +- .../peak_removal_adaptive_thresholding.py | 188 ++++++++++-------- perception/vis/vis.py | 10 +- requirements.txt | 3 +- 20 files changed, 287 insertions(+), 238 deletions(-) create mode 100644 perception/slots/__init__.py rename perception/{tasks/path_marker => slots}/play_slots_detection.py (99%) diff --git a/perception/slots/__init__.py b/perception/slots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/perception/tasks/path_marker/play_slots_detection.py b/perception/slots/play_slots_detection.py similarity index 99% rename from perception/tasks/path_marker/play_slots_detection.py rename to perception/slots/play_slots_detection.py index 0ebb4b5..4b8b741 100644 --- a/perception/tasks/path_marker/play_slots_detection.py +++ b/perception/slots/play_slots_detection.py @@ -2,6 +2,8 @@ import cv2 import sys +# TODO: port to vis + TaskPerciever format or remove + #### TODO: maybe look into pattern matching # Data fron the new course footage dropbox folder diff --git a/perception/tasks/TaskPerceiver.py b/perception/tasks/TaskPerceiver.py index f99f153..6c55e69 100644 --- a/perception/tasks/TaskPerceiver.py +++ b/perception/tasks/TaskPerceiver.py @@ -1,8 +1,8 @@ -from typing import Any, Dict, Tuple +from typing import Any, Dict import numpy as np -class TaskPerceiver: +class TaskPerceiver: def __init__(self, **kwargs): """Initializes the TaskPerceiver. Args: @@ -16,13 +16,13 @@ def __init__(self, **kwargs): def analyze(self, frame: np.ndarray, debug: bool, slider_vals: Dict[str, int]) -> Any: """Runs the algorithm and returns the result. Args: - frame: The frame to analyze + frame: The frame to analyze debug: Whether or not to display intermediate images for debugging slider_vals: A list of names of the variables which the user should be able to control from the Visualizer, mapped to current slider value for that variable - Returns: - the result of the algorithm - debug frames must each be same size as original input frame. Might change this in the future. + Returns: + the result of the algorithm + debug frames must each be same size as original input frame. Might change this in the future. """ raise NotImplementedError("Need to implement with child class.") diff --git a/perception/tasks/cross/cross_detection.py b/perception/tasks/cross/cross_detection.py index 0b49772..e256b75 100644 --- a/perception/tasks/cross/cross_detection.py +++ b/perception/tasks/cross/cross_detection.py @@ -8,10 +8,10 @@ ############################################################################# sys.path.insert(0, '../background_removal') -from peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim -from combined_filter import combined_filter +from perception.tasks.segmentation.peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim +from perception.tasks.segmentation.combinedFilter import init_combined_filter -ret, frame = True, cv2.imread('../data/cross/cross.png') # https://i.imgur.com/rjv1Vcy.png +ret, frame = True, cv2.imread('../data/cross/cross.png') # https://i.imgur.com/rjv1Vcy.png # "hsv" = Apply hsv thresholding before trying to find the path marker # "multidim" = Apply filter_out_highest_peak_multidim @@ -29,7 +29,7 @@ def find_cross(frame, draw_figs=True): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(gray, 127, 255,0) - __, contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + __, contours,hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) contours.sort(key=lambda c: cv2.contourArea(c), reverse=True) possible_crosses = [] @@ -44,17 +44,16 @@ def find_cross(frame, draw_figs=True): if defects is not None and len(defects) == 4: possible_crosses.append(defects) - if draw_figs: img = frame.copy() for defects in possible_crosses: for i in range(defects.shape[0]): - s,e,f,d = defects[i,0] + s, e, f, d = defects[i, 0] # start = tuple(cnt[s][0]) # end = tuple(cnt[e][0]) far = tuple(cnt[f][0]) # cv2.line(img,start,end,[0,255,0],2) - cv2.circle(img,far,5,[0,0,255],-1) + cv2.circle(img, far, 5, [0, 0, 255], -1) cv2.imshow('cross at contour number ' + str(i),img) cv2.imshow('original', frame) @@ -64,15 +63,15 @@ def find_cross(frame, draw_figs=True): ########################################### # Main Body ########################################### - +# TODO: port to vis if __name__ == "__main__": + combined_filter = init_combined_filter() + ret_tries = 0 while(1 and ret_tries < 50): # ret,frame = cap.read() - if ret == True: # frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5) - if thresholding == "multidim": votes1, threshed = filter_out_highest_peak_multidim(frame) threshed = cv2.morphologyEx(threshed, cv2.MORPH_OPEN, np.ones((5,5),np.uint8)) @@ -86,13 +85,9 @@ def find_cross(frame, draw_figs=True): ret_tries = 0 k = cv2.waitKey(60) & 0xff - if k == 27: # esc - if testing: - print("hsv thresholds:") - print(thresholds_used) + if k == 27: # esc break else: ret_tries += 1 cv2.destroyAllWindows() - cap.release() diff --git a/perception/tasks/gate/GateCenterAlgo.py b/perception/tasks/gate/GateCenterAlgo.py index 9e5a0cd..5c3e6f6 100644 --- a/perception/tasks/gate/GateCenterAlgo.py +++ b/perception/tasks/gate/GateCenterAlgo.py @@ -7,6 +7,7 @@ import cv2 as cv import statistics + class GateCenterAlgo(TaskPerceiver): center_x_locs, center_y_locs = [], [] output_class = namedtuple("GateOutput", ["centerx", "centery"]) @@ -18,8 +19,9 @@ def __init__(self): self.use_optical_flow = False self.optical_flow_c = 0.1 self.gate = GateSegmentationAlgoA() - self.prvs = None - + self.prvs = None + + # TODO: do input and return typing def analyze(self, frame, debug, slider_vals): self.optical_flow_c = slider_vals['optical_flow_c']/100 rect, debug_filters = self.gate.analyze(frame, True) @@ -82,7 +84,7 @@ def get_center(self, rect1, rect2, frame): x2, y2, w2, h2 = rect2 center_x, center_y = (x1 + x2) // 2, ((y1 + h1 // 2) + (y2 + h2 // 2)) // 2 self.prvs, mag, ang = self.dense_optical_flow(frame) - # print(np.mean(mag)) + if len(self.center_x_locs) < 25 or (np.mean(mag) < 40 and ((not self.use_optical_flow ) or \ (self.use_optical_flow and (center_x - self.gate_center[0])**2 + (center_y - self.gate_center[1])**2 < 50))): self.use_optical_flow = False diff --git a/perception/tasks/gate/GateSegmentationAlgoA.py b/perception/tasks/gate/GateSegmentationAlgoA.py index affcbe0..5e3173f 100644 --- a/perception/tasks/gate/GateSegmentationAlgoA.py +++ b/perception/tasks/gate/GateSegmentationAlgoA.py @@ -1,17 +1,9 @@ from perception.tasks.TaskPerceiver import TaskPerceiver from typing import Tuple -import sys -import os -sys.path.append(os.path.dirname(__file__)) - from perception.tasks.segmentation.combinedFilter import init_combined_filter import numpy as np -import math import cv2 as cv -import time -import cProfile -import statistics class GateSegmentationAlgoA(TaskPerceiver): @@ -21,15 +13,16 @@ def __init__(self): super().__init__() self.combined_filter = init_combined_filter() + # TODO: fix return typing def analyze(self, frame: np.ndarray, debug: bool, slider_vals=None) -> Tuple[float, float]: """Takes in the background removed image and returns the center between the two gate posts. Args: frame: The background removed frame to analyze debug: Whether or not tot display intermediate images for debugging - Reurns: - (x,y) coordinate with center of gate - """ + Reurns: + (x,y) coordinate with center of gate + """ rect1, rect2 = None, None filtered_frame = self.combined_filter(frame, display_figs=False) diff --git a/perception/tasks/gate/GateSegmentationAlgoB.py b/perception/tasks/gate/GateSegmentationAlgoB.py index 0c416fd..557d088 100644 --- a/perception/tasks/gate/GateSegmentationAlgoB.py +++ b/perception/tasks/gate/GateSegmentationAlgoB.py @@ -7,10 +7,9 @@ from perception.tasks.segmentation.combinedFilter import init_combined_filter import numpy as np import cv2 as cv -import time -import cProfile import statistics + class GateSegmentationAlgoB(TaskPerceiver): center_x_locs, center_y_locs = [], [] diff --git a/perception/tasks/gate/GateSegmentationAlgoC.py b/perception/tasks/gate/GateSegmentationAlgoC.py index 3166728..2e738ac 100644 --- a/perception/tasks/gate/GateSegmentationAlgoC.py +++ b/perception/tasks/gate/GateSegmentationAlgoC.py @@ -1,16 +1,11 @@ from perception.tasks.TaskPerceiver import TaskPerceiver from typing import Tuple -import sys -import os -from pathlib import Path from collections import namedtuple -sys.path.append(str(Path(__file__).parents[2]) + '/tasks') from perception.tasks.segmentation.combinedFilter import init_combined_filter import numpy as np import cv2 as cv -import time -import cProfile + class GateSegmentationAlgoC(TaskPerceiver): __past_centers = [] @@ -23,6 +18,7 @@ def __init__(self, alpha=0.1): self.__alpha = alpha self.combined_filter = init_combined_filter() + # TODO: fix return typing def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: """Takes in the background removed image and returns the center between the two gate posts. @@ -58,6 +54,7 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: self.__alpha * gate_center + (1 - self.__alpha) * self.__ema ) gate_center = (int(self.__ema[0]), int(self.__ema[1])) + # TODO: clean this up via hyperparam or move to gate center algo # if len(self.__past_centers) < 15: # self.__past_centers += [gate_center] # else: diff --git a/perception/tasks/gate/archive/detectGate.py b/perception/tasks/gate/archive/detectGate.py index cf309ff..5f7e868 100644 --- a/perception/tasks/gate/archive/detectGate.py +++ b/perception/tasks/gate/archive/detectGate.py @@ -1,10 +1,10 @@ import numpy as np import cv2 import argparse -import sys -from PIL import Image import time +# TODO: port to vis + TaskPerciever format or remove + video_file = 'truncated_semi_final_run.mp4' EPSILON = 40 OVERLAP_EPS = 40 diff --git a/perception/tasks/gate/archive/threshTest.py b/perception/tasks/gate/archive/threshTest.py index 0bd6c71..70d8aa5 100644 --- a/perception/tasks/gate/archive/threshTest.py +++ b/perception/tasks/gate/archive/threshTest.py @@ -2,6 +2,9 @@ import cv2 as cv import argparse import numpy as np + +# TODO: port to vis + TaskPerciever format or remove + #expectations #contours closest to the last ones #should know when we passed through the gate diff --git a/perception/tasks/path_marker/path_marker_detection.py b/perception/tasks/path_marker/path_marker_detection.py index f788bd5..4cacd63 100644 --- a/perception/tasks/path_marker/path_marker_detection.py +++ b/perception/tasks/path_marker/path_marker_detection.py @@ -1,6 +1,8 @@ -from combined_filter import init_combined_filter +from perception.tasks.segmentation.combinedFilter import init_combined_filter from typing import Union +# TODO: port to vis + TaskPerciever format or remove + if __name__ == "__main__": import numpy as np import cv2 diff --git a/perception/tasks/roulette/spinny_wheel_detection.py b/perception/tasks/roulette/spinny_wheel_detection.py index 8f0f72f..463c73a 100644 --- a/perception/tasks/roulette/spinny_wheel_detection.py +++ b/perception/tasks/roulette/spinny_wheel_detection.py @@ -4,33 +4,36 @@ import sys import time -#CHANGE PARAMETER IN CALL TO "THRESH" FUNCTION TO CHANGE COLOR -file_name = "GOPR1145.MP4" #video file from dropbox +# TODO: port to vis + TaskPerciever format or remove + +# CHANGE PARAMETER IN CALL TO "THRESH" FUNCTION TO CHANGE COLOR +file_name = "GOPR1145.MP4" # video file from dropbox vid = cv2.VideoCapture(file_name) frames = 0 avgLength = 10 centers = [] + def thresh(frame, color='red'): + blur = cv2.GaussianBlur(frame, (5, 5), 0) + hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV) + + if color == 'red': + lower = np.uint8([29, 77, 36]) + upper = np.uint8([130, 250, 255]) + mask = cv2.inRange(hsv, lower, upper) + mask = cv2.bitwise_not(mask) + elif color == 'blue': + lower = np.uint8([86, 141, 0]) + upper = np.uint8([106, 220, 168]) + mask = cv2.inRange(hsv, lower, upper) + else: + lower = np.uint8([66, 208, 157]) + upper = np.uint8([86, 255, 209]) + mask = cv2.inRange(hsv, lower, upper) + + return mask - blur = cv2.GaussianBlur(frame, (5, 5), 0) - hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV) - - if color == 'red': - lower = np.uint8([29,77,36]) - upper = np.uint8([130,250,255]) - mask = cv2.inRange(hsv,lower,upper) - mask = cv2.bitwise_not(mask) - elif color == 'blue': - lower = np.uint8([86,141,0]) - upper = np.uint8([106,220,168]) - mask = cv2.inRange(hsv,lower,upper) - else: - lower = np.uint8([66,208,157]) - upper = np.uint8([86,255,209]) - mask = cv2.inRange(hsv,lower,upper) - - return mask def heuristic(contour): rect = cv2.minAreaRect(contour) @@ -44,32 +47,37 @@ def heuristic(contour): cen1 = cv2.minAreaRect(likelySection[1]['cont'])[0] dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) dist = min([dis0, dis1]) - heur = area - 3 * diff - 20 * dist + heur = area - 3 * diff - 20 * dist return heur - + + def allLarger(thresh): for cnt in likelySection: if cnt['heur'] < thresh: return False return True + def drawRects(frame, contours): tempPts = [] for cnt in contours: rect = cv2.minAreaRect(cnt['cont']) boxpts = cv2.boxPoints(rect) box = np.int0(boxpts) - cv2.drawContours(frame,[box],0,(0,0,255),1) - cv2.drawContours(frame, [cnt['cont']],0,(0,255,0),1) - cv2.drawContours(frame, [cv2.convexHull(cnt['cont'])],0,(255,0,0),1) + cv2.drawContours(frame, [box], 0, (0, 0, 255), 1) + cv2.drawContours(frame, [cnt['cont']], 0, (0, 255, 0), 1) + cv2.drawContours(frame, [cv2.convexHull(cnt['cont'])], 0, (255, 0, 0), 1) tempPts.append(rect[0]) - #cv2.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) + # cv2.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) if len(tempPts) > 1 and allLarger(60): - cv2.circle(frame, (int(tempPts[0][0]), int(tempPts[0][1])), 10, (0,0,255), -1) - cv2.circle(frame, (int(tempPts[1][0]), int(tempPts[1][1])), 10, (0,0,255), -1) + cv2.circle(frame, (int(tempPts[0][0]), int(tempPts[0][1])), 10, (0, 0, 255), -1) + cv2.circle(frame, (int(tempPts[1][0]), int(tempPts[1][1])), 10, (0, 0, 255), -1) + + def midPt(pt1, pt2): return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) + def getAvgPt(pt): points.append(pt) exes = list(map(lambda x: x[0], points)) @@ -79,34 +87,33 @@ def getAvgPt(pt): del points[:10] return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) + likelySection = [] points = [] while vid.isOpened(): - start = time.time() - ret, frame = vid.read() - if(ret == False): - continue - - threshed = thresh(frame,'other') - res, contours, hierarchy = cv2.findContours(threshed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - contours.sort(key=heuristic, reverse = True) - if len(contours) > 1: - c1 = contours[0] - c2 = contours[1] - heur0 = heuristic(c1) - heur1 = heuristic(c2) - likelySection = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] - untampered = np.copy(frame) - if contours: - drawRects(frame, likelySection) - cv2.imshow("Frame", frame) - cv2.imshow('Res', res) - - if (cv2.waitKey(1) & 0xFF) == ord('q') or frames > 900: - break + start = time.time() + ret, frame = vid.read() + if (ret == False): + continue -vid.release() -cv2.destroyAllWindows() + threshed = thresh(frame, 'other') + res, contours, hierarchy = cv2.findContours(threshed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + contours.sort(key=heuristic, reverse=True) + if len(contours) > 1: + c1 = contours[0] + c2 = contours[1] + heur0 = heuristic(c1) + heur1 = heuristic(c2) + likelySection = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] + untampered = np.copy(frame) + if contours: + drawRects(frame, likelySection) + cv2.imshow("Frame", frame) + cv2.imshow('Res', res) + if (cv2.waitKey(1) & 0xFF) == ord('q') or frames > 900: + break +vid.release() +cv2.destroyAllWindows() diff --git a/perception/tasks/roulette/threshslider.py b/perception/tasks/roulette/threshslider.py index e8bf654..fadc915 100644 --- a/perception/tasks/roulette/threshslider.py +++ b/perception/tasks/roulette/threshslider.py @@ -2,20 +2,23 @@ import cv2 as cv import argparse import numpy as np -#expectations -#contours closest to the last ones -#should know when we passed through the gate + +# TODO: port to vis + TaskPerciever format or remove + +# expectations +# contours closest to the last ones +# should know when we passed through the gate """ IMPORTANT!!!! RUN THIS WITH $ python3 threshTest.py GOPR1142.mp4 """ max_value = 255 -max_value_H = 360//2 -low_H = 86#49#29#0 -low_S = 141#77#0 -low_V = 0#36#0 For Small sector, increasing lower V bound reduces -high_H = 106#130#max_value_H -high_S = 217#250#max_value -high_V = 168#max_value +max_value_H = 360 // 2 +low_H = 86 # 49#29#0 +low_S = 141 # 77#0 +low_V = 0 # 36#0 For Small sector, increasing lower V bound reduces +high_H = 106 # 130#max_value_H +high_S = 217 # 250#max_value +high_V = 168 # max_value window_capture_name = 'Video Capture' window_detection_name = 'Object Detection' low_H_name = 'Low H' @@ -24,63 +27,78 @@ high_H_name = 'High H' high_S_name = 'High S' high_V_name = 'High V' + + def on_low_H_thresh_trackbar(val): global low_H global high_H low_H = val - low_H = min(high_H-1, low_H) + low_H = min(high_H - 1, low_H) cv.setTrackbarPos(low_H_name, window_detection_name, low_H) + + def on_high_H_thresh_trackbar(val): global low_H global high_H high_H = val - high_H = max(high_H, low_H+1) + high_H = max(high_H, low_H + 1) cv.setTrackbarPos(high_H_name, window_detection_name, high_H) + + def on_low_S_thresh_trackbar(val): global low_S global high_S low_S = val - low_S = min(high_S-1, low_S) + low_S = min(high_S - 1, low_S) cv.setTrackbarPos(low_S_name, window_detection_name, low_S) + + def on_high_S_thresh_trackbar(val): global low_S global high_S high_S = val - high_S = max(high_S, low_S+1) + high_S = max(high_S, low_S + 1) cv.setTrackbarPos(high_S_name, window_detection_name, high_S) + + def on_low_V_thresh_trackbar(val): global low_V global high_V low_V = val - low_V = min(high_V-1, low_V) + low_V = min(high_V - 1, low_V) cv.setTrackbarPos(low_V_name, window_detection_name, low_V) + + def on_high_V_thresh_trackbar(val): global low_V global high_V high_V = val - high_V = max(high_V, low_V+1) + high_V = max(high_V, low_V + 1) cv.setTrackbarPos(high_V_name, window_detection_name, high_V) + def drawRects(frame, contours): tempPts = [] for cnt in contours: rect = cv.minAreaRect(cnt['cont']) boxpts = cv.boxPoints(rect) box = np.int0(boxpts) - cv.drawContours(frame,[box],0,(0,0,255),1) - cv.drawContours(frame, [cnt['cont']],0,(0,255,0),1) - cv.drawContours(frame, [cv.convexHull(cnt['cont'])],0,(255,0,0),1) + cv.drawContours(frame, [box], 0, (0, 0, 255), 1) + cv.drawContours(frame, [cnt['cont']], 0, (0, 255, 0), 1) + cv.drawContours(frame, [cv.convexHull(cnt['cont'])], 0, (255, 0, 0), 1) tempPts.append(rect[0]) cv.putText(frame, str(cnt['heur']), (int(rect[0][0]), int(rect[0][1])), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255)) if len(tempPts) > 1 and allLarger(60): - #global paused + # global paused paused = True avgPt = getAvgPt(midPt(tempPts[0], tempPts[1])) - cv.circle(frame, (avgPt[0], avgPt[1]), 10, (0,0,255), -1) + cv.circle(frame, (avgPt[0], avgPt[1]), 10, (0, 0, 255), -1) + def midPt(pt1, pt2): return ((pt1[0] + pt2[0]) / 2, (pt1[1] + pt2[1]) / 2) + def getAvgPt(pt): points.append(pt) exes = list(map(lambda x: x[0], points)) @@ -90,6 +108,7 @@ def getAvgPt(pt): del points[:10] return (int(sum(exes) / len(exes)), int(sum(whys) / len(whys))) + """ def findLikelyGate(rectList, contours): if not (rectList and contours): @@ -101,6 +120,7 @@ def findLikelyGate(rectList, contours): return """ + def heuristic(contour): rect = cv.minAreaRect(contour) area = rect[1][0] * rect[1][1] @@ -113,16 +133,18 @@ def heuristic(contour): cen1 = cv.minAreaRect(likelyGate[1]['cont'])[0] dis1 = np.linalg.norm(np.array(cent) - np.array(cen1)) dist = min([dis0, dis1]) - heur = area - 3 * diff - 20 * dist #only factor in dist with all heurs larger than 60 - #print(heur) + heur = area - 3 * diff - 20 * dist # only factor in dist with all heurs larger than 60 + # print(heur) return heur + def allLarger(thresh): for cnt in likelyGate: if cnt['heur'] < thresh: return False return True + parser = argparse.ArgumentParser(description='Code for Thresholding Operations using inRange tutorial.') parser.add_argument('camera', help='Camera devide number.', default=0, type=str) args = parser.parse_args() @@ -131,14 +153,14 @@ def allLarger(thresh): cv.namedWindow(window_capture_name) cv.namedWindow(window_detection_name) -cv.createTrackbar(low_H_name, window_detection_name , low_H, max_value_H, on_low_H_thresh_trackbar) -cv.createTrackbar(high_H_name, window_detection_name , high_H, max_value_H, on_high_H_thresh_trackbar) -cv.createTrackbar(low_S_name, window_detection_name , low_S, max_value, on_low_S_thresh_trackbar) -cv.createTrackbar(high_S_name, window_detection_name , high_S, max_value, on_high_S_thresh_trackbar) -cv.createTrackbar(low_V_name, window_detection_name , low_V, max_value, on_low_V_thresh_trackbar) -cv.createTrackbar(high_V_name, window_detection_name , high_V, max_value, on_high_V_thresh_trackbar) +cv.createTrackbar(low_H_name, window_detection_name, low_H, max_value_H, on_low_H_thresh_trackbar) +cv.createTrackbar(high_H_name, window_detection_name, high_H, max_value_H, on_high_H_thresh_trackbar) +cv.createTrackbar(low_S_name, window_detection_name, low_S, max_value, on_low_S_thresh_trackbar) +cv.createTrackbar(high_S_name, window_detection_name, high_S, max_value, on_high_S_thresh_trackbar) +cv.createTrackbar(low_V_name, window_detection_name, low_V, max_value, on_low_V_thresh_trackbar) +cv.createTrackbar(high_V_name, window_detection_name, high_V, max_value, on_high_V_thresh_trackbar) -#cv.createTrackbar('low_canny', 'canny', low_canny, 500, lcanny) +# cv.createTrackbar('low_canny', 'canny', low_canny, 500, lcanny) paused = False likelyGate = [] @@ -150,15 +172,15 @@ def allLarger(thresh): frame = untampered if ret: if not paused: - frame = cv.resize(frame, (0,0), fx=0.5, fy=0.5) + frame = cv.resize(frame, (0, 0), fx=0.5, fy=0.5) blur = cv.GaussianBlur(frame, (5, 5), 0) frame_HSV = cv.cvtColor(blur, cv.COLOR_BGR2HSV) - #frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) - #canny = cv.Canny(frame_gray, 0) - frame_threshold = cv.inRange(frame_HSV, (low_H, low_S, low_V), (high_H, high_S, high_V)) #low_S ideal = 98 - - #frame_threshold = cv.bitwise_not(frame_threshold) - res = cv.bitwise_and(frame,frame, mask= frame_threshold) + # frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + # canny = cv.Canny(frame_gray, 0) + frame_threshold = cv.inRange(frame_HSV, (low_H, low_S, low_V), (high_H, high_S, high_V)) # low_S ideal = 98 + + # frame_threshold = cv.bitwise_not(frame_threshold) + res = cv.bitwise_and(frame, frame, mask=frame_threshold) res2, contours, hierarchy = cv.findContours(frame_threshold, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) contours.sort(key=heuristic, reverse=True) @@ -169,17 +191,17 @@ def allLarger(thresh): likelyGate = [{'cont': contours[0], 'heur': heur0}, {'cont': contours[1], 'heur': heur1}] untampered = np.copy(frame) if contours: - #likelyGate.append(contours[0]) - #findLikelyGate(likelyGate, contours) + # likelyGate.append(contours[0]) + # findLikelyGate(likelyGate, contours) drawRects(frame, likelyGate) cv.imshow(window_capture_name, frame) cv.imshow(window_detection_name, frame_threshold) - #cv.imshow('canny', canny) - + # cv.imshow('canny', canny) + key = cv.waitKey(30) if key == ord('q') or key == 27: break if key == ord('p'): paused = not paused -#generalized problem, giving center of object contrasting with water +# generalized problem, giving center of object contrasting with water diff --git a/perception/tasks/sanity_test.py b/perception/tasks/sanity_test.py index e3d07a3..8b22147 100644 --- a/perception/tasks/sanity_test.py +++ b/perception/tasks/sanity_test.py @@ -1,6 +1,8 @@ import multiprocessing import pytest +# TODO: integrate with pytests testing suite + def sanity_test(algorithm, test_imgs): """ Runs a sanity test on the algorithm that checks for run time and general exceptions. diff --git a/perception/tasks/segmentation/aggregateRescaling.py b/perception/tasks/segmentation/aggregateRescaling.py index 3e2d9ea..e0f4354 100644 --- a/perception/tasks/segmentation/aggregateRescaling.py +++ b/perception/tasks/segmentation/aggregateRescaling.py @@ -3,6 +3,8 @@ import numpy as np import numpy.linalg as LA + +# TODO: port to vis + TaskPerciever format or remove # Jenny -> unsigned ints fixed the problem # Damas -> flip weight vector every frame @@ -42,9 +44,9 @@ def aggregate_rescaling(frame): # you only pca once red -= max_min['min'] red *= 255.0 / (max_min['max'] - max_min['min']) """ - if False:#not paused: - print(np.min(red), np.max(red), max_min['min'], max_min['max']) - """ + if False:#not paused: + print(np.min(red), np.max(red), max_min['min'], max_min['max']) + """ red = red.astype(np.uint8) red = np.expand_dims(red, axis=2) red = np.concatenate((red, red, red), axis=2) diff --git a/perception/tasks/segmentation/combinedFilter.py b/perception/tasks/segmentation/combinedFilter.py index 22977d1..ec32624 100644 --- a/perception/tasks/segmentation/combinedFilter.py +++ b/perception/tasks/segmentation/combinedFilter.py @@ -1,6 +1,8 @@ import cv2 import numpy as np +# TODO: port to vis + TaskPerciever format or remove + from sys import argv as args from perception.tasks.segmentation.aggregateRescaling import init_aggregate_rescaling from perception.tasks.segmentation.peak_removal_adaptive_thresholding import filter_out_highest_peak_multidim @@ -23,7 +25,7 @@ def combined_filter(frame, custom_weights=None, display_figs=False, print_weight custom_weights=custom_weights, print_weights=print_weights) - other_frame = other_frame[:,:,:1] + other_frame = other_frame[:, :, :1] if display_figs: cv2.imshow('original', frame) diff --git a/perception/tasks/segmentation/kmeans.py b/perception/tasks/segmentation/kmeans.py index f555779..507b261 100644 --- a/perception/tasks/segmentation/kmeans.py +++ b/perception/tasks/segmentation/kmeans.py @@ -4,6 +4,8 @@ from scipy.signal import find_peaks, peak_widths from sys import argv as args +# TODO: port to vis + TaskPerciever format or remove + ######################################################################## # An attempt at an adaptive thresholding algorithm based on the frequency # of pixel values ("peaks" if looking at a histogram of # pixels vs pixel value of a frame) @@ -18,6 +20,7 @@ # is equivalent to places with lots of noise ######################################################################## + def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): """ Attempts to use kmeans to segment the frame into num_group features (not including the background), denoted by a very large value in votes. @@ -74,7 +77,6 @@ def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): out = cv2.VideoWriter('out.avi', cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'), 30.0, (int(frame.shape[1] * 0.4), int(frame.shape[0] * 0.4))) - ret_tries = 0 while (1 and ret_tries < 50): @@ -83,8 +85,6 @@ def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): if ret: frame = cv2.resize(frame, None, fx=0.4, fy=0.4) - - cv2.imshow('original', frame) plt.pause(0.001) diff --git a/perception/tasks/segmentation/peak_removal_adaptive_thresholding.py b/perception/tasks/segmentation/peak_removal_adaptive_thresholding.py index 892dc02..d76a17b 100644 --- a/perception/tasks/segmentation/peak_removal_adaptive_thresholding.py +++ b/perception/tasks/segmentation/peak_removal_adaptive_thresholding.py @@ -3,6 +3,9 @@ import matplotlib.pyplot as plt from scipy.signal import find_peaks, peak_widths from sys import argv as args + +# TODO: port to vis + TaskPerciever format or remove + ######################################################################## # An attempt at an adaptive thresholding algorithm based on the frequency # of pixel values ("peaks" if looking at a histogram of # pixels vs pixel value of a frame) @@ -51,6 +54,7 @@ # thresholds_used = [h_low, s_low, v_low, h_hi, s_hi, v_hi] + def init_test_hsv_thresholds(thresholds): # Keep track of previous threhold values to see if the user is using the trackbar # is there a function that detects whether the mouse button is down? @@ -61,25 +65,25 @@ def nothing(x): pass cv2.namedWindow('ideal thresholding') - cv2.createTrackbar('h_low','ideal thresholding',h_low,255,nothing) - cv2.createTrackbar('s_low','ideal thresholding',s_low,255,nothing) - cv2.createTrackbar('v_low','ideal thresholding',v_low,255,nothing) - cv2.createTrackbar('h_high','ideal thresholding',h_hi,255,nothing) - cv2.createTrackbar('s_high','ideal thresholding',s_hi,255,nothing) - cv2.createTrackbar('v_high','ideal thresholding',v_hi,255,nothing) + cv2.createTrackbar('h_low', 'ideal thresholding', h_low, 255, nothing) + cv2.createTrackbar('s_low', 'ideal thresholding', s_low, 255, nothing) + cv2.createTrackbar('v_low', 'ideal thresholding', v_low, 255, nothing) + cv2.createTrackbar('h_high', 'ideal thresholding', h_hi, 255, nothing) + cv2.createTrackbar('s_high', 'ideal thresholding', s_hi, 255, nothing) + cv2.createTrackbar('v_high', 'ideal thresholding', v_hi, 255, nothing) def test_hsv_thresholds(frame, thresholds): nonlocal prev_h_low, prev_s_low, prev_v_low, prev_h_hi, prev_s_hi, prev_v_hi - h_low_track = cv2.getTrackbarPos('h_low','ideal thresholding') - s_low_track = cv2.getTrackbarPos('s_low','ideal thresholding') - v_low_track = cv2.getTrackbarPos('v_low','ideal thresholding') - h_hi_track = cv2.getTrackbarPos('h_high','ideal thresholding') - s_hi_track = cv2.getTrackbarPos('s_high','ideal thresholding') - v_hi_track = cv2.getTrackbarPos('v_high','ideal thresholding') + h_low_track = cv2.getTrackbarPos('h_low', 'ideal thresholding') + s_low_track = cv2.getTrackbarPos('s_low', 'ideal thresholding') + v_low_track = cv2.getTrackbarPos('v_low', 'ideal thresholding') + h_hi_track = cv2.getTrackbarPos('h_high', 'ideal thresholding') + s_hi_track = cv2.getTrackbarPos('s_high', 'ideal thresholding') + v_hi_track = cv2.getTrackbarPos('v_high', 'ideal thresholding') - if h_low_track!=prev_h_low or s_low_track!=prev_s_low or v_low_track!=prev_v_low \ - or h_hi_track!=prev_h_hi or s_hi_track!=prev_s_hi or v_hi_track!=prev_v_hi: + if h_low_track != prev_h_low or s_low_track != prev_s_low or v_low_track != prev_v_low \ + or h_hi_track != prev_h_hi or s_hi_track != prev_s_hi or v_hi_track != prev_v_hi: # If user is adjusting the trackbars, use the user input thresholds_used = [h_low_track, s_low_track, v_low_track, h_hi_track, s_hi_track, v_hi_track] else: @@ -92,9 +96,9 @@ def test_hsv_thresholds(frame, thresholds): cv2.setTrackbarPos('s_high', 'ideal thresholding', thresholds_used[4]) cv2.setTrackbarPos('v_high', 'ideal thresholding', thresholds_used[5]) - hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, np.array(thresholds_used[:3]), np.array(thresholds_used[3:])) - res = cv2.bitwise_and(frame,frame, mask= mask) + res = cv2.bitwise_and(frame, frame, mask=mask) cv2.imshow('ideal thresholding', res) @@ -103,20 +107,22 @@ def test_hsv_thresholds(frame, thresholds): return test_hsv_thresholds + def hsv_threshold(frame, thresh_used): - hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, np.array(thresh_used[:3]), np.array(thresh_used[3:])) - res = cv2.bitwise_and(frame,frame, mask= mask) + res = cv2.bitwise_and(frame, frame, mask=mask) return thresh_used, res + def disp_hist(frame, title, labels, colors): - frame0 = frame[:,:,0].flatten() + frame0 = frame[:, :, 0].flatten() frame0 = frame0[frame0 > 0] - frame1 = frame[:,:,1].flatten() + frame1 = frame[:, :, 1].flatten() frame1 = frame1[frame1 > 0] - frame2 = frame[:,:,2].flatten() + frame2 = frame[:, :, 2].flatten() frame2 = frame2[frame2 > 0] plt.figure(hash(title)) @@ -131,6 +137,7 @@ def disp_hist(frame, title, labels, colors): plt.legend() plt.draw() + def find_peak_ranges(frame, display_plots=False, title=None, labels=None, colors=None): """ Finds a returns the widest peak's x-range in all three channels of frame Result is formatted to fit cv2.inRange() -> ((low1, low2, low3), (hi1, hi2, hi3)) @@ -138,8 +145,9 @@ def find_peak_ranges(frame, display_plots=False, title=None, labels=None, colors # TODO: Maybe use a different combination of peak characteristics to more accurately # select the entire peak (only the tip is selected right now) - peak_width_height = 0.95 # How far down the peak that the algorithm draws - # the horizontal width line + peak_width_height = 0.95 # How far down the peak that the algorithm draws + + # the horizontal width line def find_highest_peak(channel, display_plots=False): """ Finds and returns the x-range of the highest peak in the @@ -148,33 +156,34 @@ def find_highest_peak(channel, display_plots=False): f = frame[:, :, channel].flatten() # Some semi-hardcoded values :) - num_bins = max(int((np.amax(f)-np.amin(f)) / 4), 10) + num_bins = max(int((np.amax(f) - np.amin(f)) / 4), 10) # num_bins = 30 hist, bins = np.histogram(f, bins=num_bins) - hist[0] = 0 # get rid of stuff that was thresholded to 0 - hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak - bins = np.hstack([bins, [bins[bins.shape[0]-1] + 1]]) + hist[0] = 0 # get rid of stuff that was thresholded to 0 + hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak + bins = np.hstack([bins, [bins[bins.shape[0] - 1] + 1]]) peaks, properties = find_peaks(hist, height=0.1) if len(peaks) > 0: i = np.argmax(properties['peak_heights']) widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] # i = np.argmax(widths) - largest_peak = (int((bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]//2), - int((bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]//2)) # beginning and end of the peak + largest_peak = (int((bins[peaks[i]] + bins[peaks[i] + 1]) // 2 - widths[i] // 2), + int((bins[peaks[i]] + bins[peaks[i] + 1]) // 2 + widths[ + i] // 2)) # beginning and end of the peak if display_plots: ax = plt.gca() print(max(f)) ax.set_xlim([0, max(255, max(f))]) - #Plot values in this channel - plt.plot(bins[1:],hist, label=labels[channel], color=colors[channel]) + # Plot values in this channel + plt.plot(bins[1:], hist, label=labels[channel], color=colors[channel]) # Plot peaks - plt.plot(bins[peaks+1], hist[peaks], "x") + plt.plot(bins[peaks + 1], hist[peaks], "x") # Plot peak widths - plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) + plt.hlines(hist[peaks] * 0.9, bins[peaks + 1] - widths // 2, bins[peaks + 1] + widths // 2) else: largest_peak = (0, 0) @@ -184,7 +193,7 @@ def find_highest_peak(channel, display_plots=False): fig = plt.figure(hash(title)) plt.clf() - background = (np.empty(frame.shape[2]),np.empty(frame.shape[2])) + background = (np.empty(frame.shape[2]), np.empty(frame.shape[2])) for channel in range(frame.shape[2]): low, high = find_highest_peak(channel, display_plots) background[0][channel] = low @@ -197,12 +206,14 @@ def find_highest_peak(channel, display_plots=False): return background + def plot_peaks(frame, title, labels, colors): # Shh this is just a helper function that makes the code more readable # Not to be used in practice. # NOTE: you need to call plt.pause(0.001) afterwards to render the plot find_peak_ranges(frame, True, title, labels, colors) + def init_filter_out_highest_peak(filters, return_colorspace="any", input_colorspace="bgr"): """ Takes in an hsv image! Returns an hsv image""" # low pass filter @@ -210,8 +221,8 @@ def init_filter_out_highest_peak(filters, return_colorspace="any", input_colorsp # lambda = 0.9-0.4 prev_hsv_threshes = [[] for i in range(len(filters))] - hsv_labels = (('H','S','V'), ("red","purple","gray")) - bgr_labels = (('B','G','R'), ("blue","green","red")) + hsv_labels = (('H', 'S', 'V'), ("red", "purple", "gray")) + bgr_labels = (('B', 'G', 'R'), ("blue", "green", "red")) # Figure out how the procedure to convert among hsv and bgr. # Format of stuff in fitler_fns: @@ -220,9 +231,9 @@ def init_filter_out_highest_peak(filters, return_colorspace="any", input_colorsp curr_color = input_colorspace for f in filters: if f != curr_color: - filter_fns.append(['c',f]) + filter_fns.append(['c', f]) curr_color = f - filter_fns.append(['f',f]) + filter_fns.append(['f', f]) if return_colorspace != "any" and return_colorspace != curr_color: filter_fns.append(['c', return_colorspace]) @@ -236,27 +247,27 @@ def filter_out_highest_peak(frame, cache, display_plots=False, title=None, label # calculate average for i in range(2): for j in range(3): - background_thresh[i][j] = (background_thresh[i][j] + sum([c[i][j] for c in cache])) // (len(cache) + 1) + background_thresh[i][j] = (background_thresh[i][j] + sum([c[i][j] for c in cache])) // ( + len(cache) + 1) background_mask = cv2.bitwise_not(cv2.bitwise_or( - cv2.inRange(frame[:, :, 0], background_thresh[0][0], background_thresh[1][0]), - cv2.inRange(frame[:, :, 1], background_thresh[0][1], background_thresh[1][1]), - cv2.inRange(frame[:, :, 2], background_thresh[0][2], background_thresh[1][2]) - )) - no_background = cv2.bitwise_and(frame,frame, mask=background_mask) + cv2.inRange(frame[:, :, 0], background_thresh[0][0], background_thresh[1][0]), + cv2.inRange(frame[:, :, 1], background_thresh[0][1], background_thresh[1][1]), + cv2.inRange(frame[:, :, 2], background_thresh[0][2], background_thresh[1][2]) + )) + no_background = cv2.bitwise_and(frame, frame, mask=background_mask) return background_thresh, raw_thresh, no_background def combine_threshes(th1, th2): - return ([min(th1[0][0], th2[0][0]), min(th1[0][1], th2[0][1]), min(th1[0][2], th2[0][2])], - [max(th1[1][0], th2[1][0]), max(th1[1][1], th2[1][1]), max(th1[1][2], th2[1][2])]) + return ([min(th1[0][0], th2[0][0]), min(th1[0][1], th2[0][1]), min(th1[0][2], th2[0][2])], + [max(th1[1][0], th2[1][0]), max(th1[1][1], th2[1][1]), max(th1[1][2], th2[1][2])]) def bgr_thresh2hsv_thresh(th): th = cv2.cvtColor(np.array([[th[0]], [th[1]]], np.uint8), cv2.COLOR_BGR2HSV).tolist() return ([min(th[0][0][0], th[1][0][0]), min(th[0][0][1], th[1][0][1]), min(th[0][0][2], th[1][0][2])], [max(th[0][0][0], th[1][0][0]), max(th[0][0][1], th[1][0][1]), max(th[0][0][2], th[1][0][2])]) - def do_filter(frame, display_plots=False): nonlocal prev_hsv_threshes if len(prev_hsv_threshes[0]) == lpf_cache_size: @@ -298,32 +309,33 @@ def do_filter(frame, display_plots=False): return do_filter + def keep_highest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, title=None, label='1', color='blue'): """ Returns a mask for the frame that keeps the num_peaks highest peaks in the histogram of pixel values. Only works for grayscale/1-channel images (to speed this up) Shape of frame must have 3 dimensions (pass in np.expand_dims(frame, 2) if erroring) """ # Some semi-thresholded values :) - num_bins = max(int((np.amax(frame)-np.amin(frame)) / 4), 10) + num_bins = max(int((np.amax(frame) - np.amin(frame)) / 4), 10) hist, bins = np.histogram(frame, bins=num_bins) - hist[0] = 0 # get rid of stuff that was thresholded to 0 - hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak - bins = np.hstack([bins, [bins[bins.shape[0]-1] + 1]]) + hist[0] = 0 # get rid of stuff that was thresholded to 0 + hist = np.hstack([hist, [0]]) # make stuff at 255 into a peak + bins = np.hstack([bins, [bins[bins.shape[0] - 1] + 1]]) peaks, properties = find_peaks(hist, prominence=100) widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] if len(peaks) > 0: i = len(peaks) - 1 - mask = cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2) + mask = cv2.inRange(frame, (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 - widths[i] * 2, + (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 + widths[i] * 2) # To support keeping multiple peaks for j in range(num_peaks - 1): i = len(peaks) - 2 - j if i >= 0: - mask = cv2.bitwise_or(cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i], - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]), mask) + mask = cv2.bitwise_or(cv2.inRange(frame, (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 - widths[i], + (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 + widths[i]), mask) # frame = cv2.bitwise_and(frame, frame, mask=mask) else: mask = np.ones(frame.shape, np.uint8) @@ -334,12 +346,12 @@ def keep_highest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, titl ax = plt.gca() ax.set_xlim([0, 255]) - #Plot values in this channel - plt.plot(bins[1:],hist, label=label, color=color) + # Plot values in this channel + plt.plot(bins[1:], hist, label=label, color=color) # Plot peaks - plt.plot(bins[peaks+1], hist[peaks], "x") + plt.plot(bins[peaks + 1], hist[peaks], "x") # Plot peak widths - plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) + plt.hlines(hist[peaks] * 0.9, bins[peaks + 1] - widths // 2, bins[peaks + 1] + widths // 2) plt.title(title) plt.legend() @@ -347,31 +359,32 @@ def keep_highest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, titl return mask + def delete_lowest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, title=None, label='1', color='blue'): """ Returns a mask for the frame that deletes the num_peaks lowest-valued peaks in the histogram of pixel values. Only works for grayscale/1-channel images (to speed this up) """ # Some semi-thresholded values :) - num_bins = max(int((np.amax(frame)-np.amin(frame)) / 4), 10) + num_bins = max(int((np.amax(frame) - np.amin(frame)) / 4), 10) hist, bins = np.histogram(frame, bins=num_bins) - hist[0] = 0 # get rid of stuff that was thresholded to 0 + hist[0] = 0 # get rid of stuff that was thresholded to 0 peaks, properties = find_peaks(hist, prominence=100) widths = peak_widths(hist, peaks, rel_height=peak_width_height)[0] if len(peaks) > 0: i = 0 - mask = cv2.bitwise_not(cv2.inRange(frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2)) + mask = cv2.bitwise_not(cv2.inRange(frame, (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 - widths[i] * 2, + (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 + widths[i] * 2)) # To support deleting multiple peaks for j in range(num_peaks - 1): i = j + 1 if len(peaks) > i: mask = cv2.bitwise_and(cv2.bitwise_not(cv2.inRange( - frame, (bins[peaks[i]]+bins[peaks[i]+1])//2-widths[i]*2, - (bins[peaks[i]]+bins[peaks[i]+1])//2+widths[i]*2)), mask) + frame, (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 - widths[i] * 2, + (bins[peaks[i]] + bins[peaks[i] + 1]) // 2 + widths[i] * 2)), mask) else: mask = np.ones(frame.shape, np.uint8) # frame = cv2.bitwise_and(frame, frame, mask=mask) @@ -382,12 +395,12 @@ def delete_lowest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, tit ax = plt.gca() ax.set_xlim([0, 255]) - #Plot values in this channel - plt.plot(bins[1:],hist, label=label, color=color) + # Plot values in this channel + plt.plot(bins[1:], hist, label=label, color=color) # Plot peaks - plt.plot(bins[peaks+1], hist[peaks], "x") + plt.plot(bins[peaks + 1], hist[peaks], "x") # Plot peak widths - plt.hlines(hist[peaks]*0.9, bins[peaks+1]-widths//2, bins[peaks+1]+widths//2) + plt.hlines(hist[peaks] * 0.9, bins[peaks + 1] - widths // 2, bins[peaks + 1] + widths // 2) plt.title(title) plt.legend() @@ -395,6 +408,7 @@ def delete_lowest_valued_peaks_mask(frame, num_peaks=1, display_plots=False, tit return mask + def remove_blotchy_chunks(frame, kernel_size=201, iterations=1, display_imgs=False): """ Works best when object isn't surrounded by blotchy stuff """ edges = cv2.Canny(frame, 100, 150) @@ -416,6 +430,7 @@ def remove_blotchy_chunks(frame, kernel_size=201, iterations=1, display_imgs=Fal return result + def filter_out_highest_peak_multidim(frame, res=69, percentile=10, custom_weights=None, print_weights=False): """ Estimates the "peak-ness" of each pixel in frame across color channels and thresholds out pixels that were "peak-like" in many colorspaces. @@ -444,7 +459,7 @@ def get_peak_votes(frame): if res == 1: vote_arr = dist[frame] else: - dist = np.array([np.mean(dist[i*res:i*res+res]) for i in range(len(dist) // res + 1)]) + dist = np.array([np.mean(dist[i * res:i * res + res]) for i in range(len(dist) // res + 1)]) vote_arr = dist[frame // res] return recommended_weight, vote_arr @@ -455,7 +470,7 @@ def get_peak_votes(frame): if print_weights: print('------------------------', custom_weights) for ch in range(frame.shape[2]): - weight, vote_arr = get_peak_votes(frame[:,:,ch]) + weight, vote_arr = get_peak_votes(frame[:, :, ch]) if custom_weights is not None: weight = custom_weights[ch] if print_weights: @@ -472,6 +487,7 @@ def get_peak_votes(frame): return overall_votes, cv2.bitwise_and(frame, frame, mask=overall_mask) + def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): """ Attempts to use kmeans to segment the frame into num_group features (not including the background), denoted by a very large value in votes. @@ -482,23 +498,23 @@ def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): # Make kmeans only consider the non-background pixels background = np.zeros(votes.shape) - background[votes>=np.percentile(votes, percentile)] = 1 - cluster_data = votes[background==0] - cluster_indexes = np.array(range(len(votes)))[background==0] + background[votes >= np.percentile(votes, percentile)] = 1 + cluster_data = votes[background == 0] + cluster_indexes = np.array(range(len(votes)))[background == 0] # Do kmeans criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) flags = cv2.KMEANS_RANDOM_CENTERS - compactness,labels,centers = cv2.kmeans(cluster_data,num_groups,None,criteria,10,flags) + compactness, labels, centers = cv2.kmeans(cluster_data, num_groups, None, criteria, 10, flags) # Reconstruct the original votes array with background's label = -1 label_arr = np.empty(votes.shape) - label_arr[background==1] = -1 + label_arr[background == 1] = -1 for i in range(num_groups): - label_arr[cluster_indexes[labels.flatten()==i]] = i + label_arr[cluster_indexes[labels.flatten() == i]] = i unique_labels, label_counts = np.unique(label_arr, return_counts=True) - label_order = list(range(np.int0(np.amax(unique_labels)) + 2)) # something is erroring here + label_order = list(range(np.int0(np.amax(unique_labels)) + 2)) # something is erroring here if len(label_counts) < num_groups + 1: # add in a slot for the background if no background is found label_counts = np.insert(label_counts, 0, 0) @@ -508,14 +524,15 @@ def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): groups = np.empty((frame_shape[0], frame_shape[1], num_groups + 1)) for i, l in enumerate(label_order): group = np.zeros(votes.shape) - group[label_arr.flatten()==l - 1] = 255 - groups[:,:,i] = np.reshape(group, frame_shape[:2]) + group[label_arr.flatten() == l - 1] = 255 + groups[:, :, i] = np.reshape(group, frame_shape[:2]) # for i in range(len(unique_labels)): # cv2.imshow(str(i) + " label", groups[:,:,i]) return groups + ########################################### # Main Body ########################################### @@ -524,7 +541,8 @@ def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): # For testing porpoises cap = cv2.VideoCapture(args[1]) ret, frame = cap.read() - out = cv2.VideoWriter('out.avi',cv2.VideoWriter_fourcc('M','J','P','G'), 30.0, (int(frame.shape[1]*0.4), int(frame.shape[0]*0.4))) + out = cv2.VideoWriter('out.avi', cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'), 30.0, + (int(frame.shape[1] * 0.4), int(frame.shape[0] * 0.4))) if testing: test_hsv_thresholds = init_test_hsv_thresholds(thresholds_used) @@ -539,8 +557,10 @@ def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): if ret: frame = cv2.resize(frame, None, fx=0.4, fy=0.4) - votes, multi_filter1 = filter_out_highest_peak_multidim(np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)]), custom_weights=[1, 1, 1, 1, 1, 1]) - votes, multi_filter2 = filter_out_highest_peak_multidim(np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)])) + votes, multi_filter1 = filter_out_highest_peak_multidim( + np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)]), custom_weights=[1, 1, 1, 1, 1, 1]) + votes, multi_filter2 = filter_out_highest_peak_multidim( + np.dstack([frame, cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)])) multi_filter1 = multi_filter1[:, :, :3] multi_filter2 = multi_filter2[:, :, :3] @@ -567,4 +587,4 @@ def k_means_segmentation(votes, frame_shape, num_groups=2, percentile=10): ret_tries += 1 cv2.destroyAllWindows() cap.release() - out.release() \ No newline at end of file + out.release() diff --git a/perception/vis/vis.py b/perception/vis/vis.py index 2a2c0d4..7d77b5e 100644 --- a/perception/vis/vis.py +++ b/perception/vis/vis.py @@ -6,10 +6,7 @@ import cv2 as cv from perception.vis.Visualizer import Visualizer import cProfile -import pstats import imageio -from matplotlib.pyplot import Figure -import numpy as np def run(data_sources, algorithm, save_video=False): @@ -20,8 +17,11 @@ def run(data_sources, algorithm, save_video=False): paused = False speed = 1 - for frame in data: + while data.has_next(): if frame_count % speed == 0 and not paused: + frame = next(data) + frame_count += 1 + if algorithm.kwargs: state, debug_frames = algorithm.analyze(frame, debug=True, slider_vals=window_builder.update_vars()) else: @@ -50,7 +50,7 @@ def run(data_sources, algorithm, save_video=False): if key == ord('o'): speed += 1 print(f'speed {speed}') - frame_count += 1 + cv.destroyAllWindows() if out: diff --git a/requirements.txt b/requirements.txt index a4fd5c0..f411292 100755 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ scipy numpy matplotlib imageio -imageio-ffmpeg \ No newline at end of file +imageio-ffmpeg +pytest \ No newline at end of file From 59037a9dfd07f47b8e755dbed9cf6d861df3e704 Mon Sep 17 00:00:00 2001 From: axquaris Date: Tue, 5 Jan 2021 16:18:52 -0800 Subject: [PATCH 17/19] random code style fixes --- perception/tasks/cross/cross_detection.py | 4 ++-- perception/tasks/gate/GateSegmentationAlgoB.py | 6 +++--- .../tasks/path_marker/path_marker_detection.py | 12 +++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/perception/tasks/cross/cross_detection.py b/perception/tasks/cross/cross_detection.py index e256b75..9410f4f 100644 --- a/perception/tasks/cross/cross_detection.py +++ b/perception/tasks/cross/cross_detection.py @@ -68,9 +68,9 @@ def find_cross(frame, draw_figs=True): combined_filter = init_combined_filter() ret_tries = 0 - while(1 and ret_tries < 50): + while 1 and ret_tries < 50: # ret,frame = cap.read() - if ret == True: + if ret: # frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5) if thresholding == "multidim": votes1, threshed = filter_out_highest_peak_multidim(frame) diff --git a/perception/tasks/gate/GateSegmentationAlgoB.py b/perception/tasks/gate/GateSegmentationAlgoB.py index 557d088..d4d3281 100644 --- a/perception/tasks/gate/GateSegmentationAlgoB.py +++ b/perception/tasks/gate/GateSegmentationAlgoB.py @@ -43,10 +43,10 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: # remove all contours with zero area cnt = [cnt[i] for i in range(len(cnt)) if cv.contourArea(cnt[i]) > 0] - for i in range(len(cnt)): - area_cnt = cv.contourArea(cnt[i]) + for c in cnt: + area_cnt = cv.contourArea(c) area_cnts.append(area_cnt) - area_rect = cv.boundingRect(cnt[i])[-2] * cv.boundingRect(cnt[i])[-1] + area_rect = cv.boundingRect(c)[-2] * cv.boundingRect(c)[-1] area_diff.append(abs((area_rect - area_cnt)/area_cnt)) if len(area_diff) >= 2: diff --git a/perception/tasks/path_marker/path_marker_detection.py b/perception/tasks/path_marker/path_marker_detection.py index 4cacd63..83eef2c 100644 --- a/perception/tasks/path_marker/path_marker_detection.py +++ b/perception/tasks/path_marker/path_marker_detection.py @@ -33,7 +33,7 @@ def thresh_by_contour_size( frame, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE ) if contours is not None: - contours.sort(key=lambda c: cv2.contourArea(c), reverse=True) + contours.sort(key=cv2.contourArea, reverse=True) contours = contours[:num_contours] threshed = np.zeros(frame.shape, np.uint8) @@ -128,13 +128,11 @@ def line_length(line): ) return bot_angle, top_angle - else: - if draw_figs: - cv2.imshow('lines bottom', frame) - cv2.imshow('lines top', frame) - cv2.imshow('frame with path marker angles', frame) - return None + if draw_figs: + cv2.imshow('lines bottom', frame) + cv2.imshow('lines top', frame) + cv2.imshow('frame with path marker angles', frame) def path_marker_get_new_heading(cap, is_approaching, draw_figs=False): From 81dd7841d7f429071f2b3b1088e4b71a8ece01ba Mon Sep 17 00:00:00 2001 From: nLevin13 Date: Tue, 12 Jan 2021 15:16:57 -0800 Subject: [PATCH 18/19] fixed some style issues --- perception/{ => tasks}/slots/__init__.py | 0 .../{ => tasks}/slots/play_slots_detection.py | 0 perception/vis/FrameWrapper.py | 15 ++++++--------- 3 files changed, 6 insertions(+), 9 deletions(-) rename perception/{ => tasks}/slots/__init__.py (100%) rename perception/{ => tasks}/slots/play_slots_detection.py (100%) diff --git a/perception/slots/__init__.py b/perception/tasks/slots/__init__.py similarity index 100% rename from perception/slots/__init__.py rename to perception/tasks/slots/__init__.py diff --git a/perception/slots/play_slots_detection.py b/perception/tasks/slots/play_slots_detection.py similarity index 100% rename from perception/slots/play_slots_detection.py rename to perception/tasks/slots/play_slots_detection.py diff --git a/perception/vis/FrameWrapper.py b/perception/vis/FrameWrapper.py index fc6d602..fe751d1 100644 --- a/perception/vis/FrameWrapper.py +++ b/perception/vis/FrameWrapper.py @@ -46,27 +46,24 @@ def __next__(self): ret, frame = self.next_data[1].read() if ret: return cv2.resize(frame, None, fx=self.resize, fy=self.resize) - else: - print("WARNING: Failed to get frame from video {}. Try {}." \ - .format(self.filenames[self.index], i), file=sys.stderr) + print("WARNING: Failed to get frame from video {}. Try {}." \ + .format(self.filenames[self.index], i), file=sys.stderr) self.next_data_obj() elif self.next_data[0] == "i": # Image img = self.next_data[1] self.next_data_obj() if img is not None: return cv2.resize(img, None, fx=self.resize, fy=self.resize) - else: - print("WARNING: Failed to get image {}." \ - .format(self.filenames[self.index-1]), file=sys.stderr) + print("WARNING: Failed to get image {}." \ + .format(self.filenames[self.index-1]), file=sys.stderr) else: # Webcam # Try to get a frame out at most WEBCAM_TRIES times. for i in range(self.WEBCAM_TRIES): ret, frame = self.next_data[1].read() if ret: return cv2.resize(frame, None, fx=self.resize, fy=self.resize) - else: - print("WARNING: Failed to get frame from webcam. Try {}." \ - .format(i), file=sys.stderr) + print("WARNING: Failed to get frame from webcam. Try {}." \ + .format(i), file=sys.stderr) self.next_data_obj() raise StopIteration From 2c6d8bde993dfc64346b710e4e5fe15b992b0fbd Mon Sep 17 00:00:00 2001 From: nLevin13 Date: Tue, 12 Jan 2021 16:21:36 -0800 Subject: [PATCH 19/19] vis now iterates through data frames without error --- perception/__init__.py | 8 ++++++- .../tasks/gate/GateSegmentationAlgoB.py | 3 --- .../tasks/gate/GateSegmentationAlgoC.py | 6 ++--- perception/vis/vis.py | 22 +++++++------------ 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/perception/__init__.py b/perception/__init__.py index e94118e..74b1032 100644 --- a/perception/__init__.py +++ b/perception/__init__.py @@ -1,8 +1,14 @@ import perception.vis.TestAlgo as TestAlgo import perception.tasks.gate.GateCenterAlgo as GateSeg +import perception.tasks.gate.GateSegmentationAlgoA as GateSegA +import perception.tasks.gate.GateSegmentationAlgoB as GateSegB +import perception.tasks.gate.GateSegmentationAlgoC as GateSegC # import perception.tasks as tasks ALGOS = { 'test': TestAlgo.TestAlgo, - 'gateseg': GateSeg.GateCenterAlgo + 'gateseg': GateSeg.GateCenterAlgo, + 'gatesegA': GateSegA.GateSegmentationAlgoA, + 'gatesegB': GateSegB.GateSegmentationAlgoB, + 'gatesegC': GateSegC.GateSegmentationAlgoC } diff --git a/perception/tasks/gate/GateSegmentationAlgoB.py b/perception/tasks/gate/GateSegmentationAlgoB.py index d4d3281..8f0a54e 100644 --- a/perception/tasks/gate/GateSegmentationAlgoB.py +++ b/perception/tasks/gate/GateSegmentationAlgoB.py @@ -1,7 +1,4 @@ from typing import Tuple -import sys -import os -sys.path.append(os.path.dirname(__file__)) from perception.tasks.TaskPerceiver import TaskPerceiver from perception.tasks.segmentation.combinedFilter import init_combined_filter diff --git a/perception/tasks/gate/GateSegmentationAlgoC.py b/perception/tasks/gate/GateSegmentationAlgoC.py index 2e738ac..f3bdc55 100644 --- a/perception/tasks/gate/GateSegmentationAlgoC.py +++ b/perception/tasks/gate/GateSegmentationAlgoC.py @@ -35,9 +35,9 @@ def analyze(self, frame: np.ndarray, debug: bool) -> Tuple[float, float]: mask = cv.inRange( stacked_filter_frames, np.array([100, 100, 100]), np.array([255, 255, 255]) ) - contours, _ = cv.findContours(mask, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE) - if contours: - contours.sort(key=self.findStraightness, reverse=True) + contours = cv.findContours(mask, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)[-2] + if len(contours) > 0: + contours.sort(reverse=True, key=self.findStraightness) cnts = contours[:2] rects = [cv.minAreaRect(c) for c in cnts] centers = [np.array(r[0]) for r in rects] diff --git a/perception/vis/vis.py b/perception/vis/vis.py index 7d77b5e..ee6a12b 100644 --- a/perception/vis/vis.py +++ b/perception/vis/vis.py @@ -14,14 +14,10 @@ def run(data_sources, algorithm, save_video=False): window_builder = Visualizer(algorithm.kwargs) data = FrameWrapper(data_sources, 0.25) frame_count = 0 - paused = False speed = 1 - while data.has_next(): - if frame_count % speed == 0 and not paused: - frame = next(data) - frame_count += 1 - + for frame in data: + if frame_count % speed == 0: if algorithm.kwargs: state, debug_frames = algorithm.analyze(frame, debug=True, slider_vals=window_builder.update_vars()) else: @@ -31,19 +27,17 @@ def run(data_sources, algorithm, save_video=False): cv.imshow('Debug Frames', to_show) if save_video: if out is None: - # height, width, _ = to_show.shape - # TODO: get codec to work - # out = cv.VideoWriter('rec.mp4', cv.VideoWriter_fourcc(*'mp4v'), 60, (height, width)) out = imageio.get_writer('vis_rec.mp4') - if out: - out_img = cv.cvtColor(to_show, cv.COLOR_BGR2RGB) - out.append_data(out_img) + out_img = cv.cvtColor(to_show, cv.COLOR_BGR2RGB) + out.append_data(out_img) + frame_count += 1 key = cv.waitKey(30) if key == ord('q') or key == 27: break if key == ord('p'): - paused = not paused + cv.waitKey(0) # pause + # TODO: be able to quit and manipulate slider vars in real time while paused if key == ord('i') and speed > 1: speed -= 1 print(f'speed {speed}') @@ -86,7 +80,7 @@ def profile(*args, stats='all'): else: data_sources = [args.data] - if args.cProfiler is not None: + if args.profile is None: run(data_sources, algorithm, args.save_video) else: profile(data_sources, algorithm, args.save_video, stats=args.profile)