在上一篇文章中,我们成功地将MinIO集成到了系统中,为实现OCR技术识别票据或发票这一核心需求奠定了基础。在本篇文章中,我们将继续深入这个需求的关键环节:通过OCR技术实现对图片内容的智能识别。这项功能将图片中的文字信息转换为可处理的数字化文本,为我们后续使用AI技术自动提取消费金额、消费类型等关键数据提供了重要支撑。通过这一功能的实现,我们将进一步提升系统的智能化水平,为用户提供更加便捷的记账体验。

一、Service 实现

我们要实现两个功能:识别图片中的文字、取识别到的图片文字。识别图片中的文字功能,我们通过MQ将识别消息发送给消费方,消费方接到识别消息后,调用百度的OCR SDK 提取图片中的文字,然后将文字存储在数据库中。取识别到的图片文字功能就很简单了,通过文件id查询识别表中的识别结果。

1.1 识别图片中的文字

SP.ResourceService 微服务的Service文件夹下新建IOCRService服务接口,在这个服务接口中新增识别图片中的文字方法RecognizeTextAsync,这个接口只接收一个文件idfileId作为唯一的参数,代码如下:

/// <summary>
/// 识别图片中的文字
/// </summary>
/// <param name="fileId">图片文件id</param>
/// <returns></returns>
Task RecognizeTextAsync(long fileId);

接着,在Service/Impl文件夹下新建IOCRService的百度OCR实现类BaiduOCRServiceImpl,在这类中实现RecognizeTextAsync方法,代码如下:

/// <summary>
/// 识别图片中的文字
/// </summary>
/// <param name="fileId">图片文件id</param>
/// <returns></returns>
public async Task RecognizeTextAsync(long fileId)
{
    // 校验图片是否存在
    Files? file = await _dbContext.Files.FirstOrDefaultAsync(p => !p.IsDeleted && p.Id == fileId);
    if (file == null)
    {
        throw new NotFoundException("文件不存在");
    }

    // 格式必须是PNG、JPG或JPEG
    if (file.ContentType != "image/png" && file.ContentType != "image/jpg" && file.ContentType != "image/jpeg")
    {
        throw new BadRequestException("仅支持PNG、JPG或JPEG格式的图片");
    }

    string fileInfoJson = JsonSerializer.Serialize(file);
    MqPublisher publisher = new MqPublisher(fileInfoJson, MqExchange.MessageExchange,
        MqRoutingKey.OCRRoutingKey, MqQueue.OCRQueue, "", ExchangeType.Direct);
    await _rabbitMqMessage.SendAsync(publisher);
}

RecognizeTextAsync实现代码中,接收一个文件ID,验证文件的有效性,然后将文件信息发送到消息队列中等待处理。首先方法通过 EF Core 的FirstOrDefaultAsync方法从数据库中查询文件信息。查询条件使LINQ表达式确保文件未被删除(IsDeletedfalse)且ID匹配。如果找不到对应的文件,则抛出NotFoundException异常。 然后代码会验证文件格式。通过检查文件的ContentType属性,来保证识别的图片只能时PNGJPGJPEG格式的图片。如果文件格式不符合要求,就会抛出BadRequestException异常。最后我们将文件信息序列化成JSON字符串,并创建MqPublisher,最后调用我们封装的消息队列的SendAsync方法将消息发送到消息队列中。

接下来,我们要实现识别图片文字MQ的消费者代码。在SP.ResourceService微服务项目根目录下新建Mq文件夹,并在这个文件夹下新建消费者类OCRConsumerService,在这个类里我们就要实现调用百度OCR提取图片文字的功能。代码如下:

using System.Text.Json;
using Baidu.Aip.Ocr;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using SP.Common.Message.Model;
using SP.Common.Message.Mq;
using SP.Common.Message.Mq.Model;
using SP.Common.Model;
using SP.ResourceService.DB;
using SP.ResourceService.Models.Config;
using SP.ResourceService.Models.Entity;
using SP.ResourceService.Service;

namespace SP.ResourceService.Mq;

/// <summary>
/// 消息队列OCR消费者服务
/// </summary>
public class OCRConsumerService : BackgroundService
{
    /// <summary>
    /// RabbitMq 消息
    /// </summary>
    private readonly RabbitMqMessage _rabbitMqMessage;

    /// <summary>
    /// 日志记录器
    /// </summary>
    private readonly ILogger<OCRConsumerService> _logger;

    /// <summary>
    /// 数据库上下文
    /// </summary>
    private readonly ResourceServiceDbContext _dbContext;

    /// <summary>
    /// OSS 服务
    /// </summary>
    private readonly IOssService _ossService;

    /// <summary>
    /// 百度OCR客户端
    /// </summary>
    private readonly Ocr _client;

    /// <summary>
    /// OCR 消费者服务构造函数
    /// </summary>
    /// <param name="options"></param>
    /// <param name="rabbitMqMessage"></param>
    /// <param name="logger"></param>
    /// <param name="dbContext"></param>
    /// <param name="ossService"></param>
    public OCRConsumerService(IOptions<BaiduOCROptions> options, RabbitMqMessage rabbitMqMessage,
        ILogger<OCRConsumerService> logger, ResourceServiceDbContext dbContext, IOssService ossService)
    {
        _logger = logger;
        _rabbitMqMessage = rabbitMqMessage;
        _dbContext = dbContext;
        _ossService = ossService;

        try
        {
            // 验证配置
            ValidateConfiguration(options.Value);
            _client = new Ocr(options.Value.APIKey, options.Value.SecretKey);
            // 修改超时时间为60秒
            _client.Timeout = 60000;
            _logger.LogInformation("百度OCR客户端构建成功");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "构建客户端失败");
            throw;
        }
    }

    /// <summary>
    /// 执行异步操作
    /// </summary>
    /// <param name="stoppingToken"></param>
    /// <returns></returns>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        MqSubscriber subscriber = new MqSubscriber(MqExchange.MessageExchange,
            MqRoutingKey.OCRRoutingKey, MqQueue.OCRQueue);
        await _rabbitMqMessage.ReceiveAsync(subscriber, async message =>
        {
            long fileId = 0L;
            try
            {
                MqMessage mqMessage = message as MqMessage;

                string body = mqMessage.Body;
                _logger.LogInformation($"接收到OCR消息,消息内容:{body}");
                Files? fileInfo = JsonSerializer.Deserialize<Files>(body);
                if (fileInfo == null)
                {
                    _logger.LogError("消息内容转换失败,消息内容为空");
                    return;
                }

                fileId = fileInfo.Id;
                // 校验图片是否存在
                Files? file = await _dbContext.Files.FirstOrDefaultAsync(p => !p.IsDeleted && p.Id == fileInfo.Id,
                    cancellationToken: stoppingToken);
                if (file == null)
                {
                    _logger.LogError("文件不存在,文件id:" + fileInfo.Id);
                    return;
                }

                // 从MinIO下载图片
                byte[] image;
                try
                {
                    using var stream = await _ossService.DownloadAsync(file.ObjectName, file.IsPublic, stoppingToken);
                    using var memoryStream = new MemoryStream();
                    await stream.CopyToAsync(memoryStream, stoppingToken);
                    image = memoryStream.ToArray();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "下载图片失败,文件id:{FileId},文件名:{ObjectName}", fileInfo.Id, fileInfo.ObjectName);
                    return;
                }

                // 如果有可选参数
                var options = new Dictionary<string, object>
                {
                    { "detect_direction", "true" },
                    { "probability", "false" }
                };
                // 带参数调用通用文字识别(高精度版)
                var result = _client.AccurateBasic(image, options);
                if (result == null)
                {
                    _logger.LogError("OCR识别失败,文件id:" + fileInfo.Id);
                    return;
                }

                _logger.LogInformation("OCR识别结果:" + result);
                var worksResult = result["words_result"];
                if (worksResult == null)
                {
                    _logger.LogError("OCR识别结果为空,文件id:" + fileInfo.Id);
                    return;
                }

                List<string> wordList = new List<string>();
                foreach (var item in worksResult)
                {
                    wordList.Add(item["words"]?.ToString() ?? string.Empty);
                }
                // 查询是否存在,如果存在就替换识别的内容
                ImageText? imageText =
                    await _dbContext.ImageTexts.FirstOrDefaultAsync(p => !p.IsDeleted && p.FileId == fileId);
                if (imageText == null)
                {
                    imageText = new ImageText
                    {
                        FileId = fileInfo.Id,
                        RecognizedText = string.Join("", wordList),
                    };
                    SettingCommProperty.Create(imageText);
                    await _dbContext.ImageTexts.AddAsync(imageText, stoppingToken);
                }
                else
                {
                    imageText.RecognizedText= string.Join("", wordList);
                    SettingCommProperty.Edit(imageText);
                    _dbContext.ImageTexts.Update(imageText);
                }

                
                await _dbContext.SaveChangesAsync(stoppingToken);
                _logger.LogInformation("OCR识别成功,文件id:" + fileInfo.Id);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "OCR识别失败,文件id:" + fileId);
            }
            finally
            {
                await Task.CompletedTask;
            }
        });
    }

    /// <summary>
    /// 验证参数
    /// </summary>
    /// <param name="optionsValue">百度OCR参数</param>
    private void ValidateConfiguration(BaiduOCROptions optionsValue)
    {
        if (string.IsNullOrWhiteSpace(optionsValue.AppId))
        {
            throw new ArgumentException("AppId不能为空");
        }

        if (string.IsNullOrWhiteSpace(optionsValue.APIKey))
        {
            throw new ArgumentException("APIKey不能为空");
        }

        if (string.IsNullOrWhiteSpace(optionsValue.SecretKey))
        {
            throw new ArgumentException("SecretKey不能为空");
        }
    }
}

OCRConsumerService类是一个后台服务,继承自BackgroundService,主要用于处理OCR图片文字识别的消息队列消费。它通过依赖注入的方式接收所需的服务和配置,包括RabbitMQ消息服务、日志记录器、数据库上下文、对象存储服务以及百度OCR配置选项。

在构造函数中,我们初始化了百度OCR客户端。首先验证配置参数的有效性,保证AppId、APIKey和SecretKey都不为空。然后使用这些参数创建百度OCR客户端实例,并设置60秒的超时时间,以保证有足够的时间处理大型图片。如果在构建过程中出现任何错误,都会被记录并向上抛出异常。

服务的核心功能在ExecuteAsync方法中实现。这个方法创建了一个消息队列订阅者,监听指定的交换机、路由键和队列。当接收到消息时,它首先将消息体反序列化为文件信息对象。接着再验证文件在数据库中是否存在且未被删除。然后从MinIO对象存储服务下载图片文件,并将图片转换为字节数组。在进行OCR识别时,服务设置了两个识别参数:启用文字方向检测detect_direction和禁用概率输出probability。我们使用的是百度OCR的高精度版API进行文字识别,如果识别成功,会从结果中提取所有识别到的文字内容。最后服务会检查数据库中是否已存在该图片的识别结果。如果不存在,创建新的记录;如果存在,则更新现有记录。

1.2 获取识别到的图片文字

获取识别到的图片文字这个功能就很简单了,不需要太多的逻辑,只需要使用文件id在识别表中查询识别结果即可。首先在IOCRService接口中新增获取识别到的图片文字方法GetRecognizedTextAsync,它只接收一个文件id作为参数,代码如下:

/// <summary>
/// 获取识别到的图片文字
/// </summary>
/// <param name="fileId">图片文件id</param>
/// <returns>识别结果文本</returns>
Task<string?> GetRecognizedTextAsync(long fileId);

然后我们在BaiduOCRServiceImpl实现类中来实现这个方法,代码如下:

/// <summary>
/// 获取识别到的图片文字
/// </summary>
/// <param name="fileId">图片文件id</param>
/// <returns></returns>
public async Task<string?> GetRecognizedTextAsync(long fileId)
{
    string? text =await _dbContext.ImageTexts.Where(p => !p.IsDeleted && p.FileId == fileId)
        .Select(p => p.RecognizedText).FirstOrDefaultAsync();
    if (text == null)
    {
        return "";
    }
    return text;
}

GetRecognizedTextAsync方法实现了从数据库中获取已识别的图片文字内容的功能。它接收一个文件ID作为参数,通过异步方式查询数据库。方法使用LINQ查询ImageTexts表,筛选条件确保记录未被删除且FileId匹配输入参数。通过Select投影仅获取RecognizedText字段的值,并使用FirstOrDefaultAsync获取第一条匹配记录的文本内容。如果没有找到匹配的记录(textnull),则返回空字符串,否则返回识别到的文本内容。

二、Controller 实现

Controllers文件夹中新建OCRController,在这个控制器中我们将实现两个Action:RecognizeTextGetRecognizedText,代码如下:

using Microsoft.AspNetCore.Mvc;
using SP.ResourceService.Service;

namespace SP.ResourceService.Controllers
{
    /// <summary>
    /// OCR控制器
    /// </summary>
    [Route("api/ocr")]
    [ApiController]
    public class OCRController : ControllerBase
    {
        /// <summary>
        /// ocr服务
        /// </summary>
        private readonly IOCRService _ocrService;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="ocrService"></param>
        public OCRController(IOCRService ocrService)
        {
            _ocrService = ocrService;
        }

        /// <summary>
        /// 识别图片中的文字
        /// </summary>
        /// <param name="fileId">图片文件id</param>
        /// <returns></returns>
        [HttpGet("recognize")]
        public async Task<ActionResult> RecognizeText([FromQuery] long fileId)
        {
            await _ocrService.RecognizeTextAsync(fileId);
            return Ok();
        }

        /// <summary>
        /// 获取识别到的图片文字
        /// </summary>
        /// <param name="fileId">图片文件id</param>
        /// <returns></returns>
        [HttpGet("text")]
        public async Task<ActionResult<string>> GetRecognizedText([FromQuery] long fileId)
        {
            string? text = await _ocrService.GetRecognizedTextAsync(fileId);
            return Ok(text);
        }
    }
}

在这段代码中,我们实现了OCR控制器,它负责处理与OCR文字识别相关的HTTP请求。控制器通过[Route("api/ocr")]特性定义了基础路由路径为"api/ocr",并使用[ApiController]特性标记这是一个API控制器。控制器通过构造函数注入的方式获取IOCRService服务实例。

控制器提供了两个主要的API端点。第一个是RecognizeTextAction,通过HTTP GET请求访问"/api/ocr/recognize"路径。这个Action接收一个查询参数fileId,用于指定需要识别的图片文件ID。方法内部调用_ocrService.RecognizeTextAsync来执行实际的文字识别操作。

第二个API端点是GetRecognizedTextAction,通过HTTP GET请求访问"/api/ocr/text"路径。同样接收fileId作为查询参数,用于获取已识别的图片文字内容。方法调用_ocrService.GetRecognizedTextAsync获取识别结果,并将结果文本作为响应内容返回给客户端。

这两个API端点共同构成了一个完整的OCR文字识别流程:用户可以先调用识别接口开始处理图片,然后通过获取接口查询识别结果,这种处理方式处避免客户端长时间等待响应。

三、总结

在本文中,我们详细介绍了如何在微服务架构中实现OCR图片文字识别功能。通过结合百度OCR SDK、消息队列和MinIO对象存储,我们构建了一个高效且可靠的文字识别服务。这个服务不仅能够异步处理图片识别请求,还提供了便捷的接口用于获取识别结果。我们采用了消息队列来处理耗时的OCR识别任务,这种设计既避免了客户端的长时间等待,又提高了系统的整体性能和可扩展性。通过这个功能的实现,我们为后续使用AI技术自动提取票据关键信息奠定了坚实的基础,进一步提升了系统的智能化水平和用户体验。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐