Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ before 1.4.0 are documented in the

### Changed

- **Ultralytics names now fail with the LibreYOLO line to type instead.**
`from libreyolo import YOLO` (also `RTDETR`, `NAS`, `YOLOWorld`, `YOLOE`, `SAM`) raises an `ImportError` naming `LibreYOLO("LibreYOLO9s.pt")` or the matching LibreYOLO class; attribute access raises an `AttributeError`, so `hasattr()` is unchanged. `LibreYOLO("yolov8n.pt")` and the other Ultralytics weight names (v3u to v26, n/s/m/l/x, `-seg`/`-pose`/`-obb`/`-cls`, `-world`, `yoloe-*`, model `.yaml`) name the closest LibreYOLO weights, and the CLI suggests the `model=` value. A checkpoint trained with Ultralytics is refused with a message to fine-tune a LibreYOLO model instead; it never reaches the autoconverter. `predict()` options `project`, `name`, `tracker`, `persist`, `save_txt` and `save_conf` name their LibreYOLO equivalents. None of these names became aliases.

- **Optical flow task (`flow`) with eight families.**
A new dense task: `Results.flow` is a `FlowField` with `(H, W, 2)` pixel displacements from frame t to frame t+1 on the original canvas, optional per-pixel confidence and the next frame. Any multi-image source (list, directory, video, stream, screen) is a sequence: N frames give N-1 results, `stream=True` yields per pair, a single image raises. Three views: the Middlebury colour wheel (default), arrows over the frame (`plot(arrows=True)`) and a warp check (`plot(warp=True)`); raw fields save as `.flo` or `.npy` (`save_flow=True`, `FlowField.save`). `FlowDataset` reads Middlebury `.flo`, KITTI 16-bit PNG and `.npy`; `FlowValidator` reports `metrics/epe`, `px1`, `px3` and `fl_all` with `px1` as fitness. CLI: `--source a.jpg,b.jpg`, `--arrows`, `--warp`, `--save-flow`, a flow block in `--json` and `libreyolo val`. ADR 0032. Inference only; training and export are rejected until their contracts land.
Families, all with recorded parity against upstream on a 23-pair clip and weights mirrored on the LibreYOLO Hugging Face org: `LibreSEARAFT` (s/m/l, BSD-3, with a confidence map from its uncertainty head), `LibreRAFT` (s/l plus Sintel and KITTI variants, BSD-3), `LibreNeuFlowv2` (s plus Sintel and Things variants, Apache-2.0, the edge model), `LibreWAFT` (t/s/m/l by backbone, BSD-3; the DINOv3 `t` weights carry the DINOv3 licence), `LibreFlowSeek` (t/s/m/l, Apache-2.0; m and l embed the CC BY-NC Depth Anything V2 Base encoder and ship with a non-commercial banner), `LibreDPFlow` (m plus Sintel, KITTI and Spring variants, Apache-2.0 code from ptlflow; its weights are CC BY-NC-SA and ship with a non-commercial banner), `LibreUniMatch` (s/l plus Sintel and KITTI variants, MIT) and `LibreMEMFOF` (m plus five variants, BSD-3, the three-frame family that uses the `FLOW_CONTEXT` window). UFM was evaluated and left out: its DPT head imports non-commercial code.
Expand Down
8 changes: 8 additions & 0 deletions libreyolo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ def __getattr__(name):
module_path, attr = _lazy[name]
mod = importlib.import_module(module_path, package=__name__)
return getattr(mod, attr)

# Ultralytics class names (YOLO, RTDETR, ...) get the LibreYOLO spelling
# in the error. Not an alias: the name still does not resolve.
from .utils.ultralytics_hints import class_name_hint, raise_class_name_hint

hint = class_name_hint(name)
if hint is not None:
raise_class_name_hint(name, __name__, __file__, hint)
raise AttributeError(f"module 'libreyolo' has no attribute '{name}'")


Expand Down
17 changes: 17 additions & 0 deletions libreyolo/cli/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@ def resolve_model_or_exit(out: OutputHandler, model: str) -> str:
suggestion="pip install libreyolo[rfdetr]",
)

from libreyolo.utils.ultralytics_hints import MODELS_URL, ultralytics_weight_hint

ultralytics_hint = ultralytics_weight_hint(model)
if ultralytics_hint is not None:
if ultralytics_hint.ultralytics:
message = (
f"'{model}' is an Ultralytics model name. LibreYOLO does not "
"load Ultralytics weights."
)
else:
message = f"Unknown model '{model}'."
if ultralytics_hint.cli:
suggestion = f"Use model={ultralytics_hint.cli} (all models: {MODELS_URL})"
Comment thread
EHxuban11 marked this conversation as resolved.
Resolved
else:
suggestion = f"In Python: {ultralytics_hint.python}"
exit_with_error(out, "model_not_found", message, suggestion=suggestion)

all_names = get_all_cli_names()
suggestion = suggest_key(model, all_names)
hint = f" Did you mean '{suggestion}'?" if suggestion else ""
Expand Down
36 changes: 34 additions & 2 deletions libreyolo/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ def LibreYOLO(
hub_source = model_path
model_path = hub_checkpoint

requested_path = model_path
model_path = _resolve_weights_path(model_path)

# librefacerec-* names route to the face-embedding family regardless of
Expand Down Expand Up @@ -548,15 +549,27 @@ def LibreYOLO(
raise
if size is None:
from ..utils.hf_hub import looks_like_repo_id
from ..utils.ultralytics_hints import (
ultralytics_weight_hint,
weight_name_message,
)

ultralytics_hint = ultralytics_weight_hint(model_path)
if ultralytics_hint is not None:
raise ValueError(
weight_name_message(model_path, ultralytics_hint)
)

hub_hint = ""
if looks_like_repo_id(model_path):
# Test what the caller typed: a bare "name" has already become
# "weights/name", which looks like owner/repo but is not one.
if looks_like_repo_id(requested_path):
# A same-named local file or directory shadowed the bare
# owner/repo form (local always wins), so point at the
# explicit Hub syntax that bypasses that precedence.
hub_hint = (
f"\nIf you meant a Hugging Face Hub model, use the "
f"explicit form: LibreYOLO('hf://{model_path}')."
f"explicit form: LibreYOLO('hf://{requested_path}')."
)
raise ValueError(
f"Model weights file not found: {model_path}\n"
Expand All @@ -572,6 +585,14 @@ def LibreYOLO(
logger.warning("Auto-download failed: %s", e)

if not Path(model_path).exists():
from ..utils.ultralytics_hints import (
ultralytics_weight_hint,
weight_name_message,
)

ultralytics_hint = ultralytics_weight_hint(model_path)
if ultralytics_hint is not None:
raise FileNotFoundError(weight_name_message(model_path, ultralytics_hint))
if download_error is not None:
raise FileNotFoundError(
f"Model weights file not found: {model_path}\n"
Expand All @@ -598,6 +619,17 @@ def LibreYOLO(
context="model inspection",
)
except Exception as e:
# Ultralytics checkpoints pickle their own classes, so they always land
# here. Never hand them to the autoconverter, and drop torch's error,
# whose advice is to unpickle the file with weights_only=False.
from ..utils.ultralytics_hints import (
checkpoint_message,
is_ultralytics_checkpoint,
)

if is_ultralytics_checkpoint(model_path, e):
raise RuntimeError(checkpoint_message(model_path)) from None

# Upstream flagship checkpoints can fail the safe inspection load — e.g.
# RF-DETR embeds an argparse.Namespace that weights_only=True rejects.
# Try auto-converting to a LibreYOLO v1.0 checkpoint and reload it.
Expand Down
21 changes: 21 additions & 0 deletions libreyolo/utils/predict_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@
"verbose",
}
REJECTED_PREDICT_KWARGS = {"visualize", "embed"}
#: What to type instead of predict options other YOLO libraries accept.
_SAVE_DIR_HINT = (
"To choose where results are saved, use "
"predict(source, save=True, output_path='runs/my_run')."
)
_TRACK_HINT = "Tracking has its own method: model.track(source, tracker='bytetrack')."
_SAVE_TXT_HINT = "To write detections out, use results.to_json() or results.summary()."
PREDICT_KWARG_HINTS = {
"project": _SAVE_DIR_HINT,
"name": _SAVE_DIR_HINT,
"tracker": _TRACK_HINT,
"persist": _TRACK_HINT,
"save_txt": _SAVE_TXT_HINT,
"save_conf": _SAVE_TXT_HINT,
}
#: Tasks ``predict(agnostic_nms=True)`` covers: the ones that return boxes.
AGNOSTIC_NMS_TASKS = ("detect", "segment", "pose", "obb")
ACCEPTED_PREDICT_KWARGS = {
Expand Down Expand Up @@ -115,11 +130,17 @@ def normalize_predict_kwargs(kwargs: dict, passthrough: set[str] | None = None)
forwarded[key] = remaining.pop(key)

if remaining:
hints = dict.fromkeys(
PREDICT_KWARG_HINTS[key]
for key in sorted(remaining)
if key in PREDICT_KWARG_HINTS
)
raise TypeError(
"Unsupported predict option(s): "
f"{', '.join(sorted(remaining))}. "
"Supported options include conf, iou, imgsz, device, classes, "
"max_det, save, stream, and vid_stride."
+ "".join(f"\n{hint}" for hint in hints)
)

return forwarded
Loading
Loading