Publicare: 24 martie 2025
Deadline: 9 aprilie 2025

Schelet de cod: tema1_pp_2025.zip

După ce ați descărcat scheletul, creați un proiect nou din IntelliJ în Scala, în care copiați folderele și fișierul din arhivă în root-ul proiectului (folderul src va fi suprascris). Dați restart la IDE și ar trebui să funcționeze.

După deadline fiecare student va prezenta tema la laborator, explicând în detaliu implementarea.
În soluție nu aveți voie să programați cu efecte laterale. Folosiți declarații doar val, nu var.

Un cod de bare este o reprezentare vizuală a datelor, ușor de citit de dispozitive. De obicei datele descriu proprietăți ale obiectului pe care se află codul de bare.

Codurile de bare tradiționale reprezintă datele prin varierea lățimilor liniilor și spațiilor paralele. Această reprezentare este lineară sau 1D. Există și reprezentări 2D, cum ar fi codurile QR, care au o logică de matrice și pot codifica mai multe date.

Un cod de bare este un număr reprezentat printr-o succesiune de bare negre (bare) și bare albe (spații). De obicei barele negre codifică biți de 1, iar barele albe biți de 0. Mai mulți biți alăturați pot crea o bară mai groasă. În imaginea de mai jos puteți vedea codul de bare pentru cifrele 5901234123457, după standardul European Article Number(EAN-13).

Codurile de bare descriu 13 cifre, însă doar 12 cifre sunt codificate efectiv deoarece prima cifră este codificată indirect. Fiecare cifra fiind codificată folosind 4 bare de lățimi diferite. Pe lângă barele care codifică cifre, există și o secvență de start de 3 bare, una de stop de 3 bare și una de centru de 5 bare (barele acestor secvențe speciale sunt mai lungi). În total sunt 59 de bare (12 * 4 + 3 + 3 + 5).
Puteți vedea mai multe în această secțiune.

Secvența de 59 de bare conține 95 de biți. De la stânga la dreapta aceștia sunt organizați astfel:

  1. 3 biți 101 pentru a marca startul
  2. 42 biți (7 pentru fiecare cifră) pentru cifrele de pe pozițiile 2→7 (prima cifră este codificată indirect)
  3. 5 biți 01010 pentru a marca centrul
  4. 42 biți (7 pentru fiecare cifră) pentru cifrele de pe pozițiile 8→13
  5. 3 biți 101 pentru a marca sfârșitul

Codificarea cifrelor

Vom considera cifrele numerotate de la stânga la dreapta după poziție, începând de la poziția 1.
Cifrele codului de bare sunt împărțite în 3 părți:

  • Prima cifră, numită cifră de paritate
  • Primul grup este reprezentat de următoarele 6 cifre, de pe pozițiile 2→7
  • Al doilea grup este reprezentat de ultimele 6 cifre, de pe pozițiile 8→13

Spunem despre codificarea unei cifre că e de paritate pară dacă conține un număr par de biți de 1. Altfel e impară.

Pentru fiecare cifră din primul grup (pozițiile 2→7) există două codificări posibile, una cu un număr impar de biți de 1 (codificare L, paritate impară) și una cu un numar par de biți de 1 (codificare G, paritate pară).

Pentru fiecare cifră din al doilea grup (pozițiile 8→13) există o singură codificare posibilă (codificare R paritate pară ).

Cifra Codificare-L Codificare-G Codificare-R
0 0001101 0100111 1110010
1 0011001 0110011 1100110
2 0010011 0011011 1101100
3 0111101 0100001 1000010
4 0100011 0011101 1011100
5 0110001 0111001 1001110
6 0101111 0000101 1010000
7 0111011 0010001 1000100
8 0110111 0001001 1001000
9 0001011 0010111 1110100

Observații:

  • Codul de bare începe cu o cifră codificată impar și se termină cu o cifră codificată par, astfel scannerele pentru codurile de bare pot determina orientarea și să citească și de la stânga la dreapta, dar și invers.
  • Codificările-R sunt complementele pe biți ale codificărilor-L.
  • Codificările-G sunt inversele codificărilor-R.
  • Codificările G și L încep cu 0 și se termină cu 1. Codificările R încep cu 1 și se termina cu 0. Astfel, fiecare cifră va fi reprezentată prin 4 bare alternante de grosimi diferite.
  • Grosimea maximă a unei bare va fi 4. Barele a două cifre nu se amestecă.

Cifra de paritate

După cum am spus anterior, prima cifră se numește cifră de paritate. Cifra de paritate nu este reprezentată direct printr-o succesiune de bare și spații, ci este codificată indirect, prin alegerea unei combinații de moduri de codificare L sau G pentru primul grup de 6 cifre, conform tabelului de mai jos. Practic, este suficient să știm ce codificare a fost folosită pentru fiecare dintre cele 6 cifre din primul grup pentru a determina cifra de paritate asociată. Dacă combinația găsită nu este asociată cu o cifră din tabelul de mai jos codul este invalid.

Cifra de paritate Primul grup
0 LLLLLL
1 LLGLGG
2 LLGGLG
3 LLGGGL
4 LGLLGG
5 LGGLLG
6 LGGGLL
7 LGLGLG
8 LGLGGL
9 LGGLGL

Calculare cifră de control

Ultima cifră a unui cod de bare EAN-13 se numeste cifră de control. Este folosită pentru a confirma citirea corectă a unui cod. Fiecare cifră din codul de bare (fară cea de control), are o pondere de 1 sau 3 în funcție de poziție la calculul cifrei de control (cele de pe poziții impare au pondere 1, iar cele de pe poziții pare au pondere 3, vezi tabelul de mai jos).

Poziție 1 2 3 4 5 6 7 8 9 10 11 12
Pondere 1 3 1 3 1 3 1 3 1 3 1 3

Formula pentru cifra de control este următoarea:

$ C = \left(10 - \left( \sum_{i=1}^{12} w_i \cdot c_i \right) \mod 10 \right) \mod 10$

Unde $ w_i$ este ponderea cifrei de pe poziția $ i$ și $ c_i$ este cifra de pe poziția $ i$ .

Exemplu

Să revenim la imaginea pentru codul de bare 5901234123457 din introducere.

Reprezentarea binară a acestor bare este următoarea: 10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101
Prima cifră, adică cifra de paritate, are valoarea 5. Conform tabelului de mai sus, asta va duce la codificarea următoarelor 6 cifre în formatul LGGLLG. Ultimele 6 cifre sunt codificate mereu în formatul RRRRRR.

Cifra start 9 0 1 2 3 4 centru 1 2 3 4 5 7 end
Codificare - L G G L L G - R R R R R R -
Reprezentare 101 0001011 0100111 0110011 0010011 0111101 0011101 01010 1100110 1101100 1000010 1011100 1001110 1000100 101

Acum să verificăm dacă cifra de control 7 este corectă.

Poziție 1 2 3 4 5 6 7 8 9 10 11 12
Pondere 1 3 1 3 1 3 1 3 1 3 1 3
Cifra 5 9 0 1 2 3 4 1 2 3 4 5

Suma produselor dintre cifre si ponderile asociate este 83. Cifra de contol este (10 - (83 % 10)) % 10 = 7, deci este corectă.

Implementare

Avem imagini cu codurile de bare (în format .ppm) ale unor produse care trebuie cumpărate. Veți implementa un decodificator simplificat de coduri de bare. Cu ajutorul acestuia magazinele obțin numărul EAN-13 si cu ajutorul bazei de date identifică produsul vândut și calculează prețul.
Veți primi codul pentru parsarea si prelucrarea de imagini, care face trecerea de la formatul color .ppm la formatul alb-negru .pbm, voi fiind nevoiți să implementați doar logica unui decodificator care primește o matrice de pixeli de 0 și de 1. Această matrice corespunde unei imagini alb-negru, fiecare pixel negru având valoarea 1 și fiecare pixel alb având valoarea 0.
Vom identifica cifrele bazându-ne pe grosimile relative ale barelor față de o unitate elementară, adică față de o bară care codifică un singur bit.

Veți rezolva TODO-urile din fișierul Decoder. Nu este nevoie să faceți nimic cu celelalte fișiere, ele v-au fost puse la dispoziție doar în scop didactic.

Notă: Pentru vizualizarea imaginilor .ppm pe WSL/Linux puteți folosi:

sudo apt install feh
feh image.ppm

Algoritm

Primim o matrice de biți 0 și 1, reprezentând imaginea în formatul alb-negru descris anterior. Decupăm din matricea de pixeli rândurile din mijloc și rulăm algoritmul de identificare a barelor pe toate aceste rânduri, pentru a avea șanse mai mari de succes. Vrem să găsim barele din imagine, așa că grupăm biții identici și consecutivi în tupluri de tipul (<număr_repetări>, <bit>), unde tuplul descrie grosimea și culoarea barei din imagine. Va rezulta următorul comportament:
0001111111010001100011111 => [(3, 0), (7, 1), (1, 0), (1, 1), (3, 0), (2, 1), (3, 0), (5, 1)]

Căutăm o secvență de 59 de bare pentru fiecare rând din imagine în care identificăm secvențele de start și de stop.
Secvențele de 59 de bare care conțin secvențele de start și stop din fiecare rând sunt returnate de funcția checkRow, a cărei implementare este dată de echipă.

Din secvența de 59 de bare extragem grupuri de 4 bare care reprezintă codificarea unei cifre (după ce am eliminat secvențele de bare care marchează start, centru și sfârșit). Pentru primele 6 cifre găsim codificarea cea mai apropiată L sau G, iar pentru ultimele 6 cifre găsim cea mai apropiată codificare R.

Pentru a identifica cifra codificată de un set de 4 bare vom calcula distanța dintre secvența de bare din poză și secvențele de bare pentru codificările L, G sau R. Vom alege codificarea cea mai apropiată (de distanță minimă) pentru fiecare din cele 12 cifre.

De la formatul descris pentru bare vom lua grupe de câte 4 bare care reprezintă o cifră și vom trece la formatul dimensiunilor relative ale segmentelor, impărțind numărul de aparitii la lungimea totală.
[(3, 0), (7, 1), (1, 0), (1, 1)] => [(3/12, 0), (7/12, 1), (1/12, 0), (1/12, 1)]

Pentru că biții alternează întotdeauna, vom reduce codificarea la un tuplu format din primul bit și dimensiunile relative ale fiecărei bare:
[(3/12, 0), (7/12, 1), (1/12, 0), (1/12, 1)] => (0, (3/12, 7/12, 1/12, 1/12))

Având codificările standard ale cifrelor în același format, vom alege codificarea care începe cu același bit și pentru care distanța (suma modulelor diferențelor dintre rapoartele aflate pe aceeași poziție) este minimă, adică codificarea pentru care barele respectă proporții de grosime similare.

După ce am identificat ultimele 12 cifre ale codului de bare, vom determina cifra de paritate. Pentru a determina cifra de paritate ne vom uita la paritatea codificărilor identificate pentru cele 6 cifre din primul grup. vezi mai sus.

După ce am determinat cifra de paritate, calculăm cifra de control și verificăm dacă este identică cu cea recunoscută din poză. vezi mai sus.

Dacă cifra de control este corectă, am identificat toate cifrele și întoarcem rezultatul. Altfel întoarcem None.

Recapitulare functii de ordin superior

Recapitulare functii de ordin superior

map()

  • Poate fi aplicată pe orice colecție (ex: List).
  • Primește o funcție ca parametru și aplică funcția respectivă pe fiecare element al colecției.
  • Întoarce o colecție de același tip (dacă era o colecție de tip List, rămâne tot List după aplicarea map()).

Exemplu:

val initialList : List[Int] = List(1, 2, 3, 5)
 
// Folosind o funcție
def func(x : Int) : Int = x * 2
val listAfterMapWithFunc: List[Int] = initialList.map(func) // List(2, 4, 6, 10)
 
// Folosind lambda
val listAfterMapWithLambda: List[Int] = initialList.map(x => x + 7) // List(8, 9, 10, 12)

Notă: Ultima variantă este echivalenta cu

 initialList.map(_ + 7) 

zip()

  • Este aplicată pe o colecție, primește ca parametru o altă colecție.
  • Întoarce o colecție de tupluri cu elementele din cele două colecții.

Exemplu:

val firstList : List[Int] = List(3, 8, 7, 5)
val secondList : List[String] = List("verde", "rosu", "albastru", "galben")
 
val result : List[(Int, String)]= firstList.zip(secondList) // List((3,verde), (8,rosu), (7,albastru), (5,galben))

foldRight() & foldLeft()

  • Operația de folding pe o listă în scala înseamnă aplicarea unei operații între un acumulator și fiecare element din listă, rezultatul fiind valoarea acumulatorului după parcurgerea întregii liste.

foldLeft(z: B)(op: (B, A) ⇒ B): B

  • Primește un acumulator inițial și o funcție, întoarce un acumulator.
  • Funcția parametru primește un tuplu format dintre acumulator și un element al listei și întoarce un acumulator.
  • Aplică operația pe listă de la stânga la dreapta.
val list : List[Int] = List(1, 2, 3)
// (((0 - 1) - 2) - 3)
val result : Int = list.foldLeft(0)((acc, el) => acc - el) // -6

Notă: Poate fi rescris ca

 list.foldLeft(0)(_-_) 

foldRight(z: B)(op: (A, B) ⇒ B): B

  • Primește un acumulator inițial și o funcție, întoarce un acumulator.
  • Funcția parametru primește un tuplu format dintre un element al listei și acumulator și întoarce un acumulator.
  • Aplică operația pe listă de la dreapta la stânga.

Exemplu:

val list : List[Int] = List(1, 2, 3)
// (1 - (2 - (3 - 0)))
val result : Int = list.foldRight(0)((el, acc) => el - acc) // 2

Notă: Poate fi rescris ca

 list.foldRight(0)(_-_) 

1.1. Avem nevoie de un TDA pentru biți. Realizați conversiile de la Char și Int la Bit.

def toBit(s: Char): Bit = ???
def toBit(s: Int): Bit = ???

Hint: Inspectați fișierul Types.

1.2. Implementați funcția care întoarce complementul unui bit.

def complement(c: Bit): Bit = ???

1.3. Pornind de la LStrings care este lista codificărilor L ca String (pe fiecare poziție gasim codificarea sa L), definiți listele pentru toate cele 3 tipuri de codificări. Soluțiile hardcodate nu vor fi luate în considerare, trebuie să vă folosiți de LStrings sau de listele deja create (pentru rightOddList și leftEvenList) împreună cu funcții de ordin superior.

val leftOddList: List[List[Bit]] = Nil // codificări L
val rightList: List[List[Bit]] = Nil // codificări R
val leftEvenList: List[List[Bit]] = Nil // codificări  G

1.4. Implementați funcția group care grupează elementele egale și consecutive dintr-o listă în liste separate.

def group[A](l: List[A]): List[List[A]] = ???

Hint: group([1, 1, 2, 2, 3, 3, 1]) = [[1, 1], [2, 2], [3, 3], [1]]

1.5. Implementați funcția runLength care grupează elementele egale și consecutive dintr-o listă în elemente noi de tipul (<număr_apariții>, <element>).

def runLength[A](l: List[A]): List[(Int, A)] = ???

Hint: Folosiți group implementat anterior.

2.1. Vrem să folosim tipul RatioInt pentru lucrul cu fracții. Implementați operațiile uzuale cu fracții în clasa RatioInt.

def -(other: RatioInt): RatioInt = ???
def +(other: RatioInt): RatioInt = ???
def *(other: RatioInt): RatioInt = ???
def /(other: RatioInt): RatioInt = ???

2.2. Implementați funcția de comparare a doua fracții. Întoarce -1 daca fracția e mai mică decât other, 1 dacă e mai mare și 0 dacă sunt egale.

def compare(other: RatioInt): Int = ???

3.1. Implementați funcția care primește o lista de elemente de tipul (<număr_apariții>, <element>) și întoarce o listă cu elemente de tipul (<frecvență relativă>, <element>), unde $ \text{frecvență relativă}=\dfrac{\text{număr apariții}}{\text{număr total de elemente}}$ , unde elementele sunt cele din lista inițială pe care am rulat runLength.

def scaleToOne[A](l: List[(Int, A)]): List[(RatioInt, A)] = ???

3.2. Implementați funcția care primeste o listă de biți și întoarce un tuplu format din primul bit și o listă cu dimensiunile relative ale segmentelor de biți egali.

def scaledRunLength(l: List[(Int, Bit)]): (Bit, List[RatioInt]) = ???

3.3. Pornind de la un șir de caractere ce reprezintă parități, unde G înseamnă par, iar L impar, convertiți-l la List[Parity].

def toParities(s: Str): List[Parity] = ???

3.4. Creați lista de parițăți pe care o vom folosi pornind de la PStrings. Soluțiile hardcodate nu vor fi luate în considerare, trebuie să vă folosiți de PStrings

val leftParityList: List[List[Parity]] = Nil

3.5. Rulați scaledRunLength pe fiecare element din listele create la task-ul 1.3. Soluțiile hardcodate nu vor fi luate în considerare, trebuie să vă folosiți de listele create anterior.

type SRL = (Bit, List[RatioInt])
val leftOddSRL:  List[SRL] = Nil
val leftEvenSRL:  List[SRL] = Nil
val rightSRL:  List[SRL] = Nil

Hint: Folosiți și runLength.

4.1. Definiți funcția de distanță dintre două codificări, care primește tupluri cu primul bit, respectiv lungimile relative ale segmentelor unei codificări (tip definit ca SRL) și întoarce suma modulelor diferențelor dintre rapoartele aflate pe aceeași poziție în ambele codificări.

def distance(l1: SRL, l2: SRL): RatioInt = ???

Hint: Dacă codificările sunt alcătuite din bare complementare (■□■□ vs □■□■, unde □ reprezintă o bară de 0, iar ■ o bară de 1), puteți întoarce o distanță “infinită” (un număr foarte mare), de exemplu RatioInt(100, 1)

Hint: Folosiți zip între listele primite.

4.2. Definiți funcția bestMatch care primește ca parametrii o listă de codificări L, G sau R în forma SRL și codificarea binară a unei cifre și găsește cea mai bună potrivire din listă. Întoarce un tuplu cu cea mai mică distanță și cifra găsită.

def bestMatch(SRL_Codes: List[SRL], digitCode: SRL): (RatioInt, Digit) = ???

Hint: Folosiți zip și min.

4.3. Întoarce paritatea și cea mai bună potrivire pentru o cifră din grupul din stânga (cifrele 2-7).

def bestLeft(digitCode: SRL): (Parity, Digit) = ???

Hint: Folosiți-vă de leftOddSRL și leftEvenSRL.

4.4. Întoarce cea mai bună potrivire pentru o cifră din grupul din dreapta (ultimele 6 cifre). Pentru codificările R vom folosi tipul NoParity.

def bestRight(digitCode: SRL): (Parity, Digit) = ???

Hint: Folosiți-vă de rightSRL.

4.5. Primiți ca argument rezultatul lui runLength pe o listă de biți. Trebuie să verificați că secvența dată are lungimea de 59 (vedeți în secțiunea Algoritm de ce). Delimitați primul grup (stânga) și al doilea grup (dreapta) folosindu-vă de chunksOf. Ignorați verificarea pentru secvențele de start, centru și sfârșit (a fost făcută la checkRow). Pentru fiecare grup de 4 bare găsiți cea mai potrivită cifră. Reuniți rezultatele din cele două liste intr-una singură. Rezultatul va avea 12 cifre.

def findLast12Digits(rle:  List[(Int, Bit)]): List[(Parity, Digit)] = ???

Hint: Folosiți funcțiile drop și take pe listă pentru a separa grupurile de câte 6 cifre din partea stângă și din partea dreaptă. Va trebui să ignorați primele 3 bare, 5 bare de la mijloc și ultimele 3 bare.

4.6. Găsește prima cifră din codul de bare pe baza parităților cifrelor din primul grup (cel din stânga). Pentru prima cifră vom considera paritatea NoParity.

def firstDigit(l: List[(Parity, Digit)]): Option[Digit] = ???

Hint: Puteți folosi zipWithIndex și leftParityList.

4.7. Calculează cifra de control (ultima cifră) pe baza primelor 12 cifre dintr-un cod de bare.

def checkDigit(l: List[Digit]): Digit = ???

Hint: Găsiți formula aici.

4.8. Pentru un cod de bare dat verificați dacă este valid. Funcția primește o listă cu perechi de (Paritate, Cifra) și verifică dacă sunt 13 cifre și dacă cifra de paritate și cifra de control sunt corecte.

def verifyCode(code: List[(Parity, Digit)]): Option[String] = ???

Hint: Folosiți firstDigit și checkDigit.

4.9. Definiți funcția de solve care primește rezultatul lui runLength și întoarce cele 13 cifre reprezentate de codul de bare, dacă potrivirea găsită este un cod valid.

def solve(rle:  List[(Int, Bit)]): Option[String] = ???

Hint: Folosiți funcțiile findLast12Digits, firstDigit si verifyCode.

În folderul src/test din proiect se află Test.scala, care poate fi rulat din IntelliJ apăsând pe butonul de Run din partea stângă a codului. Fiecare test are un buton propriu și poate fi rulat separat. Există teste pentru cele mai importante funcții, vă recomandăm să testați funcțiile de bază înainte de a începe implementarea task-urilor avansate.

La rularea testelor veți primi și un punctaj, după specificațiile din enunț.

Dacă folosiți terminalul:

sbt test
Veti incarca pe moodle o arhivă ce conține, in rădăcina acesteia, folderul src al proiectului vostru, fișierul build.sbt și un fișier text, intitulat ID.txt ce conține o singură linie, și anume id-ul vostru anonim (pe care il puteti găsi pe moodle la assignment-ul tokenID). Arhiva trebuie să fie obligatoriu .zip.

Exemplu structura arhivă:

archive.zip
|-src/
| |-main/
| | |-scala/
| | | | - ... <fisierele cu sursa scala>
|-build.sbt
|-ID.txt

Găsiți fișierul MyBarcodes.scala care poate fi rulat din IDE. Funcția readBarcodes primește numele unui folder de input și a unui folder de output. În cel de input puteți pune propriile imagini cu coduri de bare în format .ppm. Decupați doar codul de bare din imagini și aveți grijă ca acestea să fie suficient de clare, altfel algoritmul nu va funcționa. Puteți converti imagini în formatul .ppm pe https://convertio.co/. În folderul de output puteți vedea aceleași imagini .ppm, dar în format alb-negru (adică .pbm).