flame-wro-fe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +1196 -0
- package/arduino/outputProxy/platformio.ini +16 -0
- package/arduino/outputProxy/src/main.cpp +276 -0
- package/index.js +8 -0
- package/package.json +31 -0
- package/raspySrc/main.py +12 -0
- package/raspySrc/raspy/control_panel.py +586 -0
- package/raspySrc/raspy/decision_log.py +85 -0
- package/raspySrc/raspy/drive_state.py +160 -0
- package/raspySrc/raspy/install_vehicle_service.sh +3 -0
- package/raspySrc/raspy/lap_direction.py +24 -0
- package/raspySrc/raspy/robot_runtime.py +1052 -0
- package/raspySrc/raspy/settings.json +198 -0
- package/raspySrc/raspy/settings.py +206 -0
- package/raspySrc/raspy/vehicle-runtime.service +12 -0
- package/raspySrc/raspy/vision_pipeline.py +142 -0
- package/raspySrc/raspy/vision_types.py +42 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"filters": {
|
|
4
|
+
"GRAY": 56,
|
|
5
|
+
"WALL_MAX_SAT": 102,
|
|
6
|
+
"WALL_EXCLUDE_COLORS": true,
|
|
7
|
+
"WALL_EXCLUDE_DILATE": 1,
|
|
8
|
+
"REDLO": [
|
|
9
|
+
111,
|
|
10
|
+
149,
|
|
11
|
+
48
|
|
12
|
+
],
|
|
13
|
+
"REDHI": [
|
|
14
|
+
130,
|
|
15
|
+
239,
|
|
16
|
+
167
|
|
17
|
+
],
|
|
18
|
+
"GREENLO": [
|
|
19
|
+
51,
|
|
20
|
+
66,
|
|
21
|
+
33
|
|
22
|
+
],
|
|
23
|
+
"GREENHI": [
|
|
24
|
+
73,
|
|
25
|
+
171,
|
|
26
|
+
136
|
|
27
|
+
],
|
|
28
|
+
"ORANGELO": [
|
|
29
|
+
98,
|
|
30
|
+
0,
|
|
31
|
+
88
|
|
32
|
+
],
|
|
33
|
+
"ORANGEHI": [
|
|
34
|
+
118,
|
|
35
|
+
234,
|
|
36
|
+
208
|
|
37
|
+
],
|
|
38
|
+
"BLUELO": [
|
|
39
|
+
173,
|
|
40
|
+
0,
|
|
41
|
+
54
|
|
42
|
+
],
|
|
43
|
+
"BLUEHI": [
|
|
44
|
+
14,
|
|
45
|
+
255,
|
|
46
|
+
240
|
|
47
|
+
],
|
|
48
|
+
"PINKLO": [
|
|
49
|
+
158,
|
|
50
|
+
160,
|
|
51
|
+
100
|
|
52
|
+
],
|
|
53
|
+
"PINKHI": [
|
|
54
|
+
170,
|
|
55
|
+
255,
|
|
56
|
+
255
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"contours": {
|
|
60
|
+
"minSize": 80.0,
|
|
61
|
+
"pillar_morph_kernel": 5,
|
|
62
|
+
"pillar_min_width": 4,
|
|
63
|
+
"pillar_min_height": 8,
|
|
64
|
+
"pillar_min_fill_ratio": 0.18
|
|
65
|
+
},
|
|
66
|
+
"camera": {
|
|
67
|
+
"frame_width": 640,
|
|
68
|
+
"frame_height": 400,
|
|
69
|
+
"format": "RGB888",
|
|
70
|
+
"crop_height": 190,
|
|
71
|
+
"flip_180": true,
|
|
72
|
+
"awb_enable": false,
|
|
73
|
+
"mtx": [
|
|
74
|
+
[
|
|
75
|
+
495.44408865,
|
|
76
|
+
0.0,
|
|
77
|
+
294.45510998
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
0.0,
|
|
81
|
+
504.36905411,
|
|
82
|
+
259.77204181
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
0.0,
|
|
86
|
+
0.0,
|
|
87
|
+
1.0
|
|
88
|
+
]
|
|
89
|
+
],
|
|
90
|
+
"dist": [
|
|
91
|
+
[
|
|
92
|
+
-0.64066312,
|
|
93
|
+
0.41901729,
|
|
94
|
+
-0.00366722,
|
|
95
|
+
0.01957175,
|
|
96
|
+
-0.14404429
|
|
97
|
+
]
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"roi": {
|
|
101
|
+
"line": {
|
|
102
|
+
"x": 270,
|
|
103
|
+
"y": 180,
|
|
104
|
+
"w": 100,
|
|
105
|
+
"h": 30
|
|
106
|
+
},
|
|
107
|
+
"left_wall": {
|
|
108
|
+
"x": 0,
|
|
109
|
+
"y": 0,
|
|
110
|
+
"w": 100,
|
|
111
|
+
"h": 150
|
|
112
|
+
},
|
|
113
|
+
"right_wall": {
|
|
114
|
+
"x": 540,
|
|
115
|
+
"y": 0,
|
|
116
|
+
"w": 100,
|
|
117
|
+
"h": 150
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"pd": {
|
|
121
|
+
"kp": 4.5,
|
|
122
|
+
"kd": 8.0,
|
|
123
|
+
"target_black_portion": 0.45
|
|
124
|
+
},
|
|
125
|
+
"control": {
|
|
126
|
+
"speed": 160,
|
|
127
|
+
"turn_speed": 155,
|
|
128
|
+
"avoid_speed": 115,
|
|
129
|
+
"done_sleep_seconds": 1.0,
|
|
130
|
+
"max_correction": 1,
|
|
131
|
+
"max_steering_offset": 75,
|
|
132
|
+
"steering_sign": -1,
|
|
133
|
+
"drive_sign": -1,
|
|
134
|
+
"servo_center_deg": 90,
|
|
135
|
+
"servo_min_deg": 15,
|
|
136
|
+
"servo_max_deg": 165
|
|
137
|
+
},
|
|
138
|
+
"behavior": {
|
|
139
|
+
"pillars": true,
|
|
140
|
+
"turns": 120,
|
|
141
|
+
"fixed_round_dir": 0,
|
|
142
|
+
"round_dir_vote_threshold": 10,
|
|
143
|
+
"line_marker_min_portion": 0.25,
|
|
144
|
+
"turn_delay_seconds": 0,
|
|
145
|
+
"pillar_turn_delay_seconds": 0.1,
|
|
146
|
+
"turn_timeout_seconds": 1.1,
|
|
147
|
+
"state_hold_seconds": 0.25,
|
|
148
|
+
"finish_delay_seconds": 1.5,
|
|
149
|
+
"pillar_track_area": 600,
|
|
150
|
+
"pillar_avoid_area": 2800,
|
|
151
|
+
"pillar_avoid_cooldown_seconds": 0.55,
|
|
152
|
+
"pillar_avoid_seconds": 2.2,
|
|
153
|
+
"pillar_first_phase_seconds": 0.85,
|
|
154
|
+
"pillar_red_first_correction": 0.95,
|
|
155
|
+
"pillar_red_second_correction": -0.72,
|
|
156
|
+
"pillar_green_first_correction": -0.95,
|
|
157
|
+
"pillar_green_second_correction": 0.72,
|
|
158
|
+
"pillar_wall_guard": true,
|
|
159
|
+
"pillar_wall_guard_threshold": 0.62,
|
|
160
|
+
"pillar_wall_guard_correction": 1.0,
|
|
161
|
+
"pillar_area_ema_alpha": 0.5,
|
|
162
|
+
"pillar_lost_hold_frames": 4,
|
|
163
|
+
"pillar_match_max_dx": 90,
|
|
164
|
+
"pillar_track_confirm_frames": 1,
|
|
165
|
+
"pillar_avoid_confirm_frames": 1
|
|
166
|
+
},
|
|
167
|
+
"serial": {
|
|
168
|
+
"enabled": true,
|
|
169
|
+
"port": "/dev/ttyACM0",
|
|
170
|
+
"auto_detect": true,
|
|
171
|
+
"baud_rate": 9600,
|
|
172
|
+
"ready_timeout_seconds": 6.0,
|
|
173
|
+
"write_timeout_seconds": 1.0,
|
|
174
|
+
"send_interval_seconds": 0.02,
|
|
175
|
+
"keepalive_interval_seconds": 0.15
|
|
176
|
+
},
|
|
177
|
+
"web": {
|
|
178
|
+
"enabled": true,
|
|
179
|
+
"host": "0.0.0.0",
|
|
180
|
+
"port": 5000
|
|
181
|
+
},
|
|
182
|
+
"decision_log": {
|
|
183
|
+
"enabled": true,
|
|
184
|
+
"path": "decision_log.jsonl",
|
|
185
|
+
"cycle_interval_seconds": 0.25
|
|
186
|
+
},
|
|
187
|
+
"debug": {
|
|
188
|
+
"log_every_seconds": 0.5,
|
|
189
|
+
"preview_masks": true,
|
|
190
|
+
"sample_radius": 4,
|
|
191
|
+
"sample_hue_margin": 8,
|
|
192
|
+
"sample_sv_margin": 35,
|
|
193
|
+
"sample_gray_margin": 18,
|
|
194
|
+
"save_frames": false,
|
|
195
|
+
"frame_dir": "debug_frames",
|
|
196
|
+
"frame_every_seconds": 1.0
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DEFAULT_CONFIG = {
|
|
7
|
+
"version": 2,
|
|
8
|
+
"filters": {
|
|
9
|
+
"GRAY": 110,
|
|
10
|
+
"WALL_MAX_SAT": 90,
|
|
11
|
+
"WALL_EXCLUDE_COLORS": True,
|
|
12
|
+
"WALL_EXCLUDE_DILATE": 7,
|
|
13
|
+
"REDLO": [0, 100, 80],
|
|
14
|
+
"REDHI": [4, 255, 255],
|
|
15
|
+
"GREENLO": [30, 80, 30],
|
|
16
|
+
"GREENHI": [90, 255, 255],
|
|
17
|
+
"ORANGELO": [7, 20, 10],
|
|
18
|
+
"ORANGEHI": [37, 255, 255],
|
|
19
|
+
"BLUELO": [100, 85, 90],
|
|
20
|
+
"BLUEHI": [130, 255, 255],
|
|
21
|
+
"PINKLO": [158, 160, 100],
|
|
22
|
+
"PINKHI": [170, 255, 255]
|
|
23
|
+
},
|
|
24
|
+
"contours": {
|
|
25
|
+
"minSize": 80.0,
|
|
26
|
+
"pillar_morph_kernel": 5,
|
|
27
|
+
"pillar_min_width": 4,
|
|
28
|
+
"pillar_min_height": 8,
|
|
29
|
+
"pillar_min_fill_ratio": 0.18
|
|
30
|
+
},
|
|
31
|
+
"camera": {
|
|
32
|
+
"frame_width": 640,
|
|
33
|
+
"frame_height": 480,
|
|
34
|
+
"format": "RGB888",
|
|
35
|
+
"crop_height": 258,
|
|
36
|
+
"flip_180": True,
|
|
37
|
+
"awb_enable": False,
|
|
38
|
+
"mtx": [
|
|
39
|
+
[495.44408865, 0.0, 294.45510998],
|
|
40
|
+
[0.0, 504.36905411, 259.77204181],
|
|
41
|
+
[0.0, 0.0, 1.0]
|
|
42
|
+
],
|
|
43
|
+
"dist": [[-0.64066312, 0.41901729, -0.00366722, 0.01957175, -0.14404429]]
|
|
44
|
+
},
|
|
45
|
+
"roi": {
|
|
46
|
+
"line": {"x": 270, "y": 180, "w": 100, "h": 30},
|
|
47
|
+
"left_wall": {"x": 0, "y": 0, "w": 100, "h": 150},
|
|
48
|
+
"right_wall": {"x": 540, "y": 0, "w": 100, "h": 150}
|
|
49
|
+
},
|
|
50
|
+
"pd": {
|
|
51
|
+
"kp": 4.5,
|
|
52
|
+
"kd": 8.0,
|
|
53
|
+
"target_black_portion": 0.45
|
|
54
|
+
},
|
|
55
|
+
"control": {
|
|
56
|
+
"speed": 100,
|
|
57
|
+
"turn_speed": 100,
|
|
58
|
+
"avoid_speed": 100,
|
|
59
|
+
"done_sleep_seconds": 1.0,
|
|
60
|
+
"max_correction": 1.0,
|
|
61
|
+
"max_steering_offset": 55,
|
|
62
|
+
"steering_sign": -1,
|
|
63
|
+
"drive_sign": 1,
|
|
64
|
+
"servo_center_deg": 90,
|
|
65
|
+
"servo_min_deg": 15,
|
|
66
|
+
"servo_max_deg": 165
|
|
67
|
+
},
|
|
68
|
+
"behavior": {
|
|
69
|
+
"pillars": False,
|
|
70
|
+
"turns": 12,
|
|
71
|
+
"fixed_round_dir": 0,
|
|
72
|
+
"round_dir_vote_threshold": 10,
|
|
73
|
+
"line_marker_min_portion": 0.25,
|
|
74
|
+
"turn_delay_seconds": 0.7,
|
|
75
|
+
"pillar_turn_delay_seconds": 0.3,
|
|
76
|
+
"turn_timeout_seconds": 0.85,
|
|
77
|
+
"state_hold_seconds": 0.4,
|
|
78
|
+
"finish_delay_seconds": 3.1,
|
|
79
|
+
"pillar_track_area": 190,
|
|
80
|
+
"pillar_avoid_area": 530,
|
|
81
|
+
"pillar_avoid_cooldown_seconds": 1.0,
|
|
82
|
+
"pillar_avoid_seconds": 2.5,
|
|
83
|
+
"pillar_first_phase_seconds": 0.95,
|
|
84
|
+
"pillar_red_first_correction": 0.95,
|
|
85
|
+
"pillar_red_second_correction": -0.72,
|
|
86
|
+
"pillar_green_first_correction": -0.95,
|
|
87
|
+
"pillar_green_second_correction": 0.72,
|
|
88
|
+
"pillar_wall_guard": True,
|
|
89
|
+
"pillar_wall_guard_threshold": 0.62,
|
|
90
|
+
"pillar_wall_guard_correction": 1.0,
|
|
91
|
+
"pillar_area_ema_alpha": 0.35,
|
|
92
|
+
"pillar_lost_hold_frames": 3,
|
|
93
|
+
"pillar_match_max_dx": 90,
|
|
94
|
+
"pillar_track_confirm_frames": 2,
|
|
95
|
+
"pillar_avoid_confirm_frames": 2
|
|
96
|
+
},
|
|
97
|
+
"serial": {
|
|
98
|
+
"enabled": True,
|
|
99
|
+
"port": "/dev/ttyACM0",
|
|
100
|
+
"auto_detect": True,
|
|
101
|
+
"baud_rate": 9600,
|
|
102
|
+
"ready_timeout_seconds": 6.0,
|
|
103
|
+
"write_timeout_seconds": 1.0,
|
|
104
|
+
"send_interval_seconds": 0.02,
|
|
105
|
+
"keepalive_interval_seconds": 0.15
|
|
106
|
+
},
|
|
107
|
+
"web": {
|
|
108
|
+
"enabled": True,
|
|
109
|
+
"host": "0.0.0.0",
|
|
110
|
+
"port": 5000
|
|
111
|
+
},
|
|
112
|
+
"decision_log": {
|
|
113
|
+
"enabled": True,
|
|
114
|
+
"path": "decision_log.jsonl",
|
|
115
|
+
"cycle_interval_seconds": 0.25
|
|
116
|
+
},
|
|
117
|
+
"debug": {
|
|
118
|
+
"log_every_seconds": 0.5,
|
|
119
|
+
"preview_masks": True,
|
|
120
|
+
"sample_radius": 4,
|
|
121
|
+
"sample_hue_margin": 8,
|
|
122
|
+
"sample_sv_margin": 35,
|
|
123
|
+
"sample_gray_margin": 18,
|
|
124
|
+
"save_frames": False,
|
|
125
|
+
"frame_dir": "debug_frames",
|
|
126
|
+
"frame_every_seconds": 1.0
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ConfigLoader:
|
|
132
|
+
def __init__(self, file_path):
|
|
133
|
+
self.file_path = file_path
|
|
134
|
+
self.config = deepcopy(DEFAULT_CONFIG)
|
|
135
|
+
self.load_config()
|
|
136
|
+
|
|
137
|
+
def load_config(self):
|
|
138
|
+
if not os.path.exists(self.file_path):
|
|
139
|
+
return
|
|
140
|
+
with open(self.file_path, "r", encoding="utf-8") as file:
|
|
141
|
+
loaded = json.load(file)
|
|
142
|
+
if not isinstance(loaded, dict):
|
|
143
|
+
raise ValueError("config root must be an object")
|
|
144
|
+
self.config = merge_dicts(deepcopy(DEFAULT_CONFIG), loaded)
|
|
145
|
+
self._apply_legacy_keys(loaded)
|
|
146
|
+
|
|
147
|
+
def _apply_legacy_keys(self, loaded):
|
|
148
|
+
if "ArduinoSerialPort" in loaded and not loaded.get("serial", {}).get("port"):
|
|
149
|
+
self.config["serial"]["port"] = loaded["ArduinoSerialPort"]
|
|
150
|
+
if "PD" in loaded:
|
|
151
|
+
pd = loaded["PD"]
|
|
152
|
+
if "kp" in pd:
|
|
153
|
+
self.config["pd"]["kp"] = pd["kp"]
|
|
154
|
+
if "kd" in pd:
|
|
155
|
+
self.config["pd"]["kd"] = pd["kd"]
|
|
156
|
+
if "ROI" in loaded and isinstance(loaded["ROI"], dict):
|
|
157
|
+
self.config["roi"] = merge_dicts(self.config["roi"], loaded["ROI"])
|
|
158
|
+
|
|
159
|
+
def get_property(self, key, default=None):
|
|
160
|
+
return self.config.get(key, default)
|
|
161
|
+
|
|
162
|
+
def get_path(self, path, default=None):
|
|
163
|
+
current = self.config
|
|
164
|
+
for part in str(path).split("."):
|
|
165
|
+
if not isinstance(current, dict) or part not in current:
|
|
166
|
+
return default
|
|
167
|
+
current = current[part]
|
|
168
|
+
return current
|
|
169
|
+
|
|
170
|
+
def set_path(self, path, value):
|
|
171
|
+
parts = str(path).split(".")
|
|
172
|
+
current = self.config
|
|
173
|
+
for part in parts[:-1]:
|
|
174
|
+
if part not in current or not isinstance(current[part], dict):
|
|
175
|
+
current[part] = {}
|
|
176
|
+
current = current[part]
|
|
177
|
+
current[parts[-1]] = value
|
|
178
|
+
|
|
179
|
+
def save_config(self, path=None):
|
|
180
|
+
target = path or self.file_path
|
|
181
|
+
with open(target, "w", encoding="utf-8") as file:
|
|
182
|
+
json.dump(self.config, file, indent=2)
|
|
183
|
+
file.write("\n")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def merge_dicts(base, overrides):
|
|
187
|
+
for key, value in overrides.items():
|
|
188
|
+
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
|
189
|
+
base[key] = merge_dicts(base[key], value)
|
|
190
|
+
else:
|
|
191
|
+
base[key] = value
|
|
192
|
+
return base
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def parse_override_value(text):
|
|
196
|
+
try:
|
|
197
|
+
return json.loads(text)
|
|
198
|
+
except json.JSONDecodeError:
|
|
199
|
+
lowered = text.lower()
|
|
200
|
+
if lowered == "true":
|
|
201
|
+
return True
|
|
202
|
+
if lowered == "false":
|
|
203
|
+
return False
|
|
204
|
+
if lowered == "null":
|
|
205
|
+
return None
|
|
206
|
+
return text
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Run WRO vehicle runtime
|
|
3
|
+
After=multi-user.target
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=simple
|
|
7
|
+
WorkingDirectory=/home/pi/program/onboard
|
|
8
|
+
ExecStart=/usr/bin/python3 /home/pi/program/onboard/robot_runtime.py --pillars
|
|
9
|
+
Restart=on-failure
|
|
10
|
+
|
|
11
|
+
[Install]
|
|
12
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from settings import ConfigLoader
|
|
5
|
+
from vision_types import Pillar
|
|
6
|
+
|
|
7
|
+
class Pipeline:
|
|
8
|
+
def __init__(self, configloader: ConfigLoader):
|
|
9
|
+
self.configloader = configloader
|
|
10
|
+
|
|
11
|
+
def undistort(self, image: np.ndarray):
|
|
12
|
+
mtx, dist = np.array(self.configloader.get_property("camera")['mtx']), np.array(self.configloader.get_property("camera")['dist'])
|
|
13
|
+
return cv2.undistort(image, mtx, dist, None, mtx)
|
|
14
|
+
|
|
15
|
+
def crop(self, image: np.ndarray):
|
|
16
|
+
crop_height = int(self.configloader.get_property("camera")['crop_height'])
|
|
17
|
+
return image[crop_height:,:]
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def hsv_range_mask(hsv: np.ndarray, lower, upper):
|
|
21
|
+
lower = tuple(lower)
|
|
22
|
+
upper = tuple(upper)
|
|
23
|
+
if lower[0] <= upper[0]:
|
|
24
|
+
return cv2.inRange(hsv, lower, upper)
|
|
25
|
+
|
|
26
|
+
low_wrap = (0, lower[1], lower[2])
|
|
27
|
+
high_wrap = upper
|
|
28
|
+
low_direct = lower
|
|
29
|
+
high_direct = (179, upper[1], upper[2])
|
|
30
|
+
return cv2.bitwise_or(
|
|
31
|
+
cv2.inRange(hsv, low_direct, high_direct),
|
|
32
|
+
cv2.inRange(hsv, low_wrap, high_wrap),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def filter_RG_Bl(self, hsv: np.ndarray, color_image: np.ndarray):
|
|
36
|
+
"""
|
|
37
|
+
Extracts red/green pillars and the dark wall mask.
|
|
38
|
+
"""
|
|
39
|
+
filters = self.configloader.get_property("filters")
|
|
40
|
+
redMin = tuple(filters['REDLO'])
|
|
41
|
+
redMax = tuple(filters['REDHI'])
|
|
42
|
+
greenMin = tuple(filters['GREENLO'])
|
|
43
|
+
greenMax = tuple(filters['GREENHI'])
|
|
44
|
+
orangeMin = tuple(filters['ORANGELO'])
|
|
45
|
+
orangeMax = tuple(filters['ORANGEHI'])
|
|
46
|
+
blueMin = tuple(filters['BLUELO'])
|
|
47
|
+
blueMax = tuple(filters['BLUEHI'])
|
|
48
|
+
pinkMin = tuple(filters['PINKLO'])
|
|
49
|
+
pinkMax = tuple(filters['PINKHI'])
|
|
50
|
+
grayThresh = int(filters['GRAY'])
|
|
51
|
+
wallMaxSat = int(filters.get('WALL_MAX_SAT', 90))
|
|
52
|
+
excludeColors = bool(filters.get('WALL_EXCLUDE_COLORS', True))
|
|
53
|
+
excludeDilate = int(filters.get('WALL_EXCLUDE_DILATE', 7))
|
|
54
|
+
|
|
55
|
+
rMask = self.hsv_range_mask(hsv, redMin, redMax)
|
|
56
|
+
gMask = self.hsv_range_mask(hsv, greenMin, greenMax)
|
|
57
|
+
oMask = self.hsv_range_mask(hsv, orangeMin, orangeMax)
|
|
58
|
+
bMask = self.hsv_range_mask(hsv, blueMin, blueMax)
|
|
59
|
+
pMask = self.hsv_range_mask(hsv, pinkMin, pinkMax)
|
|
60
|
+
|
|
61
|
+
blurredR = cv2.medianBlur(rMask, 5)
|
|
62
|
+
blurredG = cv2.medianBlur(gMask, 5)
|
|
63
|
+
grayImage = cv2.cvtColor(color_image, cv2.COLOR_BGR2GRAY)
|
|
64
|
+
blurredImg = cv2.GaussianBlur(grayImage, (3, 3), 0)
|
|
65
|
+
darkMask = cv2.inRange(blurredImg, 0, grayThresh)
|
|
66
|
+
lowSatMask = cv2.inRange(hsv[:, :, 1], 0, wallMaxSat)
|
|
67
|
+
blackimg = cv2.bitwise_and(darkMask, lowSatMask)
|
|
68
|
+
|
|
69
|
+
if excludeColors:
|
|
70
|
+
colorMask = cv2.bitwise_or(rMask, gMask)
|
|
71
|
+
colorMask = cv2.bitwise_or(colorMask, oMask)
|
|
72
|
+
colorMask = cv2.bitwise_or(colorMask, bMask)
|
|
73
|
+
colorMask = cv2.bitwise_or(colorMask, pMask)
|
|
74
|
+
if excludeDilate > 0:
|
|
75
|
+
kernelSize = excludeDilate if excludeDilate % 2 == 1 else excludeDilate + 1
|
|
76
|
+
kernel = np.ones((kernelSize, kernelSize), np.uint8)
|
|
77
|
+
colorMask = cv2.dilate(colorMask, kernel)
|
|
78
|
+
blackimg = cv2.bitwise_and(blackimg, cv2.bitwise_not(colorMask))
|
|
79
|
+
|
|
80
|
+
blackimg = cv2.medianBlur(blackimg, 3)
|
|
81
|
+
return {"green": blurredG, "red": blurredR, "black": blackimg}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def filter_OB(self, hsv: np.ndarray):
|
|
85
|
+
"""
|
|
86
|
+
Extracts the orange and blue colors from the image -> this is used to detect the turn markers
|
|
87
|
+
"""
|
|
88
|
+
orangeMin = tuple(self.configloader.get_property("filters")['ORANGELO'])
|
|
89
|
+
orangeMax = tuple(self.configloader.get_property("filters")['ORANGEHI'])
|
|
90
|
+
blueMin = tuple(self.configloader.get_property("filters")['BLUELO'])
|
|
91
|
+
blueMax = tuple(self.configloader.get_property("filters")['BLUEHI'])
|
|
92
|
+
oMask = self.hsv_range_mask(hsv, orangeMin, orangeMax)
|
|
93
|
+
bMask = self.hsv_range_mask(hsv, blueMin, blueMax)
|
|
94
|
+
# blur images to remove noise
|
|
95
|
+
blurredO = cv2.medianBlur(oMask, 5)
|
|
96
|
+
blurredB = cv2.medianBlur(bMask, 5)
|
|
97
|
+
# return [blurredO, blurredB]
|
|
98
|
+
return {"orange": blurredO, "blue": blurredB}
|
|
99
|
+
|
|
100
|
+
def filter_parking(self, hsv: np.ndarray):
|
|
101
|
+
pinkMin = tuple(self.configloader.get_property("filters")['PINKLO'])
|
|
102
|
+
pinkMax = tuple(self.configloader.get_property("filters")['PINKHI'])
|
|
103
|
+
pMask = cv2.inRange(hsv, pinkMin, pinkMax)
|
|
104
|
+
blurredP = cv2.medianBlur(pMask, 5)
|
|
105
|
+
return blurredP
|
|
106
|
+
|
|
107
|
+
def get_pillars(self, imgIn: np.ndarray, type = "RED") -> list[Pillar]:
|
|
108
|
+
"""
|
|
109
|
+
Extracts pillars from filtered image
|
|
110
|
+
"""
|
|
111
|
+
contours_config = self.configloader.get_property("contours")
|
|
112
|
+
minSize = float(contours_config['minSize'])
|
|
113
|
+
minWidth = int(contours_config.get("pillar_min_width", 4))
|
|
114
|
+
minHeight = int(contours_config.get("pillar_min_height", 8))
|
|
115
|
+
minFillRatio = float(contours_config.get("pillar_min_fill_ratio", 0.18))
|
|
116
|
+
kernelSize = int(contours_config.get("pillar_morph_kernel", 5))
|
|
117
|
+
|
|
118
|
+
mask = cv2.medianBlur(imgIn, 5)
|
|
119
|
+
if kernelSize > 1:
|
|
120
|
+
kernelSize = kernelSize if kernelSize % 2 == 1 else kernelSize + 1
|
|
121
|
+
kernel = np.ones((kernelSize, kernelSize), np.uint8)
|
|
122
|
+
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
|
123
|
+
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
|
|
124
|
+
|
|
125
|
+
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
126
|
+
|
|
127
|
+
processedContours = []
|
|
128
|
+
for contour in contours:
|
|
129
|
+
size = cv2.contourArea(contour)
|
|
130
|
+
if size > minSize:
|
|
131
|
+
rx, ry, w, h = cv2.boundingRect(contour)
|
|
132
|
+
width = int(w)
|
|
133
|
+
height = int(h)
|
|
134
|
+
bboxArea = max(1, width * height)
|
|
135
|
+
fillRatio = float(size) / float(bboxArea)
|
|
136
|
+
|
|
137
|
+
if width < minWidth or height < minHeight or fillRatio < minFillRatio:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
x = int(rx + width / 2)
|
|
141
|
+
processedContours.append(Pillar(x, width, height, type, ry, size))
|
|
142
|
+
return processedContours
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Pillar:
|
|
5
|
+
ignore = False
|
|
6
|
+
big_correction = False
|
|
7
|
+
def __init__(self, screen_x: int, width: int, height: int, color: str, y: int = 0, contour_area: float = 0.0):
|
|
8
|
+
self.screen_x = screen_x
|
|
9
|
+
self.width = width
|
|
10
|
+
self.height = height
|
|
11
|
+
self.color = color
|
|
12
|
+
self.y = y
|
|
13
|
+
self.contour_area = float(contour_area)
|
|
14
|
+
self.stable_area = None
|
|
15
|
+
self.seen_frames = 1
|
|
16
|
+
self.held = False
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def x(self):
|
|
20
|
+
return int(self.screen_x - self.width / 2)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def rect(self):
|
|
24
|
+
return (self.x, self.y, self.width, self.height)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def raw_area(self):
|
|
28
|
+
return int(self.width * self.height)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def area(self):
|
|
32
|
+
if self.stable_area is not None:
|
|
33
|
+
return int(round(self.stable_area))
|
|
34
|
+
return self.raw_area
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_ROI(image: np.ndarray, startxy: list, endxy: list) -> np.ndarray:
|
|
38
|
+
"""
|
|
39
|
+
Extracts the ROIs from the image
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
return image[startxy[1]:endxy[1], startxy[0]:endxy[0]]
|