openmv/scripts/libraries/ml/ml-core/ml/utils.py
2025-10-18 10:11:41 -07:00

234 lines
7.8 KiB
Python

# Copyright (C) 2024 OpenMV, LLC.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# 3. Any redistribution, use, or modification in source or binary form
# is done solely for personal benefit and not for any commercial
# purpose or for monetary gain. For commercial licensing options,
# please contact openmv@openmv.io
#
# THIS SOFTWARE IS PROVIDED BY THE LICENSOR AND COPYRIGHT OWNER "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE LICENSOR OR COPYRIGHT
# OWNER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import math
from ulab import numpy as np
def logit(x):
return np.log(x / (1.0 - x))
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
def mod(a, b):
return a - (b * (a // b))
def threshold(scores, threshold, scale, find_max=False, find_max_axis=1):
if scale > 0:
if find_max:
scores = np.max(scores, axis=find_max_axis)
return np.nonzero(scores > threshold)[0]
else:
if find_max:
scores = np.min(scores, axis=find_max_axis)
return np.nonzero(scores < threshold)[0]
def quantize(model, value, index=0):
if model.output_dtype[index] == 'f':
return value
return (value / model.output_scale[index]) + model.output_zero_point[index]
def dequantize(model, value, index=0):
if model.output_dtype[index] == 'f':
return value
return (value - float(model.output_zero_point[index])) * model.output_scale[index]
class NMS:
def __init__(
self,
window_w,
window_h,
roi,
):
self.window_w = window_w
self.window_h = window_h
self.roi = roi
if roi[2] < 1 or roi[3] < 1:
raise ValueError("Invalid ROI dimensions!")
self.boxes = []
def add_bounding_box(self, xmin, ymin, xmax, ymax, score, label_index, keypoints=None):
if score >= 0.0 and score <= 1.0:
xmin = max(0.0, min(xmin, self.window_w))
ymin = max(0.0, min(ymin, self.window_h))
xmax = max(0.0, min(xmax, self.window_w))
ymax = max(0.0, min(ymax, self.window_h))
w = int(xmax - xmin)
h = int(ymax - ymin)
if w > 0 and h > 0:
self.boxes.append([int(xmin), int(ymin), w, h, score, label_index, keypoints])
def get_bounding_boxes(self, threshold=0.1, sigma=0.1):
sorted_boxes = sorted(self.boxes, key=lambda x: x[4], reverse=True)
sigma_scale = (-1.0 / sigma) if (sigma > 0.0) else 0.0
def iou(box1, box2):
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[0] + box1[2], box2[0] + box2[2])
y2 = min(box1[1] + box1[3], box2[1] + box2[3])
w = max(0, x2 - x1)
h = max(0, y2 - y1)
intersection = w * h
union = (box1[2] * box1[3]) + (box2[2] * box2[3]) - intersection
return float(intersection) / float(union)
# Perform Non Max Supression.
max_index = 0
output_boxes = []
max_label_index = 0
while len(sorted_boxes):
box = sorted_boxes.pop(max_index)
output_boxes.append(box)
max_label_index = max(max_label_index, box[5])
# Compare and supress the remaining boxes in the list against the max.
for i in range(len(sorted_boxes)):
v = iou(box, sorted_boxes[i])
sorted_boxes[i][4] = sorted_boxes[i][4] * math.exp(sigma_scale * v * v)
if sorted_boxes[i][4] < threshold:
sorted_boxes[i][4] = 0.0
# Filter out supressed boxes and find the next largest.
sorted_boxes = list(filter(lambda x: x[4] > 0.0, sorted_boxes))
if len(sorted_boxes):
max_index = max(enumerate(sorted_boxes), key=lambda x: x[1][4])[0]
# Map the output boxes back to the input image.
x_scale = self.roi[2] / float(self.window_w)
y_scale = self.roi[3] / float(self.window_h)
scale = min(x_scale, y_scale)
x_offset = ((self.roi[2] - (self.window_w * scale)) / 2) + self.roi[0]
y_offset = ((self.roi[3] - (self.window_h * scale)) / 2) + self.roi[1]
for i in range(len(output_boxes)):
output_boxes[i][0] = int((output_boxes[i][0] * scale) + x_offset)
output_boxes[i][1] = int((output_boxes[i][1] * scale) + y_offset)
output_boxes[i][2] = int(output_boxes[i][2] * scale)
output_boxes[i][3] = int(output_boxes[i][3] * scale)
keypoints = output_boxes[i][6]
if keypoints is not None:
keypoints *= scale
keypoints[:, 0] += x_offset
keypoints[:, 1] += y_offset
# Create a list per class with (rect, score) tuples.
output_list = [[] for i in range(max_label_index + 1)]
for i in range(len(output_boxes)):
rect_score = [output_boxes[i][:4], output_boxes[i][4]]
keypoints = output_boxes[i][6]
if keypoints is not None:
rect_score.append(keypoints)
output_list[output_boxes[i][5]].append(tuple(rect_score))
return output_list
def draw_predictions(
image,
boxes,
labels,
colors,
format="pascal_voc",
font_width=8,
font_height=10,
text_color=(255, 255, 255),
):
image_w = image.width()
image_h = image.height()
for i, (x, y, w, h) in enumerate(boxes):
label = labels[i]
box_color = colors[i]
if format == "pascal_voc":
x = int(x * image_w)
y = int(y * image_h)
w = int(w * image_w) - x
h = int(h * image_h) - y
image.draw_rectangle(x, y, w, h, color=box_color)
image.draw_rectangle(
x,
y - font_height,
len(label) * font_width,
font_height,
fill=True,
color=box_color,
)
image.draw_string(x, y - font_height, label.upper(), text_color)
def draw_keypoints(
image,
keypoints,
radius=4,
color=(255, 0, 0),
thickness=1,
fill=False,
):
if radius > 0:
for kp in keypoints:
image.draw_circle(int(kp[0]), int(kp[1]), radius, color=color, thickness=thickness, fill=fill)
elif radius == 0:
for kp in keypoints:
image.set_pixel(int(kp[0]), int(kp[1]), color)
def draw_skeleton(
image,
keypoints,
lines,
kp_radius=4,
kp_color=(255, 0, 0),
kp_thickness=1,
kp_fill=False,
line_color=(0, 255, 0),
line_thickness=1,
):
draw_keypoints(image, keypoints, radius=kp_radius, color=kp_color, thickness=kp_thickness, fill=kp_fill)
for line in lines:
image.draw_line(int(keypoints[line[0]][0]), int(keypoints[line[0]][1]),
int(keypoints[line[1]][0]), int(keypoints[line[1]][1]),
color=line_color, thickness=line_thickness)