60.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--识别图片文字内容
本文介绍了如何通过OCR技术实现图片文字识别功能,并将其集成到系统中。主要内容包括: 服务实现:在微服务中新建IOCRService接口和BaiduOCRServiceImpl实现类,通过消息队列处理图片识别请求 文字识别流程:验证图片有效性后,将文件信息发送至消息队列 OCR消费者服务:新建OCRConsumerService类,接收队列消息并调用百度OCR API进行文字识别 异常处理:对图片
在上一篇文章中,我们成功地将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
表达式确保文件未被删除(IsDeleted
为false
)且ID匹配。如果找不到对应的文件,则抛出NotFoundException
异常。 然后代码会验证文件格式。通过检查文件的ContentType
属性,来保证识别的图片只能时PNG、JPG或JPEG格式的图片。如果文件格式不符合要求,就会抛出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
获取第一条匹配记录的文本内容。如果没有找到匹配的记录(text
为null
),则返回空字符串,否则返回识别到的文本内容。
二、Controller 实现
在Controllers
文件夹中新建OCRController
,在这个控制器中我们将实现两个Action:RecognizeText
和GetRecognizedText
,代码如下:
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端点。第一个是RecognizeText
Action,通过HTTP GET请求访问"/api/ocr/recognize"路径。这个Action接收一个查询参数fileId
,用于指定需要识别的图片文件ID。方法内部调用_ocrService.RecognizeTextAsync
来执行实际的文字识别操作。
第二个API端点是GetRecognizedText
Action,通过HTTP GET请求访问"/api/ocr/text"路径。同样接收fileId
作为查询参数,用于获取已识别的图片文字内容。方法调用_ocrService.GetRecognizedTextAsync
获取识别结果,并将结果文本作为响应内容返回给客户端。
这两个API端点共同构成了一个完整的OCR文字识别流程:用户可以先调用识别接口开始处理图片,然后通过获取接口查询识别结果,这种处理方式处避免客户端长时间等待响应。
三、总结
在本文中,我们详细介绍了如何在微服务架构中实现OCR图片文字识别功能。通过结合百度OCR SDK、消息队列和MinIO对象存储,我们构建了一个高效且可靠的文字识别服务。这个服务不仅能够异步处理图片识别请求,还提供了便捷的接口用于获取识别结果。我们采用了消息队列来处理耗时的OCR识别任务,这种设计既避免了客户端的长时间等待,又提高了系统的整体性能和可扩展性。通过这个功能的实现,我们为后续使用AI技术自动提取票据关键信息奠定了坚实的基础,进一步提升了系统的智能化水平和用户体验。
更多推荐
所有评论(0)