Архив за месяц: Июль 2019

Наложение текста на изображение с помощью python

В основном, для базовых операций с изображениями в питоне, используются две библиотеки OpenCV и PIL. Одна из операций — это добавление произвольного текста в определенную область на фото. В данном посте рассмотрим оба варианта добавления текста и с помощью OpenCV и через PIL. Однако, забегая вперёд скажу, что в OpenCV весьма урезанная функция добавления текста.

1. Пример наложения текста с использованием библиотеки python-OpenCV

import numpy as np
import cv2

# создадим белое изображение
# или можно считать изобрежние с помощью cv2.imread("path_to_file")
img = np.zeros((256, 512, 3), np.uint8)
img[:, :, :] = 255

font = cv2.FONT_HERSHEY_COMPLEX
# вставка текста красного цвета
cv2.putText(img, 'наш произвольный текст', (10, 150), font, 1, color=(0, 0, 255), thickness=2)

cv2.imshow('Result', img)
cv2.waitKey()

# есть ограниченное кол-во вариантов выбора шрифта
# FONT_HERSHEY_COMPLEX
# FONT_HERSHEY_COMPLEX_SMALL
# FONT_HERSHEY_DUPLEX
# FONT_HERSHEY_PLAIN
# FONT_HERSHEY_SCRIPT_COMPLEX
# FONT_HERSHEY_SCRIPT_SIMPLEX
# FONT_HERSHEY_SIMPLEX
# FONT_HERSHEY_TRIPLEX
# FONT_ITALIC

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

Для более продвинутого добавления надписей (можно сказать даже художественного) давайте сформулируем требования, предъявляемые к скрипту.
1. Использование произвольного шрифта из файла ttf
2. Возможность задать угол наклона текста
3. Возможность задать прозрачность текста
4. Выравнивание текста по центру
Четвёртый пункт очень важен, ведь используя произвольный шрифт мы не можем расчитать ширину надписи, и было бы здорово передать эту предобработку библиотеке.
Теперь перейдём к коду:

2. Пример наложение текста с использованием библиотеки PIL

import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont

# создадим белое изображение
# или можно считать изобрежние с помощью cv2.imread("path_to_file")
img = np.zeros((256, 512, 3), np.uint8)
img[:, :, :] = 255


# для простоты и совместимости возьмем пустое изображение из первого примера
# Чтобы не использовать opencv, а только PIL используйте функцию Image.open()
def put_text_pil(img: np.array, txt: str):
    im = Image.fromarray(img)

    font_size = 24
    font = ImageFont.truetype('LibreFranklin-ExtraBold.ttf', size=font_size)

    draw = ImageDraw.Draw(im)
    # здесь узнаем размеры сгенерированного блока текста
    w, h = draw.textsize(txt, font=font)

    y_pos = 50
    im = Image.fromarray(img)
    draw = ImageDraw.Draw(im)

    # теперь можно центрировать текст
    draw.text((int((img.shape[1] - w)/2), y_pos), txt, fill='rgb(0, 0, 0)', font=font)

    img = np.asarray(im)

    return img


img = put_text_pil(img, 'Some Styled Black Text Here')
cv2.imshow('Result', img)
cv2.waitKey()

Пример кода с альфа каналом и наклоном будет чуть позже

Сохранение обученной ML модели (объекта python) в базе данных (BLOB)

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

import _pickle
import pymysql.cursors

## Для простоты возьмём лёгкий объект
# однако обученная модель может достигать сотни мегабайт, а то и ещё больше
listToPickle = [(10, 10), ("example", 10.0), (1.0, 2.0), "object"]

## Преобразование нашего объекта в строку
pickledList = _pickle.dumps(listToPickle)

## Соединение с базой данных mysql
connection = pymysql.connect(host='localhost', port=3306, user='dbuser', password='pass', db='somedb', cursorclass=pymysql.cursors.DictCursor)

## создание курсора
with connection.cursor() as cursor:
    ## Вставка в БД
    cursor.execute("""INSERT INTO pickleTest VALUES (NULL, 'testCard', %s)""", (pickledList, ))
    connection.commit()

    ## Выборка по сохраненной модели
    cursor.execute("""SELECT features FROM pickleTest WHERE card = 'testCard'""")

    ## Получим и распечатаем результат
    res = cursor.fetchone()

    ## Обратное преобразование
    unpickledList = _pickle.loads(res['pickledStoredList'])
    print(unpickledList)

Как видим всё просто, достаточно перед сохранием в блоб преобразовать объект с помощью _pickle.dumps() а при загрузке объекта обратно преобразовать в строку в объект с помощью _pickle.loads().
Также отмечу, что в третьем питоне не надо устанавливать дополнительных библиотек, используется встроенная библиотека _pickle. Таким образом Pickling в Python — это способ хранения весьма сложных структур данных в двоичном представлении, которые требуется восстановить через некоторое время, для получения той же структуры данных.

Как обойти строки dataframe в цикле (pandas)

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

import pandas as pd

dataframe_from_list = [[1,2], [3,4], [10,20]]
df = pd.DataFrame(dataframe_from_list, columns=['col1', 'col2'])

for index, row in df.iterrows():
    print(index, row)
    print(row['col1'], row['col2'], row['col1'] + row['col2'])

В данном примере использовалась функция iterrows для обхода датафрейма. Для обращения к колоночным значениям в строке используется row['название_колонки'].

А теперь давайте подумаем, зачем нам итерироваться по датафрейму: самое очевидно это взять некоторые колоночные значения из строки и подсчитать некоторую функцию.
Но это можно сделать и с помощью apply метода с указанием направления по оси x:

result = df.apply(lambda row: row['col1'] + row['col2'], axis=1)
print(result)
# 3, 7, 30

Соответсвенно, вместо lambda функции можно поставить свою, или в простом случае использовать оптимизированные numpy функции, например np.sum

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

from multiprocessing import Pool
import numpy as np

# для примера возьмем функцию суммы по строке, приведенную выше
def calculate_sum_column(df):
    df['sum_column'] = df.apply(lambda row: row['col1'] + row['col2'], axis=1)
    return df

# в данном примере расспараллеливаем на восемь потоков. Будьте аккуратны - при распараллеливании тратится больше оперативной памяти
def parallelize_dataframe(df, func):
    a,b,c,d,e,f,g,h = np.array_split(df, 8)
    pool = Pool(8)
    some_res = pool.map(func, [a,b,c,d,e,f,g,h])
    df = pd.concat(some_res)
    pool.close()
    pool.join()
    return df

# имитация большого датасета
df = pd.concat([df, df, df], ignore_index=True)

df = parallelize_dataframe(df, calculate_sum_column)
print(df.head(10))

С помощью данного гибкого «многоядерного» подхода можно многократно ускорить обход датафрейма и вычислить необходимую функцию