Merge branch 'dev' into dev

This commit is contained in:
ori agranat
2019-08-16 18:37:05 +03:00
10 changed files with 257 additions and 220 deletions

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ Python/app/__pycache__/
Python/app/handlers/__pycache__/
\.vscode/
backend/settings/

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,3 @@
class PipelineAlreadyExistsException(Exception):
def __init__(self, pipe_name):

View File

@@ -1,4 +1,3 @@
import socket
import os
import json
import cv2

View File

@@ -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()

View File

@@ -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")

View File

@@ -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()

View File

@@ -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}
{"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}