From f7ba17850c264ab47b2ddbd04d6a34a9a31cabb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Szkodzi=C5=84ski?= Date: Sun, 24 Nov 2013 22:38:04 +0100 Subject: [PATCH] New initial message, gui and config modules Still WIP, message handler does something at least if started. GUI is somewhat broken though. --- config.py | 39 +++++++++++ engine.py | 40 +++++++++++ gui.py | 200 +++++++++++++++++++++++++++++++++++++++++++++++++++++ message.py | 181 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 460 insertions(+) create mode 100644 config.py create mode 100644 engine.py create mode 100644 gui.py create mode 100644 message.py diff --git a/config.py b/config.py new file mode 100644 index 0000000..c77f1f5 --- /dev/null +++ b/config.py @@ -0,0 +1,39 @@ +import yaml +import errno + +class Config(object): + def __init__(self, engine, gui, message): + try: + self.config_file = open("config", "r") + self.data = yaml.load(self.config_file) + except IOError, e: + print "Dropping into developer mode due to missing or invalid config file" + gui.enable() + engine.set_debug(True) + message.set_debug(True) + self.data['timeout'] = 10.0 + self.data['status_target'] = 'http://10.8.0.171:8000/status' + + if not hasattr(self.data, 'gui') or self.data['gui']: + gui.enable() + + debug = hasattr(self.data, 'debug') and self.data['debug'] + engine.set_debug(debug) + message.set_debug(debug) + + def save(self): + self.config_file = open(self.config_file.name, "w") + json.dump(self.config, self.config_file) + self.config_file.close() + + def set_active_rect(self, rect): + self.config['active_rect'] = rect + + def set_perspective_points(self, points): + self.config['perspective_points'] = points + + def set_status_target(self, status_url): + self.config['status_target'] = status_url + + def set_image_target(self, image_url): + self.config['image_url'] = image_url diff --git a/engine.py b/engine.py new file mode 100644 index 0000000..598bf17 --- /dev/null +++ b/engine.py @@ -0,0 +1,40 @@ +import cv2 +import numpy as np + +from threading import Thread, Timer + +# TODO: GROT +import time +import random + +class Engine(object): + def __init__(self): + self.thread = Thread(target=self.run, name = "JunkVision Engine") + self.perspective = np.array( + [[ 1.33036171e+00, 3.18020707e-01, -1.38751879e+02], + [ -1.71116647e-01, 1.55350072e+00, -8.06609130e+00], + [ -3.52454848e-04, 1.09892154e-03, 1.00000000e+00]] + ) + self.areas = [(107, 68, 282, 209), + (313, 67, 493, 308), + (316, 323, 489, 475), + (105, 209, 288, 476)] + + def start(self): + self.thread.start() + self.thread.join() + + def run(self): + pass + + def get_image(self, area_id): + return open("D:/obrazek.jpg", "rb").read() + + def get_areas(self): + return self.areas + + def get_movement_time(self, area_id): + return time.time() + + def get_percent_mess(self, area_id): + return random.random() * 100 \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..3ab2873 --- /dev/null +++ b/gui.py @@ -0,0 +1,200 @@ +import cv2 +import cv2.cv + +from threading import Thread + +import numpy as np +from functools import partial + +class AreaSelector(object): + def __init__(self, window_name, orig_img, max_areas = 4): + self.window_name = window_name + self.orig_img = orig_img + self.selection = False + self.area = 0 + self.max_areas = max_areas + self.rects = [] + for i in xrange(max_areas): + self.rects.append([]) + self.colors = [ + (0,255,0), + (255,0,127), + (0,127,255), + (255,255,0) + ] + + @staticmethod + def handler(event, x, y, flags, self): + if event == cv2.EVENT_FLAG_RBUTTON: + self.area += 1 + self.area %= self.max_areas + if self.selection: + self.selection = False + self.redrawRects() + elif event == cv2.EVENT_LBUTTONDOWN and not self.selection: + self.selection = True + self.first_x = x + self.first_y = y + elif event == cv2.EVENT_LBUTTONDOWN and self.selection: + self.selection = False + self.redrawRects() + elif event == cv2.EVENT_MOUSEMOVE and self.selection: + self.rects[self.area] = (min(self.first_x, x), min(self.first_y, y), max(self.first_x, x), max(self.first_y, y)) + self.redrawRects() + + def redrawRects(self): + cv2.imshow(self.window_name, self.orig_img) + new_img = self.orig_img.copy() + for i, r in enumerate(self.rects): + if not r: + continue + cv2.rectangle(new_img, (r[0], r[1]), (r[2], r[3]), self.colors[i], 4) + cv2.imshow(self.window_name, new_img) + +class CorrectionSelector(object): + def __init__(self, window_name, orig_img): + self.selecting_rect = True + self.points = [] + self.cur_point = 0 + self.orig_img = orig_img + self.window_name = window_name + self.colors = [ + (0,255,127), + (255,0,127), + (0,127,255), + (255,255,0) + ] + self.transform = [] + self.rect_x = 0 + self.rect_y = 0 + + @staticmethod + def handler(event, x, y, flags, self): + if self.selecting_rect: + if event == cv2.EVENT_FLAG_RBUTTON: + self.selecting_rect = False + self.redraw() + elif event == cv2.EVENT_FLAG_LBUTTON: + self.rect_x = x + self.rect_y = y + elif event == cv2.EVENT_MOUSEMOVE: + self.rect = min(self.rect_x, x), min(self.rect_y, y), max(self.rect_x, x), max(self.rect_y, y) + self.redraw() + else: + if event == cv2.EVENT_FLAG_RBUTTON: + self.redraw() + self.calc_transform() + self.selecting_rect = True + elif event == cv2.EVENT_FLAG_LBUTTON: + if len(self.points) < 4: + self.points.append((x,y)) + else: + self.points[self.cur_point] = (x,y) + self.cur_point += 1 + self.cur_point %= 4 + self.redraw() + + def redraw(self): + cv2.imshow(self.window_name, self.orig_img) + new_img = self.orig_img.copy() + if self.rect: + cv2.rectangle(new_img, (self.rect[0], self.rect[1]), (self.rect[2], self.rect[3]), (0, 255, 0), 4) + self.points.sort() + for i, p in enumerate(self.points): + cv2.circle(new_img, p, 4, self.colors[i], -1) + cv2.imshow(self.window_name, new_img) + + def calc_transform(self): + self.rect_points = [ + (self.rect[0], self.rect[1]), + (self.rect[0], self.rect[3]), + (self.rect[2], self.rect[1]), + (self.rect[2], self.rect[3]) + ] + sorted_points = [] + best_match = 0 + for i in xrange(4): + min_delta = cap_width * cap_height + for j in xrange(4): + delta = (abs(self.rect_points[i][0] - self.points[j][0]), abs(self.rect_points[i][1] - self.points[j][1])) + print i, j, delta + if delta[0] + delta[1] < min_delta: + best_match = j + min_delta = delta[0] + delta[1] + print best_match + sorted_points.append(self.points[best_match]) + print np.array(self.points), np.array(self.rect_points), np.array(sorted_points) + + self.transform = cv2.getPerspectiveTransform(np.array(sorted_points, dtype=np.float32), np.array(self.rect_points, dtype=np.float32)) + print self.transform + + def get_transform(self): + return self.transform + +class ModeSelector(object): + def __init__(self, gui, min_mode, max_mode): + self.gui = gui + self.min_mode = min_mode + self.max_mode = max_mode + self.mode = self.gui.mode + + @staticmethod + def handler(event, x, y, flags, self): + if event == cv2.EVENT_MBUTTONDOWN: + mode = self.gui.mode + mode += 1 + mode %= self.max_mode + if mode < self.min_mode: + mode = self.min_mode + self.gui.mode = mode + +class Gui(object): + def __init__(self, engine): + self.engine = engine + self.enabled = False + self.mode = -2 + + def enable(self): + if self.enabled: + raise RuntimeError("Gui has already been started!") + self.thread = Thread(target=self.run, name="JunkVision GUI") + self.enabled = True + self.thread.start() + + def set_mode(self, mode): + self.mode = mode + + def disable(self): + self.enabled = False + if self.thread.is_alive: + self.thread.join() + + def is_enabled(self): + return self.enabled + + def run(self): + cv2.namedWindow("JunkVision GUI") + while self.enabled: + if self.mode == -2: + self.selector = AreaSelector("JunkVision GUI") + cv2.setMouseCallback("JunkVision GUI", CorrectionSelector.handler, self.selector) + cv2.waitKey() + elif self.mode == -1: + self.selector = CorrectionSelector("JunkVision GUI") + cv2.setMouseCallback("JunkVision GUI", AreaSelector.handler, self.selector) + cv2.waitKey() + elif self.mode >= 0 and self.mode <= 2: + if not isinstance(self.selector, ModeSelector): + self.selector = ModeSelector(self, 0, 2) + cv2.setMouseCallback("JunkVision GUI", ModeSelector.handler, self.selector) + if self.mode == 0: + img = self.engine.get_plain_image() + elif self.mode == 1: + img = self.engine.get_movement_image() + elif self.mode == 2: + img = self.engine.get_difference_image() + + cv2.imshow("JunkVision GUI", img) + if cv2.waitKey(10) == 27: + self.enabled = False + cv2.destroyWindow("JunkVision GUI") \ No newline at end of file diff --git a/message.py b/message.py new file mode 100644 index 0000000..8903d01 --- /dev/null +++ b/message.py @@ -0,0 +1,181 @@ +import requests +import json + +import BaseHTTPServer + +from threading import Thread + +class MessageHandler(BaseHTTPServer.BaseHTTPRequestHandler): + access_denied_text = """ + + JunkVision - 403 Forbidden + +

403 Forbidden

+

Only LAN is allowed.

+

Contact BOFHs for access.

+ + + """ + + not_found_text = """ + + JunkVision - 404 Not Found + +

404 Not found

+ + + """ + + bad_request_text = """ + + JunkVision - 400 Bad Request + +

400 Bad Request

+ + + """ + + def do_GET(self): + self.server_version = "JunkVision/0.5" + self.sys_version = "" + self.protocol_version = "HTTP/1.1" + if not self.client_address[0].startswith("10.8.") and self.client_address[0] != "127.0.0.1": + self.send_response(403) + self.send_default_headers() + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", "%d" % len(self.access_denied_text)) + self.end_headers() + self.wfile.write(self.access_denied_text) + return + + data = {} + data['json_response'] = self.server.get_default_data() + + if self.path.startswith("/image/"): + try: + area = int(self.path.split("/")[2]) + except (ValueError, IndexError): + self.send_bad_request() + # TODO: logging + print "Bad request", self.path + return + + img_jpg = self.server.engine.get_image(int(area)) + self.send_image(img_jpg) + return + + elif self.path.startswith("/status/"): + data['json_response'].append({'type': 'status'}) + if self.path == "/status/": + for area_id in xrange(len(self.server.engine.get_areas())): + data['json_response'].append(self.server.get_status_data(area_id)) + else: + try: + area = int(self.path.split("/")[2]) + if area < 0 or area > len(self.server.engine.get_areas()): + raise IndexError("Area out of range") + data['json_response'].append(self.server.get_status_data(area)) + except (ValueError, IndexError): + self.send_bad_request() + # TODO: logging + print "Bad request", self.path + return + + else: + self.send_response(404) + self.send_default_headers() + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", "%d" % len(self.not_found_text)) + self.end_headers() + self.wfile.write(self.not_found_text) + return + + self.send_response(200) + self.send_default_headers() + self.send_header("Content-Type", "application/json; charset=utf-8") + json_text = json.dumps(data) + self.send_header("Content-Length", "%d" % len(json_text)) + self.send_header("Cache-Control", "max-age=60") + self.end_headers() + self.wfile.write(json_text) + + def send_default_headers(self): + self.send_header("Connection", "close") + self.send_header("Transfer-Encoding", "identity") + self.send_header("Allow", "GET") + + def send_bad_request(self): + self.send_response(400) + self.send_default_headers() + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", "%d" % len(self.bad_request_text)) + self.end_headers() + self.wfile.write(self.bad_request_text) + + def send_image(self, img): + self.send_response(200) + self.send_default_headers() + # XXX: parametrize? + self.send_header("Content-Type", "image/jpeg") + self.send_header("Content-Length", "%d" % len(img)) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(img) + + + +class MessageServer(BaseHTTPServer.HTTPServer): + def __init__(self, engine): + BaseHTTPServer.HTTPServer.__init__(self, ("0.0.0.0", 4580), MessageHandler) + self.engine = engine + self.camera_id = "Unknown" + self.enabled = False + + def set_camera_id(self, camera_id): + self.camera_id = camera_id + + def set_status_target(self, status_url): + self.status_target = status_url + + def get_default_data(self): + return [{"camera_id": self.camera_id}] + + def get_status_data(self, area_id): + return { + "area_id": area_id, + "area_rect": self.engine.get_areas()[area_id], + "last_movement": self.engine.get_movement_time(area_id), + "last_percent_mess": self.engine.get_percent_mess(area_id) + } + + def send_status(self, area_id=-1): + data = {} + data['status'] = self.get_default_data() + if area_id < 0: + for area_id in engine.get_areas(): + data['status'].append(self.get_status_data(area_id)) + else: + data['status'].append(self.get_status_data(area_id)) + + json_text = json.dumps(data) + headers = [("Content-Type", "application/json; charset=utf-8"), + ("Content-Length", "%d" % len(json_text)), + ("Server", "JunkVision/0.5")] + resp = requests.post(self.status_target, data=json_text, headers=headers) + # TODO: logging + print resp + + def start(self): + if self.enabled: + raise RuntimeError("Message handler is already started!") + self.thread = Thread(target=self.run, name="JunkVision MessageHandler") + self.enabled = True + self.thread.start() + + def stop(self): + self.enabled = False + self.thread.join() + + def run(self): + while self.enabled: + self.handle_request()