Google Cloud Vision – czyli rozpoznawanie obrazów w chmurze, część 1: OCR

Google udostępniło API pozwalające na rozpoznawanie obrazów. W liście funkcjonalności możemy znaleźć wiele ciekawych pozycji:

  • Label Detection – kategoryzowanie zawartości obrazka, możemy otrzymać informację, że na obrazku znajduje się zwierzę,
  • Explicit Content Detection – wykrywanie nieodpowiedniej zawartości – takiej jak przemoc czy treści dla dorosłych
  • Logo Detection – wykrywa logo znanych marek,
  • Landmark Detection – wykrywanie znanych budowli,
  • Optical Character Recognition (OCR) – rozpoznawanie tekstu,
  • Face Detection – wykrywanie twarzy na zdjęciu,
  • Image Attributes – podaje informacje o obrazie, np. dominujący kolor

W tym poście pokażę jak skorzystać z rozpoznawania tekstu na przykładzie zdjęcia paragonu 🙂

Jak zawsze przykładowy kod można znaleźć na moim GitHubie w postaci gotowego do uruchomienia projektu. Kod z postu znajduje się pod adresem: https://github.com/mloza/google-cloud-storage

Chciałem jeszcze wcześniej wspomnieć o bardzo ważnej rzeczy, a mianowicie Vison API nie rozpoznaje polskich znaków, wspiera tylko alfabet łaciński. Jednak w zamian oferuje pierwsze 1,000 requestów do API za darmo.

2016-05-29 15.15.57

Punktem wyjścia będzie projekt z poprzedniego postu o Google Cloud Storage, tam pokazywałem jak się uwierzytelnić i jak wysłać coś do chmury. Tym razem też musimy przesłać obrazek do Cloud Storage przed jego przetworzeniem. Jak już wspomniałem wcześniej, naszą ofiarą będzie zdjęcie paragonu:

Zdjęcie jest wyraźne, paragon lekko pomięty, ale wciąż bardzo czytelny. Nie powinno być problemu z rozpoznaniem zawartości. Zatem do dzieła.

Przesłanie obrazka do bucketu.

Autoryzacja i tworzenie obiektów API opisywałem w poprzednim poście. Kod znajduje się również w GitHubie. Dla przypomnienia kod, który tworzy klienta Google Cloud Storage, wygląda następująco:

HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
JsonFactory jsonFactory = new JacksonFactory();
Credential credential = GoogleCredential
.fromStream(
Main.class.getClassLoader()
.getResourceAsStream("client-secrets.json"))
.createScoped(Collections.singleton(StorageScopes.CLOUD_PLATFORM));
Storage storage = new Storage.Builder(httpTransport, jsonFactory, credential)
.setApplicationName("Test project")
.build();
view raw Credentials.java hosted with ❤ by GitHub

Pierwszym krokiem jest przesłanie zdjęcia do chmury, gdzie usługa Vision będzie mogła się do niego dostać. Odpowiada za to następujący kod:

InputStreamContent mediaContent = new InputStreamContent("image/jpeg",
getClass().getClassLoader().getResourceAsStream(PICTURE_NAME));
StorageObject object = storage
.objects()
.insert(BUCKET, null, mediaContent)
.setName(PICTURE_NAME)
.execute();
System.out.println("Adres przesłanego obrazu: "+object.getSelfLink());
view raw Storage.java hosted with ❤ by GitHub

Mamy już obrazek w buckecie, Yay!

Rozpoznawanie tekstu

Potrzebujemy nowej zależności w pom.xml do biblioteki vision:

<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-vision</artifactId>
<version>v1-rev15-1.22.0</version>
</dependency>
view raw dependency.xml hosted with ❤ by GitHub

Podobnie jak do Storage tworzymy sobie klienta Vision, który pozwoli nam na wywoływanie metod z API:

Vision visionClient = new Vision.Builder(httpTransport, jsonFactory, credential)
.setApplicationName(APPLICATION_NAME)
.build();
view raw Client.java hosted with ❤ by GitHub

Mając klienta, możemy przejść do wywołania żądania przetworzenia obrazu. Jest ono dość duże w porównaniu z poprzednimi przykładami kodu:

Image image = new Image().setSource(new ImageSource() //1
.setGcsImageUri("gs://" + BUCKET + "/" + PICTURE_NAME));
Feature annotateFeature = new Feature() //2
.setType("TEXT_DETECTION");
AnnotateImageRequest annotateImage = new AnnotateImageRequest() // 3
.setFeatures(Collections.singletonList(annotateFeature))
.setImage(image);
BatchAnnotateImagesResponse text_detection = visionClient // 4
.images()
.annotate(
new BatchAnnotateImagesRequest()
.setRequests(Collections.singletonList(annotateImage)))
.execute();
System.out.println(text_detection.getResponses().get(0).getTextAnnotations()); //5
view raw Request.java hosted with ❤ by GitHub

Najpierw tworzymy obiekt reprezentujący nasz obrazek, podając url w postaci gs://nazwa-bucketu/nazwa-obrazka.jpg [1]. Następnie tworzymy obiekt, który będzie reprezentował, co chcemy otrzymać z obrazka, w naszym przypadku będzie to rozpoznawanie tekstu [2]. Kolejnym krokiem jest stworzenie obiektu zapytania, przekazując mu wcześniej stworzony obrazek oraz wymagany tryb przetwarzania [3]. Ostatnim krokiem jest wysłanie zapytania z użyciem wcześniej stworzonego klienta [4]. W jednym zapytaniu możemy przekazać większą ilość obrazków do przetworzenia, jeśli zajdzie taka potrzeba.

W odpowiedzi dostaniemy JSON, który zawiera pełny tekst z obrazka oraz poszczególne słowa/fragmenty wraz z koordynatami, w którym miejscu został rozpoznany. Odpowiedź z serwisu dotycząca naszego testowego zdjęcia paragonu wygląda następująco (uwaga, 1429 linii po sformatowaniu!):

[
{
"boundingPoly": {
"vertices": [
{
"x": 337,
"y": 801
},
{
"x": 2242,
"y": 801
},
{
"x": 2242,
"y": 2787
},
{
"x": 337,
"y": 2787
}
]
},
"description": "SKLEP \"ZABKA'' Z3816\nul. Mogilska 59, 31-545 Krakow\nPHU \"DANA\"\nNIP 678-185-16-34\n2016-05-29\nnr Wydr 808669\nPARAGON FISKALNY\nMLEKO KONECKIE D\n2,50 zh. 2,50 D\nMLEKO KONECKIE D 1 2,50 zt, 2,50 D\nSprzed. opod. PTU D\n5,00\nKwota D 05,00%\n0,24\nPodatek PTU\n0,24\nSUMA PLN\n5,00\n0003241 5 MACIEK\n08:43\n1EIOS-NLNTD-0GXTD-ON09 G-XYUSE\nBAE 13112254\nPtatno\na kred\n5,00\n501782\n",
"locale": "pl"
},
{
"boundingPoly": {
"vertices": [
{
"x": 876,
"y": 801
},
{
"x": 1089,
"y": 801
},
{
"x": 1089,
"y": 898
},
{
"x": 876,
"y": 898
}
]
},
"description": "SKLEP"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1133,
"y": 801
},
{
"x": 1432,
"y": 801
},
{
"x": 1432,
"y": 898
},
{
"x": 1133,
"y": 898
}
]
},
"description": "\"ZABKA''"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1475,
"y": 801
},
{
"x": 1693,
"y": 801
},
{
"x": 1693,
"y": 898
},
{
"x": 1475,
"y": 898
}
]
},
"description": "Z3816"
},
{
"boundingPoly": {
"vertices": [
{
"x": 662,
"y": 898
},
{
"x": 778,
"y": 898
},
{
"x": 778,
"y": 1009
},
{
"x": 662,
"y": 1009
}
]
},
"description": "ul."
},
{
"boundingPoly": {
"vertices": [
{
"x": 825,
"y": 898
},
{
"x": 1167,
"y": 898
},
{
"x": 1167,
"y": 1009
},
{
"x": 825,
"y": 1009
}
]
},
"description": "Mogilska"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1212,
"y": 898
},
{
"x": 1337,
"y": 898
},
{
"x": 1337,
"y": 1009
},
{
"x": 1212,
"y": 1009
}
]
},
"description": "59,"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1386,
"y": 898
},
{
"x": 1648,
"y": 898
},
{
"x": 1648,
"y": 1009
},
{
"x": 1386,
"y": 1009
}
]
},
"description": "31-545"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1692,
"y": 898
},
{
"x": 1959,
"y": 898
},
{
"x": 1959,
"y": 1009
},
{
"x": 1692,
"y": 1009
}
]
},
"description": "Krakow"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1086,
"y": 1010
},
{
"x": 1214,
"y": 1013
},
{
"x": 1211,
"y": 1107
},
{
"x": 1083,
"y": 1104
}
]
},
"description": "PHU"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1259,
"y": 1013
},
{
"x": 1512,
"y": 1020
},
{
"x": 1509,
"y": 1114
},
{
"x": 1256,
"y": 1107
}
]
},
"description": "\"DANA\""
},
{
"boundingPoly": {
"vertices": [
{
"x": 914,
"y": 1101
},
{
"x": 1037,
"y": 1106
},
{
"x": 1033,
"y": 1206
},
{
"x": 910,
"y": 1201
}
]
},
"description": "NIP"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1082,
"y": 1107
},
{
"x": 1644,
"y": 1132
},
{
"x": 1640,
"y": 1231
},
{
"x": 1078,
"y": 1207
}
]
},
"description": "678-185-16-34"
},
{
"boundingPoly": {
"vertices": [
{
"x": 390,
"y": 1208
},
{
"x": 824,
"y": 1227
},
{
"x": 820,
"y": 1313
},
{
"x": 386,
"y": 1294
}
]
},
"description": "2016-05-29"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1592,
"y": 1236
},
{
"x": 1680,
"y": 1232
},
{
"x": 1684,
"y": 1332
},
{
"x": 1596,
"y": 1336
}
]
},
"description": "nr"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1722,
"y": 1231
},
{
"x": 1907,
"y": 1223
},
{
"x": 1911,
"y": 1323
},
{
"x": 1726,
"y": 1331
}
]
},
"description": "Wydr"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1949,
"y": 1220
},
{
"x": 2236,
"y": 1207
},
{
"x": 2240,
"y": 1307
},
{
"x": 1953,
"y": 1320
}
]
},
"description": "808669"
},
{
"boundingPoly": {
"vertices": [
{
"x": 935,
"y": 1321
},
{
"x": 1254,
"y": 1328
},
{
"x": 1252,
"y": 1433
},
{
"x": 933,
"y": 1426
}
]
},
"description": "PARAGON"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1294,
"y": 1329
},
{
"x": 1638,
"y": 1336
},
{
"x": 1636,
"y": 1441
},
{
"x": 1292,
"y": 1434
}
]
},
"description": "FISKALNY"
},
{
"boundingPoly": {
"vertices": [
{
"x": 379,
"y": 1421
},
{
"x": 602,
"y": 1421
},
{
"x": 602,
"y": 1515
},
{
"x": 379,
"y": 1515
}
]
},
"description": "MLEKO"
},
{
"boundingPoly": {
"vertices": [
{
"x": 642,
"y": 1413
},
{
"x": 983,
"y": 1417
},
{
"x": 981,
"y": 1541
},
{
"x": 640,
"y": 1537
}
]
},
"description": "KONECKIE"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1029,
"y": 1421
},
{
"x": 1072,
"y": 1421
},
{
"x": 1072,
"y": 1515
},
{
"x": 1029,
"y": 1515
}
]
},
"description": "D"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1547,
"y": 1443
},
{
"x": 1720,
"y": 1443
},
{
"x": 1720,
"y": 1525
},
{
"x": 1547,
"y": 1525
}
]
},
"description": "2,50"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1765,
"y": 1427
},
{
"x": 1899,
"y": 1429
},
{
"x": 1897,
"y": 1553
},
{
"x": 1763,
"y": 1551
}
]
},
"description": "zh."
},
{
"boundingPoly": {
"vertices": [
{
"x": 1944,
"y": 1443
},
{
"x": 2140,
"y": 1443
},
{
"x": 2140,
"y": 1525
},
{
"x": 1944,
"y": 1525
}
]
},
"description": "2,50"
},
{
"boundingPoly": {
"vertices": [
{
"x": 2186,
"y": 1443
},
{
"x": 2233,
"y": 1443
},
{
"x": 2233,
"y": 1525
},
{
"x": 2186,
"y": 1525
}
]
},
"description": "D"
},
{
"boundingPoly": {
"vertices": [
{
"x": 377,
"y": 1533
},
{
"x": 588,
"y": 1533
},
{
"x": 588,
"y": 1622
},
{
"x": 377,
"y": 1622
}
]
},
"description": "MLEKO"
},
{
"boundingPoly": {
"vertices": [
{
"x": 640,
"y": 1523
},
{
"x": 981,
"y": 1527
},
{
"x": 980,
"y": 1643
},
{
"x": 639,
"y": 1639
}
]
},
"description": "KONECKIE"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1024,
"y": 1526
},
{
"x": 1101,
"y": 1527
},
{
"x": 1100,
"y": 1643
},
{
"x": 1023,
"y": 1642
}
]
},
"description": "D"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1255,
"y": 1529
},
{
"x": 1332,
"y": 1530
},
{
"x": 1331,
"y": 1646
},
{
"x": 1254,
"y": 1645
}
]
},
"description": "1"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1547,
"y": 1553
},
{
"x": 1719,
"y": 1553
},
{
"x": 1719,
"y": 1639
},
{
"x": 1547,
"y": 1639
}
]
},
"description": "2,50"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1762,
"y": 1553
},
{
"x": 1894,
"y": 1553
},
{
"x": 1894,
"y": 1639
},
{
"x": 1762,
"y": 1639
}
]
},
"description": "zt,"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1944,
"y": 1553
},
{
"x": 2141,
"y": 1553
},
{
"x": 2141,
"y": 1639
},
{
"x": 1944,
"y": 1639
}
]
},
"description": "2,50"
},
{
"boundingPoly": {
"vertices": [
{
"x": 2186,
"y": 1553
},
{
"x": 2232,
"y": 1553
},
{
"x": 2232,
"y": 1639
},
{
"x": 2186,
"y": 1639
}
]
},
"description": "D"
},
{
"boundingPoly": {
"vertices": [
{
"x": 371,
"y": 1743
},
{
"x": 675,
"y": 1743
},
{
"x": 675,
"y": 1841
},
{
"x": 371,
"y": 1841
}
]
},
"description": "Sprzed."
},
{
"boundingPoly": {
"vertices": [
{
"x": 718,
"y": 1743
},
{
"x": 926,
"y": 1743
},
{
"x": 926,
"y": 1841
},
{
"x": 718,
"y": 1841
}
]
},
"description": "opod."
},
{
"boundingPoly": {
"vertices": [
{
"x": 973,
"y": 1743
},
{
"x": 1110,
"y": 1743
},
{
"x": 1110,
"y": 1841
},
{
"x": 973,
"y": 1841
}
]
},
"description": "PTU"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1152,
"y": 1743
},
{
"x": 1196,
"y": 1743
},
{
"x": 1196,
"y": 1841
},
{
"x": 1152,
"y": 1841
}
]
},
"description": "D"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1931,
"y": 1778
},
{
"x": 2129,
"y": 1761
},
{
"x": 2138,
"y": 1862
},
{
"x": 1940,
"y": 1880
}
]
},
"description": "5,00"
},
{
"boundingPoly": {
"vertices": [
{
"x": 363,
"y": 1850
},
{
"x": 581,
"y": 1850
},
{
"x": 581,
"y": 1950
},
{
"x": 363,
"y": 1950
}
]
},
"description": "Kwota"
},
{
"boundingPoly": {
"vertices": [
{
"x": 623,
"y": 1850
},
{
"x": 670,
"y": 1850
},
{
"x": 670,
"y": 1950
},
{
"x": 623,
"y": 1950
}
]
},
"description": "D"
},
{
"boundingPoly": {
"vertices": [
{
"x": 715,
"y": 1850
},
{
"x": 979,
"y": 1850
},
{
"x": 979,
"y": 1950
},
{
"x": 715,
"y": 1950
}
]
},
"description": "05,00%"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1941,
"y": 1888
},
{
"x": 2125,
"y": 1872
},
{
"x": 2134,
"y": 1975
},
{
"x": 1950,
"y": 1992
}
]
},
"description": "0,24"
},
{
"boundingPoly": {
"vertices": [
{
"x": 356,
"y": 1958
},
{
"x": 663,
"y": 1958
},
{
"x": 663,
"y": 2048
},
{
"x": 356,
"y": 2048
}
]
},
"description": "Podatek"
},
{
"boundingPoly": {
"vertices": [
{
"x": 712,
"y": 1958
},
{
"x": 845,
"y": 1958
},
{
"x": 845,
"y": 2048
},
{
"x": 712,
"y": 2048
}
]
},
"description": "PTU"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1941,
"y": 2000
},
{
"x": 2120,
"y": 1992
},
{
"x": 2124,
"y": 2089
},
{
"x": 1945,
"y": 2097
}
]
},
"description": "0,24"
},
{
"boundingPoly": {
"vertices": [
{
"x": 352,
"y": 2070
},
{
"x": 704,
"y": 2070
},
{
"x": 704,
"y": 2240
},
{
"x": 352,
"y": 2240
}
]
},
"description": "SUMA"
},
{
"boundingPoly": {
"vertices": [
{
"x": 790,
"y": 2070
},
{
"x": 1049,
"y": 2070
},
{
"x": 1049,
"y": 2240
},
{
"x": 790,
"y": 2240
}
]
},
"description": "PLN"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1837,
"y": 2113
},
{
"x": 2196,
"y": 2082
},
{
"x": 2212,
"y": 2268
},
{
"x": 1853,
"y": 2299
}
]
},
"description": "5,00"
},
{
"boundingPoly": {
"vertices": [
{
"x": 349,
"y": 2243
},
{
"x": 656,
"y": 2243
},
{
"x": 656,
"y": 2341
},
{
"x": 349,
"y": 2341
}
]
},
"description": "0003241"
},
{
"boundingPoly": {
"vertices": [
{
"x": 725,
"y": 2243
},
{
"x": 795,
"y": 2243
},
{
"x": 795,
"y": 2341
},
{
"x": 725,
"y": 2341
}
]
},
"description": "5"
},
{
"boundingPoly": {
"vertices": [
{
"x": 838,
"y": 2243
},
{
"x": 1099,
"y": 2243
},
{
"x": 1099,
"y": 2341
},
{
"x": 838,
"y": 2341
}
]
},
"description": "MACIEK"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1984,
"y": 2282
},
{
"x": 2200,
"y": 2282
},
{
"x": 2200,
"y": 2361
},
{
"x": 1984,
"y": 2361
}
]
},
"description": "08:43"
},
{
"boundingPoly": {
"vertices": [
{
"x": 616,
"y": 2342
},
{
"x": 1576,
"y": 2362
},
{
"x": 1574,
"y": 2467
},
{
"x": 614,
"y": 2447
}
]
},
"description": "1EIOS-NLNTD-0GXTD-ON09"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1598,
"y": 2363
},
{
"x": 1887,
"y": 2369
},
{
"x": 1885,
"y": 2474
},
{
"x": 1596,
"y": 2468
}
]
},
"description": "G-XYUSE"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1109,
"y": 2471
},
{
"x": 1242,
"y": 2471
},
{
"x": 1242,
"y": 2572
},
{
"x": 1109,
"y": 2572
}
]
},
"description": "BAE"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1290,
"y": 2463
},
{
"x": 1642,
"y": 2478
},
{
"x": 1637,
"y": 2582
},
{
"x": 1285,
"y": 2567
}
]
},
"description": "13112254"
},
{
"boundingPoly": {
"vertices": [
{
"x": 337,
"y": 2580
},
{
"x": 603,
"y": 2568
},
{
"x": 606,
"y": 2653
},
{
"x": 341,
"y": 2665
}
]
},
"description": "Ptatno"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1310,
"y": 2578
},
{
"x": 1353,
"y": 2580
},
{
"x": 1348,
"y": 2673
},
{
"x": 1305,
"y": 2671
}
]
},
"description": "a"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1402,
"y": 2586
},
{
"x": 1578,
"y": 2586
},
{
"x": 1578,
"y": 2680
},
{
"x": 1402,
"y": 2680
}
]
},
"description": "kred"
},
{
"boundingPoly": {
"vertices": [
{
"x": 2011,
"y": 2593
},
{
"x": 2188,
"y": 2587
},
{
"x": 2191,
"y": 2680
},
{
"x": 2014,
"y": 2686
}
]
},
"description": "5,00"
},
{
"boundingPoly": {
"vertices": [
{
"x": 1923,
"y": 2696
},
{
"x": 2184,
"y": 2693
},
{
"x": 2185,
"y": 2782
},
{
"x": 1924,
"y": 2785
}
]
},
"description": "501782"
}
]

view raw Response.json hosted with ❤ by GitHub

Cały rozpoznany tekst w porównaniu z obrazkiem:

SKLEP "ZABKA'' Z3816
ul. Mogilska 59, 31-545 Krakow
PHU "DANA"
NIP 678-185-16-34
2016-05-29
nr Wydr 808669
PARAGON FISKALNY
MLEKO KONECKIE D
2,50 zh. 2,50 D
MLEKO KONECKIE D 1 2,50 zt, 2,50 D
Sprzed. opod. PTU D
5,00
Kwota D 05,00%
0,24
Podatek PTU
0,24
SUMA PLN
5,00
0003241 5 MACIEK
08:43
1EIOS-NLNTD-0GXTD-ON09 G-XYUSE
BAE 13112254
Ptatno
a kred
5,00
501782
2016-05-29 15.15.57

Jak widać, program poradził sobie całkiem dobrze z tym zadaniem, trochę trudności mu sprawiły polskie litery. Sprawdzałem jego działanie również z większym paragonem, gdzie literki już nie były tak wyraźne i wyniki były równie zadowalające, nie było już tak poprawnie jak w przykładzie, jednak wciąż większość była rozpoznana poprawnie. Przy cenach proponowanych przez Google wygląda to bardzo dobrze. Zastanawiam się tylko czy z powodu opublikowania API zmienili mechanizm reCaptchy z przepisywania słów na klikanie w obrazki :]

1 myśl na “Google Cloud Vision – czyli rozpoznawanie obrazów w chmurze, część 1: OCR”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *