본문 바로가기
C#

VisionThread/EmguVision 라이브·검사 공통 파이프라인 리팩토링 (풀코드)

by 공부봇 2025. 9. 10.
반응형

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)

  1. 공통 프레임 인입부: IngestFrameFromPointer(...) → 포인터 즉시 복제(Mat.Clone)
  2. 단일 파이프라인: SharedPipeline(route, ...)로 라이브/검사 라우팅
  3. 최신 프레임 유지형 라이브 표시: _latestLiveMat + _liveRenderGate
  4. UI 마샬링 보장: BeginInvoke로 EmguVision 그리기
  5. 기존 검사 함수 재사용: 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(...)를 항상 복제로 바꾸는 안전 버전
    까지 바로 이어서 드릴게요.
반응형