Привет, ребята, когда-нибудь задумывались, как 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 мы завершили его !!!

Мой LinkedIn и Twitter