Добро пожаловать в наш еженедельный блог с советами и рекомендациями FiftyOne, где мы резюмируем интересные вопросы и ответы, недавно появившиеся на Slack, GitHub, Stack Overflow и Reddit.

Подожди, а что такое FiftyOne?

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

Хорошо, давайте погрузимся в советы и рекомендации этой недели!

Выделение ложных или отсутствующих объектов

Член сообщества Slack Джордж Пирс спросил:

"Есть ли способ просто установить ограничивающие рамки вокруг возможно отсутствующих или ложных объектов в моем наборе данных?"

Здесь Джордж спрашивает о том, как изолировать потенциальные ошибки в метках наземной истины в наборе данных. При работе с новым набором данных всегда важно проверять качество наземных аннотаций. Даже высоко оцененные и хорошо цитируемые наборы данных могут содержать множество ошибок.

Два таких распространенных типа ошибок в метках обнаружения объектов:

  1. Метка достоверности была ложно добавлена ​​к данным и не соответствует объекту в разрешенных классах объектов.
  2. Объект не аннотирован, поэтому обнаружение истинности на земле отсутствует.

К счастью, FiftyOne Brain предоставляет встроенный метод, который идентифицирует возможные ложные и отсутствующие обнаружения. Они сохраняются как на уровне выборки, так и на уровне обнаружения.

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

import fiftyone as fo
import fiftyone.brain as fob
import fiftyone.zoo as foz
from fiftyone import ViewField as F

## load example dataset
dataset = foz.load_zoo_dataset("quickstart")

## find possible mistakes
fob.compute_mistakenness(dataset, "predictions")

## create a view containing only objects whose
## ground truth detections are possibly missing
pred_field = "predictions"
missing_view = dataset.filter_labels(
    pred_field, 
    F("possible_missing") > 0, 
    only_matches=True
).to_patches(pred_field)

## create a view containing only objects whose
## ground truth detections are possibly spurious
gt_field = "ground_truth"
spurious_view = dataset.filter_labels(
    gt_field, 
    F("possible_spurious") > 0, 
    only_matches=True
).to_patches(gt_field)

Затем мы можем просмотреть их в FiftyOne App. Проверьте возможные ложные исправления обнаружения, например:

session = fo.launch_app(spurious_view)

Узнайте больше о выявлении ошибок обнаружения в FiftyOne Docs.

Фильтрация по идентификатору

Член сообщества Slack Сильвия Шмитт спросила:

«Я сохраняю связанные идентификаторы образцов как объекты StringField в отдельном поле своих данных и хочу использовать их для сопоставления идентификаторов образцов, которые хранятся как объекты ObjectIdField. Как мне это сделать?'

Если бы вы сравнивали значения в двух `StringFields`, вы могли бы использовать ViewField следующим образом:

import fiftyone as fo
from fiftyone import ViewField as F

dataset = fo.Dataset(..)
dataset.match(F('field_a') == F('field_b'))

Однако идентификаторы образцов представлены в виде объектов ObjectIdField. Они хранятся под ключом _id в базовой базе данных, и на них нужно ссылаться с тем же синтаксисом, добавляя перед ними символ подчеркивания. Кроме того, объект необходимо преобразовать в строку для сравнения.

Вот как может выглядеть такая операция сопоставления:

import fiftyone as fo
import fiftyone.zoo as foz
from fiftyone import ViewField as F

dataset = foz.load_zoo_dataset("quickstart")

# Add a `str_id` field that matches `id` on 10 samples
view = dataset.take(10)
view.set_values("str_id", view.values("id"))

matching_view = dataset.match(
    F("str_id") == F("_id").to_string()
)

Узнайте больше о полях и фильтрации в FiftyOne Docs.

Объединение наборов данных с общими медиафайлами

Член сообщества Slack Джой Тиммерманс спросила:

«У меня есть три набора данных, и некоторые из моих образцов находятся в нескольких наборах данных. Я хотел бы объединить все эти наборы данных в один набор данных для экспорта, сохранив каждую копию каждого из образцов. Как мне это сделать?»

Если ваши наборы данных были созданы независимо, даже если есть образцы с одинаковыми медиафайлами (расположенными по одним и тем же путям к файлам), эти образцы будут иметь разные идентификаторы образцов. В этом случае вы можете создать комбинированный набор данных с помощью метода add_collection() без передачи дополнительных аргументов.

import fiftyone as fo
import fiftyone.zoo as foz

dataset = foz.load_zoo_dataset("quickstart")
ds1 = dataset[:100].clone()
ds2 = dataset[100:].clone()

## create temporary dataset for combining
tmp = ds1.clone()

## add ds2 samples
tmp.add_collection(ds2)

## export
tmp.export(..)

## delete temporary dataset
tmp.delete()

Если, с другой стороны, в ваших наборах данных есть образцы с одинаковыми идентификаторами образцов, то применение метода add_collection() без параметров приведет только к тому, что «комбинированный» набор данных будет иметь одну копию каждого медиафайла.

К счастью, вы можете обойти это, передав new_ids = True в add_collection(). В вашем случае объединение трех наборов данных будет выглядеть так:

import fiftyone as fo
import fiftyone.zoo as foz

## start with dataset1, dataset2, dataset3

tmp = dataset1.clone()
tmp.add_collection(dataset2, new_ids = True)
tmp.add_collection(dataset3, new_ids = True)
tmp.export(..)
tmp.delete()

Узнайте больше о объединении наборов данных в FiftyOne Docs.

Экспорт аннотаций GeoJSON

Член сообщества Slack Кайс Бедиуи спросил:

«Я записываю некоторые производственные данные в формате GeoJSON и хочу сохранить их в базе данных в том же формате. Есть ли способ включить метку ground_truth в файл labels.json, чтобы при перезагрузке набора данных GeoJSON он поставлялся со своими аннотациями?'

Для этого вы можете использовать необязательный аргумент property_makers экспортера GeoJSON, чтобы включить дополнительные свойства непосредственно в формате GeoJSON. Например:

import fiftyone as fo
import fiftyone.zoo as foz

dataset = foz.load_zoo_dataset("quickstart-geo")

dataset.export(
    labels_path="/tmp/labels.json",
    dataset_type=fo.types.GeoJSONDataset,
    property_makers={"ground_truth": lambda d: len(d.detections)},
)

В качестве альтернативы, если вы хотите сохранить аннотации, но нет необходимости сохранять все в формате GeoJSON, вы можете экспортировать их как FiftyOne Dataset:

import fiftyone as fo

dataset.export(
    export_dir="/tmp/all",
    dataset_type=fo.types.FiftyOneDataset,
    export_media=False,
)

Когда вы используете этот подход, все, что вам нужно сделать, чтобы загрузить набор данных обратно, — это использовать метод from_dir() FiftyOne:

import fiftyone as fo

dataset = fo.Dataset.from_dir(
    dataset_dir="/tmp/all",
    dataset_type=fo.types.FiftyOneDataset,
)

Узнайте больше о from_dir(), а также об импорте и экспорте данных в FiftyOne Docs.

Выбор случайных кадров из видео

Член сообщества Slack Джой Тиммерманс спросила:

"Есть ли эквивалент take() для кадров в наборе видеоданных, чтобы я мог случайным образом выбрать подмножество кадров для каждой выборки?"

Один из способов добиться этого — использовать библиотеку Python для случайной выборки в сочетании с методом select_frames(). Во-первых, вы можете использовать случайную выборку без замены, чтобы выбрать набор номеров кадров для каждого видео. Затем вы можете получить кадр id для каждого из этих кадров. Наконец, вы можете передать этот список id в select_frames().

Вот одна реализация, использующая метод случайного выбора numpy:

from numpy.random import choice as nrc
import fiftyone as fo
import fiftyone.zoo as foz
from fiftyone import ViewField as F

dataset = foz.load_zoo_dataset("quickstart-video")

## get nested list of frame ids for each sample
frame_ids = dataset.values("frames.id")

## number of frames for each sample
nframes = dataset.values(F("frames").length())

## number of samples in dataset
nsample = len(dataset)

sample_frames = [nrc(nframe, 10, replace=False) for nframe in nframes]
keep_frame_ids = []

for i in range(nsample):
    curr_frame_ids = frame_ids[i]
    for s in sample_frames[i]:
        keep_frame_ids.append(curr_frame_ids[s])

kept_view = dataset.select_frames(keep_frame_ids)

Если хотите, на этом этапе вы также можете конвертировать видео в кадры:

kept_frames_view = kept_view.to_frames()

Узнайте больше о просмотрах видео и просмотрах кадров в FiftyOne Docs.

Присоединяйтесь к сообществу FiftyOne!

Присоединяйтесь к тысячам инженеров и специалистов по данным, уже использующих FiftyOne для решения некоторых из самых сложных задач в области компьютерного зрения уже сегодня!

Что дальше?

Первоначально опубликовано на https://voxel51.com 10 февраля 2023 г.