반응형
VisionThread/EmguVision 라이브·검사 공통 파이프라인 리팩토링 (풀코드)
아래 코드는 그랩 콜백에서 포인터를 즉시 안전 복제(Mat.Clone) 하고, 라이브/검사 경로를 하나의 공통 파이프라인으로 통합한 구현입니다.
- 포인터 수명 안전: 콜백 내 즉시 Mat.Clone()
- UI 스레드 규칙 준수: 화면 갱신은 BeginInvoke로 UI 스레드에서
- 지연 최소화: 라이브는 “최신 1장만 유지” + 게이트로 중복 렌더 억제
- 재사용성: SharedPipeline(...) 한 곳에서 라이브/검사 공통 처리 분기
1) 클래스명
- VisionThread (그랩 콜백을 보유)
- EmguVision (화면 컨트롤)
2) 클래스 개요 (Overview)
- VisionThread는 카메라 프레임 수신 시 그랩 포인터를 즉시 Mat으로 복제하여, SharedPipeline으로 전달합니다.
- SharedPipeline은 라이브/검사 라우트를 분기 처리를 하되, 데이터 흘러가는 입구는 동일합니다.
- EmguVision은 SetLiveImage(Mat, ...)로 UI 스레드에서만 안전하게 이미지를 설정/갱신합니다.
3) 주요 기능 (Key Features)
- 공통 프레임 인입부: IngestFrameFromPointer(...) → 포인터 즉시 복제(Mat.Clone)
- 단일 파이프라인: SharedPipeline(route, ...)로 라이브/검사 라우팅
- 최신 프레임 유지형 라이브 표시: _latestLiveMat + _liveRenderGate
- UI 마샬링 보장: BeginInvoke로 EmguVision 그리기
- 기존 검사 함수 재사용: InspectionGrabImage(...) 그대로 호출(필요 시 Mat→포인터/Stride 전달)
4) 주요 메서드 (Methods)
4.1 VisionThread — 필드/열거형/프레임 인입부/공통 파이프라인
using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;
using Emgu.CV;
using Emgu.CV.CvEnum;
namespace VIK
{
public partial class VisionThread
{
// ===== 공통 라우트 구분 =====
private enum FrameRoute { Live, Inspection }
// ===== 라이브 최신 프레임 정책 =====
private readonly object _latestLock = new object();
private Mat _latestLiveMat = null; // 최신 1장 유지
private int _liveRenderGate = 0; // 0/1 게이트(중복 렌더 억제)
// EmguVision 컨트롤(실제 화면 컨트롤 참조로 교체)
private EmguVision m_EmguVision;
// Basler 배열/기존 구성요소들은 기존 코드와 동일
private Basler[] m_Basler;
// ===== 그랩 콜백 (기존 시그니처 유지) =====
private void OnBaslerImageGrabbed(IntPtr pImage, int nWidth, int nHeight, int nStride, bool bColor, int nCamera)
{
try
{
if (pImage == IntPtr.Zero)
return;
string sModelName = SocketDefine.g_stEquipmentData.sModelName;
int nTrigger = GlobalDefine.g_stInspectionResult.nCountTotal[nCamera]++;
int nRewinder = SocketDefine.g_stEquipmentData.nRewinderCount;
int nUnwinder = SocketDefine.g_stEquipmentData.nUnwinderCount;
int nBCD = SocketDefine.g_stEquipmentData.nBCD;
switch (m_Basler[nCamera].GetTriggerStatus())
{
case eTRIGGER.SOFTWARE:
{
// 검사 파이프라인으로 공통 진입
IngestFrameFromPointer(
nCamera, pImage, nWidth, nHeight, nStride, bColor,
FrameRoute.Inspection,
sModelName, nTrigger, nRewinder, nUnwinder, nBCD
);
}
break;
case eTRIGGER.HARDWARE:
{
if (GlobalDefine.g_stEquipmentStatus.eVisionStatus == GlobalDefine.eVisionStatus.START)
{
IngestFrameFromPointer(
nCamera, pImage, nWidth, nHeight, nStride, bColor,
FrameRoute.Inspection,
sModelName, nTrigger, nRewinder, nUnwinder, nBCD
);
}
}
break;
case eTRIGGER.CONTINUE:
{
if (GlobalDefine.g_stEquipmentStatus.eVisionStatus == GlobalDefine.eVisionStatus.STOP)
{
// 라이브(정지시 실시간 미리보기)
IngestFrameFromPointer(
nCamera, pImage, nWidth, nHeight, nStride, bColor,
FrameRoute.Live,
sModelName, nTrigger, nRewinder, nUnwinder, nBCD
);
}
else
{
// 장비 선속 주기 처리 유지
if (SocketDefine.g_stEquipmentData.nLineSpeed > 0)
{
IngestFrameFromPointer(
nCamera, pImage, nWidth, nHeight, nStride, bColor,
FrameRoute.Inspection,
sModelName, nTrigger, nRewinder, nUnwinder, nBCD
);
int nInterval = (int)(
VisionDefine.g_stCamera[nCamera].nFOV /
((((SocketDefine.g_stEquipmentData.nLineSpeed / 60.0) * 100.0) * 10.0) / 1000.0)
);
Thread.Sleep(Math.Max(0, nInterval));
}
}
}
break;
}
}
catch (Exception ex)
{
LogManager.InsertSystemLog($"VisionThread.cs - OnBaslerImageGrabbed: {ex.Message}");
}
}
// ===== 포인터 → Mat 안전 복제 + 공통 파이프라인 진입 =====
private void IngestFrameFromPointer(
int nCamera,
IntPtr pImage, int nWidth, int nHeight, int nStride, bool bColor,
FrameRoute route,
string sModelName, int nTrigger, int nRewinder, int nUnwinder, int nBCD)
{
// 콜백 내부에서 즉시 안전 복제
int ch = bColor ? 3 : 1;
using (var view = new Mat(nHeight, nWidth, DepthType.Cv8U, ch, pImage, nStride))
{
var own = view.Clone(); // 안전 복제본
try
{
SharedPipeline(
nCamera, own, bColor, route,
sModelName, nTrigger, nRewinder, nUnwinder, nBCD
);
}
finally
{
// SharedPipeline 내부에서 own을 보관하지 않았다면 여기서 해제
own.Dispose();
}
}
}
// ===== 라이브/검사 공통 파이프라인 =====
private void SharedPipeline(
int nCamera, Mat frame, bool bColor, FrameRoute route,
string sModelName, int nTrigger, int nRewinder, int nUnwinder, int nBCD)
{
switch (route)
{
case FrameRoute.Live:
HandleLiveFrame(frame, bColor);
break;
case FrameRoute.Inspection:
HandleInspectionFrame(nCamera, frame, bColor, sModelName, nTrigger, nRewinder, nUnwinder, nBCD);
break;
}
}
// ===== 라이브 경로: 최신 1장 유지 + UI 렌더 =====
private void HandleLiveFrame(Mat frame, bool bColor)
{
// 1) 최신 프레임만 보관
Mat keep = frame.Clone(); // frame(외부 소유)와 수명 분리
lock (_latestLock)
{
_latestLiveMat?.Dispose();
_latestLiveMat = keep; // VisionThread가 보유
}
// 2) UI 스레드로 1회만 렌더 요청 (바쁠 때 드롭)
if (Interlocked.Exchange(ref _liveRenderGate, 1) == 0)
{
if (m_EmguVision != null && !m_EmguVision.IsDisposed && m_EmguVision.IsHandleCreated)
{
m_EmguVision.BeginInvoke((Action)(() =>
{
try
{
Mat latest = null;
lock (_latestLock) latest = _latestLiveMat;
if (latest != null && !latest.IsEmpty)
{
m_EmguVision.SetLiveImage(latest, bColor); // UI 스레드에서 안전
}
}
catch (Exception ex)
{
Debug.WriteLine($"Live Render UI: {ex.Message}");
}
finally
{
Interlocked.Exchange(ref _liveRenderGate, 0);
}
}));
}
else
{
// 화면이 없으면 보관 프레임 폐기
lock (_latestLock)
{
_latestLiveMat?.Dispose();
_latestLiveMat = null;
}
Interlocked.Exchange(ref _liveRenderGate, 0);
}
}
}
// ===== 검사 경로: 기존 검사 함수 재사용 =====
private void HandleInspectionFrame(
int nCamera, Mat frame, bool bColor,
string sModelName, int nTrigger, int nRewinder, int nUnwinder, int nBCD)
{
// 기존 InspectionGrabImage가 포인터/stride를 받는다면 Mat에서 추출하여 동기 호출
IntPtr ptr = frame.DataPointer;
int width = frame.Cols;
int height= frame.Rows;
int stride= frame.Step;
// 기존 함수를 그대로 재사용 (frame이 살아있는 동안만 안전)
InspectionGrabImage(
nCamera, width, height, stride, bColor, ptr,
sModelName, nTrigger, nRewinder, nUnwinder, nBCD
);
}
}
}
검사 경로 주의: InspectionGrabImage(...)가 동기로 실행되는 한, 위처럼 Mat.DataPointer를 넘겨도 안전합니다(함수 리턴까지 frame이 살아있음). 만약 검사 파이프라인을 비동기로 던진다면, InspectionGrabImage(Mat 버전 오버로드) 를 만드는 편이 안전합니다.
4.2 EmguVision — Mat 버전의 라이브 세터 (UI 스레드 보장)
using System;
using System.Diagnostics;
using System.Windows.Forms;
using Emgu.CV;
namespace VIK
{
public partial class EmguVision : Control
{
// 기존 EmguImage 래퍼
private EmguImage m_SrcImage;
private EmguImage m_DstImage;
public bool LiveImage { get; set; }
public bool ColorImage { get; set; }
public bool ImageMirror { get; set; }
public bool ImageFlip { get; set; }
public bool ImageRotate { get; set; }
// ConvertImage(EmguImage) 등 기존 함수는 그대로 사용
/// <summary>
/// 라이브 프레임을 Mat로 받아 UI 스레드에서 안전하게 세팅/갱신
/// </summary>
public bool SetLiveImage(Mat mat, bool bColor = false)
{
if (this.IsDisposed || !this.IsHandleCreated || mat == null || mat.IsEmpty)
return false;
if (this.InvokeRequired)
{
// UI 마샬링
this.BeginInvoke((Action)(() => SetLiveImage(mat, bColor)));
return true;
}
try
{
this.LiveImage = true;
this.ColorImage = bColor;
if (this.ImageMirror || this.ImageFlip || this.ImageRotate)
{
// 기존 ConvertImage(EmguImage)를 재사용
using (var tmp = mat.Clone())
{
var src = new EmguImage();
src.SetImage(tmp, clone: false); // EmguImage에 Mat 세팅
ConvertImage(src); // 변환 결과는 내부 m_SrcImage/m_DstImage에 반영
}
}
else
{
// EmguImage가 내부 소유로 Clone 보관 → mat 수명과 분리
this.m_SrcImage.SetImage(mat, clone: true);
}
this.Invalidate(false); // UI 스레드에서 호출
return true;
}
catch (Exception ex)
{
LogManager.InsertSystemLog("EmguVision.SetLiveImage(Mat): " + ex.Message);
return false;
}
}
}
}
5) 추가 정보 (Notes)
5.1 왜 이 구조가 안전한가?
- 포인터 안전: 콜백 → 즉시 Mat.Clone() → SDK 버퍼 수명과 무관
- UI 안전: 모든 컨트롤 갱신은 UI 스레드(BeginInvoke)에서만
- 저지연: 라이브는 최신 1장만 유지 + 게이트로 과도 렌더 억제 (바쁠 때 자동 프레임 드롭)
5.2 기존 SetLiveImage(int, int, int, IntPtr, ...) 계속 써야 한다면
- 그 함수 내부에서 무조건 포인터→Mat→Clone으로 바꾸고,
- InvokeRequired 검사 후 UI 스레드에서만 Invalidate/이미지 교체가 일어나도록 바꾸세요.
- 다만 콜백에서 바로 Mat을 건네는 지금 구조(추천) 가 훨씬 직관적입니다.
5.3 검사 파이프라인 비동기화가 필요할 때
- InspectionGrabImage(...)를 Mat 버전으로 오버로드하고, 내부에서 Mat.Clone해 비동기 스레드와 수명 분리.
- 또는 채널(용량 1) 을 두고 소비자 스레드에서 처리(백프레셔 확보).
5.4 스레드풀/lock 패턴이 빠진 이유
- 라이브는 UI 큐에 최신 프레임만 올리면 별도의 lock 없이 자연스럽게 직렬화됩니다(WinForms UI는 단일 스레드).
- 기존 lock은 “공유 상태 파손 방지” 목적이었지만, 지금은 단일 소유/단일 경로를 보장하므로 제거했습니다.
필요하시면,
- InspectionGrabImage(Mat ...) 오버로드 예시,
- ConvertImage(Mat ...) 직접 Mat 받는 버전,
- EmguImage.SetImagePtr(...)를 항상 복제로 바꾸는 안전 버전
까지 바로 이어서 드릴게요.
반응형
'C#' 카테고리의 다른 글
“직접 델리게이트 콜백” → “.NET 표준 이벤트 패턴(Event + EventArgs)” (0) | 2025.09.19 |
---|---|
sealed를 왜 써야 할까? (0) | 2025.09.19 |
C#의 readonly 키워드 (0) | 2025.09.19 |
null-coalescing assignment 연산자(??=) 가 C# 8.0에서 추가된 문법이라, C# 7.3 컴파일러로는 사용할 수 없다는 뜻입니다. (0) | 2025.09.11 |
EmguImage 안에 IInputArray를 반환하는 getter (0) | 2025.09.09 |
private static bool IsFiniteFloat(float v) => !(float.IsNaN(v) || float.IsInfinity(v)); (0) | 2025.09.09 |
소멸자(~EmguVision)로 GDI 리소스(Pen)를 해제하는 방식은 권장되지 않습니다. (0) | 2025.09.09 |
ScrollableControl을 상속받았다는 건, “이 컨트롤은 화면(뷰포트)보다 큰 가상의 캔버스를 갖고, 자동 스크롤바/스크롤 오프셋 관리까지 운영체제가 대신 해주는 컨테이너”라는 뜻입니다. (0) | 2025.09.09 |