409 lines
14 KiB
C#
409 lines
14 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using OrpaonVision.Core.Results;
|
||
using OrpaonVision.SiteApp.Runtime.Contracts;
|
||
using OrpaonVision.SiteApp.Controls;
|
||
using System.Drawing;
|
||
using System.Drawing.Imaging;
|
||
using System.Windows.Media.Imaging;
|
||
using System.Windows.Media;
|
||
using System.Windows;
|
||
using WpfColor = System.Windows.Media.Color;
|
||
using WpfPixelFormat = System.Windows.Media.PixelFormat;
|
||
using WpfFontStyle = System.Windows.FontStyle;
|
||
using DrawingPen = System.Drawing.Pen;
|
||
|
||
namespace OrpaonVision.SiteApp.Runtime.Services;
|
||
|
||
/// <summary>
|
||
/// 图像处理服务接口。
|
||
/// </summary>
|
||
public interface IImageProcessingService
|
||
{
|
||
/// <summary>
|
||
/// 将相机帧转换为BitmapSource。
|
||
/// </summary>
|
||
Result<BitmapSource> ConvertFrameToBitmapSource(CameraFrameDto frame);
|
||
|
||
/// <summary>
|
||
/// 将推理结果转换为检测框列表。
|
||
/// </summary>
|
||
Result<List<DetectionBox>> ConvertInferenceToDetectionBoxes(InferenceResultDto inference);
|
||
|
||
/// <summary>
|
||
/// 创建占位图像。
|
||
/// </summary>
|
||
BitmapSource CreatePlaceholderImage(int width, int height, string text);
|
||
|
||
/// <summary>
|
||
/// 调整图像大小。
|
||
/// </summary>
|
||
Result<BitmapSource> ResizeImage(BitmapSource source, int width, int height);
|
||
|
||
/// <summary>
|
||
/// 旋转图像。
|
||
/// </summary>
|
||
Result<BitmapSource> RotateImage(BitmapSource source, double angle);
|
||
|
||
/// <summary>
|
||
/// 裁剪图像。
|
||
/// </summary>
|
||
Result<BitmapSource> CropImage(BitmapSource source, int x, int y, int width, int height);
|
||
|
||
/// <summary>
|
||
/// 应用图像滤镜。
|
||
/// </summary>
|
||
Result<BitmapSource> ApplyFilter(BitmapSource source, ImageFilter filter);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 图像滤镜类型。
|
||
/// </summary>
|
||
public enum ImageFilter
|
||
{
|
||
/// <summary>
|
||
/// 无滤镜。
|
||
/// </summary>
|
||
None,
|
||
|
||
/// <summary>
|
||
/// 灰度。
|
||
/// </summary>
|
||
Grayscale,
|
||
|
||
/// <summary>
|
||
/// 二值化。
|
||
/// </summary>
|
||
Binary,
|
||
|
||
/// <summary>
|
||
/// 边缘检测。
|
||
/// </summary>
|
||
EdgeDetection,
|
||
|
||
/// <summary>
|
||
/// 模糊。
|
||
/// </summary>
|
||
Blur,
|
||
|
||
/// <summary>
|
||
/// 锐化。
|
||
/// </summary>
|
||
Sharpen
|
||
}
|
||
|
||
/// <summary>
|
||
/// 图像处理服务实现。
|
||
/// </summary>
|
||
public sealed class ImageProcessingService : IImageProcessingService
|
||
{
|
||
private readonly ILogger<ImageProcessingService> _logger;
|
||
|
||
/// <summary>
|
||
/// 构造函数。
|
||
/// </summary>
|
||
public ImageProcessingService(ILogger<ImageProcessingService> logger)
|
||
{
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Result<BitmapSource> ConvertFrameToBitmapSource(CameraFrameDto frame)
|
||
{
|
||
try
|
||
{
|
||
if (frame == null)
|
||
{
|
||
return Result<BitmapSource>.Fail("FRAME_NULL", "相机帧不能为空。");
|
||
}
|
||
|
||
if (frame.ImageData == null || frame.ImageData.Length == 0)
|
||
{
|
||
return Result<BitmapSource>.Fail("IMAGE_DATA_EMPTY", "图像数据为空。");
|
||
}
|
||
|
||
_logger.LogDebug("正在转换相机帧为BitmapSource,尺寸: {Width}x{Height}", frame.Width, frame.Height);
|
||
|
||
using var ms = new System.IO.MemoryStream(frame.ImageData);
|
||
var bitmap = new Bitmap(ms);
|
||
var bitmapSource = bitmap.ToBitmapSource();
|
||
|
||
_logger.LogDebug("相机帧转换完成");
|
||
return Result<BitmapSource>.Success(bitmapSource, message: "相机帧转换成功。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "转换相机帧失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "CONVERT_FRAME_FAILED", "转换相机帧失败。", traceId);
|
||
return Result<BitmapSource>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Result<List<DetectionBox>> ConvertInferenceToDetectionBoxes(InferenceResultDto inference)
|
||
{
|
||
try
|
||
{
|
||
if (inference == null)
|
||
{
|
||
return Result<List<DetectionBox>>.Fail("INFERENCE_NULL", "推理结果不能为空。");
|
||
}
|
||
|
||
var detectionBoxes = new List<DetectionBox>();
|
||
|
||
if (inference.Detections != null)
|
||
{
|
||
foreach (var detection in inference.Detections)
|
||
{
|
||
var box = new DetectionBox
|
||
{
|
||
ClassName = detection.ClassName ?? "Unknown",
|
||
Confidence = detection.Confidence,
|
||
X = detection.X,
|
||
Y = detection.Y,
|
||
Width = detection.Width,
|
||
Height = detection.Height,
|
||
Label = $"{detection.ClassName} ({detection.Confidence:P0})"
|
||
};
|
||
|
||
detectionBoxes.Add(box);
|
||
}
|
||
}
|
||
|
||
_logger.LogDebug("推理结果转换完成,检测框数量: {Count}", detectionBoxes.Count);
|
||
return Result<List<DetectionBox>>.Success(detectionBoxes, message: "推理结果转换成功。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "转换推理结果失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "CONVERT_INFERENCE_FAILED", "转换推理结果失败。", traceId);
|
||
return Result<List<DetectionBox>>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public BitmapSource CreatePlaceholderImage(int width, int height, string text)
|
||
{
|
||
try
|
||
{
|
||
using var bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
||
using var graphics = Graphics.FromImage(bitmap);
|
||
|
||
// 清除背景
|
||
graphics.Clear(System.Drawing.Color.LightGray);
|
||
|
||
// 绘制文本
|
||
var font = new Font("Arial", 16, System.Drawing.FontStyle.Bold);
|
||
var brush = new SolidBrush(System.Drawing.Color.DarkGray);
|
||
var textSize = graphics.MeasureString(text, font);
|
||
var textX = (width - textSize.Width) / 2;
|
||
var textY = (height - textSize.Height) / 2;
|
||
|
||
graphics.DrawString(text, font, brush, textX, textY);
|
||
|
||
// 绘制边框
|
||
using var pen = new DrawingPen(System.Drawing.Color.Gray, 2);
|
||
graphics.DrawRectangle(pen, 0, 0, width - 1, height - 1);
|
||
|
||
return bitmap.ToBitmapSource();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "创建占位图像失败");
|
||
return CreateErrorPlaceholder(width, height);
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Result<BitmapSource> ResizeImage(BitmapSource source, int width, int height)
|
||
{
|
||
try
|
||
{
|
||
if (source == null)
|
||
{
|
||
return Result<BitmapSource>.Fail("SOURCE_NULL", "源图像不能为空。");
|
||
}
|
||
|
||
_logger.LogDebug("正在调整图像大小: {Width}x{Height} -> {NewWidth}x{NewHeight}",
|
||
source.PixelWidth, source.PixelHeight, width, height);
|
||
|
||
var scaled = new TransformedBitmap(source, new ScaleTransform(
|
||
(double)width / source.PixelWidth,
|
||
(double)height / source.PixelHeight));
|
||
|
||
_logger.LogDebug("图像大小调整完成");
|
||
return Result<BitmapSource>.Success(scaled, message: "图像大小调整成功。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "调整图像大小失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "RESIZE_IMAGE_FAILED", "调整图像大小失败。", traceId);
|
||
return Result<BitmapSource>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Result<BitmapSource> RotateImage(BitmapSource source, double angle)
|
||
{
|
||
try
|
||
{
|
||
if (source == null)
|
||
{
|
||
return Result<BitmapSource>.Fail("SOURCE_NULL", "源图像不能为空。");
|
||
}
|
||
|
||
_logger.LogDebug("正在旋转图像,角度: {Angle}", angle);
|
||
|
||
var rotated = new TransformedBitmap(source, new RotateTransform(angle));
|
||
|
||
_logger.LogDebug("图像旋转完成");
|
||
return Result<BitmapSource>.Success(rotated, message: "图像旋转成功。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "旋转图像失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "ROTATE_IMAGE_FAILED", "旋转图像失败。", traceId);
|
||
return Result<BitmapSource>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Result<BitmapSource> CropImage(BitmapSource source, int x, int y, int width, int height)
|
||
{
|
||
try
|
||
{
|
||
if (source == null)
|
||
{
|
||
return Result<BitmapSource>.Fail("SOURCE_NULL", "源图像不能为空。");
|
||
}
|
||
|
||
_logger.LogDebug("正在裁剪图像: ({X},{Y}) {Width}x{Height}", x, y, width, height);
|
||
|
||
var cropped = new CroppedBitmap(source, new Int32Rect(x, y, width, height));
|
||
|
||
_logger.LogDebug("图像裁剪完成");
|
||
return Result<BitmapSource>.Success(cropped, message: "图像裁剪成功。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "裁剪图像失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "CROP_IMAGE_FAILED", "裁剪图像失败。", traceId);
|
||
return Result<BitmapSource>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Result<BitmapSource> ApplyFilter(BitmapSource source, ImageFilter filter)
|
||
{
|
||
try
|
||
{
|
||
if (source == null)
|
||
{
|
||
return Result<BitmapSource>.Fail("SOURCE_NULL", "源图像不能为空。");
|
||
}
|
||
|
||
_logger.LogDebug("正在应用图像滤镜: {Filter}", filter);
|
||
|
||
// 这里可以实现各种滤镜效果
|
||
// 由于WPF的图像处理相对复杂,这里只是占位实现
|
||
BitmapSource result = filter switch
|
||
{
|
||
ImageFilter.Grayscale => ApplyGrayscaleFilter(source),
|
||
ImageFilter.Binary => ApplyBinaryFilter(source),
|
||
ImageFilter.EdgeDetection => ApplyEdgeDetectionFilter(source),
|
||
ImageFilter.Blur => ApplyBlurFilter(source),
|
||
ImageFilter.Sharpen => ApplySharpenFilter(source),
|
||
_ => source
|
||
};
|
||
|
||
_logger.LogDebug("图像滤镜应用完成");
|
||
return Result<BitmapSource>.Success(result, message: "图像滤镜应用成功。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "应用图像滤镜失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "APPLY_FILTER_FAILED", "应用图像滤镜失败。", traceId);
|
||
return Result<BitmapSource>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
private BitmapSource ApplyGrayscaleFilter(BitmapSource source)
|
||
{
|
||
// 简化的灰度滤镜实现
|
||
// 这里应该实现真正的灰度转换
|
||
// 为了简化,直接返回原图
|
||
return source;
|
||
}
|
||
|
||
private BitmapSource ApplyBinaryFilter(BitmapSource source)
|
||
{
|
||
// 简化的二值化滤镜实现
|
||
return source;
|
||
}
|
||
|
||
private BitmapSource ApplyEdgeDetectionFilter(BitmapSource source)
|
||
{
|
||
// 简化的边缘检测滤镜实现
|
||
return source;
|
||
}
|
||
|
||
private BitmapSource ApplyBlurFilter(BitmapSource source)
|
||
{
|
||
// 简化的模糊滤镜实现
|
||
return source;
|
||
}
|
||
|
||
private BitmapSource ApplySharpenFilter(BitmapSource source)
|
||
{
|
||
// 简化的锐化滤镜实现
|
||
return source;
|
||
}
|
||
|
||
private BitmapSource CreateErrorPlaceholder(int width, int height)
|
||
{
|
||
using var bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
||
using var graphics = Graphics.FromImage(bitmap);
|
||
|
||
graphics.Clear(System.Drawing.Color.Red);
|
||
using var font = new Font("Arial", 12, System.Drawing.FontStyle.Bold);
|
||
using var brush = new SolidBrush(System.Drawing.Color.White);
|
||
graphics.DrawString("Error", font, brush, 10, 10);
|
||
|
||
return bitmap.ToBitmapSource();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bitmap扩展方法。
|
||
/// </summary>
|
||
public static class BitmapExtensions
|
||
{
|
||
/// <summary>
|
||
/// 将Bitmap转换为BitmapSource。
|
||
/// </summary>
|
||
public static BitmapSource ToBitmapSource(this Bitmap bitmap)
|
||
{
|
||
var hBitmap = bitmap.GetHbitmap();
|
||
try
|
||
{
|
||
return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
|
||
hBitmap,
|
||
IntPtr.Zero,
|
||
new Int32Rect(0, 0, bitmap.Width, bitmap.Height),
|
||
BitmapSizeOptions.FromEmptyOptions());
|
||
}
|
||
finally
|
||
{
|
||
DeleteObject(hBitmap);
|
||
}
|
||
}
|
||
|
||
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
|
||
public static extern bool DeleteObject(IntPtr hObject);
|
||
}
|