Skip to content

Commit 67d2a2f

Browse files
authored
Merge pull request #14 from uug-ai/image-deresolution
Add crop image functionality and min_width, min_height conditions in project_config.yaml
2 parents 081f90d + 94eb99e commit 67d2a2f

File tree

8 files changed

+177
-41
lines changed

8 files changed

+177
-41
lines changed

Dockerfile

+2-6
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ WORKDIR /ml
4040
COPY . .
4141

4242
# Environment variables
43-
ENV MEDIA_SAVEPATH "/ml/data/input/input_video.mp4"
44-
43+
# Feature parameters
44+
ENV PROJECT_NAME=""
4545

4646
# Dataset parameters
4747
ENV DATASET_FORMAT="base"
@@ -79,9 +79,6 @@ ENV S3_ACCESS_KEY=""
7979
ENV S3_SECRET_KEY=""
8080
ENV S3_BUCKET=""
8181

82-
# Feature parameters
83-
ENV PROJECT_NAME=""
84-
8582
ENV CREATE_BBOX_FRAME "False"
8683
ENV SAVE_BBOX_FRAME "False"
8784
ENV BBOX_FRAME_SAVEPATH "/ml/data/output/output_bbox_frame.jpg"
@@ -100,7 +97,6 @@ ENV MAX_NUMBER_OF_PREDICTIONS ""
10097
ENV MIN_DISTANCE ""
10198
ENV MIN_STATIC_DISTANCE ""
10299
ENV MIN_DETECTIONS ""
103-
ENV ALLOWED_CLASSIFICATIONS "0, 1, 2, 3, 5, 7, 14, 15, 16, 24, 26, 28"
104100
ENV IOU ""
105101
ENV FRAMES_SKIP_AFTER_DETECT ""
106102
ENV MIN_DETECTIONS ""

condition.py

+106-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
var = VariableClass()
66

77

8-
def process_frame(frame, project, video_out='', frames_out=''):
8+
def process_frame(frame, project, cv2=None, frames_out=''):
99
# Perform object classification on the frame.
1010
# persist=True -> The tracking results are stored in the model.
1111
# persist should be kept True, as this provides unique IDs for each detection.
@@ -32,7 +32,7 @@ def process_frame(frame, project, video_out='', frames_out=''):
3232
total_time_class_prediction += time.time() - start_time_class_prediction
3333

3434
if len(cur_results[0]) == 0:
35-
return frame, total_time_class_prediction, False, labels_and_boxes
35+
return None, labels_and_boxes, None, total_time_class_prediction, False
3636

3737
total_results.append(cur_results[0])
3838

@@ -49,15 +49,16 @@ def process_frame(frame, project, video_out='', frames_out=''):
4949
# Since we have over 1k videos per day, the dataset we collect need to be high-quality
5050
# Valid image need to:
5151
# + Have at least MIN_DETECTIONS objects detected:
52-
# + Have to have helmet (since we are lacking of helmet dataset)
52+
# + Have to satisfy the project.condition_func which defines custom condition logics for every specific project.
5353
if project.condition_func(total_results):
5454
for index, results in enumerate(total_results):
5555
# As a convention we will store all result labels under model1's
5656
# The other models' will be mapped accordingly
5757
if not combined_results:
58-
combined_results += [(box.xywhn, box.cls, box.conf) for box in results.boxes]
58+
combined_results += [(box.xywhn, box.xyxy, box.cls, box.conf) for box in results.boxes]
5959
else:
60-
combined_results += [(box.xywhn, project.map_to_first_model(index, box.cls), box.conf) for box in results.boxes]
60+
combined_results += [(box.xywhn, box.xyxy, project.map_to_first_model(index, box.cls), box.conf) for box
61+
in results.boxes]
6162

6263
# sort results based on descending confidences
6364
sorted_combined_results = sorted(combined_results, key=lambda x: x[2], reverse=True)
@@ -68,7 +69,7 @@ def process_frame(frame, project, video_out='', frames_out=''):
6869
for element in sorted_combined_results:
6970
add_flag = True
7071
for res in combined_results:
71-
if res[1] == element[1]:
72+
if res[2] == element[2]: # classes comparison
7273
if (abs(res[0][0][0] - element[0][0][0]) < 0.01
7374
and (abs(res[0][0][1] - element[0][0][1]) < 0.01)):
7475
add_flag = False
@@ -78,8 +79,103 @@ def process_frame(frame, project, video_out='', frames_out=''):
7879
# If the combined result has at least MIN_DETECTIONS boxes found (Could belong to either class)
7980
if len(combined_results) >= var.MIN_DETECTIONS:
8081
print("Condition met, we are gathering the labels and boxes and return results")
81-
for xywhn, cls, _ in combined_results:
82-
labels_and_boxes += f'{int(cls)} {xywhn[0, 0].item()} {xywhn[0, 1].item()} {xywhn[0, 2].item()} {xywhn[0, 3].item()}\n'
83-
return frame, total_time_class_prediction, True, labels_and_boxes
82+
# Crop frane to get only the interested area to reduce storage waste
83+
cropped_frame, cropped_coordinate = __crop_frame__(frame, combined_results)
8484

85-
return frame, total_time_class_prediction, False, labels_and_boxes
85+
# <For testing> if you want to check if the labels
86+
# are transformed and applied correctly to the cropped frame -> uncomment the line below
87+
labeled_frame = None
88+
# labeled_frame = __get_labeled_frame__(cropped_frame, cropped_coordinate, cv2, combined_results)
89+
90+
# Transform the labels and boxes accordingly
91+
labels_and_boxes = __transform_labels__(cropped_frame, cropped_coordinate, combined_results)
92+
total_time_class_prediction += time.time() - start_time_class_prediction
93+
return cropped_frame, labels_and_boxes, labeled_frame, total_time_class_prediction, True
94+
95+
return None, labels_and_boxes, None, total_time_class_prediction, False
96+
97+
98+
def __crop_frame__(frame, combined_results, padding=100):
99+
"""
100+
Crop frame to get only the interesting area, meanwhile it removes the background that doesn't have any detection.
101+
102+
Args:
103+
frame: The original frame to be processed.
104+
combined_results: List of results detected by models.
105+
padding: Add some space padding to the cropped frame to avoid object cutoff.
106+
"""
107+
# If the combined result has at least MIN_DETECTIONS boxes found
108+
if len(combined_results) >= var.MIN_DETECTIONS:
109+
# Initialize bounding box limits
110+
x1_min, y1_min, x2_max, y2_max = float('inf'), float('inf'), float('-inf'), float('-inf')
111+
112+
for _, xyxy, _, _ in combined_results:
113+
x1, y1, x2, y2 = xyxy[0]
114+
x1_min, y1_min = min(x1_min, x1), min(y1_min, y1)
115+
x2_max, y2_max = max(x2_max, x2), max(y2_max, y2)
116+
117+
# Apply padding to the bounding box
118+
orig_height, orig_width = frame.shape[:2]
119+
x1_min = int(max(0, x1_min - padding))
120+
y1_min = int(max(0, y1_min - padding))
121+
x2_max = int(min(orig_width, x2_max + padding))
122+
y2_max = int(min(orig_height, y2_max + padding))
123+
124+
# Crop the frame to the union bounding box with padding
125+
cropped_frame = frame[y1_min:y2_max, x1_min:x2_max]
126+
127+
return cropped_frame, (x1_min, y1_min, x2_max, y2_max)
128+
129+
130+
def __transform_labels__(cropped_frame, cropped_coordinate, combined_results):
131+
"""
132+
Transform the labels and boxes coordinates to match with the cropped frame.
133+
134+
Args:
135+
cropped_frame: The cropped frame to transform labels.
136+
cropped_coordinate: Cropped coordinate of the frame (in xyxy format)
137+
combined_results: List of results detected by models.
138+
"""
139+
labels_and_boxes = ''
140+
frame_width, frame_height = cropped_frame.shape[:2]
141+
142+
for _, xyxy, cls, conf in combined_results:
143+
x1, y1, x2, y2 = xyxy[0]
144+
x1, y1, x2, y2 = int(abs(x1 - cropped_coordinate[0])), int(abs(y1 - cropped_coordinate[1])), int(abs(x2 - cropped_coordinate[0])), int(abs(y2 - cropped_coordinate[1]))
145+
146+
x_center = (x1 + x2) / 2
147+
y_center = (y1 + y2) / 2
148+
149+
# Calculate the xywhn values (requirement for ultralytics YOLO models dataset)
150+
x_center_norm = x_center / frame_width
151+
y_center_norm = y_center / frame_height
152+
width_norm = (x2 - x1) / frame_width
153+
height_norm = (y2 - y1) / frame_height
154+
155+
labels_and_boxes += f'{int(cls)} {x_center_norm} {y_center_norm} {width_norm} {height_norm}\n'
156+
157+
return labels_and_boxes
158+
159+
160+
def __get_labeled_frame__(cropped_frame, cropped_coordinate, cv2, combined_results):
161+
"""
162+
<Used for testing if you want to see the labeled frame>
163+
Return the cropped frame with transformed labeled applied on the frame.
164+
165+
Args:
166+
cropped_frame: The cropped frame to transform labels.
167+
cropped_coordinate: Cropped coordinate of the frame (in xyxy format)
168+
cv2: The Capture Video agent,
169+
combined_results: List of results detected by models.
170+
"""
171+
labeled_frame = cropped_frame.copy()
172+
for _, xyxy, cls, _ in combined_results:
173+
x1, y1, x2, y2 = xyxy[0]
174+
x1, y1, x2, y2 = int(abs(x1 - cropped_coordinate[0])), int(abs(y1 - cropped_coordinate[1])), int(abs(x2 - cropped_coordinate[0])), int(abs(y2 - cropped_coordinate[1]))
175+
print(f"Box: {xyxy}, Class: {int(cls)}")
176+
print(f"Width: {x2 - x1} and height: {y2 - y1}")
177+
cv2.rectangle(labeled_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
178+
cv2.putText(labeled_frame, f'{int(cls)}', (x1 - 10, y1 - 20),
179+
cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 255, 0), 2)
180+
181+
return labeled_frame

exports/flat/flat_export.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(self, name):
2525
self.proj_dir = pjoin(_cur_dir, f'../../data/{name}')
2626
self.proj_dir = pabspath(self.proj_dir) # normalise the link
2727
self.result_dir_path = None
28+
self.result_labeled_dir_path = None
2829

2930
def initialize_save_dir(self):
3031
"""
@@ -36,14 +37,17 @@ def initialize_save_dir(self):
3637
self.result_dir_path = pjoin(self.proj_dir, f'{self._var.DATASET_FORMAT}-v{self._var.DATASET_VERSION}')
3738
os.makedirs(self.result_dir_path, exist_ok=True)
3839

40+
self.result_labeled_dir_path = pjoin(self.proj_dir,
41+
f'{self._var.DATASET_FORMAT}-v{self._var.DATASET_VERSION}-labeled')
42+
3943
if os.path.exists(self.result_dir_path):
4044
print('Successfully initialize save directory!')
4145
return True
4246
else:
4347
print('Something wrong happened!')
4448
return False
4549

46-
def save_frame(self, frame, predicted_frames, cv2, labels_and_boxes):
50+
def save_frame(self, frame, predicted_frames, cv2, labels_and_boxes, labeled_frame=None):
4751
"""
4852
See iflat_export.py
4953
@@ -57,6 +61,13 @@ def save_frame(self, frame, predicted_frames, cv2, labels_and_boxes):
5761
cv2.imwrite(
5862
f'{self.result_dir_path}/{unix_time}.png',
5963
frame)
64+
65+
if labeled_frame.any():
66+
os.makedirs(self.result_labeled_dir_path, exist_ok=True)
67+
68+
cv2.imwrite(
69+
f'{self.result_labeled_dir_path}/{unix_time}.png',
70+
labeled_frame)
6071
# Save labels and boxes
6172
with open(f'{self.result_dir_path}/{unix_time}.txt',
6273
'w') as my_file:

exports/yolov8/yolov8_export.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, name):
2828
self.label_dir_path = None
2929
self.yaml_path = None
3030
self.result_dir_path = None
31+
self.result_labeled_dir_path = None
3132

3233
def initialize_save_dir(self):
3334
"""
@@ -47,6 +48,9 @@ def initialize_save_dir(self):
4748

4849
self.yaml_path = pjoin(self.result_dir_path, 'data.yaml')
4950

51+
self.result_labeled_dir_path = pjoin(self.proj_dir,
52+
f'{self._var.DATASET_FORMAT}-v{self._var.DATASET_VERSION}-labeled')
53+
5054
if (os.path.exists(self.result_dir_path)
5155
and os.path.exists(self.image_dir_path)
5256
and os.path.exists(self.label_dir_path)):
@@ -56,7 +60,7 @@ def initialize_save_dir(self):
5660
print('Something wrong happened!')
5761
return False
5862

59-
def save_frame(self, frame, predicted_frames, cv2, labels_and_boxes):
63+
def save_frame(self, frame, predicted_frames, cv2, labels_and_boxes, labeled_frame=None):
6064
"""
6165
See iyolov8_export.py
6266
@@ -70,6 +74,13 @@ def save_frame(self, frame, predicted_frames, cv2, labels_and_boxes):
7074
cv2.imwrite(
7175
f'{self.image_dir_path}/{unix_time}.png',
7276
frame)
77+
78+
if labeled_frame.any():
79+
os.makedirs(self.result_labeled_dir_path, exist_ok=True)
80+
81+
cv2.imwrite(
82+
f'{self.result_labeled_dir_path}/{unix_time}.png',
83+
labeled_frame)
7384
# Save labels and boxes
7485
with open(f'{self.label_dir_path}/{unix_time}.txt',
7586
'w') as my_file:

projects/helmet/helmet_config.yaml

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
models:
22
- helmet_dectector_1k_16b_150e.pt
3-
- yolov8n.pt
4-
- yolov8n.pt
3+
- yolov8x.pt
54
allowed_classes:
65
- [0, 1, 2]
76
- [0]
8-
- [0]
97

10-
temp: "/tmp/video.mp4" # System will temporarily download video from Integration to process
8+
min_height: 100
9+
min_width: 30
10+
11+
temp: "/tmp/video.mp4" # System will temporarily download video from Integration platform (s3, roboflow) to this path to process

projects/helmet/helmet_project.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,22 @@ def __init__(self):
2727
super().__init__()
2828
self._config = self.__read_config__(config_path)
2929
self.temp_path = self._config.get('temp')
30+
self.min_width = int(self._config.get('min_width')) if self._config.get('min_width') else 0
31+
self.min_height = int(self._config.get('min_height')) if self._config.get('min_height') else 0
3032
self.models, self.models_allowed_classes = self.connect_models()
3133
self.mapping = self.class_mapping(self.models)
3234
self.create_proj_save_dir()
3335

3436
def condition_func(self, total_results):
3537
"""
36-
See ihelmet_project.py
38+
Apply custom condition for the helmet project.
39+
For each frame processed by all models, all conditions below have to be satisfied:
40+
- All models have to return results
41+
- Model0 has PERSON detection
42+
- Model1 has PERSON detection
43+
- Model0 has HELMET detection
44+
- All models have all PERSON bounding boxes with height greater than minimum_height
45+
- All models have all PERSON bounding boxes with width greater than minimum_width
3746
3847
Returns:
3948
None
@@ -42,9 +51,19 @@ def condition_func(self, total_results):
4251
person_model1 = self.mapping[person_model0][1] # Mapping person from model1 to model0
4352
helmet_model0 = 1
4453

45-
return (any(box.cls == person_model0 for box in total_results[0].boxes)
46-
and any(box.cls == helmet_model0 for box in total_results[0].boxes)
47-
and any(box.cls == person_model1 for box in total_results[1].boxes))
54+
has_person_model0 = any(box.cls == person_model0 for box in total_results[0].boxes)
55+
has_helmet_model0 = any(box.cls == helmet_model0 for box in total_results[0].boxes)
56+
has_person_model1 = any(box.cls == person_model1 for box in total_results[1].boxes)
57+
has_minimum_width_height_model0 = all(box.xywh[0, 2] > self.min_width
58+
and box.xywh[0, 3] > self.min_height for box in total_results[0].boxes
59+
if box.cls == person_model0)
60+
has_minimum_width_height_model1 = all(box.xywh[0, 2] > self.min_width
61+
and box.xywh[0, 3] > self.min_height for box in total_results[1].boxes
62+
if box.cls == person_model1)
63+
if has_person_model0 and has_helmet_model0 and has_person_model1 and has_minimum_width_height_model0 and has_minimum_width_height_model1:
64+
return True
65+
else:
66+
return False
4867

4968
def class_mapping(self, models):
5069
"""

services/harvest_service.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def evaluate(self, video):
191191
skip_frames_counter)
192192
# Free all resources
193193
cv2.destroyAllWindows()
194+
194195
return self.export.result_dir_path
195196

196197
def get_frame(self, cap: cv2.VideoCapture, skip_frames_counter):
@@ -219,10 +220,10 @@ def predict_frame(self, frame, skip_frames_counter):
219220
int: The updated skip frames counter.
220221
"""
221222
if self.frame_number > 0 and self.frame_skip_factor > 0 and self.frame_number % self.frame_skip_factor == 0:
222-
frame, total_time_class_prediction, condition_met, labels_and_boxes = con_process_frame(frame, self.project)
223+
frame, labels_and_boxes, labeled_frame, total_time_class_prediction, condition_met = con_process_frame(frame, self.project, cv2)
223224

224225
if condition_met:
225-
self.predicted_frames = self.export.save_frame(frame, self.predicted_frames, cv2, labels_and_boxes)
226+
self.predicted_frames = self.export.save_frame(frame, self.predicted_frames, cv2, labels_and_boxes, labeled_frame)
226227
skip_frames_counter = self._var.FRAMES_SKIP_AFTER_DETECT
227228
print(f'Currently in frame: {self.frame_number}')
228229
self.frame_number += 1

0 commit comments

Comments
 (0)