339 lines
13 KiB
C#
339 lines
13 KiB
C#
using Check.Main.Common;
|
||
using Check.Main.Infer;
|
||
using HalconTemplateMatch;
|
||
using SkiaSharp;
|
||
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Generic;
|
||
using System.Drawing.Imaging;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using YoloDotNet.Extensions;
|
||
using OpenCvSharp;
|
||
namespace Check.Main.Camera
|
||
{
|
||
public class CameraProcessor : IDisposable
|
||
{
|
||
private readonly int _cameraIndex;
|
||
private readonly int _modeId;
|
||
// private readonly ModelSettings _model;
|
||
private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>();
|
||
private readonly Thread _workerThread;
|
||
private volatile bool _isRunning = false;
|
||
private long _imageCounter = 0;
|
||
private readonly object _counterLock = new object(); // 用于线程安全地重置计数器
|
||
|
||
public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted;
|
||
|
||
|
||
public CameraProcessor(int cameraIndex, int modelId)//, ModelSettings model
|
||
{
|
||
_cameraIndex = cameraIndex;
|
||
_modeId = modelId;
|
||
//_model = model;
|
||
_workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" };
|
||
}
|
||
|
||
public void Start()
|
||
{
|
||
_isRunning = true;
|
||
_workerThread.Start();
|
||
}
|
||
|
||
public void EnqueueImage(Bitmap bmp)
|
||
{
|
||
if (!_isRunning)
|
||
{
|
||
bmp?.Dispose();
|
||
return;
|
||
}
|
||
_imageCounter++;
|
||
_imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 图像处理主循环
|
||
/// </summary>
|
||
private void ProcessQueue()
|
||
{
|
||
// 从模型管理器获取此线程专属的YOLO模型
|
||
var yoloModel = YoloModelManager.GetModel(_modeId);
|
||
if (yoloModel == null)
|
||
{
|
||
ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。");
|
||
return; // 如果没有模型,此线程无法工作
|
||
}
|
||
|
||
//训练阶段(相机2)
|
||
var trainer = new LogoTemplateTrainer();
|
||
|
||
trainer.TrainAndSaveTemplates(
|
||
new List<string>
|
||
{
|
||
@"D:\HalconTemplateMatch\train2\logo1.bmp",
|
||
@"D:\HalconTemplateMatch\train2\logo2.bmp",
|
||
@"D:\HalconTemplateMatch\train2\logo3.bmp"
|
||
},
|
||
@"D:\HalconTemplateMatch\model_2");
|
||
|
||
//训练阶段(相机3)9.25修改!!
|
||
trainer.TrainAndSaveTemplates(
|
||
new List<string>
|
||
{
|
||
@"D:\HalconTemplateMatch\train3\3C_1.bmp",
|
||
@"D:\HalconTemplateMatch\train3\3C_2.bmp",
|
||
@"D:\HalconTemplateMatch\train3\3C_3.bmp"
|
||
},
|
||
@"D:\HalconTemplateMatch\model_3");
|
||
|
||
if (trainer == null)
|
||
{
|
||
ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。");
|
||
return;
|
||
}
|
||
|
||
|
||
while (_isRunning)
|
||
{
|
||
try
|
||
{
|
||
// 阻塞式地从队列中取出图像,如果队列为空则等待
|
||
ImageData data = _imageQueue.Take();
|
||
using (data)
|
||
{
|
||
string result = "";
|
||
using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期
|
||
{
|
||
if (skImage == null) continue;
|
||
var predictions = yoloModel.RunObjectDetection(skImage);
|
||
result = predictions.Any() ? "NG" : "OK";
|
||
|
||
ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},检测到 {predictions.Count} 个目标,结果: {result}");
|
||
|
||
// 将处理结果交给协调器进行组装
|
||
DetectionCoordinator.AssembleProduct(data, result);
|
||
|
||
if (OnProcessingCompleted != null)
|
||
{
|
||
using (var resultSkImage = skImage.Draw(predictions))
|
||
{
|
||
// 4. 触发事件,将绘制好的 resultSkImage 传递出去
|
||
|
||
Bitmap bitmap = CameraManager.ConvertSKImageToBitmap(resultSkImage);
|
||
// 所有权在这里被转移
|
||
OnProcessingCompleted?.Invoke(this, new ProcessingCompletedEventArgs(
|
||
_cameraIndex,
|
||
data.ProductId,
|
||
bitmap
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
//***********************************使用Halcon模板匹配进行检测****************************************************
|
||
if (data.Image == null) continue;
|
||
// 统一定义预测结果
|
||
var matcher = new LogoMatcher();
|
||
|
||
//9.25(增加一根据不同的相机编号调用不同的模型!)
|
||
string filepath = "";
|
||
if (_cameraIndex == 2)
|
||
{
|
||
matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_2");
|
||
filepath = "D:\\HalconTemplateMatch\\train2";
|
||
}
|
||
else if (_cameraIndex == 3)
|
||
{
|
||
matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_3");
|
||
filepath = "D:\\HalconTemplateMatch\\train3";
|
||
}
|
||
|
||
////原bool返回的处理
|
||
//bool found = matcher.FindLogo(data.Image);
|
||
//string result = found ? "OK":"NG";
|
||
|
||
//double返回的处理
|
||
double score = matcher.FindLogo(data.Image);
|
||
|
||
|
||
//Mat cam = ProcessImg.BitmapToMat(data.Image);
|
||
|
||
//score= ProcessImg.ProcessImagesInFolder(filepath,cam);
|
||
|
||
//string result = (score > 0.5) ? "OK" : "NG";
|
||
|
||
ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {result},得分: {score}");
|
||
|
||
// 将处理结果交给协调器进行组装
|
||
DetectionCoordinator.AssembleProduct(data, result);
|
||
|
||
|
||
//给PLC的M90、M91写值(10.10)
|
||
if (FrmMain.PlcClient != null && FrmMain.PlcClient.IsConnected)
|
||
{
|
||
if (result == "OK")
|
||
{
|
||
//吹气到合格框
|
||
FrmMain.PlcClient.WriteAsync("M90", 1).Wait(); // 写入M90为1
|
||
//Thread.Sleep(100);
|
||
FrmMain.PlcClient.WriteAsync("M90", 0).Wait(); // 写入M90为1
|
||
|
||
//var a = FrmMain.PlcClient.ReadAsync("M90");
|
||
//Console.WriteLine(a);
|
||
|
||
//FrmMain.PlcClient.WriteAsync("M91", 0).Wait();
|
||
|
||
// 延时复位
|
||
Task.Run(async () =>
|
||
{
|
||
//await Task.Delay(300); // 延时300毫秒,可根据实际气动时间调整
|
||
//await FrmMain.PlcClient.WriteAsync("M90", 0);
|
||
});
|
||
}
|
||
else
|
||
{
|
||
//吹气到不合格框
|
||
//FrmMain.PlcClient.WriteAsync("M90", 0).Wait();
|
||
FrmMain.PlcClient.WriteAsync("M91", 1).Wait();// 写入M91为1
|
||
FrmMain.PlcClient.WriteAsync("M91", 0).Wait(); // 写入M90为1
|
||
|
||
//var a = FrmMain.PlcClient.ReadAsync("M90");
|
||
//Console.WriteLine(a);
|
||
// 延时复位
|
||
Task.Run(async () =>
|
||
{
|
||
//await Task.Delay(300);
|
||
//await FrmMain.PlcClient.WriteAsync("M91", 0);
|
||
});
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ThreadSafeLogger.Log("[PLC] 未连接,跳过写入。");
|
||
}
|
||
|
||
|
||
// ③ 外部订阅事件
|
||
OnProcessingCompleted?.Invoke(
|
||
this,
|
||
new ProcessingCompletedEventArgs
|
||
(
|
||
_cameraIndex,
|
||
data.ProductId,
|
||
data.Image // 原图传出去
|
||
)
|
||
);
|
||
}
|
||
}
|
||
catch (InvalidOperationException)
|
||
{
|
||
// 当调用 Stop 时,会 CompleteAdding 队列,Take 会抛出此异常,是正常退出流程
|
||
break;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ThreadSafeLogger.Log($"[ERROR] 相机 #{_cameraIndex} 处理线程异常: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。
|
||
/// </summary>
|
||
private SKImage ConvertBitmapToSKImage(Bitmap bitmap)
|
||
{
|
||
if (bitmap == null) return null;
|
||
try
|
||
{
|
||
// 使用 using 确保 SKBitmap 被正确释放
|
||
using (var skBitmap = ToSKBitmapFast(bitmap))
|
||
{
|
||
return SKImage.FromBitmap(skBitmap);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ThreadSafeLogger.Log($"[错误] Bitmap to SKImage 转换失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
public static SKBitmap ToSKBitmapFast(Bitmap bitmap)
|
||
{
|
||
// 确保是 32bppArgb(BGRA 内存布局)
|
||
Bitmap src = bitmap;
|
||
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
|
||
{
|
||
src = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
|
||
using (Graphics g = Graphics.FromImage(src))
|
||
{
|
||
g.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height);
|
||
}
|
||
}
|
||
|
||
var rect = new Rectangle(0, 0, src.Width, src.Height);
|
||
var bmpData = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||
try
|
||
{
|
||
var info = new SKImageInfo(src.Width, src.Height, SKColorType.Bgra8888, SKAlphaType.Premul);
|
||
var skBitmap = new SKBitmap(info);
|
||
|
||
IntPtr destPtr = skBitmap.GetPixels(); // 目标内存
|
||
IntPtr srcRowPtr = bmpData.Scan0; // 源行首
|
||
int srcStride = bmpData.Stride;
|
||
int destRowBytes = skBitmap.RowBytes;
|
||
int copyBytesPerRow = Math.Min(srcStride, destRowBytes);
|
||
|
||
// 使用一次分配的缓冲区并用 Marshal.Copy 行拷贝(不分配每行)
|
||
byte[] row = new byte[copyBytesPerRow];
|
||
for (int y = 0; y < src.Height; y++)
|
||
{
|
||
IntPtr s = IntPtr.Add(bmpData.Scan0, y * srcStride);
|
||
IntPtr d = IntPtr.Add(destPtr, y * destRowBytes);
|
||
|
||
Marshal.Copy(s, row, 0, copyBytesPerRow);
|
||
Marshal.Copy(row, 0, d, copyBytesPerRow);
|
||
}
|
||
|
||
return skBitmap;
|
||
}
|
||
finally
|
||
{
|
||
src.UnlockBits(bmpData);
|
||
if (!ReferenceEquals(src, bitmap))
|
||
{
|
||
src.Dispose();
|
||
}
|
||
}
|
||
}
|
||
public void Stop()
|
||
{
|
||
_isRunning = false;
|
||
// 解除阻塞,让线程可以检查 _isRunning 标志并退出
|
||
_imageQueue.CompleteAdding();
|
||
_workerThread.Join(500); // 等待线程结束
|
||
}
|
||
/// <summary>
|
||
/// 线程安全地重置该相机的图像计数器。
|
||
/// </summary>
|
||
public void ResetCounter()
|
||
{
|
||
lock (_counterLock)
|
||
{
|
||
_imageCounter = 0;
|
||
}
|
||
}
|
||
// 别忘了在 DetectionCoordinator 中添加一个辅助方法来获取处理器
|
||
|
||
|
||
public void Dispose()
|
||
{
|
||
Stop();
|
||
_imageQueue.Dispose();
|
||
}
|
||
}
|
||
|
||
}
|