momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Recunoașterea textului și extragerea datelor unui orar școlar prezentat în format PDF (VIII)

Bash | ImageMagick | R | Tesseract
2024 sep

[1] v. partea a VII-a … (v. și I-VI)

…pățanii stupide (de trebuie luat de la capăt)

În [1]-IV constituisem modelul OCR $TESSDATA_PREFIX/hwr.traineddata, pe baza unui set [*.png, *.gt.txt] cu imagini de cuvinte (și litere), respectiv textul de recunoscut pentru ele; angajasem și o listă "new_words.txt" cu câte un cuvânt pe fiecare linie, vizând denumiri (unice) de discipline, prenume, sau nume care apar pe orarele claselor. Ulterior (v. [1]-VI), am ordonat această listă cum se cuvine pentru Tesseract: descrescător după frecvența cuvântului; totodată, am mai adăugat câteva exemple de recunoaștere a unor cuvinte — numai că spre deosebire de cuvintele anterioare, pe care le înscrisesem „automat” în fișierele "*.gt.txt", pentru aceste câteva noi cuvinte am procedat „manual” (cu un editor de text) și nu-i de mirare că apoi (după ce re-antrenasem deja Tesseract), am văzut că unele dintre aceste noi fișiere conțin și câte o linie albă (iar *.gt.txt trebuie să conțină câte o singură linie, cu textul de recunoscut pe imaginea "*.png" asociată).

Prin urmare… am luat-o de la capăt, renunțând la "hwr" și obținând "orr.traineddata" în care dicționarul cuvintelor este ordonat după frecvență, iar fișierele *gt.txt sunt corecte… Dar terminând și [1], strădania de a înțelege de ce unele rezultate sunt omise, ne-a făcut să ne tot uităm în urmă; consultând fișierul ".bash_history", am observat că în comanda "make training" prin care obținusem "*.traineddata" (pentru "hwr", apoi și pentru "orr") folosisem "--psm 8" (în loc de valoarea implicită 13, cel mai potrivită pentru antrenare).
N-ar fi aceasta o problemă: n-avem decât să folosim "make clean MODEL_NAME=orr" și să reluăm comanda "make training" dar fără parametrul "--psm"…

Numai că, obținând fișierul necesar "*.traineddata" și considerând că rezultatele OCR sunt satisfăcătoare, am șters între timp, definitiv, directorul tesstrain/ (din care, în prezența datelor respective, trebuie lansat "make training")… Totuși, păstrasem exemplele de recunoaștere în subdirectorul orrGT/, precum și dicționarul cuvintelor din orare.

A o lua de la capăt, mereu, nu este un obicei tocmai bun… Ne-o fi plăcând, dar puteam evita „pățaniile” evocate mai sus (și altele, întâlnite mai jos), dacă ne obișnuiam și să lucrăm sub controlul sistemului git (este drept pe de altă parte, că dacă nu este prostească, repetiția este totuși, „mama învățăturii”).

Antrenăm din nou… (modelul "COPy")

Vom constitui un nou model "cop.traineddata" — unde "cop" ar aminti "cls|obj|prof" și de fapt… vine de la "COPy" (că asta facem: „copiem” conținuturi ad-hoc din poze ale orarelor, în scopul de a le organiza ca set coerent de date).
Plecăm de la modelul eng (varianta "-best") existent deja în $TESSDATA_PREFIX/; înființăm (prin git clone) subdirectorul tesstrain/ și instituim subdirectorul exemplelor (copiindu-le din orrGT/ pe care-l constituisem în [1]):

git clone https://github.com/tesseract-ocr/tesstrain.git
cd tesstrain/
make tesseract-langdata
mkdir -p data/cop-ground-truth
cp ../orrGT/* data/cop-ground-truth/

Subliniem că am constituit și fișierul de configurare data/langdata/cop/cop.config, precizând ce caractere nu sunt necesare pentru a recunoaște imaginile noastre și schimbând față de valorile implicite (dar cam „pe ghicite”), unii coeficienți penalizatori:

tessedit_char_blacklist 0123456789kqwyKQWY\[\{
language_model_penalty_non_dict_word 0.03
segment_penalty_garbage 0.9

Precizăm că Tesseract admite peste 650 de parametri de configurare (pot fi listați prin tesseract --print-parameters, urmat eventual de "| grep penalty", de exemplu). Nu știm cât de potrivite sunt valorile alese mai sus (dacă vrem, le putem modifica, reluând apoi antrenarea — dar va fi o experiență cu mici șanse de a se și termina vreodată); aici am vrut doar să exemplificăm o modalitate de configurare.

Cu aceste pregătiri, putem obține acum cop.traineddata (și-l transferăm în subdirectorul indicat deja de $TESSDATA_PREFIX):

make training MODEL_NAME=cop START_MODEL=eng \
              TESSDATA=$TESSDATA_PREFIX \
              PUNC_FILE=~/24Sep/cop.punc \
              WORDLIST_FILE=~24Sep/cop.user_words
sudo cp data/cop.traineddata $TESSDATA_PREFIX
cd ..

cop.traineddata măsoară $11.2MB$ și eventual, poate fi investigat prin programele combine_tessdata, wordlist2dawg, etc. (dar până la urmă, am ajunge firește în impas — știind că nu stăpânim suficient cele necesare, de exemplu despre rețele neuronale).

Cum ajungem la date

Deocamdată folosim modelul cop pentru a extrage textul din celulele PNG corespunzătoare în subdirectorul de lecții LSS/ (v. [1]), uneia dintre clase (după ce, mai întâi constituim lista numelor fișierelor acesteia):

ls LSS/5A* > 5A.tsr  # "LSS/5A-0.png", "LSS/5A-1.png", etc.
tesseract 5A.tsr stdout -l cop --psm 6

Am preferat să afișăm pe ecran și să copiem apoi în "5A.txt" (dacă în loc de "stdout" foloseam "5A", atunci rezulta direct "5A.txt", dar așa informațiile de pagină se rezumau la un separator); formatăm "5A.txt" încât pentru fiecare celulă să avem câte exact 3 linii:

Page 0 : LSS/5A-0.png  # ziua=1, ora=1 (și cls=5A)
ed sociala
Delia Dascalu
Page 1 : LSS/5A-10.png  # avem de aici cls|zi|ora
cds                     # obj
Adrian Frincu           # prof
Page 2 : LSS/5A-11.png
                        # obj nerecunoscut (… de ce??)
Raluca Crisantha Alexa

Următoarea funcție $\mathbf{R}$ primește numele fișierului text rezultat prin tesseract și formatat cu câte exact 3 linii consecutive de fiecare pagină și returnează un set de date (de tip data.frame) LSS|obj|prof|zi|ora; câmpul LSS precizează clasa și rangul celulei PNG, rang din care prin împărțire la 8, se deduc valorile zi și ora:

library(tidyverse)

cop2df <- function(cop_txt) {
    cop <- readLines(cop_txt)
    nl <- length(cop); if(cop[nl]=="") nl <- nl-1
    lss <- str_extract(cop[seq(1,nl,by=3)], "\\/(.*)\\.", group=1)
    obj <- cop[seq(2,nl,by=3)]
    prof <- cop[seq(3,nl,by=3)]
    zo <- as.integer(str_extract(lss, "-(\\d+$)", group=1))
    zi <- zo %/% 8 +1; ora <- zo %% 8 +1
    data.frame(LSS=lss, obj=obj, prof=prof, zi=zi, ora=ora) %>%
    arrange(zi, ora)
}

Pentru clasa 5A avem (marcăm cu roșu, eventual și boldat, greșelile existente):

cdf <- cop2df("5A.txt")
print(cdf, print.gap=3, row.names=FALSE)
         LSS           obj                     prof   zi   ora
        5A-0    ed sociala            Delia Dascalu    1     1
        5A-1      biologie      Camelia Alexandriuc    1     2
        5A-2    matematica   Raluca Crisantha Alexa    1     3
        5A-3       engleza          Daria Marginean    1     4
        5A-4       istorie      Mihai Bogdan Dranca    1     5
        5A-8        romana    lonela-Andreea Sandru    2     1
        5A-9       religie           Florin Hostiuc    2     2
       5A-10           cds            Adrian Frincu    2     3
       5A-11      ?          Raluca Crisantha Alexa    2     4
       5A-12   informatica        Mihada Cristina ?    2     5
       5A-13           cds           Nicoleta Bumbu    2     6
       5A-16    ed vizuala            Adrian Erincu    3     1
       5A-17        romana    Ionela-Andreea Sandru    3     2
       5A-18   ed muzicala            Lucian Tablan    3     3
       5A-19       i?torie      Mihal Bogdan Dranca    3     4
       5A-20    consiliere           Nicoleta Bumbu    3     5
       5A-21     geografie           Nicoleta Bumbu    3     6
       5A-24           cds   Raluca Crisantha Alexa    4     1
       5A-25     ed fizica          Adrian Cojocaru    4     2
       5A-26       germana           Camelia Maftei    4     3
       5A-27        romana    Ionela-Andreea Sandru    4     4
       5A-28    matematica   Raluca Crisantha Alexa    4     5
       5A-32       germana           Camelia Maftei    5     1
       5A-33        romana    lonela-Andreea Sandru    5     2
       5A-34           tic                ? Hatmanu    5     3
       5A-35    matematica   Raluca Crisantha Alexa    5     4
       5A-36       engleza          Daria Marginean    5     5
       5A-37      ?                 Adrian Cojocaru    5     6

Sunt 6 greșeli minore: "$l$" în loc de "$I$" sau $i$, de trei ori; "itorie" în loc de "istorie" și "Mihada" în loc de "Mihaela", respectiv "Erincu" în loc de "Frincu". Probabil, asemenea greșeli „minore” s-ar putea evita, reantrenând Tesseract cu valori mai potrivite pentru anumiți parametri de configurare (dar documentația aferentă parametrilor este tulbure)…
Sunt trei greșeli importante: nu s-a recunoscut obj pentru 5A-11 și 5A-37 (și nu reușim să vedem motivele); respectiv (din cauza separării pe două linii, în celulele respective), numele unui profesor apare numai cu prenume pentru 5A-12 și numai cu "nume" pentru 5A-34 (subliniem că acest ultim aspect, care decurge din ignorarea din start a faptului că numele este o entitate indivizibilă — este foarte greu de „reparat” altfel decât "manual"…).

Geșelile semnalate sunt totuși ușor de corectat, în mod interactiv; de exemplu:

cdf$prof[cdf$prof %in% c(
         "Mihada Cristina", "Hatmanu")] <- "Mihaela Cristina Hatmanu"

Dacă apelăm tesseract nu cu "5A.tsr", ci cu lista numelor tuturor celor 822 de lecții înregistrate în LSS/ și apoi folosim pentru rezultat, funcția cop2df() — atunci setul de date notat mai sus "cdf" ar reprezenta întregul orar al școlii, [cls|obj|prof|zi|ora] (mai puțin 16 lecții, pe care le reținusem separat — v. [1]); desigur, în acest caz vor fi multe corecturi de făcut… (dar putem concepe niște funcții ajutătoare, în acest scop).

Însă avem deja altă idee de încercat (avansată cumva în [1]): despărțim lecțiile PNG obj|prof din LSS în câte două fișiere PNG (imaginea unei linii obj, respectiv imaginea pentru prof, „redusă” cumva la o singură linie); alegem (probabil, aleatoriu) un anumit număr de fișiere de fiecare tip (câte o treime ?) și folosim modelul cop constituit mai sus — obținând (după corecturile necesare) un set de exemple de recunoaștere pentru linii (nu cuvinte individuale și litere, ca în cazul cop) aferente fie câmpului obj, fie câmpului prof.
Apoi antrenăm Tesseract pe baza noului set de exemple, obținând un nou model OCR — cu așteptarea că acesta va recunoaște mai bine, lecțiile cls|obj|prof din LSS/ (unde în LSS/ am putea avea celulele oricărui orar în care s-a folosit aceeași scriere "de mână" ca în cazul ilustrat aici, măcar disciplinele școlare fiind cam aceleași).

Constituirea setului de date corespunzător orarului

Modelul cop constituit mai sus este totuși suficient pentru a ajunge „aproape direct”, la setul de date corespunzător orarului de față.

În LSS/ avem deja toate lecțiile obj|prof, ca imagini PNG $483\times280px$ („celule PNG”); ne-am asigurat anterior că obj ocupă o singură linie, aflată cam în prima treime a celulei; a doua treime din celulă, dacă nu este goală, reprezintă prenumele de prof, iar ultima zonă (pe verticală) reprezintă numele de prof (sau prenume + nume, după caz):

Segmentăm celulele PNG inițiale în câte 3 imagini (fără margini albe, dar cu câte un border alb mic): una pentru obj, a doua (care poate fi și „vidă”, $1px\times1px$, pe mai puțin de 300 octeți) pentru prenume și a treia pentru numele profesorului (sau prenume + nume, după caz).
Experimentând puțin, am găsit că valoarea potrivită pentru "treime" este $37.5\%$ din înălțimea celulei inițiale (desigur, pentru a 3-a zonă rămâne doar $25\%$); mai jos, vom vedea totuși 6 excepții ("ala" prin care convenisem pentru "antreprenori \n ala", ocupă mai mult de $37.5\%$).

Prin următoarea secvență de comenzi Bash, copiem cele 822 de celule PNG din LSS/ într-un nou subdirector LSS3/, apoi prin mogrify segmentăm în cele trei zone celulele din LSS3/, apoi eliminăm dintre imaginile rezultate pe cele „vide”, a căror mărime este mai mică decât 300 octeți, iar pe imaginile rămase (folosind iarăși mogrify) eliminăm marginile (prin "-trim") și adăugăm un border alb (de $10px$):

mkdir -p LSS3
cp LSS/*.png LSS3/
mogrify +repage -crop 100%x37.5% +repage LSS3/*.png
find LSS3/ -type f -size -300c -delete
mogrify -trim +repage -bordercolor white -border 10 LSS3/*.png

Subliniem că border-ul adăugat nu este lipsit de importanță pentru Tesseract și dacă ne luăm „după surse”, ar trebui să fie cam o cincime din "x-height" (înălțimea literei "x"); $10px$ pare suficient și pentru obj (care este scris cu litere mai mari decât cele din prof).

Înregistrăm lista numelor fișierelor rezultate în LSS3 (în număr de 1829) și aplicăm tesseract (cu PSM 13) acestei liste:

ls LSS3/*.png > L3.ls  # LSS3/5A-0-0.png, LSS3/5A-0-2.png, etc.
tesseract L3.ls lss -l cop --psm 13  # produce "lss.txt"
sed -i 's/\x0c//' lss.txt  # elimină separatorii de pagină

De data aceasta, în loc de "stdout" am indicat "lss": pe ecran se afișează informațiile "Page: ..." curente, iar rezultatele recunoașterii sunt înscrise în fișierul lss.txt, în care paginile sunt separate între ele prin caracterul "end of text" 0x0c — caractere pe care le-am eliminat în final, folosind sed.

Pentru fiecare fișier numit în lista L3.ls, se afișează pe ecran (sub "Page:") numele respectiv și se adaugă în lss.txt textul recunoscut pentru acel fișier PNG; altfel spus, L3.ls indexează liniile din lss.txt și putem combina informațiile (analog funcției cop2df() de mai sus):

library(tidyverse)
page <- readLines("L3.ls")  # fișierul curent
text <- readLines("lss.txt")  # textul recunoscut pe fișierul curent
c_rang <- str_extract(page, "\\/(.*)\\.", group=1)
rang <- str_extract(c_rang, "-(\\d+)-", group=1) %>% as.integer()
zi <- rang %/% 8 +1
ora <- rang %% 8 +1
Zile <- c("Lu", "Ma", "Mi", "Jo", "Vi")
orar <- data.frame(cls=c_rang, ocr=text, zi=zi, ora=ora, png=c_rang)
orar <- orar %>% 
        mutate(cls=str_extract(cls, "(^\\w*)", group=1),
               zi=factor(zi, labels=Zile, ordered=TRUE)) %>%
        arrange(cls, zi, ora)
saveRDS(orar, "orar.RDS")

Am obținut structura de date orar (de tip "data.frame") și am salvat pe disc (în "orar.RDS") datele respective, permițând prelucrări ulterioare (prin alte programe $\mathbf{R}$); am păstrat (nu numai pentru verificări !) și numele fișierelor PNG din care provin datele cls|ocr|zi|ora, unde "ocr" desemnează textul recunoscut (obj, prenume, sau prof) de pe fișierul respectiv.

Un exemplu simplu de „prelucrare ulterioară” ar fi afișarea unui eșantion aleatoriu de înregistrări din orarul curent salvat în orar.RDS:

library(tidyverse)
orr <- readRDS("orar.RDS")
print(slice_sample(orr, n=5), row.names=FALSE)
     cls                 ocr zi ora      png
     10E      Roxana Valeria Vi   2 10E-33-1
     11D            biologie Ma   7 11D-14-0
     12D Cristian Amoraritei Jo   3 12D-26-2
      8A          ed sociala Vi   6  8A-37-0
     11E        Sorin Tanase Lu   3  11E-2-2

În fișierul LSS3/10E-33-1.png vedem într-adevăr, valoarea indicată pe prima linie în câmpul ocr; partea finală "-1" din numele fișierului arată că avem de-a face cu prenumele (iar numele prof îl găsim în "LSS3/10E-33-2"); partea -33 arată ziua ($33/8=4$. deci ziua "Vi") și ora ($33\mod 8=1$, deci ora=2). Analog putem verifica, alte linii de date.

Numele de fișiere din coloana "png" servesc și pentru eventualitatea că am vrea fișiere "*gt.txt", (pentru a constitui un nou model OCR); de exemplu, pentru prima linie de mai sus creem fișierul "11E-33-1.gt.txt" (asociat lui 10E-33-1.png), înscriindu-i valoarea din câmpul ocr al liniei respective (valoare care în cazul de față nu necesită corecturi).

Corectarea greșelilor de recunoaștere

Să depistăm disciplinele școlare obj care au fost incorect recunoscute.
Întâi, ținem seama de faptul că valorile "obj" sunt cele din câmpul ocr aflate pe liniile de date în care valorile png se termină cu "-0" și ne amintim că în fișierul "cop.user-words" înscrisesem toate denumirile de discipline (de fapt… cuvintele componente):

cr_obj <- orr %>% 
          filter(str_ends(png, "-0")) %>% 
          select(ocr) %>% unique()
words <- readLines("cop.user-words")
print(setdiff(cr_obj$ocr, words))  # recunoscut, dar neaflat în dicționar
     [1] "viz/muz"       "ed fizica"     "nta."          "fiaica"       
     [5] "nla"           "nla."          "spanicla"      "itorie"       
     [9] "ed vizuala"    "spaniocla"     "ed sociala"    "ed muzicala"  
    [13] "ed mugicala"   "ed mugzgicala" "geemana"       "biotogie" 

În dicționarul pe care-l constituisem în [1] avem cuvinte ca "viz", muz", "ed, "biologie" etc., dar nu și denumiri complete (și „corecte”) ca "viz/muz", sau "ed fizica".
Denumirile incorecte sunt doar cele marcate cu roșu, iar dintre acestea unele au corecturi evidente ("geemana"="germana", etc.) și singurele care trebuie totuși investigate sunt cele de la indecșii 3, 5 și 6 (regăsite eventual de mai multe ori, în orr):

orr %>% filter(ocr %in% c("nta.", "nla", "nla.")) %>% print()
      cls  ocr zi ora      png
    1 10B nta. Ma   3 10B-10-0
    2 10C nta. Lu   7  10C-6-0
    3 10D  nla Vi   4 10D-35-0
    4 10E nla. Vi   1 10E-32-0
    5 10F nta. Lu   1  10F-0-0

Consultând LSS/10B-10.png înțelegem despre ce este vorba:

> orr %>% filter(cls=="10B", zi=="Ma", ora==3)
      cls                 ocr zi ora      png
    1 10B                nta. Ma   3 10B-10-0
    2 10B                uain Ma   3 10B-10-1
    3 10B Irina Geanina Harja Ma   3 10B-10-2

Pe celulele inițiale, disciplina "Ed. antreprenorială" figura pe două rânduri: "antreprenori" și dedesubt, "ala"; fiind singura situație de acest fel, am tratat-o ad-hoc: am păstrat numai rândul al doilea — iar ulterior n-am sesizat că zona celulei care îl conține ocupă mai mult de $37.5\%$ din celulă, încât rândul respectiv a fost segmentat în "10B-10-0.png" (de pe care s-a recunoscut "nta.") și "10B-10-1.png" (de pe care s-a recunoscut "uain"), cum se vede pe imaginea și datele redate mai sus.

Să observăm însă că pentru clasa 10A, "ala" a fost recunoscut (corect):

> orr %>% filter(cls=="10A", ocr=="ala")
      cls ocr zi ora      png
    1 10A ala Ma   6 10A-13-0

În LSS3/10A-13-0.png, "ala" este urcat ceva mai sus în celulă (față de celelalte 5 cazuri redate mai sus), încât Tesseract a reușit totuși, să recunoască literele respective.

Dacă vrem, putem s-o „luăm de la capăt” — re-segmentând (cu $40\%$ sau chiar $50\%$, în loc de $37.5\%$) cele 6 celule cu "ala"… Preferăm să corectăm direct:

wh <- which(with(orr, ocr %in% c("nta.", "nla", "nla.")))
orr[wh, "ocr"] <- "ala"
orr[wh, "png"] <- "10A-13-0"
orr <- orr[-(wh+1), ]
saveRDS(orr, "orar.RDS")

În vectorul wh am obținut prin which, indecșii liniilor din orr corespunzătoare celor 5 cazuri în care trebuia recunoscut "ala"; pe aceste linii am înlocuit cu "ala" valoarea greșită din câmpul ocr și cu "10A-13-0" valoarea png (am văzut mai sus, că "10A-13-0.png" este „citit” corect, "ala"). Apoi am ținut seama de faptul că orr fusese ordonat după cls|zi|ora, ceea ce înseamnă că liniile cu indecșii imediat următori celor din vectorul wh (rezultați prin (wh+1)) reprezintă porțiunile inferioare ale literelor "ala", deci am "șters" liniile respective.
În final, am salvat pe disc setul curent orr, înlocuind vechiul "orar.RDS".

Dar să observăm că ne-a scăpat ceva: în plus față de cele 5 linii indexate de (wh+1), pe care png se termină cu "-1" — trebuie ștearsă și linia corespunzătoare clasei 10A, adică linia pe care în pgn apare "10A-13-1"; aflăm indexul acesteia și o "ștergem" din orr:

> orr[orr$png=="10A-13-1", ]
       cls    ocr zi ora      png
    26 10A Raaaad Ma   6 10A-13-1  
> orr <- orr[-26, ]  # "șterge" linia de index 26 
> saveRDS(orr, "orar.RDS")

Următoarea funcție simplifică „repararea” valorilor orr$ocr greșite; fiindcă orr este în exteriorul funcției, folosim "<<-" (operatorul de "atribuire globală"):

replace_ocr <- function(old, new) 
    orr$ocr[orr$ocr==old] <<- new

De exemplu, pentru disciplinele greșit recunoscute care ne-au rămas în orr:

derr <- c("fiaica", "spanicla", "spaniocla", "itorie", 
          "ed mugicala", "ed mugzgicala", "geemana", "biotogie")
dcor <- c("fizica", rep("spaniola", 2), "istorie", 
          rep("ed muzicala", 2), "germana", "biologie")
for(i in 1:length(derr)) 
    replace_ocr(derr[i], dcor[i])
saveRDS(orr, "orar.RDS")

Textele din fișierele LSS3/*-1.png și LSS3/*-2.png conțin două sau trei cuvinte (separate prin spațiu). Următoarea funcție primește ca argument "-1", sau "-2" și returnează un vector care conține toate valorile ocr (distincte între ele) de pe liniile cu "png" terminat în "-1", respectiv "-2", pentru care măcar unul dintre cuvintele componente nu se află în lista cop.user-words:

orr <- readRDS("orar.RDS")  # orarul curent
words <- readLines("cop.user-words")

diff_ocr <- function(at_end="-1") {
    v_ocr <- orr %>% 
             filter(str_ends(png, at_end)) %>% 
             pull(ocr) %>% unique()
    for(i in 1:length(v_ocr)) {
        vi <- strsplit(v_ocr[i], " ") %>% unlist()
        if(all(vi %in% words)) 
            v_ocr[i] <- ""  # Nu necesită corecturi
    }
    v_ocr[v_ocr != ""]  # valorile "ocr" care trebuie corectate
}

Astfel, prenumele care au fost greșit recunoscute de pe celulele PNG sunt:

> ocr1 <- diff_ocr("-1")
    [1] "Gabriela Adeiana"  "Marilena cristina" "‘Roxana Valeria"  
    [4] "Mihaela cristina"  "Reoxana Valeria"   "Mchaela Cristina" 
    [7] "Miheaela Cristina" "Elena Carnen"      "Elna Carmen"      

Pentru corectare, edităm vectorul ocr1 rezultat mai sus (de exemplu, prin edit(ocr1, editor="gedit")) și reținem (cum se vede, am avut de corectat numai câte o literă):

> ocr1k <- c("Gabriela Adriana", "Marilena Cristina", "Roxana Valeria", 
              "Mihaela Cristina", "Roxana Valeria", "Mihaela Cristina", 
              "Mihaela Cristina", "Elena Carmen", "Elena Carmen")

Folosind cei doi vectori „paraleli”, putem corecta acum datele din orr:

for(i in 1:length(ocr1))
    replace_ocr(ocr1[i], ocr1k[i])
saveRDS(orr, "orar.RDS")

Ne-a rămas să „reparăm” valorile din ocr corespunzătoare câmpului prof:

> ocr2 <- diff_ocr("-2")
  [1] "Carelia Alexandriuc"     "BE/cCA"                 
  [3] "Simona Sefroni"          "loredana Epure"         
  [5] "BE/CA"                   "'Alexandra Pascariu"    
  [7] "BE /CA"                  "Florin Hostiue"         
  [9] "Frincus/Tablan"          "Aduian Cojocaru"        
 ...
[157] "Raluea Crisantha Alexa"  "Mariana Sarin Giosan"   
[159] "Aduiian Petrisor"        "lonela-Andseea Sandsu"  
[161] "Adusian Cojocaru"        "Flovin ioan Cojocaru"   
[163] "Florin loan Cojecaru"    "BE / MD"                
 ...
[191] "Adeian Cojocaru"         "Mara Morotan"  

De data aceasta avem multe corecturi de făcut, dar numai de câte una-două litere, ușor de ghicit; numai în câteva cazuri, pentru a nu greși numele, ar trebui să consultăm lista cop.user-words sau dacă aceasta nu ajunge, să afișăm orr %>% filter(ocr=="L Gman") de exemplu — pentru a vedea valoarea din câmpul png al liniei (sau liniilor) afișate și apoi, fișierul PNG respectiv din LSS3/ (văzând astfel valoarea corectă, în loc de "L Gman" — anume… "Cojocaru" !). Observând celula PNG respectivă, avem explicația obișnuită a surprizei: pe imaginea respectivă au rămas trasate în partea superioară, niște urme ale liniilor inițiale, neuniforme, dintre celule; dacă ne străduiam mai mult să „curățăm” imaginile din LSS3/, atunci n-am mai fi avut asemenea cazuri de interpretare surprinzătoare.

După ce terminăm de editat vectorul ocr2, refolosim secvența de corectare pe care am redat-o mai sus (în cazul ocr1) și ne rezultă astfel, setul de date final "orar.RDS".

Obs. 1. Fiindcă am editat manual vectorul ocr2, tot au mai rămas câteva corecturi de făcut (cum constatăm în final, relansând diff_ocr("-2")); de exemplu, ne-a scăpat să ștergem apostroful inițial în "'Adrian Frincu" — corectăm ad-hoc, prin replace_ocr("'Adrian Frincu", "Adrian Frincu").

2. Credeam că în cop.user-words am înregistrat toate cuvintele din orarele inițiale ale claselor — dar acum constatăm că nu este așa: în vectorul ocr2 rămâne de exemplu "Gabriela Nenec", fiindcă "Nenec" nu apare în "words"; pentru siguranță, investigăm prin orr %>% filter(ocr=="Gabriela Nenec") și ne uităm în fișierul PNG indicat în câmpul png al liniei afișate (constatând că într-adevăr, apare "Nenec").

3. În words înregistrasem și „cuvinte” ca "AS/MC", iar acestea au fost în general greșit recunoscute (și rămân în ocr2); corect era să fi ținut seama că inclusesem "/" drept „semn de punctuație” (în fișierul "cop.punc", care trebuie să fie prezent, pe lângă "cop.user-words") și să înregistrăm în words cuvintele "AS" și "MC". Ca urmare, acum trebuie să investigăm (și eventual să corectăm) liniile din orr pe care în câmpul ocr apar cuvinte ca "AS/MC"…

În concluzie, recunoașterea textului reușea mult mai bine (și am fi avut ulterior, mult mai puțin de muncă) dacă: 1) verificam mai atent cât de „curate” sunt imaginile din LSS3/ (folosind meniul "Draw" din display — v. [1] — pentru a elimina eventualele urme rămase pe celulele PNG respective);
2) verificam ca fișierul "cop.user-words" să aibă conținutul corect și să fie complet.

Obținând "orar.RDS", putem zice că am terminat cu Tesseract (…rămâne în aer, ideea de a constitui un nou model .traineddata, antrenându-l pe un set de valori dintre cele înregistrate în câmpurile ocr și png de pe câte o aceeași linie din setul orr).

Normalizarea datelor

Să observăm acum că organizarea datelor din orar.RDS este defectuoasă: coloana ocr conține și valori ale variabilei obj și valori de prof; de exemplu:

> orr %>% filter(cls=="5A", zi=="Vi")
       cls                    ocr zi ora     png
    1   5A                germana Vi   1 5A-32-0
    2   5A         Camelia Maftei Vi   1 5A-32-2
    3   5A                 romana Vi   2 5A-33-0
    4   5A  Ionela-Andreea Sandru Vi   2 5A-33-2
    5   5A                    tic Vi   3 5A-34-0
    6   5A       Mihaela Cristina Vi   3 5A-34-1
    7   5A                Hatmanu Vi   3 5A-34-2
    8   5A             matematica Vi   4 5A-35-0
    9   5A Raluca Crisantha Alexa Vi   4 5A-35-2
    10  5A                engleza Vi   5 5A-36-0
    11  5A        Daria Marginean Vi   5 5A-36-2
    12  5A              ed fizica Vi   6 5A-37-0
    13  5A        Adrian Cojocaru Vi   6 5A-37-2

Iar uneori, prof este reprezentat pe două linii consecutive — ca mai sus pe liniile 6 și 7, pentru obiectul "tic".

Reținem în orr0 liniile din orr corespunzătoare disciplinelor — cele în care valoarea din câmpul "png" se termină cu "-0"; fie orr2 setul liniilor din orr neincluse în orr0:

orr0 <- orr %>% filter(str_ends(png, "-0"))
orr2 <- anti_join(orr, orr0)  # png se termină cu "-1" sau "-2"

Fie pren_rows vectorul indecșilor de linii din orr2 pentru care valoarea din câmpul png se termină cu "-1"; dacă row ∈ pren_rows, atunci orr2[row, 2] conține prenumele, iar orr2[row+1, 2] conține numele profesorului curent — îmbinăm prenumele și numele, pe linia row+1 (în câmpul 2, corespunzător variabilei "ocr" din orr2):

pren_rows <- which(with(orr2, str_ends(png, "-1")))
for(row in pren_rows) {
    pren <- orr2[row, 2]
    prof <- paste(pren, orr2[row+1, 2])
    orr2[row+1, 2] <- prof
}

Eliminăm din orr2 liniile corespunzătoare prenumelor; orr2 rămâne astfel cu același număr de linii ca și orr0, iar liniile sunt în aceeași ordine: profesorului de pe linia de rang j din orr2 îi corespunde disciplina de pe linia de rang j din orr0. Instituim în setul orr2 coloana obj, copiind disciplinele din coloana ocr a lui orr0; apoi, în orr2 adoptăm numele prof, în loc de vechiul nume "ocr":

orr2 <- orr2[-pren_rows, ]
orr2$obj <- orr0$ocr
orr2 <- orr2 %>% rename(prof=ocr)

În final, eliminăm câmpul "png" (nu are de ce să ne mai intereseze), adoptăm numele de linie obișnuite (liniile rămase după ignorarea celor indicate de pren_rows păstrau numele de linie inițiale) și salvăm pe disc setul orr2, în orar_2.RDS:

orr2$png <- NULL
rownames(orr2) <- NULL
saveRDS(orr2, "orar_2.RDS")

Acum, setul de date orr2 reprezintă orarul în „forma normală” (fiecare coloană conține valori ale unei aceleiași variabile, iar cele 5 variabile sunt independente între ele; în plus, liniile sunt indexate obișnuit și nu prin indecși moșteniți):

> glimpse(orr2)
Rows: 822
Columns: 5
$ cls  <chr> "10A", "10A", "10A", "10A", "10A", "10A", "1…
$ prof <chr> "Camelia Alexandriuc", "BE/CA", "Lucian Lungu", "Cri…
$ zi   <ord> Lu, Lu, Lu, Lu, Lu, Lu, Lu, Ma, Ma, Ma, Ma, Ma, M…
$ ora  <dbl> 1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6,…
$ obj  <chr> "biologie", "engleza", "fizica", "matematica", "psih…

Bineînțeles că pentru ora ne-ar conveni să avem valori de tip int, în loc de "double" (care ocupă de cel puțin două ori, mai multă memorie); dar oricum vom ignora în cele din urmă, variabilele curente zi și ora

De observat că iar am uitat: trebuie să adăugăm în orr2 și cele 16 lecții pe care le reținusem separat în [1]; bineînțeles că între timp am rătăcit însemnările respective, dar știm clasele (9B și 10B pe "informatică" și 9E pe "franceză") și ne vom uita în fișierele PDF inițiale, pentru celulele colapsate orizontal câte două sau câte trei.
[ Apropo. Cum se vede (că de!… o luăm mereu "de la capăt"), ne place să repetăm și…
repetăm: obiceiul (facilitate chipurile, oferită de Excel) de a colapsa vizual pe un tabel de date, nu are de-a face, nicidecum, cu știința datelor, ci este un nărav funcționăresc prost, dar tipic: încurcă lucrurile și te pune pe drumuri. ]

Acum putem investiga datele respective (convenind în prealabil asupra unor codificări) și putem constata în ce măsură sunt respectate anumite principii general valabile pentru un orar școlar; apoi, putem anula câmpurile zi și ora, pentru a constitui pe datele respective (asumând consecvent anumite principii de repartizare a lecțiilor) un nou orar (important fiind, subliniem, nu orarul ca atare, ci logica / procesul de constituire a acestuia).

vezi Cărţile mele (de programare)

docerpro | Prev | Next