ultralytics 8.3.28 new Solutions CLI commands (#17233)

Signed-off-by: UltralyticsAssistant <web@ultralytics.com>
Co-authored-by: UltralyticsAssistant <web@ultralytics.com>
Co-authored-by: Glenn Jocher <glenn.jocher@ultralytics.com>
This commit is contained in:
Muhammad Rizwan Munawar 2024-11-07 04:44:05 +05:00 committed by GitHub
parent d049e22769
commit 3c976807b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 310 additions and 48 deletions

View file

@ -1,6 +1,6 @@
# Ultralytics YOLO 🚀, AGPL-3.0 license
__version__ = "8.3.27"
__version__ = "8.3.28"
import os

View file

@ -7,11 +7,15 @@ from pathlib import Path
from types import SimpleNamespace
from typing import Dict, List, Union
import cv2
from ultralytics.utils import (
ASSETS,
ASSETS_URL,
DEFAULT_CFG,
DEFAULT_CFG_DICT,
DEFAULT_CFG_PATH,
DEFAULT_SOL_DICT,
IS_VSCODE,
LOGGER,
RANK,
@ -30,6 +34,17 @@ from ultralytics.utils import (
yaml_print,
)
# Define valid solutions
SOLUTION_MAP = {
"count": ("ObjectCounter", "count"),
"heatmap": ("Heatmap", "generate_heatmap"),
"queue": ("QueueManager", "process_queue"),
"speed": ("SpeedEstimator", "estimate_speed"),
"workout": ("AIGym", "monitor"),
"analytics": ("Analytics", "process_data"),
"help": None,
}
# Define valid tasks and modes
MODES = {"train", "val", "predict", "export", "track", "benchmark"}
TASKS = {"detect", "segment", "classify", "pose", "obb"}
@ -57,6 +72,31 @@ TASK2METRIC = {
MODELS = {TASK2MODEL[task] for task in TASKS}
ARGV = sys.argv or ["", ""] # sometimes sys.argv = []
SOLUTIONS_HELP_MSG = f"""
Arguments received: {str(['yolo'] + ARGV[1:])}. Ultralytics 'yolo solutions' usage overview:
yolo SOLUTIONS SOLUTION ARGS
Where SOLUTIONS (required) is a keyword
SOLUTION (optional) is one of {list(SOLUTION_MAP.keys())}
ARGS (optional) are any number of custom 'arg=value' pairs like 'show_in=True' that override defaults.
See all ARGS at https://docs.ultralytics.com/usage/cfg or with 'yolo cfg'
1. Call object counting solution
yolo solutions count source="path/to/video/file.mp4" region=[(20, 400), (1080, 404), (1080, 360), (20, 360)]
2. Call heatmaps solution
yolo solutions heatmap colormap=cv2.COLORMAP_PARAULA model=yolo11n.pt
3. Call queue management solution
yolo solutions queue region=[(20, 400), (1080, 404), (1080, 360), (20, 360)] model=yolo11n.pt
4. Call workouts monitoring solution for push-ups
yolo solutions workout model=yolo11n-pose.pt kpts=[6, 8, 10]
5. Generate analytical graphs
yolo solutions analytics analytics_type="pie"
"""
CLI_HELP_MSG = f"""
Arguments received: {str(['yolo'] + ARGV[1:])}. Ultralytics 'yolo' commands use the following syntax:
@ -78,19 +118,24 @@ CLI_HELP_MSG = f"""
4. Export a YOLO11n classification model to ONNX format at image size 224 by 128 (no TASK required)
yolo export model=yolo11n-cls.pt format=onnx imgsz=224,128
5. Streamlit real-time webcam inference GUI
yolo streamlit-predict
6. Run special commands:
6. Ultralytics solutions usage
yolo solutions count or in {list(SOLUTION_MAP.keys())} source="path/to/video/file.mp4"
7. Run special commands:
yolo help
yolo checks
yolo version
yolo settings
yolo copy-cfg
yolo cfg
yolo solutions help
Docs: https://docs.ultralytics.com
Solutions: https://docs.ultralytics.com/solutions/
Community: https://community.ultralytics.com
GitHub: https://github.com/ultralytics/ultralytics
"""
@ -568,6 +613,100 @@ def handle_yolo_settings(args: List[str]) -> None:
LOGGER.warning(f"WARNING ⚠️ settings error: '{e}'. Please see {url} for help.")
def handle_yolo_solutions(args: List[str]) -> None:
"""
Processes YOLO solutions arguments and runs the specified computer vision solutions pipeline.
Args:
args (List[str]): Command-line arguments for configuring and running the Ultralytics YOLO
solutions: https://docs.ultralytics.com/solutions/, It can include solution name, source,
and other configuration parameters.
Returns:
None: The function processes video frames and saves the output but doesn't return any value.
Examples:
Run people counting solution with default settings:
>>> handle_yolo_solutions(["count"])
Run analytics with custom configuration:
>>> handle_yolo_solutions(["analytics", "conf=0.25", "source=path/to/video/file.mp4"])
Notes:
- Default configurations are merged from DEFAULT_SOL_DICT and DEFAULT_CFG_DICT
- Arguments can be provided in the format 'key=value' or as boolean flags
- Available solutions are defined in SOLUTION_MAP with their respective classes and methods
- If an invalid solution is provided, defaults to 'count' solution
- Output videos are saved in 'runs/solution/{solution_name}' directory
- For 'analytics' solution, frame numbers are tracked for generating analytical graphs
- Video processing can be interrupted by pressing 'q'
- Processes video frames sequentially and saves output in .avi format
- If no source is specified, downloads and uses a default sample video
"""
full_args_dict = {**DEFAULT_SOL_DICT, **DEFAULT_CFG_DICT} # arguments dictionary
overrides = {}
# check dictionary alignment
for arg in merge_equals_args(args):
arg = arg.lstrip("-").rstrip(",")
if "=" in arg:
try:
k, v = parse_key_value_pair(arg)
overrides[k] = v
except (NameError, SyntaxError, ValueError, AssertionError) as e:
check_dict_alignment(full_args_dict, {arg: ""}, e)
elif arg in full_args_dict and isinstance(full_args_dict.get(arg), bool):
overrides[arg] = True
check_dict_alignment(full_args_dict, overrides) # dict alignment
# Get solution name
if args and args[0] in SOLUTION_MAP:
if args[0] != "help":
s_n = args.pop(0) # Extract the solution name directly
else:
LOGGER.info(SOLUTIONS_HELP_MSG)
else:
LOGGER.warning(
f"⚠️ No valid solution provided. Using default 'count'. Available: {', '.join(SOLUTION_MAP.keys())}"
)
s_n = "count" # Default solution if none provided
cls, method = SOLUTION_MAP[s_n] # solution class name, method name and default source
from ultralytics import solutions # import ultralytics solutions
solution = getattr(solutions, cls)(IS_CLI=True, **overrides) # get solution class i.e ObjectCounter
process = getattr(solution, method) # get specific function of class for processing i.e, count from ObjectCounter
cap = cv2.VideoCapture(solution.CFG["source"]) # read the video file
# extract width, height and fps of the video file, create save directory and initialize video writer
import os # for directory creation
from pathlib import Path
from ultralytics.utils.files import increment_path # for output directory path update
w, h, fps = (int(cap.get(x)) for x in (cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT, cv2.CAP_PROP_FPS))
if s_n == "analytics": # analytical graphs follow fixed shape for output i.e w=1920, h=1080
w, h = 1920, 1080
save_dir = increment_path(Path("runs") / "solutions" / "exp", exist_ok=False)
save_dir.mkdir(parents=True, exist_ok=True) # create the output directory
vw = cv2.VideoWriter(os.path.join(save_dir, "solution.avi"), cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
try: # Process video frames
f_n = 0 # frame number, required for analytical graphs
while cap.isOpened():
success, frame = cap.read()
if not success:
break
frame = process(frame, f_n := f_n + 1) if s_n == "analytics" else process(frame)
vw.write(frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
finally:
cap.release()
def handle_streamlit_inference():
"""
Open the Ultralytics Live Inference Streamlit app for real-time object detection.
@ -709,6 +848,7 @@ def entrypoint(debug=""):
"logout": lambda: handle_yolo_hub(args),
"copy-cfg": copy_default_cfg,
"streamlit-predict": lambda: handle_streamlit_inference(),
"solutions": lambda: handle_yolo_solutions(args[1:]),
}
full_args_dict = {**DEFAULT_CFG_DICT, **{k: None for k in TASKS}, **{k: None for k in MODES}, **special}

View file

@ -19,7 +19,6 @@ class AIGym(BaseSolution):
up_angle (float): Angle threshold for considering the 'up' position of an exercise.
down_angle (float): Angle threshold for considering the 'down' position of an exercise.
kpts (List[int]): Indices of keypoints used for angle calculation.
lw (int): Line width for drawing annotations.
annotator (Annotator): Object for drawing annotations on the image.
Methods:
@ -51,7 +50,6 @@ class AIGym(BaseSolution):
self.up_angle = float(self.CFG["up_angle"]) # Pose up predefined angle to consider up pose
self.down_angle = float(self.CFG["down_angle"]) # Pose down predefined angle to consider down pose
self.kpts = self.CFG["kpts"] # User selected kpts of workouts storage for further usage
self.lw = self.CFG["line_width"] # Store line_width for usage
def monitor(self, im0):
"""
@ -84,14 +82,14 @@ class AIGym(BaseSolution):
self.stage += ["-"] * new_human
# Initialize annotator
self.annotator = Annotator(im0, line_width=self.lw)
self.annotator = Annotator(im0, line_width=self.line_width)
# Enumerate over keypoints
for ind, k in enumerate(reversed(tracks.keypoints.data)):
# Get keypoints and estimate the angle
kpts = [k[int(self.kpts[i])].cpu() for i in range(3)]
self.angle[ind] = self.annotator.estimate_pose_angle(*kpts)
im0 = self.annotator.draw_specific_points(k, self.kpts, radius=self.lw * 3)
im0 = self.annotator.draw_specific_points(k, self.kpts, radius=self.line_width * 3)
# Determine stage and count logic based on angle thresholds
if self.angle[ind] < self.down_angle:

View file

@ -5,7 +5,7 @@ from collections import defaultdict
import cv2
from ultralytics import YOLO
from ultralytics.utils import DEFAULT_CFG_DICT, DEFAULT_SOL_DICT, LOGGER
from ultralytics.utils import ASSETS_URL, DEFAULT_CFG_DICT, DEFAULT_SOL_DICT, LOGGER
from ultralytics.utils.checks import check_imshow, check_requirements
@ -42,8 +42,12 @@ class BaseSolution:
>>> solution.display_output(image)
"""
def __init__(self, **kwargs):
"""Initializes the BaseSolution class with configuration settings and YOLO model for Ultralytics solutions."""
def __init__(self, IS_CLI=False, **kwargs):
"""
Initializes the `BaseSolution` class with configuration settings and the YOLO model for Ultralytics solutions.
IS_CLI (optional): Enables CLI mode if set.
"""
check_requirements("shapely>=2.0.0")
from shapely.geometry import LineString, Point, Polygon
@ -63,9 +67,20 @@ class BaseSolution:
) # Store line_width for usage
# Load Model and store classes names
self.model = YOLO(self.CFG["model"] if self.CFG["model"] else "yolov8n.pt")
if self.CFG["model"] is None:
self.CFG["model"] = "yolo11n.pt"
self.model = YOLO(self.CFG["model"])
self.names = self.model.names
if IS_CLI: # for CLI, download the source and init video writer
if self.CFG["source"] is None:
d_s = "solutions_ci_demo.mp4" if "-pose" not in self.CFG["model"] else "solution_ci_pose_demo.mp4"
LOGGER.warning(f"⚠️ WARNING: source not provided. using default source {ASSETS_URL}/{d_s}")
from ultralytics.utils.downloads import safe_download
safe_download(f"{ASSETS_URL}/{d_s}") # download source from ultralytics assets
self.CFG["source"] = d_s # set default source
# Initialize environment and region setup
self.env_check = check_imshow(warn=True)
self.track_history = defaultdict(list)

View file

@ -37,6 +37,7 @@ ARGV = sys.argv or ["", ""] # sometimes sys.argv = []
FILE = Path(__file__).resolve()
ROOT = FILE.parents[1] # YOLO
ASSETS = ROOT / "assets" # default images
ASSETS_URL = "https://github.com/ultralytics/assets/releases/download/v0.0.0" # assets GitHub URL
DEFAULT_CFG_PATH = ROOT / "cfg/default.yaml"
DEFAULT_SOL_CFG_PATH = ROOT / "cfg/solutions/default.yaml" # Ultralytics solutions yaml path
NUM_THREADS = min(8, max(1, os.cpu_count() - 1)) # number of YOLO multiprocessing threads