4 заметки с тегом

Machine Learning

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

Время чтения текста – 9 минут

Есть много разных сканеров для QR, но не всегда изображение обладает хорошим качеством. В компьютерном зрении для этого используется Image Pre-processing: предобработка изображения. Сегодня рассмотрим, как средствами библиотеки scikit-image помочь QR-сканеру найти код на картинке.

from matplotlib import pyplot as plt
import skimage
from skimage import util, exposure, io, measure, feature
from scipy import ndimage as ndi
import numpy as np
import cv2

Проблема

Попробуем просканировать чек из материала «Собираем данные с чеков гипермаркетов на Python». Прочтём картинку методом imread библиотеки matplotlib и покажем его на экране:

img = plt.imread('чек.jpg')
plt.imshow(img)

Кажется, в такой каше сложно что-либо разобрать. Воспользуемся готовой функцией для чтения чтения QR-кода из библиотеки opencv:

def qr_reader(img):
    detector = cv2.QRCodeDetector()
    data, bbox, _ = detector.detectAndDecode(img)
    if data:
        print(data)
    else:
        print('Ничего не нашлось!')

И обратимся к ней, чтобы просканировать наше изображение:

qr_reader(img)
Ничего не нашлось!

И это можно понять: обилие лишних пикселей мешает сканеру распознать здесь QR-код. Тем не менее, мы можем помочь сканеру, указав где находится искомая область.

Решение

Сделаем так: уберём с картинки всё лишнее, найдём координаты прямоугольника с QR-кодом, чтобы затем передать в функцию qr_reader не исходное изображение, а исключительно QR-код. Первым делом уменьшим шум, используя медианный фильтр и сконвертируем изображение из rgb в gray: QR-код состоит всего из двух цветов, так что работать с остальными нам не нужно.

image = ndi.median_filter(util.img_as_float(img), size=9)
image = skimage.color.rgb2gray(image)
plt.imshow(image, cmap='gray')

Медианный фильтр размыл изображение, и разбросанные одинокие пиксели стали менее отчётливыми, а QR теперь выделяется на их фоне. Попробуем применить adjust_gamma к изображению. Эта функция возводит в степень gamma значение каждого пикселя: чем меньше будет этот параметр — тем меньше будет значение пикселя и тем ближе к белому он будет становиться. Попробуем взять gamma за 0.5.

pores_gamma = exposure.adjust_gamma(image, gamma=0.5)
plt.imshow(pores_gamma, cmap='gray')

Заметно, что QR стал ещё отчетливее прочего на фото. Воспользуемся этим: все пиксели, значение которых меньше 0.3 сделаем 0, а остальных — 1.

thresholded = (pores_gamma <= 0.3)
plt.imshow(thresholded, cmap='gray')

А теперь воспользуемся детектором границ canny для полученного изображения thresholded. Этот оператор сам сглаживает изображение и ищет градиенты: границы находятся там, где градиент принимает максимальное значение. С повышением параметра sigma детектор canny перестает замечать менее отчетливые границы.

edge = feature.canny(thresholded, sigma=6)
plt.imshow(edge)

Наконец, получим координаты границ: для этого нарисуем контуры. Получаем их методом find_contours и рисуем поверх изображения edge. Объекты массива contours — координаты по осям X и Y.

contours = measure.find_contours(edge, 0.5)
plt.imshow(edge)
for contour in contours:
    plt.plot(contour[:,1], contour[:,0], linewidth=2)

Возьмём максимальные и минимальные координаты по X и по Y: это будут границы видимого прямоугольника.

positions = np.concatenate(contours, axis=0)
min_pos_x = int(min(positions[:,1]))
max_pos_x = int(max(positions[:,1]))
min_pos_y = int(min(positions[:,0]))
max_pos_y = int(max(positions[:,0]))

Теперь, имея координаты, можем на исходном изображении обвести область с кодом:

start = (min_pos_x, min_pos_y)
end = (max_pos_x, max_pos_y)
cv2.rectangle(img, start, end, (255, 0, 0), 5)
io.imshow(img)

Попробуем срезать оригинальное изображение по этим координатам:

new_img = img[min_pos_y:max_pos_y, min_pos_x:max_pos_x]
plt.imshow(new_img)

И передадим новое изображение в функцию qr_reader:

qr_reader(new_img)

Получаем в ответе:

t=20190320T2303&s=5803.00&fn=9251440300007971&i=141637&fp=4087570038&n=1

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

 Нет комментариев    47   3 мес   Machine Learning   python   skimage

Строим модель для предсказания категории продуктов

Время чтения текста – 12 минут

Эта статья — продолжение серии материалов «Собираем данные с чеков гипермаркетов на Python» и «Парсим данные каталога сайта». В этот раз построим модель, которая обучится на датасете из собранного каталога и классифицирует товарные позиции чека из гипермаркета на продуктовые категории. Суть проблемы: в чеке мы видим данные о каждом товаре отдельно, а иногда хочется быстро понять сколько сегодня потратили денег на «Сладкое».

Предобработка датасета

Импортируем библиотеку pandas и прочитаем csv-файл с каталогом igoods (мы сформировали его, когда парсили каталог). Заодно посмотрим, как он выглядит:

Подробнее о том, как программе эмулировать поведение человека на сайте и собрать датасет из каталога можно прочитать в материале «Парсим данные каталога сайта»

import pandas as pd
sku = pd.read_csv('SKU_igoods.csv',sep=';')
sku.head()

После парсинга в таблице осталось несколько ненужных колонок: например, нам ни к чему знать цену на продукт и его вес, чтобы построить модель предсказания категории товара. Избавляемся от этих колонок методом drop(), а остальные переименуем через rename() и снова смотрим на таблицу:

sku.drop(columns=['Unnamed: 0', 'Weight','Price'],inplace=True)
sku.rename(columns={"SKU": "SKU", "Category": "Group"},inplace=True)
sku.head()

Сгруппируем товары по их категории и посчитаем количество функциями groupby() и agg():

sku.groupby('Group').agg(['count'])

Наша модель должна обучиться на каталоге и, увидев наименование товара, предсказать его категорию. Но в каталоге многие названия будут непонятны модели. В русском языке, например, много предлогов, союзов и других стоп-слов: мы хотим, чтобы модель понимала, что «Мангал с ребрами жесткости» и «Мангал с 6 шампурами» — продукты одной и той же категории. Для этого почистим все названия: уберём из них союзы, предлоги, междометия, частицы и приведём слова к своим основам при помощи стеммера.

Стеммер — программа, которая находит для заданного слова его основу.

import nltk
from nltk.corpus import stopwords
from pymystem3 import Mystem
from string import punctuation
nltk.download('stopwords')

Для стемминга будем использовать стеммер Яндекса из библиотеки pymystem3. Список стоп-слов необходимо расширить — каталог товаров из магазина немного отличается от бытовых ситуаций, в которых базовый набор актуален.

mystem = Mystem() 
russian_stopwords = stopwords.words("russian")
russian_stopwords.extend(['лента','ассорт','разм','арт','что', 'это', 'так', 'вот', 'быть', 'как', 'в', '—', 'к', 'на'])

Опишем функцию подготовки текста. Она приводит текст стеммером к своей основе, убирает из него знаки пунктуации, цифры и стоп-слова. Этот код был найден в одном из kernel на kaggle.

def preprocess_text(text):
    text = str(text)
    tokens = mystem.lemmatize(text.lower())
    tokens = [token for token in tokens if token not in russian_stopwords\
              and token != " " \
              and len(token)>=3 \
              and token.strip() not in punctuation \
              and token.isdigit()==False]
    text = " ".join(tokens)
    return text

Проверим, как работает функция:

preprocess_text("Мой дядя самых честных правил, Когда не в шутку занемог, Он уважать себя заставил И лучше выдумать не мог.")

Получаем:

'дядя самый честный правило шутка занемогать уважать заставлять выдумывать мочь'

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

print(‘Было:’, sku['SKU'][0])
print(‘Стало:’, preprocess_text(sku['SKU'][0]))

Получаем:

Было: Фисташки соленые жареные ТМ 365 дней
Стало: фисташка соленый жареный день

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

sku['processed']=sku['SKU'].apply(preprocess_text)
sku.head()

Строим модель предсказания категории

Для предсказания категории товара будем использовать CountVectorizer и наивный байесовский классификатор. Первый разобьёт текст на токены и посчитает их количество, а второй — простейший мультикатегорийный классификатор, позволит обучить модель предсказывать категорию товара. Также нам потребуются TfidfTransformer для подсчета весов вхождения каждого токена. Поскольку мы хотим запустить все функции одну за другой, обратимся к библиотеке Pipeline.

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
from imblearn.pipeline import Pipeline

Поделим наш датасет на X — обработанные наименования товаров и на Y — их категории. Разделим на обучающую и тестовую выборку, отдав под тесты 33% от общего числа данных.

x = sku.processed
y = sku.Group
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.33)

Пройдём пайплайном следующие команды:

  • CountVectorizer() — вернет матрицу с количеством вхождений каждого токена
  • TfidfTransformer() — преобразует эту матрицу в нормализованное представление tf-idf
  • MultinomialNB() — наивный байесовский классификатор для предсказания категории товара
text_clf = Pipeline([('vect', CountVectorizer(ngram_range=(1,2))),
                     ('tfidf', TfidfTransformer()), 
                    ('clf', MultinomialNB())])

На выходе получим модель в text_clf, которую затем обучим по обучающей выборке и посчитаем предсказания по тестовой выборке:

text_clf = text_clf.fit(X_train, y_train)
y_pred = text_clf.predict(X_test)

А теперь оценим модель:

print('Score:', text_clf.score(X_test, y_test))

Получим такую точность:

Score: 0.923949864498645

Верификация на реальных данных

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

my_products['processed']=my_products['name'].apply(preprocess_text)
my_products.head()

Заполним новый столбец prediction — он будет предсказывать категорию товара по его названию. Передаем ему колонку с обработанными названиями и создаём новую колонку с предсказаниями.

prediction = text_clf.predict(my_products['processed'])
my_products['prediction']=prediction
my_products[['name', 'prediction']]

DataFrame станет таким:

И посчитаем сумму по каждой категории:

my_products.groupby('prediction').sum()

В целом, модель справляется неплохо с предсказаниями: сосиски уходят в мясную гастрономию, творог — в молочные продукты, багет — в хлеб и выпечку. И всё же заметно, что киви почему-то относится к молочным продуктам, а груши — к эко-продуктам. Проблема в том, что в каталоге в этих разделах много товаров «со вкусом груши» или «со вкусом киви», из-за чего наивный байесовский классификатор отдаёт предпочтение тому классу, экземпляров которого в датасете больше. Это известная проблема несбалансированных классов, которую можно победить ресемплингом исходного датасета или задав нужные веса в модели.

 Нет комментариев    54   3 мес   Data Analytics   Machine Learning   python
 Нет комментариев    18   2019   Machine Learning   python

Собираем данные с чеков гипермаркетов на Python

Время чтения текста – 7 минут

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

Недавно, покупая в очередной раз продукты в гипермаркете, вспомнил, что согласно ФЗ-54 любой оператор торговли, который пробивает кассовый чек, обязан отправлять данные чека в налоговую.

Чек из гипермаркета «Лента», QR-код, который нас интересует, обведен

Что это значит для нас, аналитиков данных? Что мы можем лучше узнать себя, свои потребности и получить интересные данные о собственных покупках.

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

t=20190320T2303&s=5803.00&fn=9251440300007971&i=141637&fp=4087570038&n=1

В данной строке содержатся:

t — timestamp, время, когда вы осуществили покупку
s — сумма чека
fn — кодовый номер fss, потребуется далее в запросе к API
i — номер чека, он нам потребуется далее в запросе к API
fp — параметр fiscalsign, потребуется далее в запросе к API

В рамках решения первого шага нашей задачи мы будем парсить данные чека и собирать их в pandas dataframe, используя модули Python.

Мы воспользуемся API, который отдает данные по чеку с сайта налоговой.

В начале получим аутентификационные данные:

import requests
your_phone = '+7XXXYYYZZZZ' #нужно указать ваш телефон, на него придет СМС с паролем
r = requests.post('https://proverkacheka.nalog.ru:9999/v1/mobile/users/signup', json = {"email":"email@email.com","name":"USERNAME","phone":your_phone})

В результате выполнения POST-запроса мы получим пароль в виде SMS на указанный мобильный телефон. Далее, мы будем использовать его в переменной pwd

Теперь распарсим нашу строку со значениями из QR-кода:

import re
qr_string='t=20190320T2303&s=5803.00&fn=9251440300007971&i=141637&fp=4087570038&n=1'
t=re.findall(r't=(\w+)', qr_string)[0]
s=re.findall(r's=(\w+)', qr_string)[0]
fn=re.findall(r'fn=(\w+)', qr_string)[0]
i=re.findall(r'i=(\w+)', qr_string)[0]
fp=re.findall(r'fp=(\w+)', qr_string)[0]

Будем использовать полученные переменные для извлечения данных.
В посте на Хабре довольно подробно изучены статусы ошибок при формировании запроса к API, не буду повторять эту информацию.

В начале необходимо проверить, что по данному чеку есть данные, формируем GET-запрос.

headers = {'Device-Id':'', 'Device-OS':''}
payload = {'fiscalSign': fp, 'date': t,'sum':s}
check_request=requests.get('https://proverkacheka.nalog.ru:9999/v1/ofds/*/inns/*/fss/'+fn+'/operations/1/tickets/'+i,params=payload, headers=headers,auth=(your_phone, pwd))
print(check_request.status_code)

В запросе необходимо указать headers, хотя бы пустые. В моем случае GET-запрос возвращает ошибку 406, из чего я понимаю, что такой чек находится (почему GET-запрос возвращает 406 для меня загадка, буду рад подсказкам в комментариях). Если не указать сумму или дату, то GET-запрос вернет ошибку 400 — bad request.

Переходим к самому интересному, получаем данные чека:

request_info=requests.get('https://proverkacheka.nalog.ru:9999/v1/inns/*/kkts/*/fss/'+fn+'/tickets/'+i+'?fiscalSign='+fp+'&sendToEmail=no',headers=headers,auth=(your_phone, pwd))
print(request_info.status_code)
products=request_info.json()

Должны получить код 200 (успешное выполнение GET-запроса), а в переменной products — все, что относится к нашему чеку.

Чтобы работать с этими данными воспользуемся pandas и преобразуем все в dataframe.

import pandas as pd
from datetime import datetime
my_products=pd.DataFrame(products['document']['receipt']['items'])
my_products['price']=my_products['price']/100
my_products['sum']=my_products['sum']/100
datetime_check = datetime.strptime(t, '%Y%m%dT%H%M') #((https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior отформатируем дату))
my_products['date']=datetime_check
my_products.set_index('date',inplace=True)

Теперь мы имеем рабочий pandas.dataframe с чеками, визуально это выглядит так:

«Шапка» чековых данных

Можно построить гистограмму покупок или посмотреть на все в виде «ящика с усами»:

import matplotlib.pyplot as plt
%matplotlib inline
my_products['sum'].plot(kind='hist', bins=20)
plt.show()
my_products['sum'].plot(kind='box')
plt.show()

В завершение элементарно получим описательные статистики в текстовом виде командой .describe():

my_products.describe()

Данные удобно записать в .csv-файл, чтобы в следующий раз дополнить статистику:

with open('hyper_receipts.csv', 'a') as f:
             my_products.to_csv(f, header=True)
 9 комментариев    841   2019   analysis   Data Analytics   data science   Machine Learning   python