Быстрая разработка прототипа HTR системы на открытых данных / Хабр
В данной статье представлен способ максимально быстро получить результат используя Google Colab в качестве платформы для обучения модели HTR.
OCR задачи периодически упираются в распознавание рукописного текста, когда самая важна информация в документе написана именно человеком. На момент написания статьи популярные инструменты OCR, такие как FineReader, Tesseract и EasyOCR не имеют полноценного функционала, обеспечивающего решение задачи распознавания рукописного текста – в лучшем случае, его можно видеть в описании будущих обновлений.
Распознавание рукописного текста (handwritten text recognition, HTR) — это автоматический способ расшифровки записей с помощью компьютера. Оцифрованный вид рукописных записей позволило бы автоматизировать многие бизнес процессы, упростив работу человека. Отметим следующие сложности в разработке инструментов HTR, как общие, так и локальные кириллические:
— почерк каждого человека до некоторой степени уникален, более того, один и тот же человек может писать одну букву по-разному, с разными наклонами и т.
— проблема «русского курсива» известна, в том числе, зарубежом — например, фраза «лишишь лилии» написанная курсивом вводит в ступор многих;
— соединения между буквами очень трудно обработать;
— свободные датасеты рукописных букв для кириллицы трудно найти.
Однако, использование существующих инструментов (таких как нейросети) и свободных данных (датасетов) позволяет в кратчайшие сроки (пара часов, включая обучение модели) создать прототип инструмента HTR, обеспечивающий точность распознавания выше чем случайное предположение. К тому же, дальнейшее развитие прототипа может позволить создать действительно рабочий инструмент HTR с высокой точностью распознавания, который можно будет использовать в работе. В разработке использован подход, описанный в статье на Хабр. Суть подхода в следующем:
1. Разбить входное изображение на отдельные символы исходя из промежутков между буквами;
2. Классифицировать каждый отдельный символ предварительно обученной моделью.
Исходя из данного подхода, возникают ограничения на входные данные, а именно – рукописный текст должен быть написан буквами без соединений.
В данной статье представлен способ максимально быстро получить результат используя Google Colab в качестве платформы для обучения модели HTR.
Для создания прототипа нам понадобится датасет с рукописными символами, например, CoMNIST. Загрузим его, распакуем и удалим ненужные символы:
# скачаем и распакуем датасет CoMNIST ! wget https://github.com/GregVial/CoMNIST/raw/master/images/Cyrillic.zip ! unzip Cyrillic.zip # удалим папку с изображениями буквы "I" и "Ъ" (с последним бывают проблемы) ! rm -R Cyrillic/I # пример изображения в датасете from IPython.display import Image Image(filename=r'Cyrillic/Я/5a7747df79f11.png')
Также, нужна модель для классификации символов рукописного текста:
# Удалим неподходящие пакеты для отработки алгоритма !pip uninstall keras tensorflow h5py –y # Установим необходимые зависимости !pip install keras==2.2.5 tensorflow==1.14.0 h5py==2.10.0
Желательно после переустановки keras и tensorflow перезапустить среду. Данные виртуальной машины при этом останутся.
Далее, функции для обучения:
import os import cv2 import time from tqdm import tqdm from PIL import Image, ImageFilter, ImageOps import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg from sklearn.model_selection import train_test_split from tensorflow import keras from keras.models import Sequential from keras import optimizers from keras.layers import Convolution2D, MaxPooling2D, Dropout, Flatten, Dense, Reshape, LSTM, BatchNormalization from keras.optimizers import SGD, RMSprop, Adam from keras import backend as K from keras.constraints import maxnorm import tensorflow as tf def emnist_model(labels_num=None): model = Sequential() model.add(Convolution2D(filters=32, kernel_size=(3, 3), padding='valid', input_shape=(28, 28, 1), activation='relu')) model.add(Convolution2D(filters=64, kernel_size=(3, 3), activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(labels_num, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) return model def emnist_train(model, X_train, y_train_cat, X_test, y_test_cat): t_start = time.time() # Set a learning rate reduction learning_rate_reduction = keras.callbacks.ReduceLROnPlateau(monitor='val_acc', patience=3, verbose=1, factor=0.5, min_lr=0.00001) # Required for learning_rate_reduction: keras.backend.get_session().run(tf.global_variables_initializer()) model.fit(X_train, y_train_cat, validation_data=(X_test, y_test_cat), callbacks=[learning_rate_reduction], batch_size=64, epochs=9) print("Training done, dT:", time.time() - t_start)
Функции для работы с изображениями и аугментации:
def load_image_as_gray(path_to_image): img = Image.open(path_to_image) return np.array(img.convert("L")) def load_image(path_to_image): img = Image.open(path_to_image) return img def convert_rgba_to_rgb(pil_img): pil_img.load() background = Image.new("RGB", pil_img.size, (255, 255, 255)) background.paste(pil_img, mask = pil_img.split()[3]) return background def prepare_rgba_img(img_path): img = load_image(img_path) if np.array(img).shape[2] == 4: new_img = convert_rgba_to_rgb(img) return new_img return img # размытие изображений for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if l != ".ipynb_checkpoints": img = Image.open(f"Cyrillic/{lett}/"+l) blurImage = img.filter(ImageFilter.BoxBlur(15)) blurImage.save(f"Cyrillic/{lett}/"+"blur_"+l) # поворот изображений на +20 градусов for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if (l != ".ipynb_checkpoints") & ("blur_" not in l): img = Image.open(f"Cyrillic/{lett}/"+l) rotImage = img.
rotate(20) rotImage.save(f"Cyrillic/{lett}/"+"rot20_"+l) # поворот изображений на -20 градусов for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if (".ipynb_checkpoints" not in l) & ("rot20_" not in l) & ("blur_" not in l): img = Image.open(f"Cyrillic/{lett}/"+l) rotImage = img.rotate(-20) rotImage.save(f"Cyrillic/{lett}/"+"rot02_"+l) # изменение размера изображений до 28x28 for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if l != ".ipynb_checkpoints": img = Image.open(f"Cyrillic/{lett}/"+l) resized = img.resize((28, 28)) resized.save(f"Cyrillic/{lett}/"+l)
Следует отметить, что изображения в датасете CoMNIST представляют собой PNG RGBA, причём, полезная информация хранится только в альфа-канале, поэтому, необходимо «прокинуть» альфа-канал на RGB:
# преобразование изображений из RGBA в RGB for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if l != ".ipynb_checkpoints": rgb_img = prepare_rgba_img(f"Cyrillic/{lett}/"+l) rgb_img.save(f"Cyrillic/{lett}/"+l)
Предобработка:
y_num = {l:i+1 for i, l in enumerate(np.unique(y_train))} X_train = np.reshape(np.array(X_train), (np.array(X_train).shape[0], 28, 28, 1)) X_test = np.reshape(np.array(X_test), (np.array(X_test).shape[0], 28, 28, 1)) X_train = X_train.astype(np.float32) X_train /= 255.0 X_test = X_test.astype(np.float32) X_test /= 255.0 y_train_num = [y_num[i] for i in y_train] y_test_num = [y_num[i] for i in y_test] y_train_cat = keras.utils.to_categorical(np.array(y_train_num), 33) y_test_cat = keras.utils.to_categorical(np.array(y_test_num), 33)
Обучение:
model = emnist_model(len(y_num)+1) emnist_train(model, X_train, y_train_cat, X_test, y_test_cat)
У автора обучение в Google Colab на ВМ без TPU заняло ~40 минут.
Далее, можно протестировать модель на примерах текста, написанных вручную, например:
Рисунок 1 – изображение с рукописным текстомДалее, представлен код для разбития изображения на отдельные символы. Принцип его работы основывается на увеличении жирности отдельно стоящих (не связанных друг с другом) символов и нахождении их контуров:
# разбитие строки на отдельные буквы def letters_extract(image_file: str, out_size=28): img = cv2.imread(image_file) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) img_erode = cv2.erode(thresh, np.ones((3, 3), np.uint8), iterations=1) # Get contours contours, hierarchy = cv2.findContours(img_erode, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) output = img.copy() letters = [] for idx, contour in enumerate(contours): (x, y, w, h) = cv2.boundingRect(contour) if hierarchy[0][idx][3] == 0: cv2.rectangle(output, (x, y), (x + w, y + h), (70, 0, 0), 1) letter_crop = gray[y:y + h, x:x + w] size_max = max(w, h) letter_square = 255 * np.ones(shape=[size_max, size_max], dtype=np.uint8) if w > h: y_pos = size_max//2 - h//2 letter_square[y_pos:y_pos + h, 0:w] = letter_crop elif w < h: x_pos = size_max//2 - w//2 letter_square[0:h, x_pos:x_pos + w] = letter_crop else: letter_square = letter_crop letters.append((x, w, cv2.resize(letter_square, (out_size, out_size), interpolation=cv2.INTER_AREA))) # Sort array in place by X-coordinate letters.sort(key=lambda x: x[0], reverse=False) return letters
Демонстрация результата работы алгоритма разбиения изображения на символы:
import matplotlib.pyplot as plt %matplotlib inline lttrs = letters_extract("test.png", 28) plt.imshow(lttrs[0][2], cmap="gray")Рисунок 2 – первый символ после разбития изображения
Обученная модель будет возвращать цифры – порядковые номера классов, поэтому удобно будет, например, сделать словарь для облегчения интерпретации (здесь – y_num), однако, следует помнить, что буква «Ё», по непонятной причине, не входит в основной алфавит и идёт первой. Далее, код для вывода результата:
def get_lettr(ind, y_num): back_y = {v:k for k, v in y_num.items()} return back_y[ind] for i in range(len(lttrs)): img_arr = lttrs[i][2] img_arr = img_arr/255.0 input_img_arr = img_arr.reshape((1, 28, 28, 1)) result = model.predict_classes([input_img_arr]) print(get_lettr(result[0], y_num))
Результат работы для пары изображений:
Рисунок 3 – демонстрация результатов работы прототипа инструмента HTRНа рисунке 3 продемонстрированы результаты работы инструмента. Красным отмечены явные ошибки, жёлтым – проблемное место, о котором далее. Следует отметить, что точность распознавания удовлетворительная, однако, из-за особенностей алгоритма разбития изображения буква «Ы» представлена как два отдельных символа «Ь» и «I» и именно в таком виде подаётся модели классификации. Также, нет распознавания пробелов между словами.
Таким образом, за минимальное время удалось получить прототип инструмента HTR, обеспечивающий удовлетворительное качество распознавания. Стоит добавить, что данный прототип можно сильно улучшить, например:
— сменить/расширить датасет букв используя шрифты с сервиса handwritter.ru;
— использовать методы аугментации и распознавания, описанные в статье «Первое место на AI Journey 2020 Digital Петр», в том числе, метод разбития на отдельные символы строк текста, написанных с соединениями;
— разработать и подключить алгоритм коррекции орфографических ошибок;
— использовать скрытую марковскую модель для предсказания следующего символа/слова;
— обучить модель на распознавание отдельных слов/фраз, которые можно так же сгенерировать с помощью шрифтов Handwriter.
Быстрая разработка прототипа HTR системы на открытых данных
Время прочтения: 8 мин.
OCR задачи периодически упираются в распознавание рукописного текста, когда самая важна информация в документе написана именно человеком. На момент написания статьи популярные инструменты OCR, такие как FineReader, Tesseract и EasyOCR не имеют полноценного функционала, обеспечивающего решение задачи распознавания рукописного текста – в лучшем случае, его можно видеть в описании будущих обновлений.
Распознавание рукописного текста (handwritten text recognition, HTR) — это автоматический способ расшифровки записей с помощью компьютера. Оцифрованный вид рукописных записей позволило бы автоматизировать многие бизнес процессы, упростив работу человека. Отметим следующие сложности в разработке инструментов HTR, как общие, так и локальные кириллические:
- почерк каждого человека до некоторой степени уникален, более того, один и тот же человек может писать одну букву по-разному, с разными наклонами и т.
д.;
- проблема «русского курсива» известна, в том числе, зарубежом — например, фраза «лишишь лилии» написанная курсивом вводит в ступор многих;
- соединения между буквами очень трудно обработать;
- свободные датасеты рукописных букв для кириллицы трудно найти.
Однако, использование существующих инструментов (таких как нейросети) и свободных данных (датасетов) позволяет в кратчайшие сроки (пара часов, включая обучение модели) создать прототип инструмента HTR, обеспечивающий точность распознавания выше чем случайное предположение. К тому же, дальнейшее развитие прототипа может позволить создать действительно рабочий инструмент HTR с высокой точностью распознавания, который можно будет использовать в работе. В разработке использован подход, описанный в статье на Хабр. Суть подхода в следующем:
- Разбить входное изображение на отдельные символы исходя из промежутков между буквами;
- Классифицировать каждый отдельный символ предварительно обученной моделью.
Исходя из данного подхода, возникают ограничения на входные данные, а именно – рукописный текст должен быть написан буквами без соединений.
В данной статье представлен способ максимально быстро получить результат используя Google Colab в качестве платформы для обучения модели HTR. Для создания прототипа нам понадобится датасет с рукописными символами, например, CoMNIST. Загрузим его, распакуем и удалим ненужные символы:
# скачаем и распакуем датасет CoMNIST ! wget https://github.com/GregVial/CoMNIST/raw/master/images/Cyrillic.zip ! unzip Cyrillic.zip # удалим папку с изображениями буквы "I" и "Ъ" (с последним бывают проблемы) ! rm -R Cyrillic/I # пример изображения в датасете from IPython.display import Image Image(filename=r'Cyrillic/Я/5a7747df79f11.png')
Также, нужна модель для классификации символов рукописного текста:
# Удалим неподходящие пакеты для отработки алгоритма !pip uninstall keras tensorflow h5py –y # Установим необходимые зависимости !pip install keras==2.2.5 tensorflow==1.14.0 h5py==2.10.0
Желательно после переустановки keras и tensorflow перезапустить среду. Данные виртуальной машины при этом останутся.
Далее, функции для обучения:
import os import cv2 import time from tqdm import tqdm from PIL import Image, ImageFilter, ImageOps import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg from sklearn.model_selection import train_test_split from tensorflow import keras from keras.models import Sequential from keras import optimizers from keras.layers import Convolution2D, MaxPooling2D, Dropout, Flatten, Dense, Reshape, LSTM, BatchNormalization from keras.optimizers import SGD, RMSprop, Adam from keras import backend as K from keras.constraints import maxnorm import tensorflow as tf def emnist_model(labels_num=None): model = Sequential() model.add(Convolution2D(filters=32, kernel_size=(3, 3), padding='valid', input_shape=(28, 28, 1), activation='relu')) model.add(Convolution2D(filters=64, kernel_size=(3, 3), activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(labels_num, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) return model def emnist_train(model, X_train, y_train_cat, X_test, y_test_cat): t_start = time.time() # Set a learning rate reduction learning_rate_reduction = keras.callbacks.ReduceLROnPlateau(monitor='val_acc', patience=3, verbose=1, factor=0.5, min_lr=0.00001) # Required for learning_rate_reduction: keras.backend.get_session().run(tf.global_variables_initializer()) model.fit(X_train, y_train_cat, validation_data=(X_test, y_test_cat), callbacks=[learning_rate_reduction], batch_size=64, epochs=9) print("Training done, dT:", time.time() - t_start)
Функции для работы с изображениями и аугментации:
def load_image_as_gray(path_to_image): img = Image.open(path_to_image) return np.array(img.convert("L")) def load_image(path_to_image): img = Image.open(path_to_image) return img def convert_rgba_to_rgb(pil_img): pil_img.load() background = Image.new("RGB", pil_img.size, (255, 255, 255)) background.paste(pil_img, mask = pil_img.split()[3]) return background def prepare_rgba_img(img_path): img = load_image(img_path) if np.array(img).shape[2] == 4: new_img = convert_rgba_to_rgb(img) return new_img return img # размытие изображений for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if l != ".ipynb_checkpoints": img = Image.open(f"Cyrillic/{lett}/"+l) blurImage = img.filter(ImageFilter.BoxBlur(15)) blurImage.save(f"Cyrillic/{lett}/"+"blur_"+l) # поворот изображений на +20 градусов for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if (l != ".ipynb_checkpoints") & ("blur_" not in l): img = Image.open(f"Cyrillic/{lett}/"+l) rotImage = img.
rotate(20) rotImage.save(f"Cyrillic/{lett}/"+"rot20_"+l) # поворот изображений на -20 градусов for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if (".ipynb_checkpoints" not in l) & ("rot20_" not in l) & ("blur_" not in l): img = Image.open(f"Cyrillic/{lett}/"+l) rotImage = img.rotate(-20) rotImage.save(f"Cyrillic/{lett}/"+"rot02_"+l) # изменение размера изображений до 28x28 for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if l != ".ipynb_checkpoints": img = Image.open(f"Cyrillic/{lett}/"+l) resized = img.resize((28, 28)) resized.save(f"Cyrillic/{lett}/"+l)
Следует отметить, что изображения в датасете CoMNIST представляют собой PNG RGBA, причём, полезная информация хранится только в альфа-канале, поэтому, необходимо «прокинуть» альфа-канал на RGB:
# преобразование изображений из RGBA в RGB for lett in os.listdir("Cyrillic/"): for l in os.listdir(f"Cyrillic/{lett}"): if l != ".ipynb_checkpoints": rgb_img = prepare_rgba_img(f"Cyrillic/{lett}/"+l) rgb_img.save(f"Cyrillic/{lett}/"+l)
# загрузка и разбитие на обучающую и тестовую выборки X_train = [] X_test = [] y_train = [] y_test = [] for lett in tqdm(os.listdir("Cyrillic/")): data_count = len(os.listdir(f"Cyrillic/{lett}")) train_part = int(data_count*0.7) for c, l in enumerate(os.listdir(f"Cyrillic/{lett}")): if l != ".ipynb_checkpoints": if c+1 < train_part: X_train.append(load_image_as_gray(f"Cyrillic/{lett}/"+l)) y_train.append(lett) else: X_test.append(load_image_as_gray(f"Cyrillic/{lett}/"+l)) y_test.append(lett)
Предобработка:
y_num = {l:i+1 for i, l in enumerate(np.unique(y_train))} X_train = np.reshape(np.array(X_train), (np.array(X_train).shape[0], 28, 28, 1)) X_test = np.reshape(np.array(X_test), (np.array(X_test).shape[0], 28, 28, 1)) X_train = X_train.astype(np.float32) X_train /= 255.0 X_test = X_test.astype(np.float32) X_test /= 255.0 y_train_num = [y_num[i] for i in y_train] y_test_num = [y_num[i] for i in y_test] y_train_cat = keras.utils.to_categorical(np.array(y_train_num), 33) y_test_cat = keras.utils.to_categorical(np.array(y_test_num), 33)
Обучение:
model = emnist_model(len(y_num)+1) emnist_train(model, X_train, y_train_cat, X_test, y_test_cat)
У автора обучение в Google Colab на ВМ без TPU заняло ~40 минут. Далее, можно протестировать модель на примерах текста, написанных вручную, например:
Рисунок 1 – изображение с рукописным текстом
Далее, представлен код для разбития изображения на отдельные символы. Принцип его работы основывается на увеличении жирности отдельно стоящих (не связанных друг с другом) символов и нахождении их контуров:
# разбитие строки на отдельные буквы def letters_extract(image_file: str, out_size=28): img = cv2.imread(image_file) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) img_erode = cv2.erode(thresh, np.ones((3, 3), np.uint8), iterations=1) # Get contours contours, hierarchy = cv2.findContours(img_erode, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) output = img.copy() letters = [] for idx, contour in enumerate(contours): (x, y, w, h) = cv2.boundingRect(contour) if hierarchy[0][idx][3] == 0: cv2.rectangle(output, (x, y), (x + w, y + h), (70, 0, 0), 1) letter_crop = gray[y:y + h, x:x + w] size_max = max(w, h) letter_square = 255 * np.ones(shape=[size_max, size_max], dtype=np.uint8) if w > h: y_pos = size_max//2 - h//2 letter_square[y_pos:y_pos + h, 0:w] = letter_crop elif w < h: x_pos = size_max//2 - w//2 letter_square[0:h, x_pos:x_pos + w] = letter_crop else: letter_square = letter_crop letters.
append((x, w, cv2.resize(letter_square, (out_size, out_size), interpolation=cv2.INTER_AREA))) # Sort array in place by X-coordinate letters.sort(key=lambda x: x[0], reverse=False) return letters
Демонстрация результата работы алгоритма разбиения изображения на символы:
import matplotlib.pyplot as plt %matplotlib inline lttrs = letters_extract("test.png", 28) plt.imshow(lttrs[0][2], cmap="gray")
Рисунок 2 – первый символ после разбития изображения
Обученная модель будет возвращать цифры – порядковые номера классов, поэтому удобно будет, например, сделать словарь для облегчения интерпретации (здесь – y_num), однако, следует помнить, что буква «Ё», по непонятной причине, не входит в основной алфавит и идёт первой. Далее, код для вывода результата:
def get_lettr(ind, y_num): back_y = {v:k for k, v in y_num.items()} return back_y[ind] for i in range(len(lttrs)): img_arr = lttrs[i][2] img_arr = img_arr/255.0 input_img_arr = img_arr.reshape((1, 28, 28, 1)) result = model.predict_classes([input_img_arr]) print(get_lettr(result[0], y_num))
Результат работы для пары изображений:
Рисунок 3 – демонстрация результатов работы прототипа инструмента HTR
На рисунке 3 продемонстрированы результаты работы инструмента. Красным отмечены явные ошибки, жёлтым – проблемное место, о котором далее. Следует отметить, что точность распознавания удовлетворительная, однако, из-за особенностей алгоритма разбития изображения буква «Ы» представлена как два отдельных символа «Ь» и «I» и именно в таком виде подаётся модели классификации. Также, нет распознавания пробелов между словами. Таким образом, за минимальное время удалось получить прототип инструмента HTR, обеспечивающий удовлетворительное качество распознавания. Стоит добавить, что данный прототип можно сильно улучшить, например:
- сменить/расширить датасет букв используя шрифты с сервиса handwritter.ru;
- использовать методы аугментации и распознавания, описанные в статье «Первое место на AI Journey 2020 Digital Петр», в том числе, метод разбития на отдельные символы строк текста, написанных с соединениями;
- разработать и подключить алгоритм коррекции орфографических ошибок;
- использовать скрытую марковскую модель для предсказания следующего символа/слова;
- обучить модель на распознавание отдельных слов/фраз, которые можно так же сгенерировать с помощью шрифтов Handwriter.
Cyrillic Cursive — Etsy UK
Etsy больше не поддерживает старые версии вашего веб-браузера, чтобы обеспечить безопасность пользовательских данных. Пожалуйста, обновите до последней версии.
Воспользуйтесь всеми преимуществами нашего сайта, включив JavaScript.
( 31 соответствующий результат, с рекламой Продавцы, желающие расширить свой бизнес и привлечь больше заинтересованных покупателей, могут использовать рекламную платформу Etsy для продвижения своих товаров. Вы увидите результаты объявлений, основанные на таких факторах, как релевантность и сумма, которую продавцы платят за клик. Узнать больше. )Общие вопросы
Russian Cursive — Etsy Turkey
Etsy больше не поддерживает старые версии вашего веб-браузера, чтобы обеспечить безопасность пользовательских данных.