Progetto:Trascrizioni/Progetto Phe/Storia

L'obiettivo originale di Phe è quello di consentire l'evidenziazione del testo sull'immagine, sincronizzandola con il puntatore nel testo in modifica. Ma in base ai nostri obiettivi locali, disporre di questi dati potrebbe consentire anche una analisi automatica di alcuni elementi della formattazione; ed effettivamente la cosa è estremamente interessante.

Attualmente, Phe riferisce che:

  • per ogni djvu con testo OCR, il djvu viene caricato una volta su toolserver e viene estratto il testo delle pagine con la routine djvutxt e i testi vengono montati in un oggetto python serializzato;
  • subito dopo il file djvu viene cancellato;
  • alla creazione di una pagina l'oggetto python restituisce il corrispondente testo che viene caricato sul box testo.

Sfruttando questa procedura con modeste modifiche:

  • oltre che il testo base, potrebbe essere ottenuto dal djvu anche il testo suddiviso in linee, con le loro coordinate sulla pagina (comando djvutxt -detail=line);
  • l'output grezzo di djvutxt -detail=line potrebbe essere trasformato in un oggetto python strutturato (lista di liste) e serializzato in una stringa JSON;
  • al momento della creazione della pagina, potrebbe essere caricato nel box testo sia il testo usuale, che la stringa JSON, che sarebbe immediatamente fatta sparire caricandola in un'"area dati" nascosta della pagina Pagina e trasformata in un oggetto js (lista di liste analoga all'originale oggetto python);
  • in fase di submit, la stringa JSON potrebbe essere ricostruita e riaggiunta al testo della pagina.

Così facendo, uno script locale js, in fase di modifica della pagina, potrebbe contemporaneamente accedere, per qualsiasi elaborazione immaginabile, sia al testo in corso di edit, che al testo originale suddiviso in linee mappate sulla sulla pagina djvu originale e - con facili calcoli - sull'immagine jpg visualizzata a fronte.

Tutti i passi elementari:

  • estrazione djvutxt -detail=line;
  • parsing e trasformazione in oggetto python -> serializzazione in oggetto JSON;
  • meccanismo di caricamento/scaricamento dal testo della pagine dell'oggetto JSON;
  • creazione dell'oggetto js (lista di liste)

sono stati sperimentati nelle ultime 48 ore e non sono particolarmente complessi.

Il parsing dell'output djvutxt -detail=line modifica

Testo prodotto da djvutxt -page=55 La pastorizia.djvu:

Ifi	LA PASTORIZIA,
D'antichissime selve avete in cura,
Oreadi benigne, il vostro regno
Ne concedete; perocché, solenne
Ostia votiva, la miglior dell’agnc
A voi cadrà sui coronati altari
Devotamente al rinnovar d’ogn’anno.
Voi dagli estri mortiferi volanti
E dall’orride serpi e da vepraj
E dalle avvelenate erbe guardale
Le pecorelle ai pascoli. Per voi

Testo prodotto da djvutxt -page=55 -detail=line La pastorizia.djvu:

(page 268 465 868 1553 (line 269 1520 681 1553 "Ifi\tLA PASTORIZIA,") 
  (line 268 1479 750 1514 "D'antichissime selve avete in cura,") 
  (line 269 1432 707 1467 "Oreadi benigne, il vostro regno") 
  (line 268 1388 719 1423 "Ne concedete; perocché, solenne") 
  (line 270 1342 727 1379 "Ostia votiva, la miglior dell’agnc") 
  (line 268 1308 694 1335 "A voi cadrà sui coronati altari") 
  (line 268 1258 779 1289 "Devotamente al rinnovar d’ogn’anno.") 
  (line 268 1213 707 1245 "Voi dagli estri mortiferi volanti") 
  (line 268 1165 705 1203 "E dall’orride serpi e da vepraj") 
  (line 269 1124 730 1158 "E dalle avvelenate erbe guardale") 
  (line 269 1078 712 1115 "Le pecorelle ai pascoli. Per voi") 

Script per l'estrazione dei dati da una variabile testo che contiene l'output -detail=line::

righe=[];
patt=/(\d+)\s(\d+)\s(\d+)\s(\d+)\s\"(.+)\"\)\s\n/;
while (patt.test(testo)) {
   l=testo.match(patt);
   l1=l.slice(0,1);
   testo=testo.replace(l1,"");
   lista.push(l.slice(1));
}

Sul testo sopra, si ottiene una lista righe di 11 elementi del tipo:

["269", "1520", "681", "1553", "Ifi\tLA PASTORIZIA,"]

E' quindi possibile caricare su una pagina Pagina locale l'output grezzo di djvutxt -detail=line ed eseguire:

  1. il parsing;
  2. la ricostruzione del puro testo identico all'output di djvutxt per estrazione e accodamento delle stringhe in posizione 4;
  3. il salvataggio in una variabile locale dell'intero array righe;
  4. la serializzazione JSON di righe, che nel caso del testo di esempio è:

"[["269","1520","681","1553","Ifi\\tLA PASTORIZIA,"],["268","1479","750","1514","D'antichissime selve avete in cura,"],["269","1432","707","1467","Oreadi benigne, il vostro regno"],["268","1388","719","1423","Ne concedete; perocché, solenne"],["270","1342","727","1379","Ostia votiva, la miglior dell’agnc"],["268","1308","694","1335","A voi cadrà sui coronati altari"],["268","1258","779","1289","Devotamente al rinnovar d’ogn’anno."],["268","1213","707","1245","Voi dagli estri mortiferi volanti"],["268","1165","705","1203","E dall’orride serpi e da vepraj"],["269","1124","730","1158","E dalle avvelenate erbe guardale"],["269","1078","712","1115","Le pecorelle ai pascoli. Per voi"]]"

Roadmap modifica

Test in locale:

  • programma python per l'estrazione, pagina per pagina, di un output detail=line e il caricamento nelle pagine Pagina
  • creazione di una routine js che all'apertura di una pagina esamini se il testo proviene da un djvutxt -detail=line (es: presenza di una stringa "(page" e multiple stringhe "(line" )
  • se si:
    • lancio del parser javascript, costruzione e caricamento del testo in editbox, caricamento di array righe serializzato caricamento dell'array righe in un array righePagina;
  • se no:
    • nulla
  • al submit partirà una routine per la serializzazione di righePagina e per il caricamento di areaDatiPagina nella pagina.
  • ad ogni edit della pagina l'area dati verrà caricata in areaDatiPagina, righePagina verrà deserializzata e cancellata da editbox.

Gestione datiPagina modifica

datiPagina è un oggetto python, tipo dizionario, inizializzato in Common.js immediatamente prima di functionSALdelete():

datiPagina={};
 
function spanSALdelete() {
...

In fase di apertura in edit di una pagina Pagina, all'interno di spanSALdelete(), l'eventuale area dati viene identificata, cancellata del testo, deserializzata e caricata in datiPagina:

...
areaDatiPagina=find_stringa(editbox.value, "<!-- Area dati\n","\n-->",1);
        if (areaDatiPagina && areaDatiPagina!="") {
           editbox.value=editbox.value.replace(areaDatiPagina, "");
           datiPagina=JSON.parse(areaDatiPagina.replace("<!-- Area dati\n","").replace("\n-->",""));
           }

In fase di submit, un eventuale datiPagina non vuoto viene serializzato, marcato con codice commento html e aggiunto in fondo al testo della pagina nella funzione aggiungiSal():

if (JSON.stringify(datiPagina)!="{}") editbox.value+="<!-- Area dati\n"+JSON.stringify(datiPagina)+"\n-->";

In conclusione: datiPagina esiste solo in fase di edit di una pagina ed è inaccessibile all'utente, tranne che con javascript; la gestione è del tutto analoga a quella di proofreadpage_quality e di proofreadpage_username.

To do bot modifica

  • scaricare da Commons il file djvu
  • dumparlo e parsare il dump; memorizzare il numero pagine e l'array delle dimensioni delle pagine in un oggetto python   Fatto
  • liberare una directory djvutxt e per ogni pagina estrarre il txt con djvutxt -page=[numero] -detail=line >tex/[numero].txt   Fatto
  • caricare il contenuto dei file nell'oggetto python (trasfromato in unicode)   Fatto
  • aggiornare una lista di oggetti con coppia nome originale file-nome oggetto normalizzato e salvare l'oggetto con il nome normalizzato come pcl   Fatto
  • cancellare il djvu

secondo programma (caricamento pagine):

  • dalla lista file ottenere il nome pcl normalizzato e leggere l'oggetto   Fatto
  • per ogni pagina richiesta:
    • verificare che la pagina sia vuota   Fatto
    • caricare il pure-txt ottenuto per concatenamento delle righe di testo + i dati per datiPagina pronti per il parsing js   Fatto (il pure-txt viene calcolato da un js dall'oggetto datiPagina)

All'apertura di una pagina:

  • riconoscimento dell'area dati grezza   Fatto
  • parsing, memorizzazione in datiPagina, cancellazione area dati grezza   Fatto

Stato dell'arte 11 dicembre 2011 modifica

  • Funziona uno script python che, dato un djvu con layer testo, esegue l'estrazione e il parsing dei dati e inizializza le pagine Pagina.
  • La "guida" degli script è completamente manuale
  • il comando interattivo è del tipo:
  • >>> go([nome file djvu:stringa], [da pagina:numero], [a pagina:numero], [sovrascrivi:False|True])
  • I test sono stati fatti sulla seconda parte di Indice:La pastorizia.djvu da bot locale, e su Indice:Rivista di Scienza - Vol. II.djvu da bot su toolserver.
  • Nel secondo test sono stati calcolati via python e caricati anche alcuni "parametri pagina":
    • altezza "tipica" delle linee nella pagina
    • valore minimo del margine sin (min_x1) e massimo del margine dx (max_x2) nella pagina
    • pattern del margine pagina sin (stringa di valori 0,1,2,3, ognuno rappresenta lo scarto fra una riga e la successiva, 0=scarto minimo, 3=scarto notevole; l'indentatura paragrafi cade su 2 o su 3; un margine allineato è una stringa con molti 0 e pochi 2-3, un margine "seghettato" come quello dei versi è una stringa con molti 1,2,3 casuali, un margine "ripetitivo" come quello di versi in strofe con indentature regolari è una sequenza regolare e ripetitiva di 0 e di 2-3)
    • pattern del margine pagina dx

To do:

  1. routine caricamento djvu da Commons
  2. gestione cancellazioni dati intermedi
  3. interfaccia Python Cgi per l'interazione via web (manuale o automatica)
  4. inizio test formattazione automatica   Fatto (vedi elaborarighePagina() in Common.js e in Utente:Alex brollo/vector.js)
    1. centrato   Fatto ma non funziona su pagine in cui tutte le righe siano centrate (es. frontespizi), in quanto la riga più lunga viene presa come termine di riferimento e quindi non viene riconosciuta come centrata
    2. righe vuote (richiede la disponibilità di un'altezza linea tipica, per interpretare le pagine, tipo frontespizio, in cui vi siano poche righe a interlinea "esotica")
  5. calcolo dati "di formato generale" dell'opera
    1. altezza dell'interlinea tipica, espressa in % dell'altezza della pagina (da calcolare sulle pagine "di pieno testo", ossia quelle con il numero massimo di righe); il calcolo va fatto sull'insieme delle pagine, non può essere ricavato dall'analisi delle pagine singole (idem il punto successivo), a livello di elaborazione globale dei dati.
    2. ampiezza "tipica" della linea di testo (espressa in % della larghezza completa dell'immagine su pagine che abbiano le righe di maggior lunghezza).

Esempio: datiPagina di Pagina:Rivista di Scienza - Vol. II.djvu/21 modifica

Dati generali sulla pagina (altezza interlinea in pixel, coordinate x1 minima (riga più a sinistra) e x2 massima (riga che termina più a destra), pattern del margine sin (sostanziale allineamento con scarti, due dei quali a metà pagina) e pattern del margine dx (giustificato, con alcuni scarti coerenti e precedenti quelli del margine sin: fine paragrafi seguiti da un inizio indentato di nuovo paragrafo)

paramPagina: Object
interlinea: 76
max_x2: 1985
min_x1: 145
pattern_x1: "333000000000000033000000000033000000000"
pattern_x2: "330000000000000330000000000320000000000"

Array righe, con coordinate e testi delle prime righe. I valori 0,1,2,3 rappresentano x1,y1,x2,y2. Notare come nell'elemento 1 (con testo VII.) x1 sia "alto" e x2 sia "basso" in modo simmetrico, il che permette di concludere che il testo è centrato. Notare anche come la differenza fra y1 di elemento 3 e y1 di elemento vale 73, mentre la differenza fra elemento 1 e elemento 2 vale 167; il che significa che fra VII. e Non soltanto i numeri ottenuti misurando direttamente c'è uno spazio molto maggiore della normale interlinea.

righe: Array[40]
0: Array[5]
0: 337
1: 3525
2: 1985
3: 3574
4: "LE MISURAZIONI FISICHE E LA TEORÌA IiEOLr ERRORI 1,‘i"
length: 5
__proto__: Array[0]
1: Array[5]
0: 1030
1: 3323
2: 1152
3: 3369
4: "VII."
length: 5
__proto__: Array[0]
2: Array[5]
0: 299
1: 3156
2: 1983
3: 3218
4: "Non soltanto i numeri ottenuti misurando direttamente"
length: 5
__proto__: Array[0]
3: Array[5]
0: 174
1: 3083
2: 1981
3: 3144
4: "r.na quantità fìsica ( o come brevemente si dice: le osserra-"
length: 5
__proto__: Array[0]
..... 

Pausa: splitter e grouper modifica

In corso lavori per la classificazione in gruppi di valori numerici. Dopo un tentativo (riuscito, ma complesso) di analisi statistica, in corso lavori per un algoritmo molto più semplice basato su due piccolissimi algoritmi, molto rapidi:

  1. splitter(lista,intervalli) : suddivide i valori di una lista lista in classi, per intervallo, n. intervalli intervalli, calcolati suddividendo regolarmente i valori da min a max. Restituisce una lista di elementi [valore iniziale, valore finale, numero]
  2. grouper(lista): opera su lista, aggregando gli intervalli successivi con valori e eliminando gli intervalli senza valori.
  3. todo: ricorsivamente ripetere la sequenza splitter-grouper sugli intervalli con valori prodotti da grouper finchè il risultato non si stabilizza o non si polverizza in unità singole o l'intervallo non scende sotto un limite prefissato (x pixel, dove x potrebbe essere 5-10).

Tale algoritmo è necessario per classificare i valori-riga delle coordinate in gruppi omogenei (margini, altezza interlinee, "densità" del testo nella riga ecc), passo preliminare al loro riconoscimento, filtrando la variabilità casuale va evitando di definire limiti arbitrari.

Evidenziare una linea nell'immagine a fronte modifica

Per tracciare un rettangolo di larghezza 100px e altezza 15px nella div che contiene l'immagine a fronte, con l'angolo superiore sinistro a 600px dal top della div, e a 100px dal margine sinistro, di colore #aaaaff e con opacità 0.3, si scrive:

$('<div id="phe" style="position:absolute;
    top:600px;
    left:100px;
    width:100px;
    height:15px; 
    background-color: #aaaaff;  
    opacity:0.3;   
    filter:alpha(opacity=30); /* For IE8 and earlier */"></div>').appendTo('#pr_container')

Per cancellare la div phe:

$("#phe").remove()

I dati necessari per evidenziare una riga di testo sono:

  1. le dimensioni dell'immagine originale: datiPagina["xypagina"][0] e datiPagina["xypagina"][1]
  2. le dimensioni dell'immagine visualizzata: $('#ProofReadImage').attr("width")
  3. le coordinate della riga di testo nell'immagine originale: es, per la prima riga: datiPagina["righe"][0][0]....[3], rispettivamente x1,y1,x2,y2
  4. l'algoritmo di trasformazione delle coordinate originali per ridimensionarle e per posizionarle.
  5. fattoreScala=larghezza immagine/larghezza immagine originale=$('#ProofReadImage').attr("width")/datiPagina["xypagina"][0]   Fatto
  6. altezzaImmagine=altezza immagine originale*fattoreScala=datiPagina["xypagina"][1]*fattoreScala   Fatto
  7. posizioneX=x1*fattoreScala   Fatto
  8. posizioneY=(datiPagina["xypagina"][1]-datiPagina["righe"][0][3])*fattoreScala   Fatto
  9. larghezzaRiga=(datiPagina["righe"][0][2]-datiPagina["righe"][0][0])*fattoreScala   Fatto
  10. altezzaRiga=(datiPagina["righe"][0][3]-datiPagina["righe"][0][1])*fattoreScala   Fatto

e adesso proviamo....

$('<div id="test" style="position:absolute;top:'+posizioneY+'px;left:'+posizioneX+'px;width:'+larghezzaRiga+'px;height:'+altezzaRiga+'px; background-color: #aaaaff;  opacity:0.3;   filter:alpha(opacity=30); /* For IE8 and earlier */"></div>').appendTo('#pr_container')

  Fatto Urrà!!!!

 

= Cattura dell'intorno del cursore nel testo modifica

  1. cursore prima del doppio punto: (il doppio punto precedente)
    1. xx==$("textarea")[0] (catturo l'oggetto area di testo; ha un attributo selectionStart )
    2. intorno=$(xx).val().substring(xx.selectionStart-10,xx.selectionStart+10) risulta: "ppio punto: (il dopp"   Fatto

Miglior match dell'intorno del cursore nella lista righe modifica

(continua)

function simil(s1,s2,nc) {
if (nc==undefined) nc=4; 
var cont1=0; 
for (i=0;i<(s1.length-nc);i+=1) 
    {
    if (s2.indexOf(s1.substring(i,i+nc))!=-1) cont1+=1; 
    } 
return cont1/i;}

restituisce un valore da 0 (nessuna similitudine) a 1 (stringa 1 sottostringa di s2)

Quasi finito modifica

Mettendo tutto insieme:

// Funzione di somiglianza fra stringhe (ricerca di s1 all'interno di s2). Se s1 è sottoscringa di s2, il valore restituito 
// è 1; man mano che diminuisce la somiglianza, il valore decresce verso 0.
function simil(s1,s2,nc) {
if (nc==undefined) nc=4; 
var cont1=0; 
for (var i=0;i<(s1.length-nc);i+=1) 
    {if (s2.indexOf(s1.substring(i,i+nc))!=-1) cont1+=1;} 
return cont1/i;}
 
// All'interno di una lista di stringhe, scegliere la più simile a una stringa data (funzione generale)
function bestMatch(s1,lista) {
   var best=0; 
   var bestEl=0; 
   for (var i=0;i<lista.length;i+=1) {
      similCor=simil(s1,lista[i]); if (similCor>best) {best=similCor; bestEl=i;}
   } 
return bestEl;}
 
// restituisce l'intorno del puntatore (browser tipo Genko), da -10 a +10 caratteri
function getIntorno() {
boxTesto=$("textarea")[1]; 
var intorno=$(boxTesto).val().substring(boxTesto.selectionStart-10,boxTesto.selectionStart+10); 
return intorno;
}
 
// Adattamento per datiPagina. La funzione evidenzia, nelle pagine fornire di datiPagina e in edit, sull'immagine la riga
// contenente il puntatore nel box testo. 
function evidenziaRiga() {
   var s1=getIntorno();
   var best=0; 
   var bestEl=0; 
   for (var i=0;i<datiPagina["righe"].length;i+=1) {
      similCor=simil(s1,datiPagina["righe"][i][4]); if (similCor>best) {best=similCor; bestEl=i;}
   } 
highlightLine(bestEl);}
 
// Line highlighting: evidenzia una riga nell'immagine, nelle pagine in cui sono caricati i datiPagina; accetta un numero di
// riga
function highlightLine(riga) {
$("#lineHighlighting").remove();
if (datiPagina["righe"][riga]==undefined) {alert("Numero riga fuori scala o datiPagina non caricati"); return;}
fattoreScala=$('#ProofReadImage').attr("width")/datiPagina["xypagina"][0];
altezzaImmagine=datiPagina["xypagina"][1]*fattoreScala;
posizioneX=datiPagina["righe"][riga][0]*fattoreScala;
posizioneY=(datiPagina["xypagina"][1]-datiPagina["righe"][riga][3])*fattoreScala;
larghezzaRiga=(datiPagina["righe"][riga][2]-datiPagina["righe"][riga][0])*fattoreScala;
altezzaRiga=(datiPagina["righe"][riga][3]-datiPagina["righe"][riga][1])*fattoreScala;
$('<div id="lineHighlighting" style="position:absolute;top:'+posizioneY+'px;left:'+posizioneX+'px;width:'+larghezzaRiga+'px;height:'+altezzaRiga+'px; background-color: #aaaaff;  opacity:0.3;   filter:alpha(opacity=30); /* For IE8 and earlier */"></div>').appendTo('#pr_container');
}