AI-сервис
Products analyzer

Сервис детектирования и распознавания товаров и цен на изображениях

Лицензия
Other
Бесплатно
Подключить
Категории
CVimageobject detectionOCRretail
Разработчик
Napoleon IT
Описание

Возможности

Решение от Napoleon IT позволяет ритейлерам определять цены и классифицировать товары на изображениях. Автоматизируется процесс ручного сбора данных мониторинга цен и товаров конкурентов.

Описание сервиса: Сервис состоит из нескольких моделей, которые позволяют последовательно распознать товары и ценники по изображениям. Product analyzer обучен на изображениях из Российских ритейл сетей.

Пайплайн моделей:

  1. Находит координаты всех товаров и ценников на изображении
  2. На ценниках находит область цены
  3. OCR модель распознает цены на ценниках
  4. Извлекает из изображений товаров вектора, отличимые в векторном пространстве. Эти векторы сравниваются с эталонными и если значение меньше порогового, то это означает принадлежность к классу.

Под каждую задачу используются следующие модели:

Детекция:

  • EfficientDet5

Feature Extraction:

  • ResNet обученный с применением ArcFace Loss

Детектирование цены в области ценника:

  • Unet

OCR:

  • AttentionOCR, обученная на миллионах примеров вырезанных цен

Преимущества

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

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

Сценарии использования

  • Мониторинг цен

Изначально формирование цены на товарные позиции в крупном магазине складывается из нескольких десятков переменных: затраты на закупку, логистику, выплату налогов, а также маржа, эластичность спроса, стоимость аналогов в других торговых сетях и пр. Чтобы их отслеживать, можно использовать ценовой мониторинг. Это система, которая по фотографии распознает товары на полках, их объем и цены, а затем агрегирует их стоимость у разных ритейлеров. Фото стеллажей в магазинах-конкурентах делают специальные сотрудники — мониторщики.

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

  • Контроль выкладки товаров

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

Как использовать? Для проверки уровня заполненности полки в магазине необходимо ее сфотографировать. Далее отправить на сервис, в ответ получить все координаты товаров, на основе которых определяется сколько пространства заполнено товарами.

Инструкции по использованию

import base64
import requests
# считать изображение в байтах
with open("test_image.jpg", "rb") as fp:
    encoded_string = base64.b64encode(fp.read()).decode("utf-8")

headers = {
    "content-type": "application/json",
    "x-api-key": "your-api-key",
    "x-workspace-id": "your-workspace-id"
}

# отправить запрос на распознавание
results = requests.post(
	server_url, json={"image": encoded_string}, headers=headers
)

image_recognition_results = results.json()["raw_shelf_results"]

product_contours = image_recognition_results["raw_product_contours"]
pricetag_contours = image_recognition_results["raw_pricetag_contours"]
price_contours = image_recognition_results["raw_price_contours"]

Ответ от сервиса приходит в формате json:

"raw_shelf_results":
    {
        "raw_product_contours": Список задетектированных товаров на изображении и их эмбеддинги
        [
            {
                "coords": [x, y, width, height] - Координаты товаров на изображении
                "embedding": [] Вектор 512 значений, извлеченный из задетектированного товара
            }
        ]
        "raw_pricetag_contours": Список задетектированных ценников на изображении
        [
            {
                "coords": [x, y, width, height] - Координаты ценников на изображении
            }
        ]
        "raw_price_contours": Список распознанных цен на изображении, координаты и вероятности распознаваний
        [
            {
                "coords": [x, y, width, height] - Координаты цен на изображении
                "price": str Распознанная на изображении цена (рубли и копейки)
                "proba": float Вероятность распознанной цены
            }
        ]
    }

Отрисовать результаты распознавания на изображении можно следующим образом:

import cv2
import matplotlib.pyplot as plt

def plot(src, cmap=None, title=None, size=(10, 10)):
    %matplotlib inline
    plt.rcParams["figure.figsize"] = size
    plt.imshow(src, cmap)
    plt.title(title)

def plot_boxes(img, contours: list, color=None, labels=None, line_thickness=None):
    if not labels:
        labels = [None for i in range(len(contours))]
    # Plots one bounding box on image img
    for contour, label in zip(contours, labels):
        tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1  # line/font thickness
        c1, c2 = (contour[0], contour[1]), (contour[2]+contour[0], contour[3]+contour[1])
        img = cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
        if label:
            tf = max(tl - 1, 1)  # font thickness
            t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
            c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
            cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA)  # filled
            cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [0, 0, 0], thickness=tf, lineType=cv2.LINE_AA)
    return img
image = cv2.cvtColor(cv2.imread("test_image.jpg"), cv2.COLOR_BGR2RGB)
plot(img, title=image_name)

Изображение

products_img = plot_boxes(
  image,
  [i["coords"]
   for i in product_contours],
  color=(0, 255, 0),
  labels=["product" for i in range(len(product_contours))]
)

plot(products_img)

Изображение

pricetags_img = plot_boxes(
  image,
  [i["coords"] for i in pricetag_contours],
  color=(255, 0, 0),
  labels=["pricetag" for i in range(len(pricetag_contours))]
)

prices_img = plot_boxes(
  pricetags_img,
  [i["coords"] for i in price_contours],
  color=(125, 125, 125),
  labels=[i["price"] for i in price_contours]
)

plot(prices_img)

Изображение

Классификация товаров

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

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

Добавить свой товар в базу для распознавания можно путем добавления фотографий этого товара с разных ракурсов(чем больше вариативность тем лучше) и извлечения векторов из этих изображений. Таким образом у нас реализован подход Zero-shot learning, для добавления нового класса нам не нужно переобучать модель.

Отправим изображение для извлечения вектора и создадим базу товаров :

Изображение

import base64

with open("adrenalin.jpg", "rb") as fp:
    encoded_string = base64.b64encode(fp.read()).decode("utf-8")

results = requests.post(server_url, json={"image": encoded_string}, headers=headers)

image_recognition_results = results.json()["raw_shelf_results"]

product_contours = image_recognition_results["raw_product_contours"]
pricetag_contours = image_recognition_results["raw_pricetag_contours"]
price_contours = image_recognition_results["raw_price_contours"]

# проверим, что товар корректно задетектился
img = cv2.cvtColor(
cv2.imread(f"adrenalin.jpg"), cv2.COLOR_BGR2RGB)

products_img = plot_boxes(
    img.copy(),
    [i["coords"]
     for i in product_contours],
    color=(0, 255, 0),
    labels=["product" for i in range(len(product_contours))]
)

plot(products_img)

Изображение

from scipy.spatial import distance

def find_closest(desc_1, base):
		"функция классификации по векторному расстоянию"
    dists = []

    for d in base:
        desc_2 = d["desc"]
        dist = distance.euclidean(desc_1, desc_2)
        dists.append(dist)
    ind = int(np.argmin(dists))
    sku = base[ind]["sku"]
    return sku, dists[ind]
# добавим в базу извлеченный вектор
base = [{"sku": "adrenaline", "desc": product_contours[0]["embedding"]}]

# фильтруем вектора найденных контуров по расстоянию до вектора из base
matched_contours = []

distance_threshold = 0.7
for product_contour in product_contours:
    product_embedding = np.array(product_contour["embedding"])
    closest_product, dist = find_closest(product_embedding, base)
    if dist <= distance_threshold:
        matched_contours.append((product_contour["coords"], closest_product))

img_copy = img.copy()

products_img = plot_boxes(
    img.copy(),
    [i[0] for i in matched_contours],
    color=(0, 255, 0),
    labels=[i[1] for i in matched_contours]
)

plot(products_img)

Изображение

Ccылки

https://arxiv.org/abs/1801.07698

https://arxiv.org/abs/1911.09070

https://arxiv.org/pdf/1704.03549.pdf

https://arxiv.org/abs/1505.04597