Извлечение текста из изображения с использованием стороннего API

Всем привет.

Давайте рассмотрим гипотетическую ситуацию, что к вам обратился клиент и просит разработать для него возможность считывания информации с изображений, загруженных в Salesforce.
Это могут быть сканкопии каких-либо документов, либо фотографии, данные из которых необходимо сохранить в текстовом виде (например паспорт или водительское удостоверение) и возможно, заполнить ими какую-то форму. Запросы могут быть разнообразными. Сегодня рассмотрим простейшую реализацию этой задачи.
Этот функционал можно реализовать с помощью сервисов платформы Einstein Vision API. Для этого необходимо создать учетную запись в Einstein Platform Services, создать ключ и сгенерировать токен для доступа.
Однако, сегодня я хотел бы рассмотреть сторонний сервис. Такой как GOOGLE CLOUD VISION API. Этот API позволяет разработчикам получить содержание изображения используя мощные модели машинного обучения, просто через REST API. Довольно мощная штука, с помощью которой мне даже удавалось распознать рукописный текст, написанный печатными буквами.

Итак:

  • для начала нам нужно создать пользователя на https://console.developers.google.com, либо воспользоваться любым существующим аккаунтом Gmail.

  • далее нужно создать проект в Google Console (можно воспользоваться этой ссылкой https://console.cloud.google.com/projectcreate).
    %D0%91%D0%B5%D0%B7%D1%8B%D0%BC%D1%8F%D0%BD%D0%BD%D1%8B%D0%B9

  • после этого вы попадете на страничку своего проекта, где необходимо провести несложные действия. Нужно перейти по этой ссылке https://console.developers.google.com/apis/library в библиотеку API и подключить Google Cloud Vision к вашему проекту.

888

  • далее необходимо создать ключ API:

999

  • Выглядеть это будет примерно вот так:

  • копируем ключик. Далее он нам пригодится.
  • также необходимо не забыть добавить сайт https://vision.googleapis.com в список разрешенных сайтов в Salesforce.

111111

Да, чуть не забыл. Google попросит вас подвязать банковскую карту, чтоб верифицировать, что вы настоящий пользователь. Это можно сделать в графе Billing в боковом меню. После подвязки карты с нее снимут 1$ и тут же положат его обратно. Без совершения этих действий ваши запросы обрабатываться не будут. За отправляемые запросы гугл деньги с меня не снимал :slight_smile:

Далее, необходимо написать Apex классы, в нашем случае ImageService, которые будут отправлять и получать информацию.

public class ImageService {
    
   public static ImageResponse GetTextFromImage(blob data)
    {   
        HttpRequest httpReq = new HttpRequest();
        httpReq.setHeader('Content-Type','application/json; charset=utf-8');
        httpReq.setMethod('POST');
        httpReq.setEndpoint('https://vision.googleapis.com/v1/images:annotate?key=AIzaSyDwq************50rh3Nb4CZLhH5A'); - ключ который получили в гугл
        httpReq.setTimeout(120000);
       	
       	ImageRequest imageRequest = new ImageRequest();
        
        ImageRequest.Image image = new ImageRequest.Image();
       	image.content = data;
        
        ImageRequest.Feature feature = new ImageRequest.Feature();
        feature.type ='TEXT_DETECTION';
        
        ImageRequest.Request request = new ImageRequest.Request();
       	request.features = new List<ImageRequest.Feature>();
		request.features.add(feature);
        request.Image = image;
        
        imageRequest.requests = new List<ImageRequest.Request>();
        imageRequest.requests.add(request);
        
        httpReq.setBody(JSON.serialize(imageRequest));
        Http http = new Http();
        HTTPResponse res = http.send(httpReq);
       	return ImageResponse.parse(res.getBody());
    } 
}
public class ImageRequest{
	public List<Request> requests;
	public class Request {
		public Image image;
		public List<Feature> features;
	}
	public class Image {
		public blob content;
	}
	public class Feature {
		public String type;
	}
	public static ImageRequest parse(String json){
		return (ImageRequest) System.JSON.deserialize(json, ImageRequest.class);
	}
}

public class ImageResponse {
	public Responses[] responses;
	public class Responses {
		public TextAnnotation[] textAnnotations;
		public FullTextAnnotation fullTextAnnotation;
	}
	public class TextAnnotation {
		public String locale;
		public String description;
		public BoundingPoly boundingPoly;
	}
	public class BoundingPoly {
		public Vertice[] vertices;
	}
	public class Vertice {
		public Integer x;
		public Integer y;
	}
	public class FullTextAnnotation {
		public ImagePage[] pages;
		public String text;
	}
	public class ImagePage {
public Property property;
		public Integer width;
		public Integer height;
		public Block[] blocks;
	}
	public class Property {
		public DetectedLanguage[] detectedLanguages;
	}
	public class DetectedLanguage {
		public String languageCode;
	}
	public class Block {
		public Property property;
		public BoundingBox boundingBox;
		public Paragraph[] paragraphs;
		public String blockType;
	}
	public class BoundingBox {
		public Vertice[] vertices;
	}
	public class Paragraph {
		public Property property;
		public BoundingBox boundingBox;
		public Word[] words;
	}
	public class Word {
		public Property property;
		public BoundingBox boundingBox;
		public Symbol[] symbols;
	}
	public class Symbol {
		public Property property;
		public BoundingBox boundingBox;
		public String text;
	}
	public static ImageResponse parse(String json){
		return (ImageResponse) System.JSON.deserialize(json, ImageResponse.class);
	}
}

Информация об изображении передается в base64 кодировке и возвращается обратно в JSON формате.
Создадим кастомный объект с названием Info from file. На этом объекте создадим кастомное поле Image info (rich text), в которое и будем сохранять информацию с нашего изображения. Изображение в виде Attachments будем загружать в Account. Реализация дата-модели может быть абсолютно разной, в зависимости от потребностей бизнеса. В данном примере акцент сделан больше на сам момент использования стороннего сервиса и извлечение данных.
Для извлечения информации из полученного ответа давайте создадим Аура-компонент и контроллер для него.

public class ImageServiceController {

@auraenabled

public static ImageResponse GetTextFromImage(string record)

{

List<ContentDocumentLink> docs=[SELECT ContentDocumentId

FROM ContentDocumentLink

WHERE ContentDocument.FileType='jpg'

and LinkedEntityId =:record];

if(!docs.isempty())

{

List<ContentVersion> updateVersionList=new List<ContentVersion>();

for(ContentDocumentLink doc:docs)

{

List<ContentVersion> versions=[SELECT VersionData

FROM ContentVersion

WHERE Is_Extracted__c=false

and ContentDocumentId = :docs[0].ContentDocumentId

AND IsLatest = true];

if(versions.isempty()){

return null;

}

ContentVersion version=versions[0];

ImageResponse response=ImageService.GetTextFromImage(version.VersionData);

Info_from_file__c info = new Info_from_file__c();

info.Image_Info__c = response.responses[0].fullTextAnnotation.text;

insert info;

}

}

return null;

}

}

<aura:component controller="ImageServiceController" implements="lightning:isUrlAddressable,force:hasRecordId,force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,forceCommunity:availableForAllPageTypes,force:lightningQuickAction" access="global" >

<aura:handler name="init" value="{!this}" action="{!c.extractImage}"/>

<aura:attribute name="recordId" type="Id" />

<aura:attribute name="status" type="string" />

<div class="slds-text-align_center">

{!v.status}

</div>

</aura:component>

({extractImage : function(cmp)

{var action = cmp.get("c.GetTextFromImage");

action.setParams({ record : cmp.get("v.recordId") });

action.setCallback(this, function(response) {

var state = response.getState();

if (state === "SUCCESS") {

cmp.set("v.status",'Text extracted from image.');

}

else if (state === "INCOMPLETE") {

cmp.set("v.status",'Incomplete');

}

else if (state === "ERROR") {

var errors = response.getError();

if (errors) {

if (errors[0] && errors[0].message) {

cmp.set("v.status","Error message: " + errors[0].message);

}

} else {

cmp.set("v.status",'Unknown error');

}

}

});

$A.enqueueAction(action);

}

})

Добавим компонент на страницу Account в виде Action. Назовем его Extract.

Теперь можно приступить к самому интересному — к тестированию. Найдем несколько изображений в интернете. Пусть это будет фото текста на украинском, например:

131313

И водительское удостоверение Украины:

141414

Создадим тестовый аккаунт. И загрузим в аттачменты наше фото текста.

Нажимаем Extract…

161616

Видим через несколько секунд информационное окно, сообщающее нам об успешном завершении работы.

Переходим на вкладку с нашим кастомным объектом и видим, что создана запись.

181818

Открываем ее и видим…

191919

Как мы можем видеть, воспроизведенный текст соответствует фотографии, которую мы загрузили, с довольно высокой точностью. Это обьясняется хорошим качеством изображения и отсутствием постороннего «шума» на фото. Результат работы также можно увидеть в этом видео.
Теперь давайте посмотрим на результат преобразования водительского удостоверения:

202020

Несмотря на низкое качество фото и наличие посторонних «шумов” в виде водяных знаков — результат распознавания тоже довольно неплох.

Далее, полученную информацию можем использовать на свое усмотрение. Заполнять ею какие-то поля, формы или использовать для каких-либо других задач. Опытным путем установил, что английский текст распознается с большей точностью. Чем некачественнее фото — тем, естественно, хуже точность распознавания. Но в целом сервис довольно интересный и заслуживает внимания.
Спасибо.

11 Likes

Супер интересно. Благодарю за такое подробное описание сервиса и реализации классов.

2 Likes

Спасибо за статью!

1 Like