diff --git a/.gitignore b/.gitignore index b006761b8..1dc29ec9b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ Python/app/__pycache__/ Python/app/handlers/__pycache__/ \.vscode/ + +backend/settings/ diff --git a/README.md b/README.md index 213658449..b2c5eb89c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ These instructions will get you a copy of the project up and running on your loc so in order to run this project we will need to install python in order to run the backend and node.js with vue.js in order to run the fronted #### backend -- python 3.6 and above +- python 3.7 and above - opencv 3.4.5 - tornado web framework - robotpy-cscore @@ -29,7 +29,7 @@ so in order to run this project we will need to install python in order to run t 2. apt-get dist-upgrade 3. sudo apt-get upgrade 4. sudo apt-get install python3-pip python3-dev cmake zip unzip build-essential git libnss-mdns --fix-missing -5. sudo pip3 install numpy +5. sudo pip3 install numpy (if on raspberry pi do "sudo apt-get install python3-numpy") 6. OPENCV_VERSION=3.4.5 7. wget -O opencv.zip https://github.com/opencv/opencv/archive/${OPENCV_VERSION}.zip 8. unzip opencv.zip @@ -65,7 +65,11 @@ of you can auto serve the ui by ``` npm run serve ``` - +## Hardware +this is important when choosing your sbc it is more important to have a good usb controller that a good cpu +on the odroid xu4 which is very fast i have got many bottlenecks from the usb controller and many times making the program crach +#### networking +it is very important to install Bonjour ## docs diff --git a/backend/Main.py b/backend/Main.py index e1a47bc3e..02aa2c372 100644 --- a/backend/Main.py +++ b/backend/Main.py @@ -1,11 +1,14 @@ +from datetime import timedelta +from networktables import NetworkTables import tornado.ioloop import logging from app.ChameleonVisionApp import ChameleonApplication from app.classes.SettingsManager import SettingsManager from tornado.options import options -from app.handlers.VisionHandler import VisionHandler import threading import asyncio +from app.handlers.SocketHandler import send_all_async +from app.handlers.CameraHander import CameraHandler def run_server(): @@ -15,15 +18,24 @@ def run_server(): print(f"Serving on port {options.port}") app.listen(options.port) tornado.ioloop.IOLoop.current().start() + tornado.ioloop.IOLoop.instance().add_timeout(timedelta(seconds=1), + send_all_async) + + +def run(): + NetworkTables.startClientTeam(team=settings_manager.general_settings.get("team_number", 1577)) + # NetworkTables.initialize("localhost") + port = 5550 + for cam_name in settings_manager.usb_cameras: + CameraHandler(cam_name, port).run() + port += 1 + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) - SettingsManager() - - VisionHandler().run() + settings_manager = SettingsManager() + run() server_thread = threading.Thread(target=run_server) server_thread.start() - while True: - pass diff --git a/backend/app/ChameleonVisionApp.py b/backend/app/ChameleonVisionApp.py index cd786c272..7336ae001 100644 --- a/backend/app/ChameleonVisionApp.py +++ b/backend/app/ChameleonVisionApp.py @@ -1,7 +1,6 @@ import tornado.web import tornado.websocket import os - from .handlers.MainHandler import MainHandler from .handlers.SocketHandler import ChameleonWebSocket from tornado.options import define diff --git a/backend/app/classes/Exceptions.py b/backend/app/classes/Exceptions.py index 59a8363e3..67493d2aa 100644 --- a/backend/app/classes/Exceptions.py +++ b/backend/app/classes/Exceptions.py @@ -1,4 +1,3 @@ - class PipelineAlreadyExistsException(Exception): def __init__(self, pipe_name): diff --git a/backend/app/classes/SettingsManager.py b/backend/app/classes/SettingsManager.py index 9e74507cc..75ae2182b 100644 --- a/backend/app/classes/SettingsManager.py +++ b/backend/app/classes/SettingsManager.py @@ -1,4 +1,3 @@ -import socket import os import json import cv2 diff --git a/backend/app/handlers/CameraHander.py b/backend/app/handlers/CameraHander.py new file mode 100644 index 000000000..8690693f4 --- /dev/null +++ b/backend/app/handlers/CameraHander.py @@ -0,0 +1,225 @@ +import math +import cv2 +import numpy +from cscore import CameraServer +from app.classes.SettingsManager import SettingsManager +from ..handlers.SocketHandler import send_all_async +from multiprocessing import Process +import threading +import zmq +import asyncio +import time +from networktables import NetworkTables +import networktables +from .VisionHandler import VisionHandler + + +class CameraHandler: + def __init__(self, cam_name, port): + #settings vars up for vision loop + self.cs = CameraServer.getInstance() + self.settings_manager = SettingsManager() + self.vision_handler = VisionHandler() + self.port = port + self.cam_name = cam_name + self.image = None + self.p_image = None + self.table = None + self.nt_data = {'valid': False} + self.time_stamp = 0 + + def run(self): + #starting main thread + threading.Thread(target=self.thread_proc).start() + + def thread_proc(self): + self.settings_manager.cams_curr_pipeline[self.cam_name] = "pipeline0" + pipeline = self.settings_manager.cams[self.cam_name]["pipelines"][self.settings_manager.cams_curr_pipeline[self.cam_name]] + FOV = self.settings_manager.cams[self.cam_name]["FOV"] + + def change_camera_values(pipline): + self.settings_manager.usb_cameras[self.cam_name].setBrightness(pipeline['brightness']) + self.settings_manager.usb_cameras[self.cam_name].setExposureManual(pipeline['exposure']) + self.settings_manager.usb_cameras[self.cam_name].setWhiteBalanceAuto() + + def pipeline_listener(table, key, value, is_new): + asyncio.set_event_loop(asyncio.new_event_loop()) + self.settings_manager.cams_curr_pipeline[self.cam_name] = value + change_camera_values(pipeline) + if self.cam_name == self.settings_manager.general_settings['curr_camera']: + self.settings_manager.general_settings['curr_pipeline'] = value + update_settings = self.settings_manager.get_curr_pipeline() + update_settings['curr_pipeline'] = self.settings_manager.general_settings["curr_pipeline"] + send_all_async(update_settings) + + def mode_listener(table, key, value, is_new): + change_camera_values({ + 'brightness': 25, + 'exposure': 15 + }) + #setting up network table + self.table = NetworkTables.getTable("/Chameleon-Vision/" + self.cam_name) + self.table.putString('Pipeline', self.settings_manager.cams_curr_pipeline[self.cam_name]) + self.table.addEntryListenerEx(pipeline_listener, key="Pipeline", + flags=networktables.NetworkTablesInstance.NotifyFlags.UPDATE) + self.table.addEntryListenerEx(mode_listener, key="Driver_Mode", + flags=networktables.NetworkTablesInstance.NotifyFlags.UPDATE) + + # getting video from current camera + cv_sink = self.cs.getVideo(camera=self.settings_manager.usb_cameras[self.cam_name]) + + width = self.settings_manager.cams[self.cam_name]["video_mode"]["width"] + height = self.settings_manager.cams[self.cam_name]["video_mode"]["height"] + + # setting up a video server for camera + cv_publish = self.cs.putVideo(name=self.cam_name, width=width, height=height) + # saving camera port in cam name dict for usage in client + self.settings_manager.cams_port[self.cam_name] = self.cs._sinks['serve_' + self.cam_name].getPort() + + # setting up a zmq connection to the opencv subprocess + context = zmq.Context() + socket = context.socket(zmq.PAIR) + socket.bind('tcp://*:%s' % str(self.port)) + + # starting the process with initial values + p = Process(target=self.camera_process, args=(self.cam_name, self.port, FOV)) + p.start() + + change_camera_values(pipeline) + + def _publish_thread(): + #getting image values and publishing process image and data + self.image = numpy.zeros(shape=(width, height, 3), dtype=numpy.uint8) + self.p_image = self.image + while True: + try: + self.time_stamp, self.image = cv_sink.grabFrame(self.image) + cv_publish.putFrame(self.p_image) + self.table.putBoolean('valid', self.nt_data['valid']) + # check if point is valid + if self.nt_data['valid']: + # send the point using network tables + self.table.putNumber('pitch', self.nt_data['pitch']) + self.table.putNumber('yaw', self.nt_data['yaw']) + self.table.putNumber('fps', self.nt_data['fps']) + self.table.putNumber('time_stamp', self.time_stamp) + # if the selected camera in ui is this cam send the point to the ui + except: + pass + + def _socket_thread(): + #publishing to websocket at slower interval + asyncio.set_event_loop(asyncio.new_event_loop()) + while True: + time.sleep(0.1) + if self.settings_manager.general_settings['curr_camera'] == self.cam_name: + try: + send_all_async({ + 'raw_point': self.nt_data['raw_point'], + 'point': { + 'pitch': self.nt_data['pitch'], + 'yaw': self.nt_data['yaw'], + 'fps': self.nt_data['fps'] + } + }) + except: + pass + + threading.Thread(target=_publish_thread).start() + threading.Thread(target=_socket_thread).start() + + while True: + #sending and reciving data from opencv sub process + pipeline = self.settings_manager.cams[self.cam_name]["pipelines"][ + self.settings_manager.cams_curr_pipeline[self.cam_name]] + socket.send_json(dict( + pipeline=pipeline + ), zmq.SNDMORE) + + socket.send_pyobj(self.image) + self.p_image = socket.recv_pyobj() + self.nt_data = socket.recv_json() + + def camera_process(self, cam_name, port, FOV): + from fractions import Fraction + #calc fov + diagonalView = math.radians(FOV) + + width = self.settings_manager.cams[cam_name]["video_mode"]["width"] + height = self.settings_manager.cams[cam_name]["video_mode"]["height"] + centerX = (width / 2) - .5 + centerY = (height / 2) - .5 + cam_area = width * height + + aspect_fraction = Fraction(width, height) + horizontal_ratio = aspect_fraction.numerator + vertical_ratio = aspect_fraction.denominator + + horizontalView = math.atan(math.tan(diagonalView / 2) * (horizontal_ratio / diagonalView)) * 2 + verticalView = math.atan(math.tan(diagonalView / 2) * (vertical_ratio / diagonalView)) * 2 + + H_FOCAL_LENGTH = width / (2 * math.tan((horizontalView / 2))) + V_FOCAL_LENGTH = height / (2 * math.tan((verticalView / 2))) + #setting up zmq socket + context = zmq.Context() + socket = context.socket(zmq.PAIR) + socket.connect('tcp://localhost:%s' % str(port)) + #setting up filter countours class + filter_contours = self.vision_handler.Filter_Contours(center_x=centerX, center_y=centerY) + + x = 1 + counter = 0 + start_time = time.time() + fps = 0 + + while True: + obj = socket.recv_json() + image = socket.recv_pyobj() + curr_pipeline = obj["pipeline"] + if curr_pipeline['orientation'] == "Inverted": + M = cv2.getRotationMatrix2D((width / 2, height / 2), 180, 1) + image = cv2.warpAffine(image, M, (width, height)) + hsv_image = self.vision_handler._hsv_threshold(curr_pipeline["hue"], + curr_pipeline["saturation"], curr_pipeline["value"], + image, curr_pipeline["erode"], curr_pipeline["dilate"]) + # if table.getBoolean("Driver_Mode", False): + contours = self.vision_handler.find_contours(hsv_image) + filtered_contours = filter_contours.filter_contours(input_contours=contours, area=curr_pipeline['area'], + ratio=curr_pipeline['ratio'], + extent=curr_pipeline['extent'], + sort_mode=curr_pipeline['sort_mode'], cam_area=cam_area, + target_grouping=curr_pipeline['target_group'], + target_intersection= + curr_pipeline['target_intersection']) + final_contour = self.vision_handler.output_contour(filtered_contours) + try: + center = final_contour[0] + center_x = (center[1] - curr_pipeline['B']) / curr_pipeline["M"] + center_y = (center[0] * curr_pipeline["M"]) + curr_pipeline["B"] + pitch = self.vision_handler.calculate_pitch(pixel_y=center[1], center_y=center_y, v_focal_length=V_FOCAL_LENGTH) + yaw = self.vision_handler.calculate_yaw(pixel_x=center[0], center_x=center_x, h_focal_length=H_FOCAL_LENGTH) + valid = True + except IndexError: + center = None + pitch = None + yaw = None + valid = False + + if curr_pipeline['is_binary']: + draw_image = hsv_image + else: + draw_image = image + res = self.vision_handler.draw_image(input_image=draw_image, contour=final_contour) + socket.send_pyobj(res) + socket.send_json(dict( + pitch=pitch, + yaw=yaw, + valid=valid, + raw_point=center, + fps=fps + )) + counter += 1 + if (time.time() - start_time) > x: + fps = (counter / (time.time() - start_time)) + counter = 0 + start_time = time.time() diff --git a/backend/app/handlers/SocketHandler.py b/backend/app/handlers/SocketHandler.py index 35c5750ee..b6f54d136 100644 --- a/backend/app/handlers/SocketHandler.py +++ b/backend/app/handlers/SocketHandler.py @@ -1,12 +1,10 @@ -import asyncio - import tornado.websocket import json from ..classes.Exceptions import NoCameraConnectedException from ..classes.SettingsManager import SettingsManager -web_socket_clients = [] +web_socket_clients = set() def send_all_async(message): @@ -18,6 +16,7 @@ def send_all_async(message): class ChameleonWebSocket(tornado.websocket.WebSocketHandler): + actions = {} set_this_camera_settings = ["exposure", "brightness"] @@ -38,7 +37,7 @@ class ChameleonWebSocket(tornado.websocket.WebSocketHandler): def open(self): self.send_full_settings() if self not in web_socket_clients: - web_socket_clients.append(self) + web_socket_clients.add(self) print("WebSocket opened") diff --git a/backend/app/handlers/VisionHandler.py b/backend/app/handlers/VisionHandler.py index 2d5a1f6af..b669c0177 100644 --- a/backend/app/handlers/VisionHandler.py +++ b/backend/app/handlers/VisionHandler.py @@ -1,22 +1,10 @@ -import asyncio -import time - -from networktables import NetworkTables -import networktables import cv2 import numpy -from cscore import CameraServer -from app.classes.SettingsManager import SettingsManager -from ..classes.Singleton import Singleton -from multiprocessing import Process -import threading -import zmq import math from enum import Enum, unique -from ..handlers.SocketHandler import send_all_async -class VisionHandler(metaclass=Singleton): +class VisionHandler(): def __init__(self): self.kernel = numpy.ones((5, 5), numpy.uint8) @@ -243,193 +231,3 @@ class VisionHandler(metaclass=Singleton): def calculate_yaw(self, pixel_x, center_x, h_focal_length): yaw = math.degrees(math.atan((pixel_x - center_x) / h_focal_length)) return yaw - - def run(self): - NetworkTables.startClientTeam(team=SettingsManager.general_settings.get("team_number", 1577)) - # NetworkTables.initialize("localhost") - - cs = CameraServer.getInstance() - port = 5550 - - for cam_name in SettingsManager().usb_cameras: - threading.Thread(target=self.thread_proc, args=(cs, cam_name, port)).start() - port += 1 - - def thread_proc(self, cs, cam_name, port=5557): - asyncio.set_event_loop(asyncio.new_event_loop()) - SettingsManager.cams_curr_pipeline[cam_name] = "pipeline0" - pipeline = SettingsManager().cams[cam_name]["pipelines"][SettingsManager.cams_curr_pipeline[cam_name]] - FOV = SettingsManager().cams[cam_name]["FOV"] - - def change_camera_values(pipline): - SettingsManager.usb_cameras[cam_name].setBrightness(pipeline['brightness']) - SettingsManager.usb_cameras[cam_name].setExposureManual(pipeline['exposure']) - SettingsManager.usb_cameras[cam_name].setWhiteBalanceAuto() - - def pipeline_listener(table, key, value, is_new): - asyncio.set_event_loop(asyncio.new_event_loop()) - SettingsManager.cams_curr_pipeline[cam_name] = value - change_camera_values(pipeline) - if cam_name == SettingsManager().general_settings['curr_camera']: - SettingsManager().general_settings['curr_pipeline'] = value - update_settings = SettingsManager().get_curr_pipeline() - update_settings['curr_pipeline'] = SettingsManager().general_settings["curr_pipeline"] - send_all_async(update_settings) - - def mode_listener(table, key, value, is_new): - change_camera_values({ - 'brightness': 25, - 'exposure': 15 - }) - - table = NetworkTables.getTable("/Chameleon-Vision/" + cam_name) - table.putString('Pipeline', SettingsManager.cams_curr_pipeline[cam_name]) - table.addEntryListenerEx(pipeline_listener, key="Pipeline", - flags=networktables.NetworkTablesInstance.NotifyFlags.UPDATE) - table.addEntryListenerEx(mode_listener, key="Driver_Mode", - flags=networktables.NetworkTablesInstance.NotifyFlags.UPDATE) - #gettings video from curent camera - cv_sink = cs.getVideo(camera=SettingsManager.usb_cameras[cam_name]) - - width = SettingsManager().cams[cam_name]["video_mode"]["width"] - height = SettingsManager().cams[cam_name]["video_mode"]["height"] - - image = numpy.zeros(shape=(width, height, 3), dtype=numpy.uint8) - - #setting up a video server for camera - cv_publish = cs.putVideo(name=cam_name, width=width, height=height) - # saving camera port in cam name dict for usage in client - SettingsManager().cams_port[cam_name] = cs._sinks['serve_'+cam_name].getPort() - - #setting up a zmq connection to the opencv subprocess - context = zmq.Context() - socket = context.socket(zmq.PAIR) - socket.bind('tcp://*:%s' % str(port)) - - #starting the process with inital values - p = Process(target=self.camera_process, args=(cam_name, port, FOV)) - p.start() - - change_camera_values(pipeline) - - while True: - pipeline = SettingsManager().cams[cam_name]["pipelines"][SettingsManager.cams_curr_pipeline[cam_name]] - _, image = cv_sink.grabFrame(image) - socket.send_json(dict( - pipeline=pipeline - ), zmq.SNDMORE) - - socket.send_pyobj(image) - p_image = socket.recv_pyobj() - nt_data = socket.recv_json() - table.putBoolean('valid', nt_data['valid']) - # check if point is valid - - # print(nt_data['fps']) - - if nt_data['valid']: - #send the point using network tables - table.putNumber('pitch', nt_data['pitch']) - table.putNumber('yaw', nt_data['yaw']) - #if the selected camera in ui is this cam send the point to the ui - - if SettingsManager().general_settings['curr_camera'] == cam_name: - try: - if nt_data['raw_point'] is not None: - send_all_async({ - 'raw_point': nt_data['raw_point'], - 'point': { - 'pitch': nt_data['pitch'], - 'yaw': nt_data['yaw'], - 'fps': nt_data['fps'] - } - }) - except Exception as e: - print(e) - #send the image to the camera server - - cv_publish.putFrame(p_image) - - def camera_process(self, cam_name, port, FOV): - from fractions import Fraction - - diagonalView = math.radians(FOV) #needs to be implemented in client - - width = SettingsManager().cams[cam_name]["video_mode"]["width"] - height = SettingsManager().cams[cam_name]["video_mode"]["height"] - centerX = (width / 2) - .5 - centerY = (height / 2) - .5 - cam_area = width * height - - aspect_fraction = Fraction(width,height) - horizontal_ratio = aspect_fraction.numerator - vertical_ratio = aspect_fraction.denominator - - horizontalView = math.atan(math.tan(diagonalView/2) * (horizontal_ratio / diagonalView)) * 2 - verticalView = math.atan(math.tan(diagonalView/2) * (vertical_ratio / diagonalView)) * 2 - - H_FOCAL_LENGTH = width / (2*math.tan((horizontalView/2))) - V_FOCAL_LENGTH = height / (2*math.tan((verticalView/2))) - - context = zmq.Context() - socket = context.socket(zmq.PAIR) - socket.connect('tcp://localhost:%s' % str(port)) - filter_contours = self.Filter_Contours(center_x=centerX, center_y=centerY) - x = 1 - counter = 0 - start_time = time.time() - fps = 0 - while True: - obj = socket.recv_json() - image = socket.recv_pyobj() - curr_pipeline = obj["pipeline"] - if curr_pipeline['orientation'] == "Inverted": - M = cv2.getRotationMatrix2D((width / 2, height / 2), 180, 1) - image = cv2.warpAffine(image, M, (width, height)) - hsv_image = self._hsv_threshold(curr_pipeline["hue"], - curr_pipeline["saturation"], curr_pipeline["value"], - image, curr_pipeline["erode"], curr_pipeline["dilate"]) - # if table.getBoolean("Driver_Mode", False): - contours = self.find_contours(hsv_image) - filtered_contours = filter_contours.filter_contours(input_contours=contours, area=curr_pipeline['area'], - ratio=curr_pipeline['ratio'], - extent=curr_pipeline['extent'], - sort_mode=curr_pipeline['sort_mode'], cam_area=cam_area, - target_grouping=curr_pipeline['target_group'], - target_intersection= - curr_pipeline['target_intersection']) - final_contour = self.output_contour(filtered_contours) - try: - center = final_contour[0] - center_x = (center[1] - curr_pipeline['B']) / curr_pipeline["M"] - center_y = (center[0] * curr_pipeline["M"]) + curr_pipeline["B"] - pitch = self.calculate_pitch(pixel_y=center[1], center_y=center_y, v_focal_length=V_FOCAL_LENGTH) - yaw = self.calculate_yaw(pixel_x=center[0], center_x=center_x, h_focal_length=H_FOCAL_LENGTH) - valid = True - except IndexError: - center = None - pitch = None - yaw = None - valid = False - - if curr_pipeline['is_binary']: - draw_image = hsv_image - else: - draw_image = image - res = self.draw_image(input_image=draw_image, contour=final_contour) - socket.send_pyobj(res) - socket.send_json(dict( - pitch=pitch, - yaw=yaw, - valid=valid, - raw_point=center, - fps=fps - )) - counter += 1 - if (time.time() - start_time) > x: - fps = (counter / (time.time() - start_time)) - counter = 0 - start_time = time.time() - - - diff --git a/backend/settings/cams/USB Camera-B4.09.24.1.json b/backend/settings/cams/USB Camera-B4.09.24.1.json index 2dd7aa24f..91187d8b0 100644 --- a/backend/settings/cams/USB Camera-B4.09.24.1.json +++ b/backend/settings/cams/USB Camera-B4.09.24.1.json @@ -1 +1 @@ -{"pipelines": {"pipeline0": {"exposure": 50, "brightness": 15, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": 0, "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline1": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": 0, "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline2": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline3": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline4": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline5": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline6": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline7": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline8": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline9": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": 0, "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}}, "path": "/dev/v4l/by-path/pci-0000:02:03.0-usb-0:1:1.0-video-index0", "video_mode": {"fps": 187, "width": 320, "height": 240, "pixel_format": "kYUYV"}, "resolution": 0, "FOV": 60.8} \ No newline at end of file +{"pipelines": {"pipeline0": {"exposure": 50, "brightness": 15, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": true, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": 0, "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline1": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": 0, "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline2": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline3": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline4": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline5": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline6": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline7": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline8": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": "Normal", "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}, "pipeline9": {"exposure": 50, "brightness": 50, "orientation": "Normal", "hue": [0, 100], "saturation": [0, 100], "value": [0, 100], "erode": false, "dilate": false, "area": [0, 100], "ratio": [0, 20], "extent": [0, 100], "is_binary": 0, "sort_mode": "Largest", "target_group": "Single", "target_intersection": "Up", "M": 1, "B": 0}}, "path": "/dev/v4l/by-path/pci-0000:02:03.0-usb-0:1:1.0-video-index0", "video_mode": {"fps": 187, "width": 320, "height": 240, "pixel_format": "kYUYV"}, "resolution": 0, "FOV": 60.8} \ No newline at end of file