Посмотреть как Markdown

Ансамбли детекторов

LibreEnsemble прогоняет два или более детектора по одному и тому же декодированному изображению и сливает их рамки в один объект Results. Каждый участник сохраняет свои веса, пороги, устройства и список классов.

Что такое ансамбль

LibreEnsemble берёт два или более детектора, прогоняет каждый по одному и тому же изображению и сливает их рамки в один Results. Это конструкция уровня предсказания: обучать здесь нечего, а участники остаются независимыми моделями, которые можно валидировать и экспортировать по отдельности.

Единственная поддерживаемая задача — детекция. Участник с любой другой задачей вызывает ValueError при создании ансамбля, и в сообщении указаны индекс участника и его задача.

Оба имени импортируются лениво, поэтому до использования ничего не стоят:

python
from libreyolo import LibreEnsemble, ExternalDetector

Создание ансамбля

Два детектора, слияние
from libreyolo import LibreEnsemble, SAMPLE_IMAGE # Участниками могут быть пути к чекпойнтам или уже загруженные модели.ensemble = LibreEnsemble(["LibreYOLO9s.pt", "LibreRFDETRs.pt"]) result = ensemble(SAMPLE_IMAGE)for xyxy, conf, cls in zip(    result.boxes.xyxy.tolist(),    result.boxes.conf.tolist(),    result.boxes.cls.tolist(),):    print(result.names[int(cls)], round(float(conf), 3), xyxy)
Веса и требование к голосам
from libreyolo import LibreEnsemble, SAMPLE_IMAGE ensemble = LibreEnsemble(    ["LibreYOLO9s.pt", "LibreRFDETRs.pt"],    weights=[1.0, 1.3],   # по соглашению, пропорционально mAP на валидации    fusion="wbf",    fusion_iou=0.55,    min_votes=2,          # оставить только рамки, найденные обоими участниками) result = ensemble(SAMPLE_IMAGE)print(len(result.boxes), "agreed detections")
Пороги по участникам
from libreyolo import LibreEnsemble, SAMPLE_IMAGE ensemble = LibreEnsemble(["LibreYOLO9s.pt", "LibreRFDETRs.pt"]) # Скаляр применяется ко всем участникам; список читается по участникам.result = ensemble(SAMPLE_IMAGE, conf=[0.3, 0.5], iou=0.5)print(len(result.boxes))

python
LibreEnsemble(
    members,
    *,
    weights=None,
    fusion="wbf",
    fusion_iou=0.55,
    min_votes=1,
)

members — последовательность из двух и более элементов. Элемент типа str или Path загружается через LibreYOLO(); всё остальное должно быть вызываемым и иметь словарь names. Если элементов меньше двух, будет ValueError, а если передать одну строку — TypeError, а не перебор её символов.

По умолчанию weights равен None — это одинаковые веса для всех. Если веса заданы, их должно быть ровно по одному на участника, и все они должны быть строго положительными, поэтому нулевой вес приводит к ошибке, а не молча отбрасывает участника. По документированному соглашению их задают пропорционально mAP участника на валидации.

По умолчанию fusion_iou равен 0.55; это IoU, при котором рамки от разных участников собираются в один кластер. Это не тот же порог, что iou в вызове, — тот отвечает за собственный NMS каждого участника.

По умолчанию min_votes равен 1: рамку может провести в результат любой отдельный участник. Если поднять значение, останутся только кластеры, подтверждённые таким числом разных участников. Значение должно быть положительным целым не больше числа участников, и по каждому классу оно ограничивается сверху числом участников, которые этот класс вообще знают, — так класс, на котором обучался лишь один участник, не стирается молча.

Методы слияния

Три метода принимаются по имени, а ещё можно передать вызываемый объект.

fusionПоведение
"wbf"Weighted boxes fusion: последовательный алгоритм, в точности как в статье. Значение по умолчанию
"wbf_seeded"Weighted boxes fusion в один проход; затравки кластеров выбирает NMS с учётом классов
"nms"Склеить рамки всех участников, затем NMS с учётом классов

Weighted boxes fusion усредняет координаты кластера с весами по уверенности и выдаёт рамку, которую не предлагал ни один участник. Два взвешенных варианта совпадают, когда кластеры однозначны, и могут слегка расходиться на цепочках перекрывающихся кластеров. "nms" вместо усреднения выбирает выжившую рамку, поэтому выжившие сохраняют исходные оценки, а веса влияют только на то, какая рамка победит. Поскольку он отбирает, а не кластеризует, считать голоса он не может: сочетание fusion="nms" с min_votes больше 1 вызывает ValueError.

Weighted boxes fusion пересчитывает оценку кластера пропорционально доле веса участников, которые его поддержали. При двух участниках с одинаковыми весами рамка, найденная только одним из них, сохраняет половину оценки: 0.9 превращается в 0.45. Поэтому итоговая уверенность может оказаться ниже conf, с которым запускался каждый участник, — фильтруйте по итоговой оценке и не рассчитывайте, что порог участника всё ещё действует.

Участники с разными списками классов

Участникам не обязательно иметь общий список классов. Их пространства меток объединяются по именам, и каждый участник получает таблицу соответствия, переводящую его собственные id классов в объединение. ensemble.names — это объединение, и именно его несёт возвращаемый Results.

Рамки сливаются только в пределах одного имени класса. Класс, который знает лишь один участник, проходит насквозь без слияния и не штрафуется за это: пересчёт оценки использует знаменатель по каждому классу, поэтому класс, известный только одному участнику, сохраняет свою оценку.

При частичном пересечении в лог пишется предупреждение с перечислением классов, которые есть не у всех участников. Именно это предупреждение стоит читать внимательно, потому что чекпойнт, у которого имена классов — заглушки вроде class_0, строит объединение, не пересекающееся ни с одним другим участником, и слияния между участниками не происходит вовсе.

Участник, вернувший id класса за пределами собственного names, вызывает RuntimeError.

Сторонние детекторы

Подключение детектора, который LibreYOLO не загружал
from libreyolo import ExternalDetector, LibreEnsemble, SAMPLE_IMAGE def my_detector(pil_image):    # Вернуть (boxes, scores, labels): xyxy в пикселях исходного изображения.    return ([[100.0, 100.0, 200.0, 300.0]], [0.9], [0]) external = ExternalDetector(my_detector, names={0: "person"}) ensemble = LibreEnsemble(["LibreYOLO9s.pt", external])result = ensemble(SAMPLE_IMAGE)print(len(result.boxes))

ExternalDetector(fn, names) оборачивает любой вызываемый объект, который принимает PIL-изображение и возвращает (boxes, scores, labels), где рамки заданы как xyxy в пикселях исходного изображения. Он проверяет число аргументов, форму рамок, совпадение длин и то, что каждый id класса есть в names, и сам применяет порог conf.

Так в слиянии участвует детектор, который LibreYOLO не загружал.

Вызов

Те же источники, что и у одиночной модели
from libreyolo import LibreEnsemble ensemble = LibreEnsemble(["LibreYOLO9s.pt", "LibreRFDETRs.pt"]) # Замените clip.mp4 на видеофайл на диске.for result in ensemble("clip.mp4", stream=True, vid_stride=2):    print(result.frame_idx, len(result.boxes))

Сигнатура вызова повторяет сигнатуру одиночной модели и принимает те же источники: изображения, папки, списки, видео, захват экрана, веб-камеры и сетевые потоки. Живым источникам нужен stream=True — по той же причине, что и везде.

АргументПо умолчаниюПримечания
conf0.25Задаётся по участникам: скаляр применяется ко всем, либо по одному значению на участника
iou0.45Собственный порог NMS каждого участника, а не порог слияния
imgszNonelist читается по участникам; int или кортеж применяется ко всем
deviceNoneСкаляр или по одному на участника, так что участники могут находиться на разных устройствах
classesNoneФильтрует итоговый результат по id объединённых классов
max_det300Применяется к итоговому результату

Поскольку list для imgsz означает «по участникам», imgsz=[480, 640] — это 480 для первого участника и 640 для второго, а imgsz=(480, 640) — один прямоугольный размер для всех. На этом различии легко споткнуться.

Участники вызываются с max_det не меньше 300 независимо от того, что вы запросили, поэтому каждый работает с запасом, а ансамбль обрезает результат один раз в конце.

Изображение декодируется один раз, и каждому участнику передаётся один и тот же объект. batch принимается для совместимости и игнорируется; изображения обрабатываются последовательно.

Что возвращается

Обычный Results — тот же тип, что возвращает одиночная модель, — с names, равным объединённому пространству классов. Всё, что написано на странице Работа с результатами, действует без изменений.

Единственное отличие — result.speed, который ансамбль всё-таки заполняет. Его ключи — member_0, member_1 и так далее, плюс fusion, в миллисекундах. Это единственное место в библиотеке, где speed заполняется.

Строки с неконечными значениями в рамках или оценках отбрасываются до слияния. Если участники находятся на разных устройствах, слияние выполняется на устройстве первого участника, который что-то вернул.

Чего ансамбль не умеет

val() и export() вызывают NotImplementedError и отсылают к участникам: валидировать и экспортировать нужно каждого по отдельности. Метода train нет вовсе, поэтому его вызов приводит к AttributeError.

Половинная точность на уровне ансамбля не обрабатывается. half=True попадает в тот же путь с предупреждением и без эффекта, что и везде; настраивайте точность на каждом участнике.

Интерфейса командной строки для ансамблей нет. Это Python API.

Сигнатуры конструктора и вызова, значения по умолчанию, ошибки валидации, объединение пространства классов, подсчёт голосов и возвращаемый Results прочитаны из libreyolo/ensemble/model.py. Алгоритмы слияния и их аргументы — из libreyolo/ops/fusion.py. Замысел из docs/adr/0004-model-ensembling.md. Сценарии использования сверены с tests/unit/test_ensemble.py и tests/unit/test_ops_fusion.py.