Привет, ребята, когда-нибудь задумывались, как Google, Instagram и т. д. читают ваши тексты с ваших изображений? Вы когда-нибудь задумывались, как компьютер понимает, что написано на изображении, и показывает вам результат? Ответом на этот вопрос является термин под названием Оптическое распознавание символов или OCR.
Оптическое распознавание символов. Оптическое распознавание символов или оптическое считывание символов представляет собой электронное или механическое преобразование изображений печатного, рукописного или печатного текста в машинно-кодированный текст, будь то отсканированный документ, фотография документа. , фото сцены или из текста субтитров, наложенного на изображение.
Прочитав эту статью, вы сможете создать свой собственный OCR буквально с нуля.
Мы будем снабжать изображение некоторым текстом, и наш скрипт сможет дать нам предсказание текстов, которые он находит.
Предпосылки-
- Базовые знания Python и библиотек (Numpy, pandas, skimage)
- Базовые знания о глубоком обучении
Примечание. Я не буду предоставлять руководство по настройке кода, однако в конце будет предоставлен полный исходный код. Цель этого руководства — дать вам идею или алгоритм разработки OCR, а не научить программировать.
результаты-
Входное изображение-
Ограничивающие рамки для отображения того, как обнаруживаются символы.
Извлеченный текст-
Итак, начнем..
БЛОК-СХЕМА
Вот блок-схема для справки, если вы заблудились в шагах ниже.
Начнем с набора данных,
НАБОР ДАННЫХ
Таким образом, используемый набор данных — это набор данных Chars74k с 64 классами (0–9, AZ, az). Для простоты я выбрал сгенерированный компьютером набор данных символов, который содержит 62992 синтезированных символа из компьютерных шрифтов. Вот ссылка.
Предварительно обработайте набор данных и обучите нашу модель глубокого обучения
Поскольку изображения в наборе данных уже являются двоичными (т. е. значения в пикселях либо 0-черные, либо 255-белые), мы не конвертируем их в двоичные. Итак, при предварительной обработке мы возьмем каждое изображение, добавим к нему немного белого поля и, наконец, изменим их размер до размера 32x32 пикселя, чтобы сохранить сходство, чтобы модель могла лучше обобщать.
Примечание. Кроме того, во время обучения я не использовал для обучения графический процессор, поэтому я тренировал модель только с 10–15 образцами для каждого класса. Это причина, по которой у меня не самая лучшая точность, и поэтому я также не разделял набор данных для обучения поезду и использовал весь набор данных для обучения.
Итак, начнем.
1- Импорт
import numpy as np from skimage import io import os from PIL import Image import tensorflow from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D from tensorflow.keras.layers import MaxPooling2D from tensorflow.keras.layers import Flatten from tensorflow.keras.layers import Dense, Dropout from tensorflow.keras.utils import to_categorical
2- Предварительная обработка и подготовка набора данных
Эта функция добавляет белое поле
def add_margin(pil_img, top, right, bottom, left, color): width, height = pil_img.size new_width = width + right + left new_height = height + top + bottom result = Image.new(pil_img.mode, (new_width, new_height), color) result.paste(pil_img, (left, top)) return result
Список с именем набор данных будет содержать значения пикселей всех изображений в наборе данных, а список с именем ярлыки будет иметь подсчет чисел, где каждое число соответствует определенному классу. Также мы добавим немного поля и изменим размер всех изображений набора данных до размера 32x32.
dataset=[] labels=[] count=0 folders=os.listdir(r”S:/Directory/English/Fnt/”) for i in folders: for j in os.listdir(r”S:/Directory/English/Fnt/”+str(i)): im_new= add_margin(Image.open(r”S:/Directory/English/Fnt/”+str(i)+’/’+str(j)), 10, 10,10, 10, (255)) resized_image = im_new.resize((32,32)) dataset.append(np.array([ np.asarray(resized_image)/255.0 ] )) labels.append(count) count+=1
Наконец, измените набор данных и список меток в соответствии с требованиями модели глубокого обучения.
dataset = np.array(dataset).reshape('total number of samples',32,32,1) labels = np.array(labels).reshape(-1,1)
Преобразуем метки в OneHotEncoding (Подробнее об этом)
from sklearn.preprocessing import OneHotEncoder type_encoder = OneHotEncoder() labels=type_encoder.fit_transform(labels).toarray()
3- Построить модель
Входные данные для модели имеют размер 32x32, так как мы подаем изображения размером 32x32, а выходной слой имеет размер 62, потому что у нас есть данные в 62 классах.
model = Sequential() # Convolution model.add(Conv2D(32, (3, 3), input_shape = (32,32,1), activation = 'relu')) # Pooling model.add(MaxPooling2D(pool_size = (2, 2))) # Convolution model.add(Conv2D(32, (3, 3), activation = 'relu')) # Flattening model.add(Flatten()) # Full connection #model.add(Dense(units = 512, activation = 'relu')) #model.add(Dropout(0.5)) model.add(Dense(units = 256, activation = 'relu')) # Add Dropout to prevent overfitting model.add(Dropout(0.5)) model.add(Dense(units = 128, activation = 'relu')) model.add(Dense(units = 62, activation = 'softmax')) # Compiling the CNN model.compile(optimizer = 'adam', loss = 'categorical_crossentropy', metrics = ['accuracy']) model.summary()
4- Обучите и сохраните модель для дальнейшего использования
model.fit(dataset , labels, batch_size = 4, epochs = 100) model.save(‘Model.h5’)
После этого вы найдете файл Model.h5 в каталоге.
УРА!!! мы сделали часть машинного обучения!
Пользовательский ввод и прогноз
(Здесь я просто показываю фрагменты, не волнуйтесь, я предоставлю весь код)
1- Преобразование любого цветного изображения в черно-белое (примечание: входное изображение должно состоять только из двух цветов).
Мы возьмем значения пикселей края и преобразуем все пиксели, соответствующие этому значению, в белые (т.е. 255), а значение пикселя, отличное от пикселя края, в черный (т.е. 0), чтобы получить наш стандартный вывод.
logo=Image.open(UPLOAD_FOLDER+'/'+patho)# input image location logo=ImageOps.grayscale(logo) # rgb to grayscale conversion logo=np.asarray(logo) #pil image to array # color to black and white a=logo.copy() for i in range(len(logo)): for j in range(len(logo[0,:])): if logo[i][j]==logo[0][0]: a[i][j]=255 else: a[i][j]=0 # variable 'a' is the output
2- Преобразование изображения в градациях серого в двоичное
Примечание. Существуют более сложные методы для реализации этого
def black_and_white(a):# takes np array image m=a.copy() for i in range(len(m)): for j in range(len(m[0])): if m[i][j] >200: m[i][j]=255 else: m[i][j]=0 return m
3- Обнаружение текста
Мы будем проходить изображение строка за строкой по горизонтальной оси и контрольной точке всякий раз, когда линия переключается со всех белых пикселей на строку, содержащую несколько черных пикселей вместе с белыми, и наоборот. Этот алгоритм помогает нам определять координаты строк текста.
Мы получим выходные данные в виде 4 координатных точек прямоугольника.
coords=[] xycoords=[] def line_coords(coords): xmin=coords[0][0][0] xmax=coords[-1][0][0] ymin=20000 ymax=0 for i in coords: for j in i: if j[1] >ymax: ymax=j[1] if j[1] < ymin: ymin=j[1] xycoords.append([xmin,xmax+2,ymin+1,ymax]) for i in range(len(logo[1:-1,1:-1])): coo=[] flag=0 for c in logo[1:-1,1:-1][i]: if c<200: flag=1 if flag==1: for b in range(len(logo[1:-1,1:-1][i])): if logo[1:-1,1:-1][i][b]>200: try: if logo[1:-1,1:-1][i][b+1] <200: coo.append([i,b+1]) except: pass if logo[1:-1,1:-1][i][b]<200: try: if logo[1:-1,1:-1][i][b+1] > 200: coo.append([i,b]) except: pass else: if len(coords)>0: line_coords(coords) coords=[] if len(coo)>0: coords.append(coo)
Список xycoords содержит список координат обнаруженных текстов.
Eg-
4- Обнаружение пробелов и линий
Идея обнаружения пространства заключается в том, что мы просматриваем указанные выше вырезы координат (на этот раз по вертикали), подсчитываем количество непрерывных вертикальных белых линий и передаем этот массив для кластеризации k-средних. Что создает 2 кластера и делает массив эквивалентным количеству символов, например [0,1,0,0,0,1], чтобы мы могли понять, что во 2-й позиции есть пробел. И после каждой строки мы можем добавить 2 в приведенный выше массив, чтобы у нас была контрольная точка для разделителей строк, например, для текста — «Я буду любить тебя/nForever» будет иметь кодировку [0,1, 0,0,0,0,1,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0].
import ckwrap # library for k means spaces=np.array([0]) ctr=0 for y in xycoords: sp=[] a=black_and_white(logo[y[0]:y[1],y[2]:y[3]]) for i in range(len(a[0,:])): f=0 for j in a[:,i]: if j==0: f=1 if f!=1: ctr+=1 if f==1: sp.append(ctr) ctr=0 nums= np.array([jj for jj in sp if jj!=0]) if len(nums)==0: spaces=np.concatenate((spaces,np.array([2])), axis=None) else: print('nums are - '+str(nums)) #print(nums) km = ckwrap.ckmeans(nums,2) print('labs are - '+str(km.labels)) print('final are - '+str(finalXY)) spaces=np.concatenate((spaces,km.labels,np.array([2])), axis=None)
5- Дамп фрагментов символов из обнаруженных строк
Теперь мы обрежем изображения из обнаруженных строк в предыдущем разделе и пройдемся по пиксельным линиям по вертикали, чтобы найти непосредственную строку со всеми пикселями, равными белому (255).
Я сделал функцию, которая возвращает значение флага, содержит ли вертикальная линия черный пиксель или нет. И при обходе вертикальных линий по одной, если флаг меняется, мы считаем это концом символа на изображении.
Как вы можете видеть на изображении, красные линии — это начальные и конечные X-координаты символов, поэтому теперь мы можем дополнительно обрезать эти фрагменты и сохранить их в папке с соглашением об именах как целое число от 0 до количество символов, чтобы сохранить порядок.
col=[] # dump pieces of characters count=0 for hoe in finalXY: newC=[0] def flagCalc(i): flag = 1 jo=0 for j in range(len(logo[hoe[0]:hoe[1],hoe[2]:hoe[3]][:,i])): if logo[hoe[0]:hoe[1],hoe[2]:hoe[3]][:,i][j]<150: flag=0 jo=j #print(flag) return flag for i in range(len(logo[hoe[0]:hoe[1],hoe[2]:hoe[3]][0,:])): try: if flagCalc(i)<flagCalc(i+1): newC.append(i+1) except: pass newC.append(hoe[3]) col.append(newC) for i in range(len(newC)-1): A=black_and_white(logo[hoe[0]:hoe[1],hoe[2]:hoe[3]][:,newC[i]:newC[i+1]]) im = Image.fromarray(A) im.save("dump/"+str(count)+".png") count+=1
6- Предварительная обработка частей персонажей перед подачей модели
Теперь у нас есть фрагменты символов, но одно замечание, они не соответствуют изображению. Для большей ясности возьмите этот фрагмент из предыдущего примера:
Здесь символ «t» находится в углу изображения и занимает очень мало места на изображении, а символ «h» находится посередине и занимает больше места. Кроме того, соотношение сторон двух обрезанных фрагментов различается. что затрудняет прогнозирование модели, поскольку, как вы помните, модель обучается на изображениях, которые находятся в центре белого квадрата размером 32x32. Итак, наша следующая задача — сделать эти вырезы такими, чтобы они выглядели близко к нашим изображениям набора данных.
Шаги-
После удаления границ символ «t» будет выглядеть примерно так:
Который будет вставлен в центр белого изображения 32x32, и будут добавлены те же поля, что и в наборе данных.
def borderRemoval(path): a = io.imread(path) #print(a) for i in range(len(a)): for j in range(len(a[0])): if a[i][j] >200: a[i][j]=255 else: a[i][j]=0 def flagCalc(i): flag = 0 for j in range(len(i)): if i[j]==0: flag=1 return flag y1=0 y2=a.shape[0] x1=0 x2=a.shape[1] for i in range(len(a)-1): #print(flagCalc(a[i])) if flagCalc(a[i])<flagCalc(a[i+1]): y2=a.shape[0] if (i+1)< y2: y1=i+1 elif flagCalc(a[i])>flagCalc(a[i+1]): if (i-1)>y1: y2=i-1 for i in range(len(a[0,:])-1): #print(flagCalc(a[i])) if flagCalc(a[:,i])<flagCalc(a[:,i+1]): if (i+1)< x2: x1=i+1 elif flagCalc(a[:,i])>flagCalc(a[:,i+1]): if (i-1)>x1: x2=i-1 im = Image.fromarray(a[y1:y2,x1:x2]) # print(y1,y2,x1,x2) im.save(path) def PasteImage(path): a = io.imread(path) if a.shape[0] > a.shape[1] : f=28/a.shape[0] else: f=28/a.shape[1] b=Image.fromarray(a,mode='L').resize(( int(a.shape[1]*f),int(a.shape[0]*f)),Image.BICUBIC) c=Image.fromarray(np.full((32, 32), 255).astype('uint8'),mode='L') img_w, img_h = b.size bg_w, bg_h = c.size offset = ((bg_w - img_w) // 2, (bg_h - img_h) // 2) c.paste(b, offset) c.save(path) for mm in os.listdir(r"dump/"): #print(r"dump/"+str(mm)) borderRemoval(r"dump/"+str(mm)) PasteImage(r"dump/"+str(mm)) black_and_white(r"dump/"+str(mm)) # we had implemented this function above
Это сделает предварительную обработку всех обрезанных изображений персонажей и сохранит их там.
ПРИМЕЧАНИЕ. Мы добавим маржу при прогнозировании, чтобы сохранить расчет конверсии.
7- Создание этикетки
Поскольку наша модель будет выводить метку OneHotEncoded, которая будет выглядеть как [0,1,0,……] для предсказания класса в местоположении 2. Использование функции Argmax может вывести нам местоположение самого высокого значения в списке, в нашем случае это будет вывод '1' (индекс списка начинается с 0). Теперь мы знаем, что существует всего 62 класса, поэтому мы можем создавать преобразования меток вручную.
labs={0: 0,1: 1, 2: 2, 3: 3,4: 4,5: 5,6: 6,7: 7,8: 8,9: 9, 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F', 16: 'G', 17: 'H', 18: 'I', 19: 'J',20: 'K', 21: 'L',22: 'M', 23: 'N', 24: 'O', 25: 'P',26: 'Q', 27: 'R', 28: 'S', 29: 'T', 30: 'U',31: 'V', 32: 'W',33: 'X', 34: 'Y',35: 'Z',36: 'a',37: 'b',38: 'c',39: 'd',40: 'e',41: 'f',42: 'g',43: 'h',44: 'i',45: 'j',46: 'k',47: 'l',48: 'm', 49: 'n',50: 'o',51: 'p', 52: 'q', 53: 'r', 54: 's', 55: 't', 56: 'u', 57: 'v', 58: 'w', 59: 'x', 60: 'y', 61: 'z'}
8- Прогноз модели
Переменная «STRINGS» — это наша основная переменная, в которую мы собираемся добавить наши прогнозы, разделители строк и пробелы. Итак, на этом шаге мы пройдемся по нашему списку именованных пробелов, чтобы проверить, есть ли пробел, разрыв строки или символ. Если есть символ, мы перейдем к нашим выгруженным изображениям и преобразуем их в массив, добавим те же поля, которые мы добавили в наш набор данных, и пропустим их через нашу модель. Он выведет значение OneHotEncoded, которое будет преобразовано в номер местоположения с помощью функции Argmax, которая в конечном итоге сопоставляется со словарем с именем «labs», который мы создали вручную на предыдущем шаге, чтобы получить прогнозируемую строку, которая добавляется к переменной «STRING». и то же самое относится к пробелам (‘ ‘) и разделителю строки (‘‹br›’ или ‘\n’). И после того, как предсказание сделано, мы очищаем все сброшенные изображения из папки.
def add_margin(pil_img, top, right, bottom, left, color): width, height = pil_img.size new_width = width + right + left new_height = height + top + bottom result = Image.new(pil_img.mode, (new_width, new_height), color) result.paste(pil_img, (left, top)) return result co=-1 STRING=’’ charPred=[] while True: co+=1 try: if spaces[co]==1: STRING=STRING+' ' elif spaces[co]==2: STRING=STRING+'<br>' image = Image.open('dump/'+str(co)+'.png') #print(co) im_new = add_margin(image, 10, 10,10, 10, (255)) resized_image = im_new.resize((32,32)) a=np.asarray(resized_image)/255 hehe=labs[np.argmax(model.predict([ a.reshape(32,32,1).tolist()]))] charPred.append(hehe) #print(hehe) os.remove('dump/'+str(co)+'.png') #image.save("imgo/"+str(co)+'_'+str(hehe)+ ".png") #Image.fromarray((a*255).astype('uint8'), mode='L').save("imgi/"+str(co)+'_'+str(hehe)+ ".png") #model.predict([a.tolist()]) STRING+=str(hehe) except: break
Переменная STRING будет иметь требуемый результат.
Полный исходный код с реализацией веб-приложения Flask.
YAY мы завершили его !!!