Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 41 additions & 2 deletions libreyolo/cli/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ def get_loaded_model_input_size(
return default


def _current_command_name() -> Optional[str]:
"""Name of the running CLI subcommand (``train``, ``info``...), if any."""
ctx = _current_context()
return ctx.info_name if ctx is not None else None


def resolve_model_or_exit(out: OutputHandler, model: str) -> str:
"""Resolve a model reference or fail with a consistent CLI error."""
from libreyolo.backends.triton import is_triton_model_url
Expand Down Expand Up @@ -226,6 +232,33 @@ 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 and ultralytics_hint.untrained:
# Only train accepts pretrained=; elsewhere show the full command.
if _current_command_name() == "train":
suggestion = f"Use model={ultralytics_hint.cli} pretrained=False"
else:
suggestion = (
"To train it from scratch: libreyolo train "
f"model={ultralytics_hint.cli} pretrained=False data=your_data.yaml"
)
suggestion += f" (all models: {MODELS_URL})"
elif ultralytics_hint.cli:
suggestion = f"Use model={ultralytics_hint.cli} (all models: {MODELS_URL})"
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 Expand Up @@ -321,8 +354,8 @@ def exit_imgsz_error(out: OutputHandler, exc: BaseException) -> NoReturn:
)


def get_user_provided_params() -> Set[str]:
"""Return parameter names explicitly provided on the current command line."""
def _current_context():
"""The running click context, from click or typer's vendored click."""
ctx = click.get_current_context(silent=True)
if ctx is None:
# typer >= 0.26 vendors its own click as typer._click with a separate
Expand All @@ -334,6 +367,12 @@ def get_user_provided_params() -> Set[str]:
ctx = _typer_get_ctx(silent=True)
except ImportError:
pass
return ctx


def get_user_provided_params() -> Set[str]:
"""Return parameter names explicitly provided on the current command line."""
ctx = _current_context()
if ctx is None:
return set()
# KeyValueCommand.parse_args stores the set in ctx.meta for reliable access
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