mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-10-27 10:52:11 +01:00
* Save initial frame of new objects to frame cache Objects that move quickly through the frame and are only seen briefly may not have the update() method called to save thumbnail_data, and may not have the initial frame saved to the tracked object frame cache. This caused a "Frame missing from frame cache" message that was patched by #7313 but this sometimes caused the wrong frame to be chosen for the thumb/snapshot. * Tracking tweaks - When registering new objects, use the past detections from Norfair to populate self.positions and self.stationary_box_history. This prevents the first call of update_position() from triggering a +1 on the object's stationary count (because the iou would be 1.0). - Add a specific tracker for dedicated LPR cam license_plate objects using a lower R value and higher distance threshold to account for fast moving plates. - Add helpful debug messages and keep them disabled with `if False:`
663 lines
25 KiB
Python
663 lines
25 KiB
Python
"""Object attribute."""
|
|
|
|
import logging
|
|
import math
|
|
import os
|
|
from collections import defaultdict
|
|
from statistics import median
|
|
from typing import Optional
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from frigate.config import (
|
|
CameraConfig,
|
|
ModelConfig,
|
|
SnapshotsConfig,
|
|
UIConfig,
|
|
)
|
|
from frigate.const import CLIPS_DIR, THUMB_DIR
|
|
from frigate.review.types import SeverityEnum
|
|
from frigate.util.image import (
|
|
area,
|
|
calculate_region,
|
|
draw_box_with_label,
|
|
draw_timestamp,
|
|
is_better_thumbnail,
|
|
)
|
|
from frigate.util.object import box_inside
|
|
from frigate.util.velocity import calculate_real_world_speed
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TrackedObject:
|
|
def __init__(
|
|
self,
|
|
model_config: ModelConfig,
|
|
camera_config: CameraConfig,
|
|
ui_config: UIConfig,
|
|
frame_cache,
|
|
obj_data: dict[str, any],
|
|
):
|
|
# set the score history then remove as it is not part of object state
|
|
self.score_history = obj_data["score_history"]
|
|
del obj_data["score_history"]
|
|
|
|
self.obj_data = obj_data
|
|
self.colormap = model_config.colormap
|
|
self.logos = model_config.all_attribute_logos
|
|
self.camera_config = camera_config
|
|
self.ui_config = ui_config
|
|
self.frame_cache = frame_cache
|
|
self.zone_presence: dict[str, int] = {}
|
|
self.zone_loitering: dict[str, int] = {}
|
|
self.current_zones = []
|
|
self.entered_zones = []
|
|
self.attributes = defaultdict(float)
|
|
self.false_positive = True
|
|
self.has_clip = False
|
|
self.has_snapshot = False
|
|
self.top_score = self.computed_score = 0.0
|
|
self.thumbnail_data = None
|
|
self.last_updated = 0
|
|
self.last_published = 0
|
|
self.frame = None
|
|
self.active = True
|
|
self.pending_loitering = False
|
|
self.speed_history = []
|
|
self.current_estimated_speed = 0
|
|
self.average_estimated_speed = 0
|
|
self.velocity_angle = 0
|
|
self.path_data = []
|
|
self.previous = self.to_dict()
|
|
|
|
@property
|
|
def max_severity(self) -> Optional[str]:
|
|
review_config = self.camera_config.review
|
|
|
|
if (
|
|
self.camera_config.review.alerts.enabled
|
|
and self.obj_data["label"] in review_config.alerts.labels
|
|
and (
|
|
not review_config.alerts.required_zones
|
|
or set(self.entered_zones) & set(review_config.alerts.required_zones)
|
|
)
|
|
):
|
|
return SeverityEnum.alert
|
|
|
|
if (
|
|
self.camera_config.review.detections.enabled
|
|
and (
|
|
not review_config.detections.labels
|
|
or self.obj_data["label"] in review_config.detections.labels
|
|
)
|
|
and (
|
|
not review_config.detections.required_zones
|
|
or set(self.entered_zones)
|
|
& set(review_config.detections.required_zones)
|
|
)
|
|
):
|
|
return SeverityEnum.detection
|
|
|
|
return None
|
|
|
|
def _is_false_positive(self):
|
|
# once a true positive, always a true positive
|
|
if not self.false_positive:
|
|
return False
|
|
|
|
threshold = self.camera_config.objects.filters[self.obj_data["label"]].threshold
|
|
return self.computed_score < threshold
|
|
|
|
def compute_score(self):
|
|
"""get median of scores for object."""
|
|
return median(self.score_history)
|
|
|
|
def update(self, current_frame_time: float, obj_data, has_valid_frame: bool):
|
|
thumb_update = False
|
|
significant_change = False
|
|
path_update = False
|
|
autotracker_update = False
|
|
# if the object is not in the current frame, add a 0.0 to the score history
|
|
if obj_data["frame_time"] != current_frame_time:
|
|
self.score_history.append(0.0)
|
|
else:
|
|
self.score_history.append(obj_data["score"])
|
|
|
|
# only keep the last 10 scores
|
|
if len(self.score_history) > 10:
|
|
self.score_history = self.score_history[-10:]
|
|
|
|
# calculate if this is a false positive
|
|
self.computed_score = self.compute_score()
|
|
if self.computed_score > self.top_score:
|
|
self.top_score = self.computed_score
|
|
self.false_positive = self._is_false_positive()
|
|
self.active = self.is_active()
|
|
|
|
if not self.false_positive and has_valid_frame:
|
|
# determine if this frame is a better thumbnail
|
|
if self.thumbnail_data is None or is_better_thumbnail(
|
|
self.obj_data["label"],
|
|
self.thumbnail_data,
|
|
obj_data,
|
|
self.camera_config.frame_shape,
|
|
):
|
|
# use the current frame time if the object's frame time isn't in the frame cache
|
|
selected_frame_time = (
|
|
current_frame_time
|
|
if obj_data["frame_time"] not in self.frame_cache.keys()
|
|
else obj_data["frame_time"]
|
|
)
|
|
self.thumbnail_data = {
|
|
"frame_time": selected_frame_time,
|
|
"box": obj_data["box"],
|
|
"area": obj_data["area"],
|
|
"region": obj_data["region"],
|
|
"score": obj_data["score"],
|
|
"attributes": obj_data["attributes"],
|
|
"current_estimated_speed": self.current_estimated_speed,
|
|
"velocity_angle": self.velocity_angle,
|
|
"path_data": self.path_data,
|
|
"recognized_license_plate": obj_data.get(
|
|
"recognized_license_plate"
|
|
),
|
|
"recognized_license_plate_score": obj_data.get(
|
|
"recognized_license_plate_score"
|
|
),
|
|
}
|
|
thumb_update = True
|
|
|
|
# check zones
|
|
current_zones = []
|
|
bottom_center = (obj_data["centroid"][0], obj_data["box"][3])
|
|
in_loitering_zone = False
|
|
in_speed_zone = False
|
|
|
|
# check each zone
|
|
for name, zone in self.camera_config.zones.items():
|
|
# if the zone is not for this object type, skip
|
|
if len(zone.objects) > 0 and obj_data["label"] not in zone.objects:
|
|
continue
|
|
contour = zone.contour
|
|
zone_score = self.zone_presence.get(name, 0) + 1
|
|
|
|
# check if the object is in the zone
|
|
if cv2.pointPolygonTest(contour, bottom_center, False) >= 0:
|
|
# if the object passed the filters once, dont apply again
|
|
if name in self.current_zones or not zone_filtered(self, zone.filters):
|
|
# Calculate speed first if this is a speed zone
|
|
if (
|
|
zone.distances
|
|
and obj_data["frame_time"] == current_frame_time
|
|
and self.active
|
|
):
|
|
speed_magnitude, self.velocity_angle = (
|
|
calculate_real_world_speed(
|
|
zone.contour,
|
|
zone.distances,
|
|
self.obj_data["estimate_velocity"],
|
|
bottom_center,
|
|
self.camera_config.detect.fps,
|
|
)
|
|
)
|
|
|
|
if self.ui_config.unit_system == "metric":
|
|
self.current_estimated_speed = (
|
|
speed_magnitude * 3.6
|
|
) # m/s to km/h
|
|
else:
|
|
self.current_estimated_speed = (
|
|
speed_magnitude * 0.681818
|
|
) # ft/s to mph
|
|
|
|
self.speed_history.append(self.current_estimated_speed)
|
|
if len(self.speed_history) > 10:
|
|
self.speed_history = self.speed_history[-10:]
|
|
|
|
self.average_estimated_speed = sum(self.speed_history) / len(
|
|
self.speed_history
|
|
)
|
|
|
|
# we've exceeded the speed threshold on the zone
|
|
# or we don't have a speed threshold set
|
|
if (
|
|
zone.speed_threshold is None
|
|
or self.average_estimated_speed > zone.speed_threshold
|
|
):
|
|
in_speed_zone = True
|
|
|
|
logger.debug(
|
|
f"Camera: {self.camera_config.name}, tracked object ID: {self.obj_data['id']}, "
|
|
f"zone: {name}, pixel velocity: {str(tuple(np.round(self.obj_data['estimate_velocity']).flatten().astype(int)))}, "
|
|
f"speed magnitude: {speed_magnitude}, velocity angle: {self.velocity_angle}, "
|
|
f"estimated speed: {self.current_estimated_speed:.1f}, "
|
|
f"average speed: {self.average_estimated_speed:.1f}, "
|
|
f"length: {len(self.speed_history)}"
|
|
)
|
|
|
|
# Check zone entry conditions - for speed zones, require both inertia and speed
|
|
if zone_score >= zone.inertia:
|
|
if zone.distances and not in_speed_zone:
|
|
continue # Skip zone entry for speed zones until speed threshold met
|
|
|
|
# if the zone has loitering time, update loitering status
|
|
if zone.loitering_time > 0:
|
|
in_loitering_zone = True
|
|
|
|
loitering_score = self.zone_loitering.get(name, 0) + 1
|
|
|
|
# loitering time is configured as seconds, convert to count of frames
|
|
if loitering_score >= (
|
|
self.camera_config.zones[name].loitering_time
|
|
* self.camera_config.detect.fps
|
|
):
|
|
current_zones.append(name)
|
|
|
|
if name not in self.entered_zones:
|
|
self.entered_zones.append(name)
|
|
else:
|
|
self.zone_loitering[name] = loitering_score
|
|
else:
|
|
self.zone_presence[name] = zone_score
|
|
else:
|
|
# once an object has a zone inertia of 3+ it is not checked anymore
|
|
if 0 < zone_score < zone.inertia:
|
|
self.zone_presence[name] = zone_score - 1
|
|
|
|
# Reset speed if not in speed zone
|
|
if zone.distances and name not in current_zones:
|
|
self.current_estimated_speed = 0
|
|
|
|
# update loitering status
|
|
self.pending_loitering = in_loitering_zone
|
|
|
|
# maintain attributes
|
|
for attr in obj_data["attributes"]:
|
|
if self.attributes[attr["label"]] < attr["score"]:
|
|
self.attributes[attr["label"]] = attr["score"]
|
|
|
|
# populate the sub_label for object with highest scoring logo
|
|
if self.obj_data["label"] in ["car", "package", "person"]:
|
|
recognized_logos = {
|
|
k: self.attributes[k] for k in self.logos if k in self.attributes
|
|
}
|
|
if len(recognized_logos) > 0:
|
|
max_logo = max(recognized_logos, key=recognized_logos.get)
|
|
|
|
# don't overwrite sub label if it is already set
|
|
if (
|
|
self.obj_data.get("sub_label") is None
|
|
or self.obj_data["sub_label"][0] == max_logo
|
|
):
|
|
self.obj_data["sub_label"] = (max_logo, recognized_logos[max_logo])
|
|
|
|
# check for significant change
|
|
if not self.false_positive:
|
|
# if the zones changed, signal an update
|
|
if set(self.current_zones) != set(current_zones):
|
|
significant_change = True
|
|
|
|
# if the position changed, signal an update
|
|
if self.obj_data["position_changes"] != obj_data["position_changes"]:
|
|
significant_change = True
|
|
|
|
if self.obj_data["attributes"] != obj_data["attributes"]:
|
|
significant_change = True
|
|
|
|
# if the state changed between stationary and active
|
|
if self.previous["active"] != self.active:
|
|
significant_change = True
|
|
|
|
# update at least once per minute
|
|
if self.obj_data["frame_time"] - self.previous["frame_time"] > 60:
|
|
significant_change = True
|
|
|
|
# update autotrack at most 3 objects per second
|
|
if self.obj_data["frame_time"] - self.previous["frame_time"] >= (1 / 3):
|
|
autotracker_update = True
|
|
|
|
# update path
|
|
width = self.camera_config.detect.width
|
|
height = self.camera_config.detect.height
|
|
bottom_center = (
|
|
round(obj_data["centroid"][0] / width, 4),
|
|
round(obj_data["box"][3] / height, 4),
|
|
)
|
|
|
|
# calculate a reasonable movement threshold (e.g., 5% of the frame diagonal)
|
|
threshold = 0.05 * math.sqrt(width**2 + height**2) / max(width, height)
|
|
|
|
if not self.path_data:
|
|
self.path_data.append((bottom_center, obj_data["frame_time"]))
|
|
path_update = True
|
|
elif (
|
|
math.dist(self.path_data[-1][0], bottom_center) >= threshold
|
|
or len(self.path_data) == 1
|
|
):
|
|
# check Euclidean distance before appending
|
|
self.path_data.append((bottom_center, obj_data["frame_time"]))
|
|
path_update = True
|
|
logger.debug(
|
|
f"Point tracking: {obj_data['id']}, {bottom_center}, {obj_data['frame_time']}"
|
|
)
|
|
|
|
self.obj_data.update(obj_data)
|
|
self.current_zones = current_zones
|
|
return (thumb_update, significant_change, path_update, autotracker_update)
|
|
|
|
def to_dict(self):
|
|
event = {
|
|
"id": self.obj_data["id"],
|
|
"camera": self.camera_config.name,
|
|
"frame_time": self.obj_data["frame_time"],
|
|
"snapshot": self.thumbnail_data,
|
|
"label": self.obj_data["label"],
|
|
"sub_label": self.obj_data.get("sub_label"),
|
|
"top_score": self.top_score,
|
|
"false_positive": self.false_positive,
|
|
"start_time": self.obj_data["start_time"],
|
|
"end_time": self.obj_data.get("end_time", None),
|
|
"score": self.obj_data["score"],
|
|
"box": self.obj_data["box"],
|
|
"area": self.obj_data["area"],
|
|
"ratio": self.obj_data["ratio"],
|
|
"region": self.obj_data["region"],
|
|
"active": self.active,
|
|
"stationary": not self.active,
|
|
"motionless_count": self.obj_data["motionless_count"],
|
|
"position_changes": self.obj_data["position_changes"],
|
|
"current_zones": self.current_zones.copy(),
|
|
"entered_zones": self.entered_zones.copy(),
|
|
"has_clip": self.has_clip,
|
|
"has_snapshot": self.has_snapshot,
|
|
"attributes": self.attributes,
|
|
"current_attributes": self.obj_data["attributes"],
|
|
"pending_loitering": self.pending_loitering,
|
|
"max_severity": self.max_severity,
|
|
"current_estimated_speed": self.current_estimated_speed,
|
|
"average_estimated_speed": self.average_estimated_speed,
|
|
"velocity_angle": self.velocity_angle,
|
|
"path_data": self.path_data,
|
|
"recognized_license_plate": self.obj_data.get("recognized_license_plate"),
|
|
}
|
|
|
|
return event
|
|
|
|
def is_active(self):
|
|
return not self.is_stationary()
|
|
|
|
def is_stationary(self):
|
|
return (
|
|
self.obj_data["motionless_count"]
|
|
> self.camera_config.detect.stationary.threshold
|
|
)
|
|
|
|
def get_thumbnail(self, ext: str):
|
|
img_bytes = self.get_img_bytes(
|
|
ext, timestamp=False, bounding_box=False, crop=True, height=175
|
|
)
|
|
|
|
if img_bytes:
|
|
return img_bytes
|
|
else:
|
|
_, img = cv2.imencode(f".{ext}", np.zeros((175, 175, 3), np.uint8))
|
|
return img.tobytes()
|
|
|
|
def get_clean_png(self):
|
|
if self.thumbnail_data is None:
|
|
return None
|
|
|
|
try:
|
|
best_frame = cv2.cvtColor(
|
|
self.frame_cache[self.thumbnail_data["frame_time"]],
|
|
cv2.COLOR_YUV2BGR_I420,
|
|
)
|
|
except KeyError:
|
|
logger.warning(
|
|
f"Unable to create clean png because frame {self.thumbnail_data['frame_time']} is not in the cache"
|
|
)
|
|
return None
|
|
|
|
ret, png = cv2.imencode(".png", best_frame)
|
|
if ret:
|
|
return png.tobytes()
|
|
else:
|
|
return None
|
|
|
|
def get_img_bytes(
|
|
self,
|
|
ext: str,
|
|
timestamp=False,
|
|
bounding_box=False,
|
|
crop=False,
|
|
height: int | None = None,
|
|
quality: int | None = None,
|
|
):
|
|
if self.thumbnail_data is None:
|
|
return None
|
|
|
|
try:
|
|
best_frame = cv2.cvtColor(
|
|
self.frame_cache[self.thumbnail_data["frame_time"]],
|
|
cv2.COLOR_YUV2BGR_I420,
|
|
)
|
|
except KeyError:
|
|
logger.warning(
|
|
f"Unable to create jpg because frame {self.thumbnail_data['frame_time']} is not in the cache"
|
|
)
|
|
return None
|
|
|
|
if bounding_box:
|
|
thickness = 2
|
|
color = self.colormap.get(self.obj_data["label"], (255, 255, 255))
|
|
|
|
# draw the bounding boxes on the frame
|
|
box = self.thumbnail_data["box"]
|
|
draw_box_with_label(
|
|
best_frame,
|
|
box[0],
|
|
box[1],
|
|
box[2],
|
|
box[3],
|
|
self.obj_data["label"],
|
|
f"{int(self.thumbnail_data['score'] * 100)}% {int(self.thumbnail_data['area'])}"
|
|
+ (
|
|
f" {self.thumbnail_data['current_estimated_speed']:.1f}"
|
|
if self.thumbnail_data["current_estimated_speed"] != 0
|
|
else ""
|
|
),
|
|
thickness=thickness,
|
|
color=color,
|
|
)
|
|
|
|
# draw any attributes
|
|
for attribute in self.thumbnail_data["attributes"]:
|
|
box = attribute["box"]
|
|
draw_box_with_label(
|
|
best_frame,
|
|
box[0],
|
|
box[1],
|
|
box[2],
|
|
box[3],
|
|
attribute["label"],
|
|
f"{attribute['score']:.0%}",
|
|
thickness=thickness,
|
|
color=color,
|
|
)
|
|
|
|
if crop:
|
|
box = self.thumbnail_data["box"]
|
|
box_size = 300
|
|
region = calculate_region(
|
|
best_frame.shape,
|
|
box[0],
|
|
box[1],
|
|
box[2],
|
|
box[3],
|
|
box_size,
|
|
multiplier=1.1,
|
|
)
|
|
best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
|
|
|
|
if height:
|
|
width = int(height * best_frame.shape[1] / best_frame.shape[0])
|
|
best_frame = cv2.resize(
|
|
best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA
|
|
)
|
|
if timestamp:
|
|
color = self.camera_config.timestamp_style.color
|
|
draw_timestamp(
|
|
best_frame,
|
|
self.thumbnail_data["frame_time"],
|
|
self.camera_config.timestamp_style.format,
|
|
font_effect=self.camera_config.timestamp_style.effect,
|
|
font_thickness=self.camera_config.timestamp_style.thickness,
|
|
font_color=(color.blue, color.green, color.red),
|
|
position=self.camera_config.timestamp_style.position,
|
|
)
|
|
|
|
quality_params = None
|
|
|
|
if ext == "jpg":
|
|
quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality or 70]
|
|
elif ext == "webp":
|
|
quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), quality or 60]
|
|
|
|
ret, jpg = cv2.imencode(f".{ext}", best_frame, quality_params)
|
|
|
|
if ret:
|
|
return jpg.tobytes()
|
|
else:
|
|
return None
|
|
|
|
def write_snapshot_to_disk(self) -> None:
|
|
snapshot_config: SnapshotsConfig = self.camera_config.snapshots
|
|
jpg_bytes = self.get_img_bytes(
|
|
ext="jpg",
|
|
timestamp=snapshot_config.timestamp,
|
|
bounding_box=snapshot_config.bounding_box,
|
|
crop=snapshot_config.crop,
|
|
height=snapshot_config.height,
|
|
quality=snapshot_config.quality,
|
|
)
|
|
if jpg_bytes is None:
|
|
logger.warning(f"Unable to save snapshot for {self.obj_data['id']}.")
|
|
else:
|
|
with open(
|
|
os.path.join(
|
|
CLIPS_DIR, f"{self.camera_config.name}-{self.obj_data['id']}.jpg"
|
|
),
|
|
"wb",
|
|
) as j:
|
|
j.write(jpg_bytes)
|
|
|
|
# write clean snapshot if enabled
|
|
if snapshot_config.clean_copy:
|
|
png_bytes = self.get_clean_png()
|
|
if png_bytes is None:
|
|
logger.warning(
|
|
f"Unable to save clean snapshot for {self.obj_data['id']}."
|
|
)
|
|
else:
|
|
with open(
|
|
os.path.join(
|
|
CLIPS_DIR,
|
|
f"{self.camera_config.name}-{self.obj_data['id']}-clean.png",
|
|
),
|
|
"wb",
|
|
) as p:
|
|
p.write(png_bytes)
|
|
|
|
def write_thumbnail_to_disk(self) -> None:
|
|
directory = os.path.join(THUMB_DIR, self.camera_config.name)
|
|
|
|
if not os.path.exists(directory):
|
|
os.makedirs(directory)
|
|
|
|
thumb_bytes = self.get_thumbnail("webp")
|
|
|
|
with open(os.path.join(directory, f"{self.obj_data['id']}.webp"), "wb") as f:
|
|
f.write(thumb_bytes)
|
|
|
|
|
|
def zone_filtered(obj: TrackedObject, object_config):
|
|
object_name = obj.obj_data["label"]
|
|
|
|
if object_name in object_config:
|
|
obj_settings = object_config[object_name]
|
|
|
|
# if the min area is larger than the
|
|
# detected object, don't add it to detected objects
|
|
if obj_settings.min_area > obj.obj_data["area"]:
|
|
return True
|
|
|
|
# if the detected object is larger than the
|
|
# max area, don't add it to detected objects
|
|
if obj_settings.max_area < obj.obj_data["area"]:
|
|
return True
|
|
|
|
# if the score is lower than the threshold, skip
|
|
if obj_settings.threshold > obj.computed_score:
|
|
return True
|
|
|
|
# if the object is not proportionally wide enough
|
|
if obj_settings.min_ratio > obj.obj_data["ratio"]:
|
|
return True
|
|
|
|
# if the object is proportionally too wide
|
|
if obj_settings.max_ratio < obj.obj_data["ratio"]:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class TrackedObjectAttribute:
|
|
def __init__(self, raw_data: tuple) -> None:
|
|
self.label = raw_data[0]
|
|
self.score = raw_data[1]
|
|
self.box = raw_data[2]
|
|
self.area = raw_data[3]
|
|
self.ratio = raw_data[4]
|
|
self.region = raw_data[5]
|
|
|
|
def get_tracking_data(self) -> dict[str, any]:
|
|
"""Return data saved to the object."""
|
|
return {
|
|
"label": self.label,
|
|
"score": self.score,
|
|
"box": self.box,
|
|
}
|
|
|
|
def find_best_object(self, objects: list[dict[str, any]]) -> Optional[str]:
|
|
"""Find the best attribute for each object and return its ID."""
|
|
best_object_area = None
|
|
best_object_id = None
|
|
best_object_label = None
|
|
|
|
for obj in objects:
|
|
if not box_inside(obj["box"], self.box):
|
|
continue
|
|
|
|
object_area = area(obj["box"])
|
|
|
|
# if multiple objects have the same attribute then they
|
|
# are overlapping, it is most likely that the smaller object
|
|
# is the one with the attribute
|
|
if best_object_area is None:
|
|
best_object_area = object_area
|
|
best_object_id = obj["id"]
|
|
best_object_label = obj["label"]
|
|
else:
|
|
if best_object_label == "car" and obj["label"] == "car":
|
|
# if multiple cars are overlapping with the same label then the label will not be assigned
|
|
return None
|
|
elif object_area < best_object_area:
|
|
# if a car and person are overlapping then assign the label to the smaller object (which should be the person)
|
|
best_object_area = object_area
|
|
best_object_id = obj["id"]
|
|
best_object_label = obj["label"]
|
|
|
|
return best_object_id
|