Expérimentations avec Tesseract OCR
Requis
- Tesseract OCR avec Pillow
- Python 3.8
- Docker
- AWS Lambda Layers
- Tesseract
- Python
- AWS Lambda avec SAM
Création de la lambda et layer
J’utilise AWS SAM,
layers/
lambda-tesseract-api/
lambda/
src/handlers/index.py
README.md
requirements.txt
template.yml
Layers
J’ai cloné le projet https://github.com/amtam0/lambda-tesseract-api
comme base, puis modifié quelques trucs pour que le tout fonctionne.
Le lambda-tesseract-api/buildpy/requirements.txt
Voir le requirements.txt
NoteShrinker==0.2.0
Pillow==8.3.1
PyWavelets==1.1.1
cycler==0.10.0
deskew==0.10.33
imageio==2.9.0
kiwisolver==1.3.1
matplotlib==3.4.2
networkx==2.6.2
numpy==1.21.1
opencv-python==4.5.3.56
pyparsing==2.4.7
pytesseract==0.3.8
python-dateutil==2.8.2
scikit-image==0.18.2
scipy==1.7.0
six==1.16.0
tifffile==2021.7.30
Le lambda-tesseract-api/buildtesseract/Dockerfile-tess4
Voir le Dockerfile
FROM lambci/lambda-base:build
#FROM lambci/lambda:build-python3.7
#Proxy setup if exists
#ENV http_proxy 'http://ip:port'
#ENV https_proxy 'https://ip:port'
ARG LEPTONICA_VERSION=1.78.0
ARG TESSERACT_VERSION=4.1.0-rc4
ARG AUTOCONF_ARCHIVE_VERSION=2017.09.28
ARG TMP_BUILD=/tmp
ARG TESSERACT=/opt/tesseract
ARG LEPTONICA=/opt/leptonica
ARG DIST=/opt/build-dist
# change OCR_LANG to enable the layer for different languages
ARG OCR_LANG=fra
# change TESSERACT_DATA_SUFFIX to use different datafiles (options: "_best", "_fast" and "")
ARG TESSERACT_DATA_SUFFIX=_best
ARG TESSERACT_DATA_VERSION=4.0.0
RUN yum makecache fast; yum clean all && yum -y update && yum -y upgrade; yum clean all && \
yum install -y yum-plugin-ovl; yum clean all && yum -y groupinstall "Development Tools"; yum clean all
RUN yum -y install gcc gcc-c++ make autoconf aclocal automake libtool \
libjpeg-devel libpng-devel libtiff-devel zlib-devel \
libzip-devel freetype-devel lcms2-devel libwebp-devel \
libicu-devel tcl-devel tk-devel pango-devel cairo-devel; yum clean all
WORKDIR ${TMP_BUILD}/leptonica-build
RUN curl -L https://github.com/DanBloomberg/leptonica/releases/download/${LEPTONICA_VERSION}/leptonica-${LEPTONICA_VERSION}.tar.gz | tar xz && cd ${TMP_BUILD}/leptonica-build/leptonica-${LEPTONICA_VERSION} && \
./configure --prefix=${LEPTONICA} && make && make install && cp -r ./src/.libs /opt/liblept
RUN echo "/opt/leptonica/lib" > /etc/ld.so.conf.d/leptonica.conf && ldconfig
WORKDIR ${TMP_BUILD}/autoconf-build
RUN curl https://ftp.gnu.org/gnu/autoconf-archive/autoconf-archive-${AUTOCONF_ARCHIVE_VERSION}.tar.xz | tar xJ && \
cd autoconf-archive-${AUTOCONF_ARCHIVE_VERSION} && ./configure && make && make install && cp ./m4/* /usr/share/aclocal/
WORKDIR ${TMP_BUILD}/tesseract-build
RUN curl -L https://github.com/tesseract-ocr/tesseract/archive/${TESSERACT_VERSION}.tar.gz | tar xz && \
cd tesseract-${TESSERACT_VERSION} && ./autogen.sh && PKG_CONFIG_PATH=/opt/leptonica/lib/pkgconfig LIBLEPT_HEADERSDIR=/opt/leptonica/include \
./configure --prefix=${TESSERACT} --with-extra-includes=/opt/leptonica/include --with-extra-libraries=/opt/leptonica/lib && make && make install
WORKDIR /opt
RUN mkdir -p ${DIST}/lib && mkdir -p ${DIST}/bin && \
cp ${TESSERACT}/bin/tesseract ${DIST}/bin/ && \
cp ${TESSERACT}/lib/libtesseract.so.4 ${DIST}/lib/ && \
cp ${LEPTONICA}/lib/liblept.so.5 ${DIST}/lib/liblept.so.5 && \
cp /usr/lib64/libwebp.so.4 ${DIST}/lib/ && \
cp /usr/lib64/libpng12.so.0 ${DIST}/lib/ && \
cp /usr/lib64/libjpeg.so.62 ${DIST}/lib/ && \
cp /usr/lib64/libtiff.so.5 ${DIST}/lib/ && \
cp /usr/lib64/libgomp.so.1 ${DIST}/lib/ && \
cp /usr/lib64/libjbig.so.2.0 ${DIST}/lib/ && \
cp /usr/lib64/libGL.so.1 ${DIST}/lib/ && \
cp /usr/lib64/libgthread-2.0.so.0 ${DIST}/lib/ && \
cp /usr/lib64/libglib-2.0.so.0 ${DIST}/lib/ && \
cp /usr/lib64/libGLX.so.0 ${DIST}/lib/ && \
cp /usr/lib64/libX11.so.6 ${DIST}/lib/ && \
cp /usr/lib64/libXext.so.6 ${DIST}/lib/ && \
cp /usr/lib64/libxcb.so.1 ${DIST}/lib/ && \
cp /usr/lib64/libGLdispatch.so.0 ${DIST}/lib/ && \
cp /usr/lib64/libXau.so.6 ${DIST}/lib/ && \
echo -e "LEPTONICA_VERSION=${LEPTONICA_VERSION}\nTESSERACT_VERSION=${TESSERACT_VERSION}\nTESSERACT_DATA_FILES=tessdata${TESSERACT_DATA_SUFFIX}/${TESSERACT_DATA_VERSION}" > ${DIST}/TESSERACT-README.md && \
find ${DIST}/lib -name '*.so*' | xargs strip -s
WORKDIR ${DIST}/tesseract/share/tessdata
RUN curl -L https://github.com/tesseract-ocr/tessdata${TESSERACT_DATA_SUFFIX}/raw/${TESSERACT_DATA_VERSION}/osd.traineddata > osd.traineddata && \
curl -L https://github.com/tesseract-ocr/tessdata${TESSERACT_DATA_SUFFIX}/raw/${TESSERACT_DATA_VERSION}/eng.traineddata > eng.traineddata && \
curl -L https://github.com/tesseract-ocr/tessdata${TESSERACT_DATA_SUFFIX}/raw/${TESSERACT_DATA_VERSION}/${OCR_LANG}.traineddata > ${OCR_LANG}.traineddata
COPY . ${DIST}/tesseract/share/tessdata/
RUN rm Dockerfile*; exit 0
RUN rm READM*; exit 0
RUN rm build_*; exit 0
RUN rm *.zip; exit 0
RUN rm -rf example; exit 0
WORKDIR /var/task
Lambda
Le fichier src/handlers/index.py
Voir le index.py
import json
import pytesseract
from PIL import Image, ImageFilter
from NoteShrinker import NoteShrinker
import base64
import math
from typing import Tuple, Union
import cv2
import numpy as np
from deskew import determine_skew
def rotate(
image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]]
) -> np.ndarray:
old_width, old_height = image.shape[:2]
angle_radian = math.radians(angle)
width = abs(np.sin(angle_radian) * old_height) + \
abs(np.cos(angle_radian) * old_width)
height = abs(np.sin(angle_radian) * old_width) + \
abs(np.cos(angle_radian) * old_height)
image_center = tuple(np.array(image.shape[1::-1]) / 2)
rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0)
rot_mat[1, 2] += (width - old_width) / 2
rot_mat[0, 2] += (height - old_height) / 2
return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background)
def handler(event, context):
# Create a NoteShrink object full of images, either an array of filepaths, PIL images or numpy arrays
ns = NoteShrinker(['test_note.png'])
# Shrink the images by calling the shrink method, this returns an array of PIL images encoded as RGB
shrunk = ns.shrink()
# Carry on with your image processing....
for img in shrunk:
img.save('/tmp/example.png')
image = cv2.imread("/tmp/example.png")
grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
angle = determine_skew(grayscale)
rotated = rotate(image, angle, (0, 0, 0))
cv2.imwrite('/tmp/output.png', rotated)
im = Image.open("/tmp/output.png")
gr_image = im.convert('L') # grayscale
gr_image = gr_image.filter(ImageFilter.MedianFilter())
# SHould be saved in S3
encoded = f'data:image/png;base64,' + \
base64.b64encode(open("/tmp/output.png", "rb").read()).decode("utf-8")
body = {
"text_eng": pytesseract.image_to_string(gr_image, lang="eng", config="pcm=3 oem=1"),
"text_fra": pytesseract.image_to_string(gr_image, lang="fra", config="pcm=3 oem=1"),
"text": pytesseract.image_to_string(gr_image,),
# "output": encoded
}
response = {
"statusCode": 200,
"body": json.dumps(body)
}
return response
Ce script fait 4 choses,
- Convertir l’image avec NoteShrink, j’utilise cette étape pour convertir mes notes écrites à la main.
- Renvoyer le base64 de l’image convertie, cette étape devrait utiliser quelque chose comme S3 pour sauvegarder l’image et mettre les metadata dans DynamoDB (non couvert dans cet article).
- Extraire le texte de l’image en utilisant le data “fra” en “eng”, ce metadata devrait être sauvegardé dans DynamoDB avec l’image originale.
- Renvoyer l’information au client.
Outils et debug
Comment valider l’image en base64 avec le CLI,
L’image est renvoyée en utilisant le champ output
,
vous devez seulement garder le base64 (enlever : data:image/png;base64,
), enregistrer le contenu comme suit : image.base64
ensuite avec le CLI, entrer la commande suivante:
base64 -i image.base64 -o image.png --decode
file image.png
image.png: PNG image data, 3024 x 4032, 8-bit/color RGB, non-interlaced
La commande file
permet de vérifier que le tout a fonctionné.
Configurations et résultats
J’ai testé seulement une image. Puis voici les résultats:
pcm=13 & oem=0
pytesseract.image_to_string(Image.open('/tmp/test_note.png'), lang="eng", config="pcm=13 oem=0"),
pytesseract.image_to_string(Image.open('/tmp/test_note.png'), lang="fra", config="pcm=13 oem=0"),
-\\n\\n \\n\\n\\u00ab\\n\\n \\n\\n1 3 3 1 3 A ra Se a\\n\\n \\n \\n\\nEy a Cormnt fice garde 1:\\n\\n: i :\\nv . { 3 T ' -\\n1) |\\n\\\\ | ! i : : ; :\\nAY SE SR ee\\n| Ch es\\ni | f i al i ,\\n! : |\\nr \\\" . i\\nPo ;\\n|\\n! } } :\\n\\n \\n\\nPa ings pp ib\\n0 Chaph PI Tom pes enlace.\\nlant | yet Lizzi\\n\\n) oo a\\nMe\\n=\\n\\nCorn nTnannasfaeestfereiadi\\n\\n \\n\\n \\n\\f
\\u00e0\\n\\n \\n\\n\\u00ab\\n\\n \\n\\n2, 3 \\u00e0 4, \\u00c0. \\u00c0. La 1\\n\\n \\n \\n\\nete Thot cormat foire Srandir H\\n\\n! j :\\n\\u2018 . | \\u00bb \\u00ef \\u2018 =\\nL ;\\n, | ! ! | : ; ; ;\\n-\\u2014\\u2018Ay. pr er Lecce dei 1 5\\nPP 0 TA T -\\ni | f j \\u00b0 | ; \\u2018\\n| {\\n\\u201d - - *\\n| \\u20ac ; |\\n! :\\n: } ; :\\n\\n \\n\\na imp ipe Piges de\\n\\u00a9. chophe; 4 Pt Cdn * eee meuler\\nlent { ee Lessration\\n\\n\\u2018 2e\\n| oe\\na\\n\\nPL RE EEE EEE EN TERRE\\n\\n \\n\\n \\n\\f
pcm=3 & oem=1
pytesseract.image_to_string(Image.open('/tmp/test_note.png'), lang="eng", config="pcm=3 oem=1"),
pytesseract.image_to_string(Image.open('/tmp/test_note.png'), lang="fra", config="pcm=3 oem=1"),
CC\\n\\nPPP OPIS EII ECCT OEY 4 4 hb 11 da\\n\\n \\n\\n. ;\\nf\\ni\\n\\n \\n\\nEy \\u201cled | Conmnt fire grondic u\\n\\n-\\n\\n \\n\\n \\n\\n \\n\\nNe pparsonre N\\n\\n \\n\\nCy\\n\\n~Cogengo PR\\n\\nole Kinplo J .\\u00a5\\n7 - rg pp 1ene\\n\\n\\u00ae ox. Chepl A food. \\u201d ie ce.\\n\\nlh | yet Lenigpein \\\\\\n\\n \\n\\n \\n\\n \\n\\n \\n\\f
\\u20ac\\n\\nLT TS # & k \\u00c0 1 3 114 LA\\n\\n \\n\\ne |\\nf\\ni\\n\\n \\n\\nete ll | Cornmnt Taie = gnaie \\u0178\\n\\nVe\\n\\n \\n\\n \\n\\n \\n\\n1 \\u00c0 Ye fRETSONNE 51 =\\n\\n \\n\\ntb\\n\\nBpargo I\\n\\nOle \\u00ab amples 1 M\\n? roc ptiere\\n\\n=. chophe A dure \\u2026 ie os\\n\\nee { ee Leniguetion ;\\n\\n \\n\\n \\n\\n \\n\\n \\n\\f
Conclusion
Définitivement, il y a beaucoup d’optimisation à faire pour obtenir un meilleur résultat.
Je vais peut-être essayer quelque chose comme : https://tesseract-ocr.github.io/tessdoc/tess4/TrainingTesseract-4.00.html, par contre je n’ai pas encore de dataset.
Sources
- https://stackoverflow.com/questions/44619077/pytesseract-ocr-multiple-config-options
- https://superuser.com/a/883511
- https://github.com/ghandic/NoteShrinker
- https://github.com/amtam0/lambda-tesseract-api
- https://medium.com/analytics-vidhya/build-tesseract-serverless-api-using-aws-lambda-and-docker-in-minutes-dd97a79b589b