<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Bolts and Pixels</title>
    <link>https://arosyoung.tistory.com/</link>
    <description>비전 검사, 모션 제어, 자동화 장비 개발 10+년.
현장 중심 기술 블로그 Bolts and Pixels에서는 비전 시스템, 모션 튜닝, 정밀 오차 보정, 센서 및 검사 알고리즘 관련 실무 인사이트와 문제 해결 경험을 공유합니다.

</description>
    <language>ko</language>
    <pubDate>Sun, 10 May 2026 09:22:22 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>PixelMechanic</managingEditor>
    <image>
      <title>Bolts and Pixels</title>
      <url>https://tistory1.daumcdn.net/tistory/6698233/attach/ab830267b7c2463d9630fe36a0858270</url>
      <link>https://arosyoung.tistory.com</link>
    </image>
    <item>
      <title>[LensCal] 아직 끝나지 않은 회고 &amp;mdash; 렌즈 캘리브레이션 작업 중간 정리</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-%EC%95%84%EC%A7%81-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EC%9D%80-%ED%9A%8C%EA%B3%A0-%E2%80%94-%EB%A0%8C%EC%A6%88-%EC%BA%98%EB%A6%AC%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9E%91%EC%97%85-%EC%A4%91%EA%B0%84-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LlVJc/dJMb99MKx4o/j4Hrxa4RU9dmeTYKRzqEw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LlVJc/dJMb99MKx4o/j4Hrxa4RU9dmeTYKRzqEw1/img.png&quot; data-alt=&quot;남은일들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LlVJc/dJMb99MKx4o/j4Hrxa4RU9dmeTYKRzqEw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLlVJc%2FdJMb99MKx4o%2Fj4Hrxa4RU9dmeTYKRzqEw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;820&quot; height=&quot;500&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;남은일들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 7/7.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 마지막 편입니다. 보통 마지막 편은 &quot;이렇게 해서 성공적으로 마무리했습니다&quot; 라고 쓰는 게 자연스러운데, 이번 시리즈는 그 모양으로 끝낼 수가 없네요. 솔직하게 말해서 &lt;b&gt;작업은 여전히 진행 중이고, 제일 큰 결정 (검출기 선택) 은 아직 내려지지 않았습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 편은 &quot;완료 회고&quot; 가 아니라 &quot;중간 회고&quot; 입니다. 지금까지 한 일을 정리하고, 아직 정해지지 않은 것들을 정해지지 않은 상태 그대로 적어두려 해요. 반년쯤 뒤에 이 글을 다시 읽었을 때 &quot;아 그때 이런 고민을 했었지&quot; 하고 스스로 피드백할 자료가 될 거라 생각합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금까지 한 일 &amp;mdash; 완료된 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈를 시작할 때만 해도 &quot;OpenCV 단독 체커보드 검출기가 실장비에서 불안정하다&quot; 는 고민에서 출발했었습니다. 지금은 그때보다 한참 나아간 상태예요. 구체적으로 보면 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검출기 두 벌이 같은 인터페이스로 정리되었습니다.&lt;/b&gt; HWTester 에서 이식한 Cognex 기반 &lt;code&gt;CCalibrationTarget&lt;/code&gt; 과, 새로 짠 OpenCV 기반 &lt;code&gt;CCalibrationTargetCV&lt;/code&gt; 가 &lt;b&gt;완전히 똑같은 &lt;code&gt;CALIBRATION_TARGET_RESULT&lt;/code&gt;&lt;/b&gt; 를 채워서 돌려줍니다. 엔진 쪽에서는 한 줄 수정으로 둘을 갈아끼울 수 있어요. 어느 쪽을 선택해도 그 위의 코드는 건드릴 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그리드 인덱스 부여를 검출기 책임으로 통일했습니다.&lt;/b&gt; 자체 OpenCV 검출기 안에서 goodFeaturesToTrack &amp;rarr; 4사분면 검증 &amp;rarr; cornerSubPix &amp;rarr; 피치 추정 &amp;rarr; 축 결정 &amp;rarr; BFS 로 인덱스까지 자동 부여합니다. 엔진은 검출기가 준 인덱스를 &lt;code&gt;m_vGridMap[r][c]&lt;/code&gt; 2차원 배열로 재배치만 합니다. 빈 셀은 -1 로 허용하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;품질 분석 레이어 7종이 올라갔습니다.&lt;/b&gt; 재투영 오차 (Similarity Transform 기반), 격자 회전 각도, 직교성, 중심-주변 편차, 영역별 Michelson 콘트라스트, Sobel 기반 샤프니스, 방사 왜곡 k1&amp;middot;k2. 여기에 기본 지표 (해상도 H/V, 등방성, 9영역 균일도) 까지 합쳐서 총 12개 지표를 체커보드 한 장에서 뽑아냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대시보드를 4단 합성 구조로 만들었습니다.&lt;/b&gt; 히트맵 / 품질 지표 바 / 거리 분포 차트 / 요약 텍스트. 왼쪽 이미지 위에는 OverlayLevel 에 따라 단계별로 오버레이를 얹을 수 있게 했어요. MINIMAL 에서 DETAIL, HEATMAP 까지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트가 135개 PASS 상태입니다.&lt;/b&gt; 중요한 건 이 테스트가 &lt;b&gt;검출기 의존성 없이&lt;/b&gt; 돌아간다는 거예요. &lt;code&gt;_UNIT_TEST&lt;/code&gt; 매크로 하위에 주입용 API (&lt;code&gt;SetDetectedPointsForTest&lt;/code&gt;) 를 뚫어놔서, 이상 격자를 직접 만들어 엔진에 넣고 수학 검증을 할 수 있습니다. CI 가 매일 밤 이걸 돌립니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그 과정에서 배운 것 네 가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 중에 머리에 박힌 것들을 정리해 둡니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &quot;아직 결정 안 했다&quot; 도 하나의 유효한 상태다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 원래 &quot;양쪽을 다 유지하는 건 우유부단한 거다&quot; 라고 생각했어요. 한쪽으로 결정하고 다른 쪽을 걷어내는 게 깔끔한 엔지니어링이라고요. 근데 이번에 해보니 꼭 그런 건 아니더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;결정을 미루는 것&quot; 과 &quot;결정을 내릴 수 있는 상태로 만들어두는 것&quot; 은 다르다는 점입니다. 전자는 그냥 미루는 거고, 후자는 &lt;b&gt;옵션 가치를 유지하는 전략&lt;/b&gt;입니다. 두 검출기를 드롭인 교체 가능한 형태로 만들어 두면, &quot;이 데이터를 더 보고 결정할게&quot; 라는 말이 코드 위에서 실제로 가능해져요. 결정을 미룬 건데 비용이 거의 안 듭니다. 이건 꽤 괜찮은 상태예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 상태가 오래 유지되면 안 됩니다. 반년 이상 끌면 결국 &quot;둘 다 쓸 수 있다&quot; 가 &quot;둘 다 디버깅해야 한다&quot; 로 바뀝니다. 그래서 &quot;언제까지는 결정한다&quot; 는 데드라인이 필요하긴 해요. 저한테 이번 시리즈를 쓴 이유 중 하나가 이 데드라인 감각을 붙잡아 두는 겁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 바꾸지 않아도 되는 걸 바꾸지 않는 게 가장 어렵다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링을 할 때 &quot;이 김에 이것도 바꾸자&quot; 가 정말 유혹적입니다. 결과 구조체가 옛날 스타일인 거, 포인터 메모리 소유권이 애매한 거, 이런 게 눈에 밟혀요. 그런데 이번엔 건드리지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 단순해요. &lt;b&gt;변경 범위가 부풀면 회귀 리스크가 부풀고, 롤백이 어려워집니다.&lt;/b&gt; 검출기 교체라는 리스크 큰 실험을 해보려면, 그 외의 변경을 최대한 눌러두는 게 맞습니다. 결과 구조체가 안 예쁜 건 나중에 따로 손보면 돼요. 지금은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;안 해도 되는 일을 안 하는 감각&quot; 은 사실 &quot;해야 할 일을 하는 감각&quot; 보다 어려운 것 같아요. 전자는 성장의 표시 같지 않거든요. 근데 이걸 못 하면 리팩토링이 끝도 없이 번집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 검출기와 분석 레이어의 분리가 구조적 자유를 준다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 이번 작업에서 가장 크게 느낀 점이에요. 검출기 선택을 미뤄도 분석 레이어는 계속 쌓을 수 있었습니다. 왜냐면 분석 레이어는 검출기를 몰라요. 좌표와 그리드 인덱스만 있으면 돌아가거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분리가 주는 자유는 예상보다 컸습니다. &quot;검출기가 정해진 다음에 분석을 쌓는다&quot; 는 순서가 아니라, 두 작업을 &lt;b&gt;병렬로&lt;/b&gt; 할 수 있었어요. 검출기는 검출기대로 비교 검토하고, 분석은 분석대로 구현하고. 서로 기다리지 않습니다. 이게 가능했던 건 둘 사이의 인터페이스 (&lt;code&gt;CALIBRATION_TARGET_RESULT&lt;/code&gt;) 가 이미 정립되어 있었기 때문이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 설계에서 &quot;경계를 잘 긋는 것&quot; 이 왜 중요한가에 대한 설명이 많은데, 저는 이번에 &lt;b&gt;&quot;경계가 잘 그어져 있으면 양쪽을 병렬로 작업할 수 있다&quot;&lt;/b&gt; 가 제일 피부에 와닿았습니다. 순차로만 가능하던 게 병렬이 되니까 체감 속도가 완전히 달라져요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 숫자는 그림이 되어야 현장에서 쓰인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12개 지표를 아무리 잘 뽑아도, 현장 운용자가 PropertyGrid 의 숫자 리스트를 뚫어져라 보고 있지 않는 한 아무 일도 안 일어납니다. 대시보드를 만들고 나서야 이 지표들이 &quot;존재하기 시작했다&quot; 는 느낌이 들었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤집어 말하면, &lt;b&gt;아무리 좋은 지표도 잘못된 UI 에 얹으면 존재하지 않는 것과 같다&lt;/b&gt;는 뜻입니다. 지표 개발과 시각화 설계는 같은 비중으로 투자해야 하는데, 저는 지금까지 이 균형을 많이 놓쳤던 것 같아요. 이번에 시각화 쪽에 의식적으로 시간을 많이 썼고, 결과적으로 도구가 처음으로 &lt;b&gt;&quot;나 이외의 사람에게 유용한 상태&quot;&lt;/b&gt; 에 가까워졌습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아직 결정 못 한 것들 &amp;mdash; 앞으로 해볼 것&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 큰 질문: 검출기 최종 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 다룬 그대로입니다. Cognex 정식 vs 자체 OpenCV. 현재까지 수집한 데이터로는 정확도/속도 차이는 허용 범위 안이고, OpenCV 쪽이 속도는 소폭 유리합니다. 그런데도 결정을 못 하는 이유는 &lt;b&gt;장기 신뢰성&lt;/b&gt;과 &lt;b&gt;자체 코드의 유지보수 부담&lt;/b&gt; 이라는 두 개의 안 보이는 비용 때문이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 해볼 것:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;더 다양한 조명/반사 조건의 이미지 스트레스 테스트.&lt;/b&gt; 며칠 돌려보고 괜찮았다고 해서 3개월 뒤에도 괜찮다는 보장은 없으니까요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검출 실패 발생 시 원인 로깅 강화.&lt;/b&gt; 자체 OpenCV 검출기에서 실패가 나면 어느 단계에서 났는지 (코너 후보 부족? 4사분면 검증 실패? 축 클러스터링 실패?) 로그로 남도록 고쳐야 합니다. 그래야 &quot;원인 불명 실패&quot; 가 사라져요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;회귀 테스트용 이미지 아카이브 구축.&lt;/b&gt; 두 검출기를 동일 세트에 계속 돌려서 결과 드리프트를 모니터링하기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그리고 이 모든 것에 데드라인 설정.&lt;/b&gt; 언제까지는 결정한다, 라는 스스로와의 약속.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보조적인 미구현 지표들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검출기와 독립인 쪽은 이 몇 가지가 남아 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FOV / 배율&lt;/b&gt; &amp;mdash; 스케일 &amp;times; 센서 크기. 가장 싸게 붙일 수 있는 지표라 다음에 바로 넣을 예정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TV Distortion (%)&lt;/b&gt; &amp;mdash; SMIA/EIA 기준 왜곡률. 보고서 쪽에서 요구하는 표준&lt;/li&gt;
&lt;li&gt;&lt;b&gt;격자 직선성&lt;/b&gt; &amp;mdash; 같은 행/열 코너의 직선 피팅 잔차. 직교성과 교차 검증용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커버리지&lt;/b&gt; &amp;mdash; 코너가 이미지를 얼마나 덮는지, Convex Hull 면적 비율&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실장비 데이터로 임계값 튜닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 중간중간에 &quot;RMS 0.3 px 이하면 우수&quot; 같은 숫자가 나왔는데, 이것들은 전부 &lt;b&gt;교과서 값에 가깝습니다&lt;/b&gt;. 실제 장비에서 수집한 데이터로 재튜닝이 필요해요. 값 자체가 정확해도 기준이 틀어져 있으면 판정이 빗나가거든요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬러 카메라가 오면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 먼 얘기지만, 컬러 카메라로 가면 색수차와 화이트밸런스 균일도가 추가 가능합니다. 현재는 8bit grayscale 고정이라 불가능한 영역이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈를 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체커보드 한 장으로 뽑아낼 수 있는 정보가 생각보다 훨씬 많다는 게 이번 작업의 가장 큰 교훈이었습니다. 저는 오랫동안 이 중 일부만 뽑고 나머지는 버리고 있었어요. &quot;검출기가 좋은가&quot; 에만 신경 쓰고 &quot;좋은 검출기 위에서 뭘 더 뽑을 수 있는가&quot; 는 안 보고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시리즈의 결론을 한 줄로 요약하면 이럴 것 같아요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검출기 선택은 신중하게, 그 위의 분석 레이어는 과감하게, 시각화는 운용자 시선에서.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;검출기는 신중하게&lt;/b&gt; &amp;mdash; 이게 흔들리면 위가 다 흔들립니다. 급하게 결정하지 말고, 드롭인 교체 가능한 상태로 비교 데이터를 쌓아가며 결정하는 게 맞아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분석은 과감하게&lt;/b&gt; &amp;mdash; 검출이 안정되면, 그 위에 쌓을 수 있는 지표는 생각보다 많습니다. 여기서 아끼지 마세요. 자료구조 하나 (&lt;code&gt;m_vGridMap&lt;/code&gt;) 만 잘 설계해두면 분석 함수를 O(1) 인덱스 접근으로 줄줄이 쌓을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시각화는 운용자 시선에서&lt;/b&gt; &amp;mdash; 아무리 정교한 지표도 PropertyGrid 숫자로만 있으면 안 쓰입니다. 히트맵, 바 차트, 임계선 &amp;mdash; 눈이 1초 안에 판정할 수 있는 형태로 바꿔야 그제서야 지표가 &quot;존재&quot; 하기 시작해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &quot;아직 결정 안 했다&quot; 는 말을 부끄럽지 않게 적을 수 있는 것도 엔지니어링의 한 부분이라고 생각하게 됐습니다. 완결된 이야기만 기록하면 고민의 과정이 전부 사라지거든요. 결정의 결과보다 결정의 이유가 나중엔 더 값지더라구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검출기 선택이 결정되면 후속 포스트로 돌아오겠습니다. 그때는 &quot;이렇게 갔고 그 이유는 이거였습니다&quot; 라는 단정적인 이야기를 쓸 수 있을 거예요. 그 전까지는 이 중간보고로 대신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 시리즈 읽어 주셔서 감사합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;larr; 시리즈 목차&lt;/b&gt;: &lt;a href=&quot;00_Index.md&quot;&gt;00_Index.md&lt;/a&gt;&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/55</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-%EC%95%84%EC%A7%81-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EC%9D%80-%ED%9A%8C%EA%B3%A0-%E2%80%94-%EB%A0%8C%EC%A6%88-%EC%BA%98%EB%A6%AC%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9E%91%EC%97%85-%EC%A4%91%EA%B0%84-%EC%A0%95%EB%A6%AC#entry55comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:59:24 +0900</pubDate>
    </item>
    <item>
      <title>[LensCal] 숫자보다 히트맵 &amp;mdash; 현장에서 실제로 쓰이는 대시보드 만들기</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-%EC%88%AB%EC%9E%90%EB%B3%B4%EB%8B%A4-%ED%9E%88%ED%8A%B8%EB%A7%B5-%E2%80%94-%ED%98%84%EC%9E%A5%EC%97%90%EC%84%9C-%EC%8B%A4%EC%A0%9C%EB%A1%9C-%EC%93%B0%EC%9D%B4%EB%8A%94-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1983&quot; data-origin-height=&quot;1624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cI2WmM/dJMcagLVyBa/eg26iWgxm5Pgp0115Jp3pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cI2WmM/dJMcagLVyBa/eg26iWgxm5Pgp0115Jp3pK/img.png&quot; data-alt=&quot;대쉬보드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI2WmM/dJMcagLVyBa/eg26iWgxm5Pgp0115Jp3pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI2WmM%2FdJMcagLVyBa%2Feg26iWgxm5Pgp0115Jp3pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1983&quot; height=&quot;1624&quot; data-origin-width=&quot;1983&quot; data-origin-height=&quot;1624&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대쉬보드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 6/7. 12 개 지표를 뽑아놨는데 운용자가 안 본다면 소용이 없습니다. &quot;1 초 안에 판정 가능한&quot; UI 를 만들기 위해 어떤 고민을 했는지에 대한 기록입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. PropertyGrid 만으로는 왜 부족한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지표 12 개를 전부 &lt;code&gt;PropertyGrid&lt;/code&gt; 에 쏟아부으면 이렇게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;▸ 검출 결과
    검출 코너 수              96
    회전 각도                 1.23&amp;deg;
▸ 품질 지표
    재투영 오차 RMS           0.15 px
    재투영 오차 Max           0.42 px
    직교성 평균               0.08&amp;deg;
    직교성 최대               0.22&amp;deg;
    중심-주변 편차 (최대)     0.12%
    콘트라스트 최소           0.82
    콘트라스트 최대           0.91
    샤프니스 최소             45.2
    샤프니스 최대             52.1
    왜곡 k1                   1.2e-8
    왜곡 k2                   -3.4e-12
    최대 왜곡량               0.02 px&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 숫자들이 &lt;b&gt;전부 나란히 있어서&lt;/b&gt; 중요도가 안 보인다는 점입니다. 눈에 띄는 색도, 위계도 없습니다. 현장 운용자는 &quot;정상인가 불량인가&quot; 를 1 초 안에 판단해야 하는데, 숫자 리스트는 그걸 해주지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 운용자는 렌즈 전문가가 아닌 경우가 많습니다. &quot;왜곡 k1 = 1.2e-8&quot; 이라는 숫자를 보고 &quot;아 정상이구나&quot; 라고 판단하려면 먼저 &lt;b&gt;임계값이 얼마인지 기억&lt;/b&gt; 해야 하고, 그 기억을 매번 불러와야 합니다. 이 인지 부담이 누적되면 결국 &lt;b&gt;&quot;그냥 안 본다&quot;&lt;/b&gt; 는 결과로 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우측 패널을 &lt;b&gt;4 단 합성 이미지&lt;/b&gt;로 구성하고, &lt;code&gt;PropertyGrid&lt;/code&gt; 는 세부 수치 확인용으로 한정했습니다. 역할을 명확히 나눈 겁니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 4 단 합성 레이아웃&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;┌──────────────────────────┐
│   [1] 히트맵             │  25%
│   3&amp;times;3 스케일 / 콘트라스트 │
├──────────────────────────┤
│   [2] 품질 지표 바       │  25%
│   재투영 / 직교성 / 샤프니스│
├──────────────────────────┤
│   [3] 거리 분포 차트     │  25%
│   H / V 코너 간 거리     │
├──────────────────────────┤
│   [4] 요약 텍스트        │  25%
│   스케일&amp;middot;등방성&amp;middot;왜곡&amp;middot;중심편차│
└──────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4 단으로 나눈 이유는 각 단이 &lt;b&gt;서로 다른 종류의 정보&lt;/b&gt; 를 담기 때문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[1] 히트맵 &amp;rarr; 공간 분포&lt;/li&gt;
&lt;li&gt;[2] 바 차트 &amp;rarr; 정량 수치의 임계값 대비 위치&lt;/li&gt;
&lt;li&gt;[3] 분포 차트 &amp;rarr; 분산/이상치&lt;/li&gt;
&lt;li&gt;[4] 텍스트 &amp;rarr; 확정된 수치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 정보를 네 번 반복해서 보여주는 게 아니라, &lt;b&gt;네 가지 다른 질문에 답하는&lt;/b&gt; 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. [1] 히트맵 &amp;mdash; 공간적 불균일을 눈으로 잡기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3&amp;times;3 영역의 스케일 또는 콘트라스트를 색으로 표현합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정상: 9 칸이 모두 비슷한 색&lt;/li&gt;
&lt;li&gt;모서리 4 칸만 어두움 &amp;rarr; &lt;b&gt;광학 문제 (필드 커버리지 부족)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;한쪽 변 3 칸만 어두움 &amp;rarr; &lt;b&gt;조명 비대칭&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;중심 밝고 주변 어두움 &amp;rarr; &lt;b&gt;전형적인 비네팅 패턴&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자로 &quot;0.0012, 0.0011, 0.0013, ...&quot; 을 비교하려면 몇 초가 걸립니다. 반면 히트맵은 이 패턴이 &lt;b&gt;눈에 바로 꽂힙니다&lt;/b&gt;. 이게 시각화의 본질이라고 생각합니다. 시각화는 정보를 &quot;담는&quot; 게 아니라 &lt;b&gt;&quot;패턴 인식 능력을 빌려오는&quot;&lt;/b&gt; 도구입니다. 사람의 시각 피질은 수치 비교보다 색 패턴 인식에 수십 배 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. [2] 품질 지표 바 &amp;mdash; 임계값 대비 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재투영, 직교성, 샤프니스를 가로 바로 그리되, 임계선을 함께 그립니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;재투영 RMS     ▓▓▓▓░░░░░░  0.15 px   [우수 | 보통 | 불량]
직교성 평균    ▓▓░░░░░░░░  0.08&amp;deg;     [우수 | 보통 | 불량]
샤프니스       ▓▓▓▓▓▓▓░░░  47.3      [정상 범위]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운용자가 숫자를 읽지 않아도 &lt;b&gt;&quot;초록 영역에 있는가 빨간 영역에 있는가&quot;&lt;/b&gt; 만 보면 됩니다. &quot;임계값이 0.3 이었나 0.5 였나...&quot; 를 기억하지 않아도 되는 거죠. 기억의 짐을 UI 가 대신 져주는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수치에 익숙하지 않은 운용자에게는 이 표현이 훨씬 빠르고, 익숙한 엔지니어에게도 &quot;빠르게 훑어볼 때&quot; 유용합니다. 엔지니어가 상세값이 필요하면 바 옆의 숫자를 읽으면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. [3] 거리 분포 차트 &amp;mdash; 이상치와 분산 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H / V 코너 간 거리의 히스토그램 또는 박스 플롯입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분포가 좁고 가운데 모여 있으면 &amp;rarr; 검출 안정&lt;/li&gt;
&lt;li&gt;꼬리가 길거나 이중 봉우리가 보이면 &amp;rarr; 일부 코너 오검출 의심&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;dStdDistH_px&lt;/code&gt;, &lt;code&gt;dStdDistV_px&lt;/code&gt; 를 숫자 하나로 보는 것과, 분포 자체를 보는 것은 정보량이 다릅니다. 특히 &lt;b&gt;&quot;평균은 괜찮은데 이상치가 하나 있다&quot;&lt;/b&gt; 는 상황은 숫자로는 드러나지 않고 차트에서만 드러납니다. 이상치의 존재는 대부분 &lt;b&gt;특정 코너 하나의 오검출&lt;/b&gt; 을 가리키는데, 이게 뒤늦게 발견되면 원인 찾기가 매우 어렵습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4. [4] 요약 텍스트 &amp;mdash; 확정 수치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 정확한 수치가 필요한 값만 압축해서 표시합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Scale H : 0.01234 mm/px
Scale V : 0.01235 mm/px
Isotropy : 0.08%  [OK]
Distortion k1 : 1.2e-8  [Normal]
Center-Edge Dev : 0.12%&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케일 같은 값은 &quot;초록/빨강&quot; 이 아니라 &lt;b&gt;정확한 숫자&lt;/b&gt; 로 봐야 하는 경우가 있습니다. 예를 들어 장비 설치 보고서에 &quot;이 장비의 해상도는 0.01234 mm/pixel 이다&quot; 라는 값을 기록해야 할 때, 히트맵 색깔로는 부족합니다. 이런 건 그림보다 텍스트가 맞습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 오버레이 레벨 &amp;mdash; 점진적으로 정보를 얹기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽의 Cognex 디스플레이에는 원본 이미지 위에 코너 오버레이를 그립니다. 그런데 오버레이가 너무 많으면 이미지 자체를 가려버립니다. 그래서 단계별로 켤 수 있게 만들었습니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum OverlayLevel {
    OVERLAY_MINIMAL = 0,  // 코너 점 + 스케일 텍스트 (기본)
    OVERLAY_NORMAL  = 1,  // + 코너 인덱스 + 첫 코너 강조 + 등방성
    OVERLAY_DETAIL  = 2,  // + H/V 거리 라인 + 피치 + 잔차/왜곡 벡터
    OVERLAY_HEATMAP = 3   // 로컬 스케일 히트맵 (독립 모드)
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 레벨별 설계 의도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MINIMAL&lt;/b&gt; &amp;mdash; 기본값입니다. 이미지를 최대한 방해하지 않는 최소 정보만 올립니다. 현장 운용자가 &quot;이미지 그대로 보고 싶어&quot; 할 때 이 상태가 기본이어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NORMAL&lt;/b&gt; &amp;mdash; 디버깅 시작 단계입니다. &quot;첫 코너가 어딘가&quot; 를 표시하는 이유는, 그리드 정렬이 맞는지 가장 먼저 확인할 때 필요하기 때문입니다. 개발 단계에서 &lt;code&gt;m_vGridMap[0][0]&lt;/code&gt; 이 의도한 자리에 찍혔는지 확인하는 것이 검증의 출발점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DETAIL&lt;/b&gt; &amp;mdash; 기하 정밀도 분석 전용입니다. &lt;b&gt;재투영 잔차 벡터&lt;/b&gt; 는 방향성 있는 오차를 드러냅니다. 모든 벡터가 같은 방향을 가리키면 렌즈 비틀림이나 설치 기울어짐을 의심해야 합니다. 색으로 된 대시보드만으로는 &quot;방향성&quot; 정보가 잘 안 드러나는데, 이미지 위에 벡터를 직접 그리면 방향성이 즉시 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HEATMAP&lt;/b&gt; &amp;mdash; 공간 분포 분석 전용 모드입니다. 기존 이미지를 덮어쓰면서 영역별 스케일을 색으로 표시합니다. 다른 레벨과 달리 &quot;독립 모드&quot; 로 분리한 이유는, 이미지와 히트맵을 동시에 겹치면 둘 다 읽기 어려워지기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본은 깨끗하게, 필요할 때만 정보를 추가로 얹는&lt;/b&gt; 원칙이 핵심입니다. UI 에 정보를 추가하는 것보다 &lt;b&gt;언제 숨길 것인가&lt;/b&gt; 를 결정하는 게 더 어렵고 중요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 왜 이 구조가 실제로 효과적인가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. 계층적 정보 전달&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; 1 초: 히트맵 색깔 / 바 차트 초록-빨강 &amp;rarr; OK or NG 판정
 5 초: 바 차트 수치 / 요약 텍스트     &amp;rarr; 어느 지표가 문제인가
30 초: 오버레이 DETAIL / PropertyGrid &amp;rarr; 왜 그런가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계가 독립적으로 의미가 있고, 깊이 들어갈수록 더 많은 정보를 줍니다. 운용자는 1 초만 쓰고 끝낼 수 있고, 엔지니어는 30 초를 써서 원인까지 파고들 수 있습니다. &lt;b&gt;두 사용자 집단이 같은 UI 를 각자의 속도로 사용&lt;/b&gt;할 수 있는 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. &quot;나쁜 쪽&quot; 으로 눈이 먼저 가도록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상 상황일수록 눈에 띄는 색과 위치에 배치했습니다. 정상일 때는 대시보드 전체가 차분한 초록/파란 톤이고, 문제가 생기면 빨간색 바가 튀어나오는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이상이 눈에 먼저 들어오는 디자인&lt;/b&gt; 이 현장 운용 도구의 기본기라고 생각합니다. 평상시에 운용자가 대시보드를 무심코 스치듯 보는 상황에서도, 문제가 있으면 &quot;가만히 있던 초록 바에서 갑자기 빨간색이 튀어나온다&quot; 는 변화로 주의를 끌 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3. 숫자와 그림의 역할 분리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;그림&lt;/b&gt;: 판정, 공간 분포, 경향성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;숫자&lt;/b&gt;: 기록, 보고서, 정밀 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 필요하지만, &lt;b&gt;동시에 같은 공간에 있으면 안 됩니다&lt;/b&gt;. 한 공간에 섞여 있으면 둘 다 제대로 못 읽습니다. 대시보드는 그림 중심, &lt;code&gt;PropertyGrid&lt;/code&gt; 는 숫자 중심으로 역할을 명확히 나눴습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 한 줄 요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;주 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;히트맵&lt;/td&gt;
&lt;td&gt;공간 불균일 즉시 감지&lt;/td&gt;
&lt;td&gt;운용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;품질 바&lt;/td&gt;
&lt;td&gt;임계값 대비 위치&lt;/td&gt;
&lt;td&gt;운용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;거리 차트&lt;/td&gt;
&lt;td&gt;분산 / 이상치&lt;/td&gt;
&lt;td&gt;엔지니어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요약 텍스트&lt;/td&gt;
&lt;td&gt;확정 수치&lt;/td&gt;
&lt;td&gt;엔지니어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오버레이 (MIN ~ DETAIL)&lt;/td&gt;
&lt;td&gt;이미지 레벨 디버깅&lt;/td&gt;
&lt;td&gt;엔지니어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PropertyGrid&lt;/td&gt;
&lt;td&gt;전체 수치 아카이브&lt;/td&gt;
&lt;td&gt;보고서&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운용자가 판정을 내리는 데 필요한 정보와, 엔지니어가 원인을 찾는 데 필요한 정보는 &lt;b&gt;다릅니다&lt;/b&gt;. 대시보드는 이 두 요구를 한 화면에서 동시에 만족시켜야 합니다. 이 균형을 잡는 게 가장 어려웠고, 동시에 가장 중요했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 한 줄 교훈&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시각화는 정보를 담는 도구가 아니라, 사람의 패턴 인식 능력을 빌려오는 도구다. 그래서 어떤 정보를 숨길 것인가가 어떤 정보를 보여줄 것인가만큼 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편은 시리즈 마지막 &amp;mdash; 이번 리팩토링 전체 과정에서 배운 것과, 앞으로 추가할 항목에 대한 회고입니다.&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/54</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-%EC%88%AB%EC%9E%90%EB%B3%B4%EB%8B%A4-%ED%9E%88%ED%8A%B8%EB%A7%B5-%E2%80%94-%ED%98%84%EC%9E%A5%EC%97%90%EC%84%9C-%EC%8B%A4%EC%A0%9C%EB%A1%9C-%EC%93%B0%EC%9D%B4%EB%8A%94-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry54comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:56:16 +0900</pubDate>
    </item>
    <item>
      <title>[LensCal] 텔레센트릭 렌즈에도 왜곡 측정이 필요한 이유 &amp;mdash; 광학 품질 3종 세트</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-%ED%85%94%EB%A0%88%EC%84%BC%ED%8A%B8%EB%A6%AD-%EB%A0%8C%EC%A6%88%EC%97%90%EB%8F%84-%EC%99%9C%EA%B3%A1-%EC%B8%A1%EC%A0%95%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0-%E2%80%94-%EA%B4%91%ED%95%99-%ED%92%88%EC%A7%88-3%EC%A2%85-%EC%84%B8%ED%8A%B8</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qMSNV/dJMcagZqdm1/hKdKFcBGGm7CWeukHM2qFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qMSNV/dJMcagZqdm1/hKdKFcBGGm7CWeukHM2qFk/img.png&quot; data-alt=&quot;광학&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qMSNV/dJMcagZqdm1/hKdKFcBGGm7CWeukHM2qFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqMSNV%2FdJMcagZqdm1%2FhKdKFcBGGm7CWeukHM2qFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;820&quot; height=&quot;480&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;광학&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 5/7. 같은 체커보드 이미지에서 렌즈의 &lt;b&gt;광학적 품질&lt;/b&gt;까지 뽑아냅니다. 그런데 첫 번째 질문은 이겁니다. &quot;텔레센트릭 렌즈는 왜곡이 0 이라고 하는데, 굳이 측정해야 할까요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 기하 지표와 광학 지표의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 편에서 다룬 기하학적 지표 (재투영 오차, 직교성) 는 &quot;격자가 반듯한가&quot; 를 봅니다. 그런데 이 지표들로는 답하지 못하는 질문이 여전히 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 렌즈의 왜곡 특성이 스펙 범위 안에 들어오는가?&lt;/li&gt;
&lt;li&gt;이미지 전체에 조명이 균일하게 들어왔는가?&lt;/li&gt;
&lt;li&gt;포커스가 영역별로 균일하게 맞았는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 격자의 &lt;b&gt;기하학적&lt;/b&gt; 반듯함이 아니라, 렌즈 자체의 &lt;b&gt;물리적/광학적&lt;/b&gt; 특성입니다. 이번 편에서 다룰 세 지표가 정확히 이 영역을 담당합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;방사 왜곡 계수 (Radial Distortion k1, k2)&lt;/li&gt;
&lt;li&gt;영역별 콘트라스트 (Michelson Contrast)&lt;/li&gt;
&lt;li&gt;에지 샤프니스 (Sobel-based Sharpness)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 방사 왜곡 계수 &amp;mdash; &quot;0 인 걸 알면서 왜 재나요?&quot;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 텔레센트릭 렌즈에 왜곡 측정이 왜 필요한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 장비는 &lt;b&gt;텔레센트릭 렌즈&lt;/b&gt;를 사용합니다. 이론적으로 텔레센트릭 렌즈의 왜곡 계수는 &lt;code&gt;&amp;asymp; 0&lt;/code&gt; 이어야 합니다. 그러면 &quot;어차피 0 인 걸 아는데, 왜 굳이 측정하나요?&quot; 라는 질문이 자연스럽게 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 이 질문을 스스로에게 던져 봤고, 결론은 이랬습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;이론상 0&quot; 이라는 전제 자체를 실측으로 검증하는 것이 캘리브레이션의 본질이다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텔레센트릭 렌즈도 제조 편차가 있고, 오래 쓰면 미세하게 틀어집니다. 충격을 받거나 온도 변화가 큰 환경에서는 광학 요소의 정렬이 바뀔 수 있습니다. &lt;b&gt;&lt;code&gt;k1&lt;/code&gt; 이 기준값을 벗어나는 순간&lt;/b&gt; 이 그 렌즈를 의심해야 할 시점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 지표는 &quot;값이 얼마인가&quot; 가 아니라 &lt;b&gt;&quot;임계값을 넘었는가&quot;&lt;/b&gt; 를 판정하기 위해 존재합니다. 대부분의 경우 측정값은 &lt;code&gt;1e-9&lt;/code&gt; 수준이고 사람이 직접 볼 필요도 없습니다. 그러다 어느 날 &lt;code&gt;1e-5&lt;/code&gt; 가 찍히면, 그때 이 지표가 밥값을 합니다. &lt;b&gt;캘리브레이션 도구의 많은 부분이 &quot;안 쓰이다가 결정적인 순간에 한 번 쓰이는&quot; 식으로 동작합니다.&lt;/b&gt; 평소에 값이 0 이라는 것 자체가 도구가 잘 만들어졌다는 증거입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 계산 과정&lt;/h3&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;1) 이미지 중심 (cx, cy) 기준 각 코너의 반경
   r_detected[i] = sqrt((x[i] - cx)&amp;sup2; + (y[i] - cy)&amp;sup2;)

2) 재투영 오차에서 구한 projected 점들의 반경
   r_ideal[i]    = sqrt((px[i] - cx)&amp;sup2; + (py[i] - cy)&amp;sup2;)

3) 반경 방향 편차
   dr[i] = r_detected[i] - r_ideal[i]

4) 최소자승법으로 모델 피팅
   dr = k1 &amp;middot; r&amp;sup3; + k2 &amp;middot; r⁵
   &amp;rarr; cv::solve(A, b, x, DECOMP_SVD) 로 [k1, k2] 추정

5) 최대 왜곡량
   dMaxRadialDistortion = max(|dr[i]|)  (pixels)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. 왜 3차, 5차 항만 쓰는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Brown&amp;ndash;Conrady 왜곡 모델의 방사 성분은 이렇게 생겼습니다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;dr = k1&amp;middot;r&amp;sup3; + k2&amp;middot;r⁵ + k3&amp;middot;r⁷ + ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 렌즈 캘리브레이션에서는 &lt;code&gt;k3&lt;/code&gt; 까지 쓰기도 합니다. 그런데 &lt;b&gt;텔레센트릭 렌즈에서는 &lt;code&gt;k1&lt;/code&gt;, &lt;code&gt;k2&lt;/code&gt; 만으로 충분&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대값이 거의 0 인 영역에서 고차항을 넣으면 오히려 노이즈에 피팅되어 해석이 어려워집니다. 모델의 자유도를 필요 이상으로 늘리면 &quot;아무것도 없는 곳에서 패턴을 찾아내는&quot; 현상이 벌어집니다. &lt;b&gt;모델은 목적에 맞게 가볍게 유지하는 편이 좋습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4. 판정 기준&lt;/h3&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;|k1| &amp;lt; 1e-7   : 왜곡 없음 (정상 텔레센트릭)
|k1| &amp;gt; 1e-5   : 비텔레센트릭 의심 &amp;mdash; 렌즈 점검 필요&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.5. 구조체&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// STEP 9: 왜곡
double dDistortionK1;
double dDistortionK2;
double dMaxRadialDistortion;  // pixels&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 영역별 콘트라스트 &amp;mdash; Michelson&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 무엇을 보는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조명과 노출이 9 영역에 균일하게 들어왔는지를 봅니다. 체커보드의 흑/백 타일이 각 영역에서 얼마나 명확하게 구분되는가로 정량화합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. Michelson 콘트라스트&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;contrast = (V_max - V_min) / (V_max + V_min)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 밝기 값의 차이를 합으로 정규화한 값입니다. 0 ~ 1 사이로 나오고, 1 에 가까울수록 &lt;b&gt;흑과 백이 명확하게 구분된다&lt;/b&gt; 는 뜻입니다. 체커보드는 이 계산에 딱 어울리는 패턴입니다. 각 코너 주변에는 흑/백 타일이 대각선 배치로 놓여 있어서, 작은 패치만 샘플링해도 V_min / V_max 를 뽑아낼 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. 계산 방식&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;각 코너 주변 NxN 패치 (예: 20&amp;times;20 px):

  코너를 중심으로 4분면으로 나눔
  ┌─────┬─────┐
  │  A  │  B  │   체커보드 패턴상
  ├─────┼─────┤   A, D = black,  B, C = white
  │  C  │  D  │   (또는 그 반대)
  └─────┴─────┘

  V_white = (B + C) 평균
  V_black = (A + D) 평균
  contrast = (V_white - V_black) / (V_white + V_black)

영역 인덱스 (r, c) 로 9 영역 소속 판정 &amp;rarr; dRegionContrast[9] 평균&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4. 왜 Michelson 인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘트라스트 정의는 여러 가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Weber&lt;/b&gt;: &lt;code&gt;(V_max - V_min) / V_min&lt;/code&gt; &amp;mdash; 배경이 균일할 때&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Michelson&lt;/b&gt;: &lt;code&gt;(V_max - V_min) / (V_max + V_min)&lt;/code&gt; &amp;mdash; 주기 패턴에 적합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RMS&lt;/b&gt;: 픽셀 강도의 표준편차 &amp;mdash; 전역 이미지 통계용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체커보드는 &lt;b&gt;주기적인 흑/백 패턴&lt;/b&gt; 이므로 Michelson 이 표준입니다. 또한 평균 밝기에 의존하지 않고 정규화되어 있어서, &lt;b&gt;노출 조건이 다른 이미지끼리도 비교&lt;/b&gt; 할 수 있다는 장점이 있습니다. 이 점이 산업 현장에서 특히 중요합니다. 조명 세팅이 미묘하게 바뀌더라도 같은 기준으로 비교할 수 있어야 하거든요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.5. 구조체&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// STEP 7: 콘트라스트
double dRegionContrast[9];   // 영역별 0 ~ 1
double dContrastMin;
double dContrastMax;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 에지 샤프니스 &amp;mdash; &quot;포커스는 잘 맞았는가&quot;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. 계산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 코너 주변의 작은 crop 에서 &lt;b&gt;Sobel 그래디언트의 평균 크기&lt;/b&gt; 를 계산합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 각 코너 위치에서 31 &amp;times; 31 crop
cv::Sobel(crop, sobelX, CV_64F, 1, 0);
cv::Sobel(crop, sobelY, CV_64F, 0, 1);
cv::magnitude(sobelX, sobelY, gradient);
double sharpness = cv::mean(gradient)[0];

// 영역별 평균 &amp;rarr; dRegionSharpness[9]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. 절대값이 아니라 상대값&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤프니스는 &lt;code&gt;mean(|gradient|)&lt;/code&gt; 이기 때문에 절대 수치 자체는 조명/노출에 따라 달라집니다. 그래서 우리가 실제로 보는 건 &lt;b&gt;영역 간 상대 편차&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;double dSharpnessMin;
double dSharpnessMax;
// 사용 예: (max - min) / avg 가 크면 영역별 포커스 불균일&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3. 왜 유용한가 &amp;mdash; &quot;가장 흔한 보이지 않는 불량&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포커스 문제는 렌즈 캘리브레이션에서 &lt;b&gt;가장 흔한 &quot;보이지 않는 불량&quot;&lt;/b&gt; 입니다. 현장 운용자는 이미지가 살짝 흐릿해도 검출이 성공하면 그냥 진행해 버리는 경우가 많습니다. 검출률만 체크하는 시스템이라면 이 상태로 계속 측정이 돌고, 어느 순간 측정값이 누적으로 틀어지기 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤프니스 지표가 있으면 &lt;b&gt;&quot;검출은 성공했지만 포커스가 일부 영역에서 나쁘다&quot;&lt;/b&gt; 는 상황을 잡을 수 있습니다. 유용한 조합 예시는 이런 것들입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중심 샤프니스 대비 모서리가 50% 이하 &amp;rarr; 필드 커버리지 문제 (곡률 수차 의심)&lt;/li&gt;
&lt;li&gt;전체 샤프니스가 기준값 이하 &amp;rarr; 포커스 재조정 필요&lt;/li&gt;
&lt;li&gt;특정 변만 샤프니스 급락 &amp;rarr; 렌즈 한쪽 오염 또는 기울어진 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4. 구조체&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// STEP 8: 샤프니스
double dRegionSharpness[9];
double dSharpnessMin;
double dSharpnessMax;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 전체 12 개 지표 총정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 편까지 소개한 지표를 한 표로 정리하면 이렇습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;기본&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;해상도 H / V&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dScaleH&lt;/code&gt;, &lt;code&gt;dScaleV&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;격자 회전 각도&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dGridAngle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;검출 코너 수&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iDetectedCount&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;균일도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;9 영역 스케일&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dRegionScaleAvg[9]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;등방성&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dIsotropyRatio&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;중심-주변 편차&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dCenterEdgeDevMax&lt;/code&gt;, &lt;code&gt;dCenterEdgeDevAvg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;기하 정밀도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;재투영 오차&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dReprojErrorRMS&lt;/code&gt;, &lt;code&gt;dReprojErrorMax&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;직교성&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dOrthogonalityMean&lt;/code&gt;, &lt;code&gt;dOrthogonalityMax&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;광학 특성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;방사 왜곡 k1, k2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dDistortionK1&lt;/code&gt;, &lt;code&gt;dDistortionK2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;최대 왜곡량&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dMaxRadialDistortion&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;영역별 콘트라스트&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dRegionContrast[9]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;영역별 샤프니스&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dRegionSharpness[9]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체커보드 한 장에서 이 정도를 뽑아낼 수 있다는 건, 결국 &lt;b&gt;코너 좌표에 들어있는 정보 밀도가 그만큼 높다&lt;/b&gt; 는 뜻입니다. 우리가 지금까지 그 대부분을 버리고 있었던 거죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 한 줄 교훈&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;측정의 목적은 &quot;값을 알기 위해서&quot; 가 아니라 &quot;임계값을 넘었는지 판정하기 위해서&quot; 다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관점이 잡히고 나니, 앞으로 추가할 지표들의 우선순위가 훨씬 명확해졌습니다. &quot;값을 보여주기 위한&quot; 지표가 아니라 &lt;b&gt;&quot;판정을 내리기 위한&quot; 지표&lt;/b&gt; 를 먼저 쌓는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이렇게 열심히 지표를 뽑아내도, &lt;b&gt;&quot;어떻게 보여주느냐&quot;&lt;/b&gt; 에서 실패하면 현장에서 안 쓰이게 됩니다. 다음 편은 이 12 개 지표를 운용자가 1 초 안에 해석할 수 있도록 만든 &lt;b&gt;대시보드 설계 이야기&lt;/b&gt; 입니다.&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/53</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-%ED%85%94%EB%A0%88%EC%84%BC%ED%8A%B8%EB%A6%AD-%EB%A0%8C%EC%A6%88%EC%97%90%EB%8F%84-%EC%99%9C%EA%B3%A1-%EC%B8%A1%EC%A0%95%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0-%E2%80%94-%EA%B4%91%ED%95%99-%ED%92%88%EC%A7%88-3%EC%A2%85-%EC%84%B8%ED%8A%B8#entry53comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:42:09 +0900</pubDate>
    </item>
    <item>
      <title>[LensCal] 재투영 오차와 직교성 &amp;mdash; 격자는 얼마나 반듯한가</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-%EC%9E%AC%ED%88%AC%EC%98%81-%EC%98%A4%EC%B0%A8%EC%99%80-%EC%A7%81%EA%B5%90%EC%84%B1-%E2%80%94-%EA%B2%A9%EC%9E%90%EB%8A%94-%EC%96%BC%EB%A7%88%EB%82%98-%EB%B0%98%EB%93%AF%ED%95%9C%EA%B0%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rCyxR/dJMb99TxyFz/0rBWmG6v7K040fs5b46sL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rCyxR/dJMb99TxyFz/0rBWmG6v7K040fs5b46sL1/img.png&quot; data-alt=&quot;재투영 오차, 직교성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rCyxR/dJMb99TxyFz/0rBWmG6v7K040fs5b46sL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrCyxR%2FdJMb99TxyFz%2F0rBWmG6v7K040fs5b46sL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;820&quot; height=&quot;480&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;재투영 오차, 직교성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 4/7.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편까지 해서 기본 지표 5개가 쌓였습니다. 해상도, 코너 간 거리 분포, 등방성, 9영역 균일도, 중심-주변 편차. 운용 현장에서 대부분의 판정은 이 기본 지표들로 해결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 한 가지 질문이 계속 남아요. &lt;b&gt;&quot;평균 스케일은 괜찮아 보이는데, 격자 자체가 미묘하게 휘어 있지는 않은가?&quot;&lt;/b&gt; 평균이 가려버리는 케이스가 있거든요. 코너 100개 중 5개만 조금씩 엇나가 있으면, 평균은 여전히 멀쩡해 보입니다. 이 5개를 잡으려면 개별 코너의 잔차를 봐야 하고, 그게 &lt;b&gt;재투영 오차&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편의 진짜 핵심은 마지막의 교차 진단표입니다. 재투영 오차 혼자서도 유용하지만, 직교성을 같이 봤을 때 진단이 결정적으로 달라지는 순간이 있어요. 그 이야기가 이번 편 전체의 결론이 될 것 같네요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재투영 오차 &amp;mdash; 이상 격자와 몇 픽셀 차이냐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 말하면 이 질문에 답하는 지표입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;내가 검출한 코너들이, 이상적인 반듯한 격자와 얼마나 일치하는가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 지표가 평균을 본다면, 재투영 오차는 &lt;b&gt;개별 코너의 잔차&lt;/b&gt; 를 봅니다. 결과적으로 &quot;이 검출을 믿어도 되는가&quot; 의 최종 게이트 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산 흐름은 이렇습니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;1) 이상 격자 생성
   ideal[r][c] = (c &amp;middot; pitch, r &amp;middot; pitch)
   &amp;rarr; rows &amp;times; cols 개의 완벽한 격자 점

2) Similarity Transform 추정 (스케일 + 회전 + 이동)
   M = cv::estimateAffinePartial2D(ideal_points, detected_points)

3) 이상 격자에 변환 적용
   projected[i] = M &amp;times; ideal[i]

4) 각 코너의 잔차
   error[i] = || detected[i] - projected[i] ||₂

5) 통계
   RMS = sqrt(mean(error[i]&amp;sup2;))
   Max = max(error[i])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수식만 보면 단순한데, 이 중에서 2번 Similarity Transform 선택이 이번 편에서 가장 하고 싶은 얘기입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Similarity Transform 인가 &amp;mdash; 자유도 선택 이야기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상 격자는 원점 기준의 가상의 반듯한 점들입니다. 실제 검출 코너와 비교하려면 위치/크기/각도를 맞춰야 하는데, 이때 쓸 수 있는 변환이 여러 종류 있어요. 저는 처음에 Affine 을 쓸 뻔했습니다. 그리고 이게 정말 큰 실수가 될 뻔했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유도 선택에 따라 결과가 이렇게 달라집니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;변환&lt;/th&gt;
&lt;th&gt;자유도&lt;/th&gt;
&lt;th&gt;이런 효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Euclidean (회전 + 이동)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;스케일 차이를 잔차로 치환해서 부풀림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Similarity (+ 균등 스케일)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;스케일 맞춘 후 순수 격자 오차만 잔차로&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affine (+ 비균등 + 전단)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;등방성 불량을 변환이 흡수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homography&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;왜곡까지 흡수 &amp;mdash; 본말전도&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Euclidean 은 부족합니다. 스케일을 안 맞추면 이상 격자와 실제 검출의 &quot;단위&quot; 가 달라서 잔차가 그냥 단위 차이로 채워집니다. 이건 격자 왜곡 정보가 아니에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Affine 이 진짜 함정입니다. 자유도가 많아지니까 &lt;b&gt;변환 자체가 왜곡을 흡수&lt;/b&gt;해버립니다. 특히 Affine 은 비균등 스케일과 전단을 모두 흡수하기 때문에, &lt;b&gt;등방성 불량이 잔차로 드러나지 않습니다&lt;/b&gt;. 처음에 저는 &quot;자유도가 많을수록 잘 맞춰주니까 좋은 거 아니냐&quot; 라고 생각했다가, 합성 이미지 테스트를 해보고 나서 경악했어요. 일부러 k1 = 1e-4 로 왜곡을 넣었는데 잔차가 거의 0 이 나옵니다. 변환이 왜곡을 다 먹어버린 거죠. &lt;b&gt;보려던 걸 못 보게 만드는 변환&lt;/b&gt;을 골랐던 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Similarity 는 딱 맞습니다. 스케일은 이미 &lt;code&gt;CalcScaleFactor()&lt;/code&gt; 에서 측정했으니까, 재투영 오차에서는 &lt;b&gt;&quot;그 스케일을 빼고 남는 순수한 격자 뒤틀림&quot;&lt;/b&gt; 만 보고 싶은 거예요. 균등 스케일만 맞춰주고, 그 이상은 잔차로 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenCV 에서 이걸 해주는 함수가 &lt;code&gt;cv::estimateAffinePartial2D()&lt;/code&gt; 입니다. 이름에 &quot;Affine&quot; 이 들어가 있어서 저도 한참 헷갈렸는데, &lt;code&gt;Partial&lt;/code&gt; 이 붙으면 Similarity Transform 이라는 뜻이에요. 문서를 처음 볼 때 이 차이를 놓치기 쉽습니다. 저만 그런 건지는 모르겠는데, 한 번 데고 나서 기억하게 된 포인트입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RMS 와 Max 를 둘 다 저장하는 이유&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// STEP 4
double dReprojErrorRMS;   // 전체 RMS (pixels)
double dReprojErrorMax;   // 최대 단일 코너 오차 (pixels)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 RMS 만 저장했어요. 그런데 현장에서 한 번 이런 케이스를 봤습니다. RMS 는 0.25 px 로 아주 양호한데 측정값이 이상하게 들쭉날쭉한 거예요. 찾아보니 &lt;b&gt;코너 하나&lt;/b&gt;가 Max 2.3 px 로 튀고 있었습니다. 나머지가 전부 좋아서 RMS 에 가려졌던 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 체커보드 표면의 먼지였습니다. 특정 코너 위치에 먼지가 앉아서 그 코너만 오검출되고 있었던 거예요. 이런 &quot;국소 이상치&quot; 는 평균 지표로는 절대 못 잡습니다. 그때 이후로 Max 를 반드시 같이 저장하고, 대시보드에도 둘 다 띄우도록 바꿨어요. &lt;b&gt;평균이 좋아도 Max 가 튀면 뭔가 있는 겁니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판정 기준은 대략 이렇게 잡았습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;RMS &amp;le; 0.3 px     : 우수  (운용 품질)
0.3 ~ 1.0 px     : 보통  (경고, 모니터링 필요)
&amp;gt; 1.0 px         : 불량&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 수치는 교과서 값에 가까우니까 현장 데이터로 재튜닝 예정입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;격자 회전 각도 &amp;mdash; 검출기 값과 교차 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 간단합니다. 체커보드가 이미지 축에 대해 얼마나 기울어져 있는지를 계산합니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;int i0 = m_vGridMap[0][0];
int i1 = m_vGridMap[0][cols - 1];
double dx = m_vDetectedPoints[i1].x - m_vDetectedPoints[i0].x;
double dy = m_vDetectedPoints[i1].y - m_vDetectedPoints[i0].y;
double angle_deg = atan2(dy, dx) * 180.0 / CV_PI;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값 자체가 얼마나 대단한 정보를 주는 건 아닙니다. 그런데 한 가지 용도가 있어요. &lt;b&gt;검출기가 자체적으로 계산해서 돌려준 회전 각도 (&lt;code&gt;m_dCogAngle&lt;/code&gt;) 와 비교&lt;/b&gt;합니다. 두 값이 크게 다르면 그리드 맵 재배치 단계에서 뭔가 꼬였다는 뜻이에요. 예를 들어 검출기가 부여한 인덱스에 순서 오류가 있거나, 일부 코너가 잘못된 행/열에 배치된 경우.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 교차 검증은 싸고 효과적입니다. &quot;이 값은 저 값과 거의 같아야 한다&quot; 는 invariant 가 하나라도 있으면, 그걸로 검출 단계의 건강 상태를 언제든 점검할 수 있거든요. 저는 가능하면 이런 invariant 를 여러 개 박아두려고 해요. 나중에 이상이 생기면 어느 단계에서 꼬였는지 빨리 찾을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직교성 &amp;mdash; RMS 가 못 잡는 걸 잡는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 코너에서 수평 이웃 벡터와 수직 이웃 벡터의 사이각이 얼마나 90&amp;deg; 에 가까운지를 봅니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;for (int r = 0; r &amp;lt; rows - 1; ++r) {
    for (int c = 0; c &amp;lt; cols - 1; ++c) {
        int i0 = m_vGridMap[r][c];
        int iR = m_vGridMap[r][c+1];
        int iD = m_vGridMap[r+1][c];
        if (i0 &amp;lt; 0 || iR &amp;lt; 0 || iD &amp;lt; 0) continue;

        cv::Point2f p  = m_vDetectedPoints[i0];
        cv::Point2f vH = m_vDetectedPoints[iR] - p;
        cv::Point2f vV = m_vDetectedPoints[iD] - p;

        double cosT = vH.dot(vV) / (cv::norm(vH) * cv::norm(vV));
        double theta_deg = acos(cosT) * 180.0 / CV_PI;
        double ortho_err = fabs(90.0 - theta_deg);
        // mean / max 업데이트
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판정 기준은 평균 0.1&amp;deg; 이하면 우수, 0.5&amp;deg; 넘어가면 불량으로 잡았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이 지표가 왜 필요하냐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재투영 오차 RMS 만 있으면 안 되나요? 저도 처음엔 그렇게 생각했습니다. 근데 RMS 혼자서는 &lt;b&gt;체계적 방향성&lt;/b&gt; 을 못 잡아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 렌즈가 한 방향으로 살짝 비틀려 있으면, 모든 코너가 같은 방향으로 미세하게 밀립니다. 이 경우 Similarity Transform 이 평행이동 성분을 흡수해버리기 때문에 RMS 로는 그냥 &quot;작은 잔차&quot; 로만 나와요. 사실은 렌즈 자체가 문제인데 &quot;검출이 약간 흔들렸나 보다&quot; 정도로 착각하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 직교성은 각 코너의 &lt;b&gt;로컬 구조&lt;/b&gt;를 봅니다. &quot;이 코너에서 오른쪽 이웃과 아래쪽 이웃이 진짜로 90&amp;deg; 인가.&quot; 이건 렌즈의 접선 왜곡이나 설치 비틀림에 극도로 민감합니다. 로컬 정보라서 전역 변환이 흡수할 수가 없거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직교성을 추가하고 나서 그 전엔 못 보던 케이스가 잡히기 시작했습니다. &quot;RMS 는 0.28 로 양호한데 직교성이 0.35&amp;deg; 로 경고&quot; 라고 나오는 상황. 이게 뭘 뜻하냐면, 이 렌즈는 &lt;b&gt;검출은 잘 되는데 렌즈 자체에 비대칭 왜곡이 있다&lt;/b&gt;는 거예요. RMS 만 봤다면 그냥 OK 를 줬을 텐데, 직교성 덕분에 이상을 잡을 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편의 하이라이트 &amp;mdash; 교차 진단표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 지표를 나란히 놓고 조합해서 보면 이런 진단이 가능해집니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RMS&lt;/th&gt;
&lt;th&gt;직교성&lt;/th&gt;
&lt;th&gt;진단&lt;/th&gt;
&lt;th&gt;1차 대응&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;정상&lt;/td&gt;
&lt;td&gt;진행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;&lt;b&gt;높음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;렌즈 비대칭 / 접선 왜곡&lt;/td&gt;
&lt;td&gt;렌즈 설치 각도 점검&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;높음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;검출 품질 문제&lt;/td&gt;
&lt;td&gt;조명&amp;middot;초점&amp;middot;체커보드 표면 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;높음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;높음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;렌즈 자체 이상 또는 심각한 설치 오류&lt;/td&gt;
&lt;td&gt;렌즈 교체 또는 전면 재설치&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직교성 축을 추가하기 전까지 저는 RMS 만 보고 &quot;검출 이상이냐 렌즈 이상이냐&quot; 의 두 가능성 사이에서 매번 헤맸습니다. 같은 증상에 원인이 두 개니까, 대응이 매번 찍기가 되는 거예요. &quot;일단 조명부터 고쳐볼까, 안 되면 렌즈를 건드려볼까&quot; 이런 식으로요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직교성이 들어오자마자 판단이 결정적으로 바뀌었습니다. &lt;b&gt;두 축을 교차하니까 단일 축으로는 풀 수 없던 케이스가 구분됩니다.&lt;/b&gt; 지표를 &quot;하나 더 추가할지 말지&quot; 고민될 때 저는 요즘 이 기준으로 판단해요. &lt;b&gt;기존 지표로 판정 못 하던 케이스를 이 지표가 결정적으로 판정할 수 있는가?&lt;/b&gt; 그렇다면 추가할 가치가 있고, 아니면 그냥 노이즈입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 다룬 구조체 필드&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// STEP 4
double dReprojErrorRMS;
double dReprojErrorMax;

// STEP 5
double dGridAngle;
double dOrthogonalityMean;
double dOrthogonalityMax;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편은 광학 지표로 넘어갑니다. 방사 왜곡, 콘트라스트, 샤프니스. 특히 &quot;텔레센트릭 렌즈인데 왜곡을 굳이 재나요?&quot; 라는 질문을 직접 던지고 답해보려고 합니다. 제가 실제로 받은 질문이기도 하고, 답을 정리하고 나니 캘리브레이션 도구의 존재 이유 자체에 대한 생각이 좀 정리되더군요.&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/52</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-%EC%9E%AC%ED%88%AC%EC%98%81-%EC%98%A4%EC%B0%A8%EC%99%80-%EC%A7%81%EA%B5%90%EC%84%B1-%E2%80%94-%EA%B2%A9%EC%9E%90%EB%8A%94-%EC%96%BC%EB%A7%88%EB%82%98-%EB%B0%98%EB%93%AF%ED%95%9C%EA%B0%80#entry52comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:38:09 +0900</pubDate>
    </item>
    <item>
      <title>[LensCal] 체커보드에서 뽑아내는 기본 지표 5가지</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-%EC%B2%B4%EC%BB%A4%EB%B3%B4%EB%93%9C%EC%97%90%EC%84%9C-%EB%BD%91%EC%95%84%EB%82%B4%EB%8A%94-%EA%B8%B0%EB%B3%B8-%EC%A7%80%ED%91%9C-5%EA%B0%80%EC%A7%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iy7eM/dJMcahqwDj9/qnrZSiQW4AzD1kTLyzhIT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iy7eM/dJMcahqwDj9/qnrZSiQW4AzD1kTLyzhIT1/img.png&quot; data-alt=&quot;지표에 대해서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iy7eM/dJMcahqwDj9/qnrZSiQW4AzD1kTLyzhIT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIy7eM%2FdJMcahqwDj9%2FqnrZSiQW4AzD1kTLyzhIT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;820&quot; height=&quot;480&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;지표에 대해서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 3/7.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편까지 해서 정렬된 그리드 (&lt;code&gt;m_vGridMap&lt;/code&gt;) 가 준비됐습니다. 이제 여기 위에 분석 함수들을 얹기 시작할 텐데, 가장 먼저 올리는 건 화려한 지표가 아니라 &lt;b&gt;기본 중의 기본&lt;/b&gt;입니다. 해상도, 등방성, 9영역 균일도, 중심-주변 편차. 이름만 들으면 심심해 보이지만, 뒤에 올라갈 모든 고급 지표는 결국 이 바닥 위에 서 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본이 흔들리면 고급 지표가 아무리 정교해도 숫자에 의미가 없어져요. 재투영 오차 0.1 px 라는 값도, 애초에 스케일 계산이 편향되어 있으면 그 0.1 px 는 그냥 위장된 거짓말입니다. 그래서 이번 편은 &quot;당연해 보이지만 당연하지 않은&quot; 것들에 대한 얘기입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해상도 &amp;mdash; 정의는 단순한데&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 핵심이 되는 값이고, 수식은 한 줄이면 끝납니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;해상도 = 타일 실제 크기 (mm) / 코너 간 픽셀 거리 (px)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현도 어렵지 않아요. 수평과 수직을 분리해서 계산합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;dScaleH = dKnownPitch_mm / dMeanDistH_px;
dScaleV = dKnownPitch_mm / dMeanDistV_px;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &quot;코너 간 픽셀 거리&quot; 를 어떻게 뽑느냐가 실전에서는 생각보다 신경 쓸 게 많습니다. &lt;code&gt;m_vGridMap[r][c]&lt;/code&gt; 을 이용해서 수평/수직 이웃 쌍의 거리를 전부 수집한 다음 평균을 내는데, 이때 빈 셀이 껴 있는 경우를 가드해야 해요.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;for (int r = 0; r &amp;lt; rows; ++r) {
    for (int c = 0; c &amp;lt; cols - 1; ++c) {
        int i0 = m_vGridMap[r][c];
        int i1 = m_vGridMap[r][c+1];
        if (i0 &amp;lt; 0 || i1 &amp;lt; 0) continue;

        double d = cv::norm(m_vDetectedPoints[i0] - m_vDetectedPoints[i1]);
        m_vDistH.push_back(d);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수직도 똑같이 돌립니다. 평균은 &lt;code&gt;dMeanDistH_px&lt;/code&gt;, 표준편차는 &lt;code&gt;dStdDistH_px&lt;/code&gt; 에 저장하고요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;H 와 V 를 굳이 분리하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 저도 &quot;그냥 평균 하나 쓰면 되는 거 아닌가?&quot; 생각했습니다. 근데 실장비 이미지를 몇 장 돌려보니 H 와 V 가 미묘하게 다릅니다. 같은 렌즈에서 말이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 여러 가지입니다. 센서의 픽셀이 완전한 정사각형이 아닐 수도 있고, 렌즈가 미세한 타원 왜곡을 가질 수도 있고, 광축이 센서에 완전 수직이 아닐 수도 있어요. 이 차이가 다음에 나올 &lt;b&gt;등방성&lt;/b&gt; 지표의 근거가 됩니다. 만약 H 와 V 를 평균으로 합쳐버리면, 이 미세한 편차가 평균 속에 녹아 없어집니다. 문제가 있어도 안 보이는 상태가 되는 거죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;표준편차를 함께 저장하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;dStdDistH_px&lt;/code&gt; 는 처음에 &quot;그냥 통계 넣어두면 좋으니까&quot; 정도로 추가했는데, 나중에 이 값이 의외로 결정적인 역할을 하더군요. 이상적이라면 코너 간 거리는 거의 일정해야 해요. 표준편차가 지나치게 크다면 셋 중 하나입니다. &lt;b&gt;일부 코너가 오검출됐거나, 격자 정렬이 어긋났거나, 왜곡이 예상보다 크거나.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 중요하냐면, 뒤에서 나올 재투영 오차와 이 값이 서로 다른 방향에서 같은 문제를 가리키는 경우가 있거든요. 두 지표가 동시에 경고를 내면 &quot;아 이건 진짜 검출 문제다&quot; 라고 확신 있게 말할 수 있습니다. 확신은 단일 지표에서 나오지 않고, 서로 독립적인 두 지표의 일치에서 나옵니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;등방성 &amp;mdash; 0.1% 라는 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X축과 Y축 해상도가 얼마나 일치하는가. 수식은 다시 한 줄입니다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;dIsotropyRatio = |dScaleH - dScaleV| / avg(dScaleH, dScaleV) &amp;times; 100  (%)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판정 기준은 이렇게 잡았습니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum IsotropyStatus {
    ISOTROPY_OK      = 0,   // &amp;lt; 0.1%
    ISOTROPY_WARNING = 1,   // 0.1% ~ 0.5%
    ISOTROPY_ERROR   = 2    // &amp;gt; 0.5%
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.1% 라는 숫자가 어디서 나왔냐면, 저희가 쓰는 텔레센트릭 렌즈의 공칭 정밀도가 대략 이 수준이거든요. 이 값 위로 올라가면 렌즈 자체의 문제든, 설치 각도 문제든 뭔가 있는 겁니다. 참고로 이건 현장 데이터로 다시 튜닝해야 할 값이긴 해요. 교과서에서 온 값이라서요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사실 이 값은 1차 게이트입니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등방성의 진짜 역할은 &quot;H 와 V 가 얼마나 일치하는지&quot; 를 보여주는 게 아니라, &lt;b&gt;&quot;이 측정을 믿어도 되는가&quot; 의 1차 필터&lt;/b&gt;입니다. 등방성이 ERROR 로 뜨면 그 뒤의 재투영 오차, 직교성 값은 &lt;b&gt;거의 100%&lt;/b&gt; 같이 나빠집니다. 원인이 렌즈나 설치 쪽에 있을 가능성이 높거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대시보드에서도 등방성 배지를 가장 눈에 띄는 위치에 놓았습니다. 초록(OK) / 노랑(WARNING) / 빨강(ERROR). 운용자는 이 배지 색 하나만 봐도 &quot;오늘 이 장비는 믿을 수 있다/없다&quot; 를 3초 안에 판단할 수 있어요. 숫자가 0.08% 인지 0.12% 인지 구별하는 것보다, &lt;b&gt;&quot;색이 바뀌었다&quot;&lt;/b&gt; 는 사실 자체가 운용자에게는 훨씬 강한 신호입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9영역 균일도 &amp;mdash; 3&amp;times;3 으로 나눈 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 &lt;code&gt;3&amp;times;3&lt;/code&gt; 으로 나눠서 각 영역에서 독립적으로 스케일을 냅니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[0] TL   [1] T   [2] TR
[3] ML   [4] C   [5] MR
[6] BL   [7] B   [8] BR&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영역 인덱스 결정은 정수 나눗셈 두 번이면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int GetRegion9(int r, int c, int totalRows, int totalCols) {
    int rBand = (r * 3) / totalRows;
    int cBand = (c * 3) / totalCols;
    return rBand * 3 + cBand;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 영역에 속한 수평/수직 이웃 쌍의 거리로 국소 스케일을 냅니다. 영역 경계에 걸친 코너를 어디에 넣을지, 코너 수가 적은 영역을 어떻게 다룰지 같은 디테일이 의외로 결과에 영향을 줘요. 경험적으로는 &quot;경계 코너는 상위 영역에 포함, 코너 수 3개 미만 영역은 평균에서 제외&quot; 가 가장 안정적이었습니다. 이걸 정하느라 반나절쯤 씨름했어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 하필 3&amp;times;3 이냐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 답하려고 5&amp;times;5, 4&amp;times;4, 2&amp;times;2 를 다 돌려봤습니다. 결론은 이래요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5&amp;times;5 는 영역이 작아져서 영역당 코너 수가 부족해집니다. 특히 작은 체커보드 (예: 7&amp;times;5 패턴) 에서는 한 영역에 코너가 1~2개밖에 없어서 통계가 안 잡혀요. 2&amp;times;2 는 너무 거칠어서 &quot;중심이 좋은가 주변이 나쁜가&quot; 를 구분할 수 없고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3&amp;times;3 이 딱 &lt;b&gt;&quot;중심 / 변 / 모서리&quot;&lt;/b&gt; 라는 세 그룹을 구분할 수 있는 최소 단위입니다. 이 세 그룹은 렌즈 설계에서 실제로 물리적 의미가 다릅니다. 대부분의 렌즈는 모서리가 가장 나쁘고, 변이 그 다음, 중심이 제일 깨끗해요. 이 경향을 검증할 수 있느냐가 판정 능력을 좌우합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 모서리 4칸만 이상하게 나오면 &quot;광학적 필드 커버리지 문제&quot; 를 의심해야 합니다. 반면 한쪽 변 3칸만 이상하면 &quot;조명 비대칭&quot; 일 가능성이 높아요. 이 두 진단은 전혀 다른 방향이고, 대응 방법도 다릅니다. 3&amp;times;3 이 있어야 이 구분이 가능해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중심-주변 편차 &amp;mdash; 9영역을 한 숫자로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9영역 균일도를 한 단계 더 압축한 지표입니다. 중앙 (C) 을 기준으로 주변 8영역이 얼마나 벗어나는지만 봅니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;double center = m_tStepData.dRegionScaleAvg[4];
double maxDev = 0.0;
double sumDev = 0.0;

for (int i = 0; i &amp;lt; 9; ++i) {
    if (i == 4) continue;
    double dev = fabs(m_tStepData.dRegionScaleAvg[i] - center) / center * 100.0;
    maxDev = std::max(maxDev, dev);
    sumDev += dev;
}

m_tStepData.dCenterEdgeDevMax = maxDev;
m_tStepData.dCenterEdgeDevAvg = sumDev / 8.0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지표가 특히 잘 잡는 케이스가 세 가지 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;텔레센트릭 렌즈 품질 검증&lt;/b&gt; &amp;mdash; 이론상 중심과 주변 스케일이 0.1% 이내여야 정상입니다. 그보다 크면 렌즈 불량입니다. 위에서 얘기한 &quot;이론상 0 을 실측으로 검증&quot; 의 한 케이스죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설치 각도 점검&lt;/b&gt; &amp;mdash; 광축이 기울어지면 특정 &lt;b&gt;방향으로&lt;/b&gt; 편차가 커집니다. 예를 들어 오른쪽 영역 3개만 편차가 크다면 렌즈가 오른쪽으로 기울어져 있을 가능성이 있어요. 이걸 max 하나로 보면 &quot;편차 크다&quot; 만 알고 방향은 모릅니다. 그래서 대시보드에서는 이 값을 9영역 히트맵의 &lt;b&gt;색상 강도&lt;/b&gt;로도 함께 표시합니다. 숫자 압축 + 방향성 표시 두 개를 같이 보여주는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수차&lt;/b&gt; &amp;mdash; 방사 왜곡과는 다른 종류의 &quot;스케일 편차&quot; 를 잡아냅니다. 방사 왜곡은 중심에서 방사 방향으로 생기지만, 중심-주변 편차는 단순히 &quot;영역별 스케일 차이&quot; 라서 원인이 왜곡이 아니어도 잡힙니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 지표만으로 답할 수 없는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서 소개한 지표들만으로 &quot;이 렌즈의 스케일이 정상인가&quot; 는 충분히 판단할 수 있습니다. 대부분의 운용 상황에서는 사실 이 기본 지표들만 봐도 답이 나와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 한 가지 질문에는 여전히 답을 못 합니다. &lt;b&gt;&quot;격자 자체가 얼마나 반듯한가?&quot;&lt;/b&gt; 평균 스케일은 정상인데 격자가 미묘하게 휘어져 있는 경우가 있거든요. 이건 기본 지표로는 안 잡힙니다. 평균이 가려버려요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 답하려면 개별 코너의 잔차를 봐야 하고, 그게 &lt;b&gt;재투영 오차&lt;/b&gt; 입니다. 다음 편에서 다루겠습니다. 그리고 재투영 오차 혼자로는 또 잡지 못하는 게 있어서 &lt;b&gt;직교성&lt;/b&gt; 도 같이 들어갑니다. 두 지표를 교차해서 봐야 진단이 결정적으로 달라지는 순간이 있는데, 그 부분이 다음 편의 하이라이트가 될 것 같네요.&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/51</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-%EC%B2%B4%EC%BB%A4%EB%B3%B4%EB%93%9C%EC%97%90%EC%84%9C-%EB%BD%91%EC%95%84%EB%82%B4%EB%8A%94-%EA%B8%B0%EB%B3%B8-%EC%A7%80%ED%91%9C-5%EA%B0%80%EC%A7%80#entry51comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:31:46 +0900</pubDate>
    </item>
    <item>
      <title>[LensCal] 검출기를 갈아끼울 수 있게 만든 설계</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-%EA%B2%80%EC%B6%9C%EA%B8%B0%EB%A5%BC-%EA%B0%88%EC%95%84%EB%81%BC%EC%9A%B8-%EC%88%98-%EC%9E%88%EA%B2%8C-%EB%A7%8C%EB%93%A0-%EC%84%A4%EA%B3%84</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdmr03/dJMcaaZdhXA/UTe95ZqjXeaaJacI8xb9uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdmr03/dJMcaaZdhXA/UTe95ZqjXeaaJacI8xb9uk/img.png&quot; data-alt=&quot;전체구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdmr03/dJMcaaZdhXA/UTe95ZqjXeaaJacI8xb9uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdmr03%2FdJMcaaZdhXA%2FUTe95ZqjXeaaJacI8xb9uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;820&quot; height=&quot;540&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전체구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 2/7.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 &quot;Cognex 정식 검출기와 자체 OpenCV 검출기를 같이 두고 비교 중&quot; 이라는 얘기를 했습니다. 이게 가능한 건 두 검출기가 &lt;b&gt;완전히 똑같은 결과 구조체를 채워서 돌려주도록&lt;/b&gt; 맞춰놨기 때문이에요. 이번 편은 그 한 조각에 대한 이야기입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼핏 &quot;인터페이스 통일&quot; 이라는 말로 한 줄 정리될 것 같은데, 실제로 해보면 몇 가지 미묘한 선택이 끼어 있어요. 특히 &lt;b&gt;그리드 인덱스 부여를 누가 책임질 것인가&lt;/b&gt; 라는 부분이 이번 구조의 핵심이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출발점 &amp;mdash; 엔진이 알아야 하는 것과 몰라도 되는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캘리브레이션 엔진 (&lt;code&gt;CLensCalibEngine&lt;/code&gt;) 이 하는 일은 대략 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검출된 코너 좌표들을 &lt;code&gt;m_vGridMap[r][c]&lt;/code&gt; 라는 &lt;b&gt;그리드 인덱스 맵&lt;/b&gt;으로 정리&lt;/li&gt;
&lt;li&gt;그 위에 &lt;code&gt;CalcScaleFactor()&lt;/code&gt; 로 스케일 계산&lt;/li&gt;
&lt;li&gt;그 위에 재투영 오차, 직교성, 9영역 히트맵, 왜곡, 콘트라스트, 샤프니스 분석&lt;/li&gt;
&lt;li&gt;대시보드용 요약 데이터 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 &lt;b&gt;검출기에 의존하는 부분은 사실상 첫 줄뿐&lt;/b&gt;입니다. 나머지는 &quot;좌표 + 그리드 인덱스&quot; 만 있으면 돌아가는 순수한 계산이에요. 즉 엔진이 검출기로부터 받고 싶은 것은 딱 두 가지입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 코너의 픽셀 좌표 &lt;code&gt;(x, y)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;그 코너가 &lt;b&gt;&quot;그리드의 몇 번째 열, 몇 번째 행&quot;&lt;/b&gt; 인지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지만 받으면 엔진은 내부를 뭘로 구현했든 신경 쓸 필요가 없어집니다. Cognex 든 OpenCV 든, 혹은 앞으로 추가될 또 다른 검출기든.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공통 결과 구조체 &amp;mdash; 이미 있었던 자산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 원래 Cognex 래퍼가 쓰던 결과 구조체가 이 요구를 거의 그대로 만족하고 있었습니다. (이 구조체의 내용물이 Cognex OCX 의 직접 반환이 아니라는 점은 1편에서 짚었습니다. OCX 는 좌표만 주고, 인덱스&amp;middot;각도&amp;middot;해상도는 래퍼가 계산해서 채우는 거예요.)&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;typedef struct {
    int     nCorner;            // 검출된 코너 수
    double* pdCornerX;          // 코너 X 좌표 배열
    double* pdCornerY;          // 코너 Y 좌표 배열
    int*    piIndexX;           // 그리드 인덱스 X
    int*    piIndexY;           // 그리드 인덱스 Y
    double  dAngle;             // 회전 각도 (degree)
    double  dResolutionX;       // X 해상도 (um/pixel)
    double  dResolutionY;       // Y 해상도 (um/pixel)
} CALIBRATION_TARGET_RESULT;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;code&gt;pdCornerX&lt;/code&gt;, &lt;code&gt;pdCornerY&lt;/code&gt;) 가 좌표, (&lt;code&gt;piIndexX&lt;/code&gt;, &lt;code&gt;piIndexY&lt;/code&gt;) 가 그리드 인덱스. 좌표 배열의 i번째 원소와 인덱스 배열의 i번째 원소가 &lt;b&gt;같은 코너를 가리킵니다&lt;/b&gt;. 즉 &quot;i 번째 검출 코너는 픽셀 &lt;code&gt;(pdCornerX[i], pdCornerY[i])&lt;/code&gt; 에 있고, 그리드 상으로는 &lt;code&gt;(piIndexX[i], piIndexY[i])&lt;/code&gt; 위치다&quot; 라고 해석합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조체가 좀 옛날 스타일이라 &lt;code&gt;double*&lt;/code&gt; 포인터가 여기저기 박혀 있고 메모리 소유권이 다소 애매한 감은 있어요. &lt;code&gt;std::vector&amp;lt;Corner&amp;gt;&lt;/code&gt; 같은 현대적 형태면 더 깔끔했겠지만, 기존 HWTester 코드와의 호환성 때문에 이 모양을 유지하고 있습니다. &quot;잘 돌아가는 인터페이스는 예쁘지 않아도 건드리지 않는다&quot; 가 이번 작업 내내 지킨 원칙이었어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자체 구현 쪽에서 이 구조체를 그대로 채우기로 결정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 구조체가 이미 있으니 방법은 두 가지였습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;새 검출기용으로 더 예쁜 구조체를 따로 만들고, 엔진이 양쪽을 다 받아들이도록 고친다&lt;/li&gt;
&lt;li&gt;새 검출기가 기존 구조체를 그대로 채운다 (엔진은 안 건드림)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 두 번째를 택했습니다. 이유는 단순해요. &lt;b&gt;&quot;바꾸지 않아도 되는 걸 바꾸면 그 자체가 리스크&quot;&lt;/b&gt; 입니다. 엔진 쪽 코드는 지금 잘 돌아가고 있고 테스트 커버리지도 제법 쌓여 있어요. 여기를 건드리는 순간 회귀 테스트가 전부 재검증 대상이 됩니다. 반면 검출기를 교체하는 것만으로는 엔진 코드에 손을 대지 않아도 되니까, 변경 범위가 검출기 파일 두 개 안에만 갇힙니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// CalibrationTargetCV.h &amp;mdash; OpenCV 기반 체커보드 코너 자동 검출 클래스
//
// Cognex 의존성 없이 OpenCV만으로 체커보드 코너를 검출하고
// 해상도(um/pixel) 및 회전 각도를 계산한다.
// 기존 CCalibrationTarget과 동일한 CALIBRATION_TARGET_RESULT를 출력하여
// LensCalibEngine에서 드롭인 교체가 가능하다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 엔진 쪽에서는 &quot;어느 검출기가 왔는가&quot; 를 아예 신경 쓸 필요가 없어졌습니다. &lt;code&gt;DetectPattern()&lt;/code&gt; 함수 안에 있는 한 줄&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;CCalibrationTargetCV target;   // 또는 CCalibrationTarget (Cognex)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 바꾸면 검출기가 교체됩니다. 이게 현재 소스 트리의 상태예요. 두 헤더가 나란히 있고, 한 줄 수정으로 전환 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심은 &quot;그리드 인덱스를 검출기가 책임진다&quot; 는 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이번 편의 진짜 포인트를 얘기하고 싶습니다. 결과 구조체가 공통이라는 건 절반의 얘기예요. 나머지 절반은 &lt;b&gt;&quot;그리드 인덱스를 누가 부여하느냐&quot;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검출기가 좌표만 돌려주고 그리드 인덱스는 엔진이 복원하는 구조도 가능합니다. 사실 이게 더 흔한 패턴이에요. &quot;검출기는 점만 주고, 정렬은 상위 레이어가 한다.&quot; 그런데 저는 반대로 갔습니다. &lt;b&gt;검출기가 자체적으로 인덱스까지 부여해서 돌려줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 엔진 쪽에 &quot;좌표 &amp;rarr; 그리드&quot; 복원 로직이 들어가면, &lt;b&gt;검출기를 갈아낄 때마다 이 복원 로직이 새 검출기의 출력 특성에 맞춰 튜닝되어야&lt;/b&gt; 합니다. Cognex 가 주는 좌표 순서와 OpenCV 가 주는 좌표 순서가 미묘하게 다를 수 있고, 이걸 엔진에서 일반화해서 처리하려면 결국 조건문이 늘어나요. &quot;어느 검출기가 왔는가를 엔진이 몰라도 된다&quot; 는 원칙이 깨집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;b&gt;복원의 맥락은 검출기 내부에 있습니다.&lt;/b&gt; 검출기는 자기가 검출한 코너들의 분포, 예상 피치, 예상 축 방향 같은 내부 정보를 이미 알고 있어요. 이걸 외부로 내보내서 엔진이 다시 복원하게 하는 건 정보를 한 번 버렸다가 다시 주워오는 꼴입니다. 그냥 검출기가 복원까지 해버리는 게 자연스러워요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, &lt;b&gt;Cognex 래퍼 (&lt;code&gt;CCalibrationTarget&lt;/code&gt;) 는 원래부터 인덱스를 채워서 돌려주고 있었습니다.&lt;/b&gt; 여기에 중요한 디테일이 있는데, Cognex OCX 자체는 사실 코너 좌표만 주지 코너의 그리드 인덱스를 직접 주진 않습니다. HWTester 에서 이 클래스를 만들 때 &lt;b&gt;OCX 가 찾아준 포인트들을 기반으로 래퍼 안에서 직접 격자를 복원하고 인덱스를 부여&lt;/b&gt;해서 결과 구조체에 담아주고 있었던 거예요. 즉 &quot;검출기가 인덱스까지 돌려준다&quot; 는 책임 경계는 Cognex 쪽에서 이미 이렇게 만들어져 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 자체 OpenCV 버전이 똑같은 일을 하도록 맞추면 &lt;b&gt;두 쪽의 책임 경계가 정확히 동일해집니다&lt;/b&gt;. 엔진은 어느 쪽이 왔는지 몰라도 되고, 변경할 이유도 사라져요. 반대로 &quot;자체 OpenCV 버전은 인덱스 안 주니까 엔진에서 복원&quot; 이라는 분기를 만들었다면, 엔진 안에 &quot;이 검출기는 이렇게, 저 검출기는 저렇게&quot; 라는 지저분한 if 가 들어갔을 겁니다. 저는 이미 확립되어 있던 경계를 건드리지 않는 쪽을 택했어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자체 OpenCV 검출기 안에서 그리드를 어떻게 복원하나 &amp;mdash; 간단 스케치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디테일은 이번 편의 주제를 넘어가지만, 뼈대만 보여드리면 이렇습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// CCalibrationTargetCV 내부 파이프라인
1) goodFeaturesToTrack 으로 전체 코너 후보 검출
2) 4사분면 명암 패턴으로 체커보드 코너만 필터링
3) cornerSubPix 로 서브픽셀 정밀도 확보
4) 최근접 이웃 거리의 중앙값 &amp;rarr; 그리드 피치 추정
5) 이웃 방향 벡터 클러스터링 &amp;rarr; 그리드 2축 결정
6) BFS 탐색 &amp;rarr; 자동 그리드 인덱스 부여 &amp;larr; 여기서 piIndexX/piIndexY 를 채움&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 6번입니다. 한 코너를 시작점으로 삼고, 2축을 따라 이웃을 하나씩 추적하면서 (BFS) 각 코너에 &lt;code&gt;(col, row)&lt;/code&gt; 를 부여합니다. 그리드 크기를 UI 에서 미리 받지 않아도 자동으로 범위가 결정돼요. 일부 코너가 빠져도 격자 형태만 유지되면 복원이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 하면 Cognex 버전과 &lt;b&gt;완전히 똑같은 형태&lt;/b&gt;의 &lt;code&gt;piIndexX[]&lt;/code&gt;, &lt;code&gt;piIndexY[]&lt;/code&gt; 가 채워져서 결과 구조체에 실립니다. 엔진 입장에서는 &quot;Cognex 가 돌려준 인덱스인가 자체 구현이 돌려준 인덱스인가&quot; 가 보이지 않아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엔진 쪽 &amp;mdash; 맵 재배치만 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔진이 검출 결과를 받으면 하는 일은 단순합니다. 인덱스를 2차원 배열 &lt;code&gt;m_vGridMap[r][c]&lt;/code&gt; 로 재배치하는 거예요.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;BOOL CLensCalibEngine::BuildGridMap(const CALIBRATION_TARGET_RESULT&amp;amp; tResult)
{
    int nCorner = tResult.nCorner;
    if (nCorner &amp;lt; 4) return FALSE;

    // 그리드 인덱스 유효성 확인 &amp;mdash; 검출기가 이걸 안 주면 탈출
    if (tResult.piIndexX == nullptr || tResult.piIndexY == nullptr)
        return FALSE;

    // 1) 인덱스 범위 &amp;rarr; 격자 크기 결정
    int iMinX = INT_MAX, iMaxX = INT_MIN;
    int iMinY = INT_MAX, iMaxY = INT_MIN;
    for (int i = 0; i &amp;lt; nCorner; i++) {
        iMinX = std::min(iMinX, tResult.piIndexX[i]);
        iMaxX = std::max(iMaxX, tResult.piIndexX[i]);
        iMinY = std::min(iMinY, tResult.piIndexY[i]);
        iMaxY = std::max(iMaxY, tResult.piIndexY[i]);
    }
    int nCols = iMaxX - iMinX + 1;
    int nRows = iMaxY - iMinY + 1;

    // 2) 좌표 배열 저장
    m_vDetectedPoints.clear();
    m_vDetectedPoints.reserve(nCorner);
    for (int i = 0; i &amp;lt; nCorner; i++) {
        m_vDetectedPoints.push_back(
            cv::Point2f((float)tResult.pdCornerX[i], (float)tResult.pdCornerY[i]));
    }

    // 3) 그리드 맵 채우기 &amp;mdash; 빈 셀은 -1
    m_vGridMap.assign(nRows, std::vector&amp;lt;int&amp;gt;(nCols, -1));
    for (int i = 0; i &amp;lt; nCorner; i++) {
        int c = tResult.piIndexX[i] - iMinX;
        int r = tResult.piIndexY[i] - iMinY;
        if (c &amp;gt;= 0 &amp;amp;&amp;amp; c &amp;lt; nCols &amp;amp;&amp;amp; r &amp;gt;= 0 &amp;amp;&amp;amp; r &amp;lt; nRows)
            m_vGridMap[r][c] = i;
    }
    return TRUE;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 &quot;정렬&quot; 같은 이름을 붙일까 하다가 &lt;b&gt;&quot;맵 재배치&quot;&lt;/b&gt; 가 더 정확해서 그렇게 부르고 있어요. 진짜 정렬은 검출기 안에서 이미 끝나 있고, 엔진은 그걸 자기가 쓰기 편한 2차원 배열 형태로 옮기기만 합니다. 코드가 짧고 단순한 게 의도된 거예요. 여기가 길어지면 &quot;검출기가 인덱스 부여를 제대로 안 했구나&quot; 라는 신호입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빈 셀 (-1) 을 허용한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;m_vGridMap[r][c]&lt;/code&gt; 에 &lt;code&gt;-1&lt;/code&gt; 이 들어갈 수 있게 만들었습니다. 처음엔 &quot;모든 셀이 채워져야 정상&quot; 이라고 생각했는데, 현장 이미지 몇 장 물려보고 바로 이 가드를 넣었어요. 이런 일이 실제로 일어납니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장자리 코너가 FOV 에 걸려서 일부만 보임&lt;/li&gt;
&lt;li&gt;조명 국소 어두움으로 특정 영역 검출 누락&lt;/li&gt;
&lt;li&gt;체커보드 표면에 먼지나 스크래치&lt;/li&gt;
&lt;li&gt;반사 플레어로 특정 코너가 흐림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에 &quot;코너 하나 빠졌으니 전체 분석 중단&quot; 이라고 반응하는 도구는 현장에서 안 쓰입니다. &lt;b&gt;부분적으로라도 의미 있는 답을 주는&lt;/b&gt; 쪽이 맞아요. 그래서 후속 분석 함수들은 전부 &lt;code&gt;if (idx &amp;lt; 0) continue;&lt;/code&gt; 같은 가드를 가집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엔진 내부의 핵심 자료구조 두 개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해 보면 엔진 내부에는 결국 이 두 개가 핵심입니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;class CLensCalibEngine {
    std::vector&amp;lt;cv::Point2f&amp;gt;       m_vDetectedPoints;   // 검출 코너 좌표
    std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;  m_vGridMap;          // [row][col] &amp;rarr; 인덱스, -1=빈셀

    // (스케일, STEP 데이터, 검출기 각도 등은 생략)
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;m_vDetectedPoints[i]&lt;/code&gt; 가 i번째 코너의 좌표이고, &lt;code&gt;m_vGridMap[r][c]&lt;/code&gt; 가 (r, c) 위치의 코너가 &lt;code&gt;m_vDetectedPoints&lt;/code&gt; 의 몇 번째에 있는지를 알려줍니다. (빈 셀은 -1.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘만 있으면 이후 분석 함수 전부가 &lt;b&gt;O(1) 인덱스 접근&lt;/b&gt;으로 끝납니다. 재투영 오차, 직교성, 9영역 히트맵, 왜곡 &amp;mdash; 다 여기서 파생돼요. 자료구조 하나의 모양이 그 위의 분석 코드 전체의 복잡도를 결정짓는 전형적인 케이스입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검출기 없이 테스트 &amp;mdash; &lt;code&gt;_UNIT_TEST&lt;/code&gt; 주입 경로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 더. &lt;b&gt;검출기 선택을 미뤄놓고도 엔진 쪽 분석 코드 테스트는 계속 돌려야 했습니다.&lt;/b&gt; 그래서 테스트 전용 주입 API 를 뚫어뒀어요.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#ifdef _UNIT_TEST
public:
    void SetDetectedPointsForTest(const std::vector&amp;lt;cv::Point2f&amp;gt;&amp;amp; vPoints);
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서는 이상 격자를 직접 만들어 주입하고, &lt;code&gt;BuildGridMapFromRowMajor()&lt;/code&gt; 로 맵을 재구성한 뒤 분석 함수들을 돌립니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;auto grid = MakeIdealGrid(7, 5, 10.0);
engine.SetPatternSize(7, 5);
engine.SetKnownPitch_mm(10.0);
engine.SetDetectedPointsForTest(grid);
engine.BuildGridMapFromRowMajor();

ASSERT_TRUE(engine.CalcScaleFactor());
ASSERT_NEAR(engine.GetScaleH(), engine.GetScaleV(), 1e-9);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경로 덕분에 Cognex OCX 나 OpenCV 검출기 중 어느 것에도 의존하지 않고 &lt;b&gt;수학 검증 전용 테스트&lt;/b&gt;를 CI 에서 돌릴 수 있습니다. 135개 테스트가 매일 밤 검출기 없는 환경에서도 PASS 하는 상태를 유지하는 게 이 구조의 열매예요. 검출기 선택이 아직 결정 안 났는데도 그 위의 레이어를 마음 놓고 쌓을 수 있었던 이유가 바로 여기 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 편을 쓰면서 다시 깨달은 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구조를 만들면서 제가 제일 잘했다 싶은 건 &lt;b&gt;&quot;바꾸지 않아도 되는 걸 바꾸지 않은&quot; 것&lt;/b&gt;입니다. 결과 구조체가 좀 옛날 스타일이었지만 건드리지 않았고, 엔진 쪽 코드도 건드리지 않았어요. 변경 범위가 검출기 파일 두 개와 그 안의 알고리즘에만 갇혀 있습니다. 덕분에 검출기 교체라는 리스크 큰 작업을 변경 범위가 작은 상태로 실험할 수 있었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링을 할 때 &quot;이 김에 이것도 바꾸자&quot; 가 정말 유혹적입니다. 근데 그 유혹에 넘어가는 순간 변경 범위가 두 배, 세 배로 부풀어요. 그러면 회귀 테스트도 두 배로 늘어나고, 롤백도 어려워지고, 결국 &quot;이 김에 하려던 것&quot; 이 오히려 본 작업을 방해하게 됩니다. 이번엔 그 유혹을 잘 참았던 것 같아요 (참은 게 아니라 시간 없어서 못 한 거일 수도 있습니다만, 결과적으로는 다행이었습니다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편부터는 검출기와 독립적인 레이어로 넘어갑니다. 3편에서는 이 그리드 맵 위에서 가장 먼저 계산하는 기본 지표들 &amp;mdash; 해상도, 등방성, 9영역 균일도, 중심-주변 편차 &amp;mdash; 을 다루겠습니다.&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/50</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-%EA%B2%80%EC%B6%9C%EA%B8%B0%EB%A5%BC-%EA%B0%88%EC%95%84%EB%81%BC%EC%9A%B8-%EC%88%98-%EC%9E%88%EA%B2%8C-%EB%A7%8C%EB%93%A0-%EC%84%A4%EA%B3%84#entry50comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:27:45 +0900</pubDate>
    </item>
    <item>
      <title>[LensCal] Cognex 정식 검출기 vs 자체 OpenCV 검출기, 아직 고민 중입니다</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-Cognex-%EC%A0%95%EC%8B%9D-%EA%B2%80%EC%B6%9C%EA%B8%B0-vs-%EC%9E%90%EC%B2%B4-OpenCV-%EA%B2%80%EC%B6%9C%EA%B8%B0-%EC%95%84%EC%A7%81-%EA%B3%A0%EB%AF%BC-%EC%A4%91%EC%9E%85%EB%8B%88%EB%8B%A4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RjZ67/dJMcaiXddR1/pxczFty3IcJvulfo9r3LD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RjZ67/dJMcaiXddR1/pxczFty3IcJvulfo9r3LD0/img.png&quot; data-alt=&quot;Cognex Vs OpenCV&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RjZ67/dJMcaiXddR1/pxczFty3IcJvulfo9r3LD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRjZ67%2FdJMcaiXddR1%2FpxczFty3IcJvulfo9r3LD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;820&quot; height=&quot;480&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Cognex Vs OpenCV&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 1/7.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직하게 먼저 말씀드리면, 이 글을 쓰는 지금도 저는 결정을 못 했습니다. Cognex VisionPro 의 정식 체커보드 검출기를 계속 쓸지, 아니면 최근에 직접 짠 OpenCV 기반 검출기로 갈아탈지. 양쪽을 같은 엔진에 번갈아 꽂아보며 며칠째 고민 중이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 &quot;리팩토링 결정 완료&quot; 같은 깔끔한 결론으로 글을 쓰려고 했는데, 쓰다 보니 진짜 상태랑 안 맞아서 접었습니다. 대신 &quot;지금 이런 상황에서 이런 것들을 저울질하고 있습니다&quot; 라는 중간보고 형태로 남기려 합니다. 결정이 나면 후속 포스트로 돌아올 테니, 이번 편은 &lt;b&gt;결정을 내리기 직전까지의 생각 정리&lt;/b&gt;라고 봐주시면 좋겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경 &amp;mdash; 원래 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장비에 들어가는 렌즈 캘리브레이션 모듈은 원래 HWTester 에서 이식한 &lt;code&gt;CCalibrationTarget&lt;/code&gt; 이라는 클래스를 썼습니다. 이름은 평범하지만 내부는 &lt;b&gt;Cognex ICogCalibCheckerboard&lt;/b&gt; 기반입니다. Cognex VisionPro 라이선스가 장비에 깔려 있어서 OCX 를 호출하는 구조예요.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// CalibrationTarget.h &amp;mdash; HWTester 원본 체커보드 캘리브레이션 클래스
// Cognex ICogCalibCheckerboard 기반 체커보드 코너 검출 + 해상도 계산.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검출 결과는 이런 구조체로 내려옵니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;typedef struct {
    int     nCorner;            // 검출된 코너 수
    double* pdCornerX;          // 코너 X 좌표 배열
    double* pdCornerY;          // 코너 Y 좌표 배열
    int*    piIndexX;           // 그리드 인덱스 X
    int*    piIndexY;           // 그리드 인덱스 Y
    double  dAngle;             // 회전 각도 (degree)
    double  dResolutionX;       // X 해상도 (um/pixel)
    double  dResolutionY;       // Y 해상도 (um/pixel)
} CALIBRATION_TARGET_RESULT;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 &lt;code&gt;piIndexX&lt;/code&gt;, &lt;code&gt;piIndexY&lt;/code&gt; 입니다. 검출된 코너마다 &quot;그리드에서 몇 번째 열/행인가&quot; 가 함께 돌아와요. 이게 있으면 후속 분석 (재투영, 직교성, 9영역 히트맵 등) 이 한결 수월해집니다. 반대로 없다면 엔진 쪽에서 직접 복원해야 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 짚고 가야 할 부분이 있는데, 사실 &lt;b&gt;Cognex OCX 자체가 이 인덱스나 회전 각도를 직접 뱉어주는 건 아닙니다&lt;/b&gt;. OCX 가 돌려주는 건 코너 좌표뿐이에요. 그 좌표들을 가지고 &lt;code&gt;CCalibrationTarget&lt;/code&gt; 래퍼 (HWTester 이식본) 가 내부에서 직접 계산합니다. 포인트들의 분포를 보고 격자 구조를 복원해서 &lt;code&gt;piIndexX/Y&lt;/code&gt; 를 채우고, 포인트 간 방향 벡터에서 &lt;code&gt;dAngle&lt;/code&gt; 을 뽑아내는 거죠. 그러니까 정확히 말하면 이 결과 구조체는 &quot;Cognex OCX 의 반환값&quot; 이 아니라 &lt;b&gt;&quot;Cognex OCX 가 찾아준 코너 + 래퍼가 그 위에 덧붙인 후처리 결과&quot;&lt;/b&gt; 입니다. 이 사실이 나중에 2편 이야기의 중요한 실마리가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 몇 년간 잘 돌아가는 코드였습니다. 그런데 최근 들어 제가 이걸 계속 고민하게 된 이유가 몇 가지 있었어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고민의 시작 &amp;mdash; Cognex 에 종속되는 게 점점 부담스러워짐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cognex OCX 자체가 싫은 건 아닙니다. 실제로 꽤 안정적이에요. 다만 &lt;b&gt;빌드와 배포가 좀 번거롭습니다&lt;/b&gt;.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI 서버에 Cognex 라이선스를 깔 수가 없어서 자동 테스트가 반쪽이 됩니다&lt;/li&gt;
&lt;li&gt;개발자 PC 마다 라이선스가 있는 것도 아니라서 &quot;로컬에서는 일단 스킵&quot; 같은 테스트 코드가 섞입니다&lt;/li&gt;
&lt;li&gt;버전 업 할 때마다 OCX 호환성을 확인해야 합니다&lt;/li&gt;
&lt;li&gt;OCX 는 타입 정보가 런타임에 확정되는 부분이 있어서, 정적 분석 도구에 잘 안 잡히는 코드가 여기저기 생깁니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 하나하나는 사소한데 쌓이면 꽤 귀찮아집니다. 그래서 &lt;b&gt;&quot;Cognex 의존성을 줄일 수 있다면 줄이고 싶다&quot;&lt;/b&gt; 는 생각이 한동안 마음속에 있었어요. 지우는 게 아니라, 대체 가능한 옵션을 하나쯤 가지고 있고 싶었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 &lt;code&gt;CCalibrationTargetCV&lt;/code&gt; 를 만들었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 요 근래의 작업입니다. &lt;b&gt;Cognex 없이 OpenCV 만으로 같은 결과 구조체를 만들어 내는 검출기&lt;/b&gt;를 하나 짰어요. 핵심 아이디어는 &quot;인터페이스만 맞춰두면 엔진 쪽은 건드릴 필요 없다&quot; 입니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// CalibrationTargetCV.h &amp;mdash; OpenCV 기반 체커보드 코너 자동 검출 클래스
//
// Cognex 의존성 없이 OpenCV만으로 체커보드 코너를 검출하고
// 해상도(um/pixel) 및 회전 각도를 계산한다.
// 기존 CCalibrationTarget과 동일한 CALIBRATION_TARGET_RESULT를 출력하여
// LensCalibEngine에서 드롭인 교체가 가능하다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 내부 알고리즘은 이렇게 잡았습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1) goodFeaturesToTrack 으로 전체 코너 후보 검출
2) 4사분면 명암 패턴으로 체커보드 코너 검증 (축 정렬 + 45&amp;deg; 회전 양쪽 다)
3) cornerSubPix 로 서브픽셀 정밀도 확보
4) 최근접 이웃 거리 중앙값 &amp;rarr; 그리드 피치 추정
5) 방향 클러스터링 &amp;rarr; 그리드 2축 결정
6) BFS 탐색 &amp;rarr; 자동 그리드 인덱스 부여&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &lt;code&gt;cv::findChessboardCorners()&lt;/code&gt; 를 안 쓰고 이렇게 짰냐면 &amp;mdash; 예전에 &lt;code&gt;findChessboardCorners()&lt;/code&gt; 로 해봤다가 실장비 이미지에서 실패율이 불안정했던 경험이 있거든요. 조명이 살짝 치우치거나 가장자리 코너가 잘리면 통째로 실패하는 경우가 있어서, &lt;b&gt;결국 내 손으로 코너 후보부터 다시 쌓는 게 낫겠다&lt;/b&gt; 싶었습니다. goodFeaturesToTrack 으로 후보를 넉넉히 뽑고, 4사분면 명암 검증으로 체커보드 코너만 필터링하는 쪽이 현장 이미지에 더 견고하더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 검출기의 특징 하나를 미리 짚고 가면, &lt;b&gt;그리드 인덱스 부여까지 검출기 안에서 처리한다&lt;/b&gt;는 점입니다. 즉 Cognex 버전과 똑같이 &lt;code&gt;piIndexX&lt;/code&gt;, &lt;code&gt;piIndexY&lt;/code&gt; 를 채워서 돌려줍니다. 그래서 엔진 쪽은 &lt;b&gt;어느 검출기가 결과를 줬는지 알 필요가 없어요&lt;/b&gt;. 이 부분이 2편에서 다룰 내용입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 상태 &amp;mdash; 두 검출기가 나란히 있습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 트리에는 지금 두 파일이 같이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;core/
├─ CalibrationTarget.h/cpp       &amp;larr; Cognex 버전 (남아있음)
├─ CalibrationTargetCV.h/cpp     &amp;larr; OpenCV 버전 (추가)
└─ LensCalibEngine.cpp            &amp;larr; 현재 CV 버전을 기본 호출&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LensCalibEngine::DetectPattern()&lt;/code&gt; 안을 들여다보면 &lt;code&gt;CCalibrationTargetCV&lt;/code&gt; 를 쓰고 있는데, 한 줄만 바꾸면 Cognex 버전으로 돌아가도록 되어 있습니다. 완전히 걷어낸 게 아니에요. 의도적으로 &lt;b&gt;양쪽을 번갈아 비교할 수 있는 상태&lt;/b&gt;로 유지하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재미있는 건 UI 다이얼로그 코드예요. 로그 메시지에는 아직 &quot;Cognex 검출...&quot; 이라는 문자열이 남아 있습니다. 처음엔 버그로 착각했는데, 알고 보니 &quot;검출기 선택이 아직 끝나지 않아서 로그 문구를 바꾸지 않고 두었다&quot; 는 게 저의 무의식의 표현이었나 봅니다. 결정을 내리면 그때 로그도 일관되게 정리할 생각입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;판단 기준 &amp;mdash; 제가 중요하게 보는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 판단 기준을 명확히 해두지 않으면 계속 왔다갔다 할 것 같아서, 기준을 미리 세워두었습니다. 두 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째, 유지보수성.&lt;/b&gt; 5년 뒤 이 코드를 유지보수할 사람이 저든 다른 사람이든, 얼마나 쉽게 이해하고 고칠 수 있느냐가 중요합니다. Cognex OCX 는 &quot;내부는 블랙박스지만 Cognex 가 책임져 주는&quot; 외주 같은 느낌이고, 자체 구현은 &quot;내가 완전히 이해하지만 버그도 내가 끌어안아야 하는&quot; 집밥 같은 느낌이에요. 둘 다 장단이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째, 검출 실패가 없어야 한다.&lt;/b&gt; 이게 정말 중요합니다. 현장 운용에서 &quot;가끔 실패하는데 원인을 모름&quot; 은 치명적입니다. 운용자 입장에서는 장비 전체에 대한 신뢰가 무너지거든요. 그래서 저는 &quot;99% 성공률 + 1% 원인 불명 실패&quot; 보다는 &lt;b&gt;&quot;느리지만 실패 없이 돌아가는&quot;&lt;/b&gt; 쪽을 훨씬 선호합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확도나 속도는 이 두 기준 아래 순위예요. 허용 범위 안에 들어오면 결정 요인이 아닙니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재까지 본 것 &amp;mdash; 정확도 비슷, 속도 OpenCV 소폭 우세&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 이미지 여러 장을 두 검출기에 물려서 비교해 봤는데, 솔직히 말해서 &lt;b&gt;정확도 차이는 현장 허용 범위 안에서 크게 차이가 없었습니다&lt;/b&gt;. 재투영 오차 RMS 도 둘 다 양호한 범위고, 스케일 값도 소수점 아래 몇 자리에서 차이 나는 수준이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의외였던 건 &lt;b&gt;속도입니다.&lt;/b&gt; 측정해 보니 자체 OpenCV 검출기가 소폭 빠릅니다. Cognex OCX 호출 오버헤드가 생각보다 있었던 건지, 단순 파이프라인이 더 가벼운 건지 정확한 원인은 더 파봐야 알겠지만, 일단 결과만 보면 OpenCV 쪽이 유리해요. 물론 속도 차이도 운용에 크게 영향 주는 수준은 아닙니다. &quot;느려서 못 쓴다&quot; 는 아니에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;정확도/속도만 보면 OpenCV 가 살짝 앞서거나 비슷한 상태&lt;/b&gt;입니다. 이 정도면 &quot;갈아타자&quot; 로 기울어야 할 것 같은데, 그럼 왜 아직 못 하고 있느냐.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데도 결정을 못 내리는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 &lt;b&gt;장기 신뢰성&lt;/b&gt;. Cognex 는 수많은 현장에서 검증된 코드입니다. 이상한 이미지가 들어와도 어떻게든 합리적으로 처리해요. 반면 자체 OpenCV 검출기는 제가 최근에 짠 코드라서, 아직 &lt;b&gt;&quot;아직 못 본 이상한 케이스&quot;&lt;/b&gt; 가 어딘가에 남아 있을 가능성이 있습니다. 며칠 전 테스트에선 잘 돌았지만, 3개월 뒤 예상 못 한 조명 조건에서 실패할지도 모르는 거죠. 이 리스크를 어떻게 관리할지가 제 가장 큰 고민입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 &lt;b&gt;자체 구현의 유지보수 부담&lt;/b&gt;. 자체 코드는 버그가 나면 제가 떠안아야 합니다. Cognex 는 버그가 나면 최소한 &quot;Cognex 쪽에 원인이 있다&quot; 고 주장할 수 있어요 (실제 해결까지 가는 건 별개지만, 책임 경계가 분명합니다). 자체 코드는 그런 경계 없이 전부 내 책임이에요. 이게 코드 수명 전체에 걸쳐 쌓이는 비용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 &lt;b&gt;&quot;지금 바꿔야 하나?&quot;&lt;/b&gt; 라는 질문. Cognex 로도 잘 돌아가고 있는데, 단지 CI 때문에, 단지 빌드 의존성 때문에 갈아타는 게 정말 가치가 있는 결정인지 저도 확신이 안 서요. 기술적으로 더 깨끗해지는 것과 실제 사업 가치 사이에 어느 정도 거리가 있거든요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 택한 절충 &amp;mdash; &quot;갈아끼울 수 있는 상태&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 지금 택한 방향은 &quot;양쪽을 다 가지고 있고, 언제든 갈아끼울 수 있게 만들어 둔다&quot; 입니다. 한 줄 수정으로 전환 가능하도록 말이에요. 이게 우유부단해서라고 하실 수도 있지만, 저는 이걸 &lt;b&gt;결정을 미루는 대신 옵션을 유지하는 전략&lt;/b&gt; 이라고 생각하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 얻은 이점이 있어요. 품질 분석 레이어 (재투영 오차, 직교성, 9영역 히트맵, 왜곡, 콘트라스트, 샤프니스 등) 는 &lt;b&gt;검출기와 완전히 독립적으로 쌓을 수 있게 됐습니다.&lt;/b&gt; 검출기 선택을 미뤄도 이 위의 작업은 계속 진행할 수 있었어요. 시리즈 3편부터 다룰 내용이 바로 이 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조가 가능했던 건 결과 구조체를 두 검출기가 똑같이 쓰도록 맞춰놨기 때문인데, 그 얘기가 다음 편입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 편을 쓰면서 다시 정리한 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그에 &quot;나 이렇게 결정했어요&quot; 를 쓰는 건 쉬운데, &quot;아직 결정 못 했어요&quot; 를 쓰는 건 왠지 쑥스럽습니다. 근데 엔지니어링 작업의 상당 부분은 사실 &quot;아직 결정 못 한&quot; 상태에서 일어나요. 완결된 이야기만 기록하면 정작 고민의 과정은 전부 사라지고, 남는 건 결과뿐입니다. 그래서 이번엔 과정을 그대로 남겨 보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정이 나면 후속 포스트로 돌아올게요. 그때는 &quot;이렇게 갔고 그 이유는 이거였습니다&quot; 라는 정리된 버전을 쓸 수 있겠죠. 그 전까지는 시리즈의 나머지 편에서 &lt;b&gt;검출기와 독립적인 품질 분석 레이어&lt;/b&gt;를 계속 다루게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 두 검출기를 갈아끼울 수 있게 만든 &lt;b&gt;&quot;공통 결과 구조체 + 그리드 인덱스를 검출기가 책임지는 구조&quot;&lt;/b&gt; 를 얘기하겠습니다. 이 설계 한 조각이 지금의 우유부단(?)한 상태를 가능하게 해준 핵심입니다.&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/49</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-Cognex-%EC%A0%95%EC%8B%9D-%EA%B2%80%EC%B6%9C%EA%B8%B0-vs-%EC%9E%90%EC%B2%B4-OpenCV-%EA%B2%80%EC%B6%9C%EA%B8%B0-%EC%95%84%EC%A7%81-%EA%B3%A0%EB%AF%BC-%EC%A4%91%EC%9E%85%EB%8B%88%EB%8B%A4#entry49comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:19:52 +0900</pubDate>
    </item>
    <item>
      <title>[LensCal] 렌즈 캘리브레이션 엔진을 다시 짜면서 &amp;mdash; 시리즈 소개</title>
      <link>https://arosyoung.tistory.com/entry/LensCal-%EB%A0%8C%EC%A6%88-%EC%BA%98%EB%A6%AC%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%94%EC%A7%84%EC%9D%84-%EB%8B%A4%EC%8B%9C-%EC%A7%9C%EB%A9%B4%EC%84%9C-%E2%80%94-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EC%86%8C%EA%B0%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Coxnm/dJMcacJw0Mg/5zuhTEIvGsGF6hheWuOrEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Coxnm/dJMcacJw0Mg/5zuhTEIvGsGF6hheWuOrEK/img.png&quot; data-alt=&quot;체커보드 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Coxnm/dJMcacJw0Mg/5zuhTEIvGsGF6hheWuOrEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCoxnm%2FdJMcacJw0Mg%2F5zuhTEIvGsGF6hheWuOrEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;634&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;체커보드 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머신비전 장비에서 &quot;이 렌즈가 정상이냐&quot; 에 답하는 도구, 바로 렌즈 캘리브레이션 모듈입니다. 저희 장비에서 이걸 한 번 크게 손봤는데, 과정 자체가 좀 길어져서 한 편으로는 안 되겠더라구요. 7편짜리 시리즈로 나눠서 기록해 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;기존 모듈을 Cognex 로 통합하는 리팩토링&quot; 으로 시작했는데, 진행하면서 상황이 바뀌었습니다. &lt;b&gt;Cognex 의 검출 엔진과 구조적으로 호환되는 자체 OpenCV 검출기&lt;/b&gt;를 하나 만들었고, 지금은 두 검출기를 같은 엔진에 꽂아보며 어느 쪽이 장기적으로 더 나은지 고민하는 중입니다. 결정이 난 이야기가 아니라 &lt;b&gt;&quot;아직 결정 안 난&quot; 이야기&lt;/b&gt;예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 시리즈는 &quot;이렇게 했습니다&quot; 라는 완성형 튜토리얼이 아니고, &lt;b&gt;&quot;이런 고민을 하면서 이 구조를 만들었습니다&quot;&lt;/b&gt; 에 가깝습니다. 중간중간 &quot;이건 아직도 모르겠어요&quot; 라는 말이 나올 거예요. 미리 양해 부탁드립니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 구성&lt;/h2&gt;
&lt;table style=&quot;height: 212px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;#&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;제목&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;핵심 주제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;Cognex 정식 검출기 vs 자체 OpenCV 검출기, 아직 고민 중입니다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;두 검출기를 같이 품고 있는 이유, 판단 기준, 현재까지 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;검출기를 갈아끼울 수 있게 만든 설계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;공통 결과 구조체 + 그리드 인덱스를 검출기가 책임지는 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;체커보드에서 뽑아내는 기본 지표 5가지&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;해상도&amp;middot;등방성&amp;middot;9영역 균일도&amp;middot;중심-주변 편차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;재투영 오차와 직교성 &amp;mdash; 격자는 얼마나 반듯한가&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Similarity Transform, 직교성, 교차 진단표&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;텔레센트릭 렌즈에도 왜곡 측정이 필요한 이유&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;방사 왜곡 k1/k2, Michelson 콘트라스트, Sobel 샤프니스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;[Insight] 숫자보다 히트맵 &amp;mdash; 현장에서 실제로 쓰이는 대시보드 만들기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;4단 합성, 오버레이 레벨, 계층적 정보 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;[Insight] 아직 끝나지 않은 회고&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;결정 못 한 것들, 앞으로 해볼 것, 이 과정에서 배운 것&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편부터 6편까지는 검출기 선택과 &lt;b&gt;독립적&lt;/b&gt;인 이야기입니다. 품질 지표와 시각화는 어느 검출기를 쓰든 그 위에 똑같이 올라갈 수 있으니까요. 그래서 검출기 고민이 아직 결정 안 난 상태에서도 이 부분은 안심하고 먼저 만들어 둘 수 있었습니다. 이게 이번 구조의 나름 괜찮은 점이기도 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대상 독자&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;머신비전 장비나 검사기를 직접 만드시는 분&lt;/li&gt;
&lt;li&gt;상용 SDK (Cognex, Halcon 등) 와 자체 OpenCV 구현 사이에서 고민해보신 분&lt;/li&gt;
&lt;li&gt;&quot;캘리브레이션 도구가 있긴 한데 현장에서 안 쓰인다&quot; 는 경험이 있으신 분&lt;/li&gt;
&lt;li&gt;C++/MFC 기반 비전 툴을 유지보수하시는 분&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 스택&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;언어&lt;/b&gt;: C++17, MFC&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비전&lt;/b&gt;: OpenCV 4.x, Cognex VisionPro OCX (CogDisplay, CogCalibCheckerboard)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대상 렌즈&lt;/b&gt;: 텔레센트릭 렌즈 (이론상 왜곡 &amp;asymp; 0)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입력&lt;/b&gt;: 8bit grayscale 체커보드 이미지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트&lt;/b&gt;: 총 135 케이스 PASS (수학 검증용은 Cognex 의존 우회)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 그림&lt;/h2&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;이미지 입력
   │
   ▼
[검출기]                    &amp;larr; 여기가 1~2편의 주제
  ├─ CCalibrationTarget      (Cognex &amp;mdash; HWTester 원본)
  └─ CCalibrationTargetCV    (자체 OpenCV 구현, 현재 기본)
                             &amp;darr;
                   둘 다 CALIBRATION_TARGET_RESULT 반환
   │
   ▼
[그리드 인덱스 맵]          &amp;larr; m_vGridMap[r][c]
   │
   ▼
[스케일 계산]               &amp;larr; 3편
   │
   ▼
[품질 분석 7종]             &amp;larr; 3, 4, 5편
   │
   ▼
[대시보드 + PropertyGrid]   &amp;larr; 6편&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;검출기 두 개가 같은 결과 구조체로 떨어진다&quot; 는 점이 이 구조의 핵심입니다. 엔진 쪽 코드는 어느 검출기가 들어와도 그대로 돌아가요. 이걸 가능하게 만든 설계가 2편의 주제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다음 편부터 본론으로 들어가겠습니다.&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/48</guid>
      <comments>https://arosyoung.tistory.com/entry/LensCal-%EB%A0%8C%EC%A6%88-%EC%BA%98%EB%A6%AC%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%94%EC%A7%84%EC%9D%84-%EB%8B%A4%EC%8B%9C-%EC%A7%9C%EB%A9%B4%EC%84%9C-%E2%80%94-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EC%86%8C%EA%B0%9C#entry48comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:18:48 +0900</pubDate>
    </item>
    <item>
      <title>[Vision/C++] 반복 패턴 이미지에서의 Grid Center 검출 알고리즘 구현</title>
      <link>https://arosyoung.tistory.com/entry/VisionC-%EB%B0%98%EB%B3%B5-%ED%8C%A8%ED%84%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90%EC%84%9C%EC%9D%98-Grid-Center-%EA%B2%80%EC%B6%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B5%AC%ED%98%84</link>
      <description>&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;웨이퍼나 PCB 검사 프로젝트를 진행하다 보면 동일한 패턴이 반복되는 이미지를 자주 다루게 된다. 이러한 이미지에서 미세 결함(Defect)을 검출하기 위해서는, 강한 에지 성분을 가진 반복 패턴을 먼저 제거(Pattern Removal)해야 한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;패턴 제거를 위해 FFT 필터링이나 마스터 이미지와의 Subtraction 방식을 고려하고 있는데, 이를 위해서는 선행적으로 각 디펙트의 인덱싱정보를 위해 &lt;b&gt;각 패턴의 정확한 중심 좌표(Center Position)&lt;/b&gt;를 확보해야 한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;본 포스팅에서는 2D 이미지 프로세싱 부하를 줄이기 위해 &lt;b data-index-in-node=&quot;32&quot; data-path-to-node=&quot;6&quot;&gt;투영(Projection)&lt;/b&gt; 기법을 활용하여 Grid Pattern의 중심을 검출하는 로직(GridPatternMatching) 구현 과정을 정리한다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;7&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;1. 검증 환경 구성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;실제 현장에서 다루는 검사 시료(Sample)는 고객사의 공정 노하우가 포함된 대외비(Confidential) 자료이므로 본 포스팅에 공개할 수 없다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;따라서 실제 시료와 유사한 환경을 가정하되, 보안 이슈를 피하고 알고리즘의 순수 로직(Logic) 검증에 집중하기 위해 가상의 테스트 이미지를 생성하여 검증을 진행했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;테스트 이미지:&lt;/b&gt; BGA(Ball Grid Array) 스타일의 10x10 격자 패턴&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;해상도:&lt;/b&gt; 1024x1024&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0&quot;&gt;목표:&lt;/b&gt; 100개 원형 패턴의 중심 좌표를 Sub-pixel 단위로 검출&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deFMYZ/dJMcacaCvPv/4N1vRHLLyw56OQXlsvttVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deFMYZ/dJMcacaCvPv/4N1vRHLLyw56OQXlsvttVK/img.png&quot; data-alt=&quot;테스트에 사용한 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deFMYZ/dJMcacaCvPv/4N1vRHLLyw56OQXlsvttVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeFMYZ%2FdJMcacaCvPv%2F4N1vRHLLyw56OQXlsvttVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트에 사용한 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;12&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13&quot;&gt;2. 접근 방식: Projection (차원 축소)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;수많은 패턴을 개별 Blob으로 인식하여 처리하는 것은 연산 비용 측면에서 비효율적이다. 패턴이 격자(Grid) 형태로 정렬되어 있다는 특성을 활용하여, 2차원 이미지를 가로/세로 방향으로 각각 투영(Projection)하여 1차원 데이터로 변환하는 방식을 택했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;OpenCV의 reduce 함수를 사용하면 간단하게 구현 가능하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1766312119190&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// [Code Fragment] Projection 데이터 추출
std::vector&amp;lt;float&amp;gt; CGridPatternMatching::GetProjection(const cv::Mat&amp;amp; img, bool bIsRow)
{
    cv::Mat sumMat;
    // 행(Row) 또는 열(Col) 방향으로 픽셀 강도를 누적 합산 (Sum)
    cv::reduce(img, sumMat, bIsRow ? 1 : 0, cv::REDUCE_SUM, CV_32F);
    
    // Mat 형태를 vector로 변환하여 반환
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;17&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18&quot;&gt;3. 신호 분석 및 Peak 검출&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;추출된 1차원 Projection 데이터를 그래프로 시각화하면 아래와 같이 패턴이 존재하는 구간에서 신호값이 상승하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;이 그래프에서 패턴의 중심을 찾기 위해 다음과 같은 로직을 적용했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;21&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터의 평균값을 기준으로 임계값(Threshold) 설정&lt;/li&gt;
&lt;li&gt;임계값을 초과하는 구간의 시작점(Start)과 끝점(End) 인덱스 검출&lt;/li&gt;
&lt;li&gt;해당 구간의 &lt;b&gt;중간값((Start + End) / 2)&lt;/b&gt;을 패턴의 중심(Peak)으로 정의&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;단순히 최댓값(Max)을 찾는 것보다, 패턴의 크기(Width)를 고려하여 중심을 잡는 것이 더 안정적이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMburV/dJMcaiBRrBn/bIilDJTJKrn9EzaBNQoFW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMburV/dJMcaiBRrBn/bIilDJTJKrn9EzaBNQoFW1/img.png&quot; data-alt=&quot;Projection 데이터 그래프&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMburV/dJMcaiBRrBn/bIilDJTJKrn9EzaBNQoFW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMburV%2FdJMcaiBRrBn%2FbIilDJTJKrn9EzaBNQoFW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;400&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Projection 데이터 그래프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;25&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25&quot;&gt;4. 결과 검증 (Grid Fitting)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;X축 방향의 Peak와 Y축 방향의 Peak를 조합하여 최종적으로 격자(Grid) 좌표를 산출했다. 검출된 좌표의 정확성을 확인하기 위해 원본 이미지에 Crossline을 오버레이 하여 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27,0,0&quot;&gt;Red Line:&lt;/b&gt; X축 중심 좌표&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27,1,0&quot;&gt;Blue Line:&lt;/b&gt; Y축 중심 좌표&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;확대 확인 결과, 교차점이 각 원형 패턴의 중심과 일치함을 확인했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXgHv/dJMcagYobKv/fezBMnOqZb6AwFLQDoopG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXgHv/dJMcagYobKv/fezBMnOqZb6AwFLQDoopG0/img.png&quot; data-alt=&quot;최종검출 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXgHv/dJMcagYobKv/fezBMnOqZb6AwFLQDoopG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXgHv%2FdJMcagYobKv%2FfezBMnOqZb6AwFLQDoopG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종검출 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;29&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30&quot;&gt;5. 정리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;이상적인 환경(BGA 테스트 이미지)에서 Projection 기반의 Grid Center 검출 로직이 정상 동작함을 확인했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;실제 구현된 클래스(CGridPatternMatching)에는 현장 적용을 위해 다음과 같은 기능들이 추가되어 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;33&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;33,0,0&quot;&gt;Adaptive Threshold:&lt;/b&gt; 조명 불균일 대응&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;33,1,0&quot;&gt;Master Grid Fitting:&lt;/b&gt; 렌즈 왜곡 등을 고려하여, 학습된(Train) 마스터 패턴의 간격 정보를 기반으로 검출된 Peak를 보정하는 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;인덱싱에 사용될 대략적인 위치만 필요하기에 정확도는 고려 하지 않았다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;확보된 좌표 정보를 바탕으로 추후 FFT 주파수 제거 혹은 Reference Image Subtraction 작업을 수행하여 결함 검출 단계로 넘어갈 예정이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;혹시 필요 할지 몰라 테스트 했던 Consol 프로젝트도 첨부한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/c2bMUH/dJMcagjMHjS/KaPcoWp8yuSSwNCzA64uBK/Grid.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;Grid.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;4.66MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/47</guid>
      <comments>https://arosyoung.tistory.com/entry/VisionC-%EB%B0%98%EB%B3%B5-%ED%8C%A8%ED%84%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90%EC%84%9C%EC%9D%98-Grid-Center-%EA%B2%80%EC%B6%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B5%AC%ED%98%84#entry47comment</comments>
      <pubDate>Sun, 21 Dec 2025 19:23:51 +0900</pubDate>
    </item>
    <item>
      <title>[C++/CUDA] 90GB 대용량 버퍼풀에서 4,000개 ROI만 쏙 뽑아 초고속 어파인 변환하기 (Zero-Copy &amp;amp; Batch Assembly)</title>
      <link>https://arosyoung.tistory.com/entry/CCUDA-90GB-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%B2%84%ED%8D%BC%ED%92%80%EC%97%90%EC%84%9C-4000%EA%B0%9C-ROI%EB%A7%8C-%EC%8F%99-%EB%BD%91%EC%95%84-%EC%B4%88%EA%B3%A0%EC%86%8D-%EC%96%B4%ED%8C%8C%EC%9D%B8-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0-Zero-Copy-Batch-Assembly</link>
      <description>&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;최근 uled 검사 장비 개발 프로젝트를 진행하면서 극한의 성능 요구사항에 부딪혔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시스템에는 &lt;b&gt;90GB&lt;/b&gt;에 달하는 초대형 Raw 이미지가 메모리에 로드되어 있습니다. 제 미션은 이 거대한 이미지 전체를 건드리는 것이 아니라, 검사가 필요한 &lt;b&gt;4,000개 이상의 특정 영역(ROI)만 빠르게 잘라내어 어파인 변환(Affine Transform)&lt;/b&gt;을 수행하는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음에는 &quot;GPU가 빠르니까 금방 하겠지&quot;라고 생각했지만, 현실은 달랐습니다. 90GB라는 거대한 바다에서 작은 조각 4,000개를 건져 올리는 과정에서 &lt;b&gt;PCIe 통신 병목(Latency)&lt;/b&gt;이 발목을 잡았기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오늘은 이 문제를 해결하기 위해 적용한 &lt;b&gt;대용량 메모리 핀(Pin) 등록&lt;/b&gt;과 &lt;b&gt;필요한 부분만 처리하는 GPU 배치 조립(Batch Assembly)&lt;/b&gt; 기법을 공유합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKysXw/dJMcafkNJCe/MzR8fYRVmJnHHCPvthbWTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKysXw/dJMcafkNJCe/MzR8fYRVmJnHHCPvthbWTk/img.png&quot; data-alt=&quot;GPU 배치 조립(Batch Assembly) 및 Zero-Copy 데이터 흐름도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKysXw/dJMcafkNJCe/MzR8fYRVmJnHHCPvthbWTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKysXw%2FdJMcafkNJCe%2FMzR8fYRVmJnHHCPvthbWTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GPU 배치 조립(Batch Assembly) 및 Zero-Copy 데이터 흐름도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;1. 문제의 핵심: &quot;4,000번의 왕복 달리기&quot;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;일반적인 OpenCV CUDA 방식으로 4,000개의 칩(Die)을 검사한다고 가정해 봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;90GB 원본에서 1번 칩 위치 Crop&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;GPU로 upload&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;warpAffine (회전/보정)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;CPU로 download&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;... (이걸 4,000번 반복)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아무리 작은 이미지라도, CPU와 GPU 사이를 4,000번이나 왔다 갔다 하면 &lt;b&gt;데이터를 처리하는 시간보다 데이터를 옮기려고 대기하는 시간(Overhead)&lt;/b&gt;이 훨씬 더 커집니다. 배달 기사님이 피자 4,000판을 배달하는데, 한 번에 한 판씩만 들고 4,000번을 왕복하는 꼴입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;2. 해결책 1: 90GB 전체를 '준비 태세'로 (Memory Registration)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우선 90GB 원본 데이터가 OS의 간섭 없이 언제든 접근 가능해야 합니다. 윈도우 환경에서 이렇게 큰 메모리를 페이징(Swap) 없이 고정(Pinning)하려면, &lt;b&gt;물리 메모리 사용 한도&lt;/b&gt;을 강제로 늘려줘야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1765108084437&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// [ImsGpuEngine.cpp 발췌]
// 1. 프로세스의 Working Set 크기를 90GB+a로 강제 증액
// 이것이 없으면 대용량 메모리 등록 시 'Out Of Memory' 발생
SetProcessWorkingSetSize(hProcess, requestSize, requestSize);

// 2. OpenCV Mat의 크기 한계 극복 (Reshaping)
// 90GB를 1D로 등록할 수 없어, Width=64로 고정하고 Height를 늘려서 등록
cv::Mat wrapper((int)heightCheck, width, CV_8UC1, pBuffer);

// 3. CUDA에 Pinned Memory로 등록 (Zero-Copy 활성화)
// 이제 90GB 중 '어디든' GPU가 즉시 접근할 수 있는 상태가 됨
cv::cuda::registerPageLocked(wrapper);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 과정은 90GB를 복사하는 게 아니라, &lt;b&gt;&quot;이 메모리는 건드리지 마(Lock)&quot;&lt;/b&gt;라고 OS에 신고만 하는 것이므로 부팅 시 딱 한 번만 수행하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;3. 해결책 2: 필요한 것만 담는 GPU 조립 라인 (Batch Assembly)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이제 90GB 버퍼에서 4,000개의 ROI를 효율적으로 가져와야 합니다. 저는 &lt;b&gt;&quot;4,000번의 왕복&quot;을 &quot;단 1번&quot;으로 줄이는 전략&lt;/b&gt;을 세웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이름하여 &lt;b&gt;GPU Atlas Assembly&lt;/b&gt; 기법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;21&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Partial Upload (부분 업로드):&lt;/b&gt; 90GB 전체를 GPU에 올리는 건 미친 짓입니다. 4,000개 ROI가 포함된 &lt;b&gt;'필요한 영역'만 스마트하게 추려서 GPU로 보냅니다.&lt;/b&gt; (Zero-Copy라 빠릅니다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;GPU Assembly (내부 조립):&lt;/b&gt; 8개의 CUDA Stream을 돌려서 병렬로 warpAffine을 수행합니다. 이때 결과를 CPU로 바로 보내지 않고, **GPU 메모리 내부에 미리 할당해 둔 결과용 버퍼(Atlas)**에 차곡차곡 쌓습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Single Download (일괄 전송):&lt;/b&gt; 4,000개 처리가 다 끝나면, 조립이 완료된 결과물 컨테이너를 &lt;b&gt;단 한 번의 다운로드&lt;/b&gt;로 가져옵니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1765108155039&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// [배치 처리 핵심 로직]
for (size_t i = 0; i &amp;lt; requests.size(); i++)
{
    // GPU 메모리 내의 결과 버퍼(Atlas) 위치 계산
    unsigned char* pGpuPtr = m_dBatchResultGpu.data + currentOffset;
    
    // *핵심*: 결과를 CPU로 보내지 않고, GPU 메모리 안에서 그리기만 함!
    // srcView: 90GB 중 필요한 부분만 업로드된 GPU 메모리
    cv::cuda::warpAffine(srcView, dstGpuRoi, M, ... , curStream);

    currentOffset += frameBytes;
}

// 모든 조립이 끝나면 '한 번에' 다운로드
m_dBatchResultGpu.download(m_hBatchPinnedBuffer);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;23&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;4. 결과: PCIe 병목 해소와 초고속 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 아키텍처의 핵심은 &lt;b&gt;&quot;GPU는 90GB 전체를 알 필요가 없다&quot;&lt;/b&gt;와 &lt;b&gt;&quot;통행료(PCIe Latency)는 한 번만 낸다&quot;&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;25&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Before:&lt;/b&gt; ROI 1개마다 통신 -&amp;gt; 4,000번 통신 대기 발생 -&amp;gt; &lt;b&gt;매우 느림&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;After:&lt;/b&gt; 필요한 데이터만 묶어서 업로드 -&amp;gt; GPU 내부에서 지지고 볶고 조립 -&amp;gt; 결과만 한 번에 다운로드 -&amp;gt; &lt;b&gt;PCIe 효율 극대화&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결과적으로 사용자는 pOutputMat 포인터만 확인하면, 복사 비용 없이(Zero-Copy) 변환이 완료된 4,000장의 이미지를 즉시 사용할 수 있게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;5. 마무리하며&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;대용량 데이터를 다룰 때 가장 큰 적은 '연산량'이 아니라 &lt;b&gt;'이동 비용'&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;90GB라는 거대한 데이터에서 필요한 정보만 핀셋처럼 뽑아내어(Crop), 병목 없이 GPU 가속을 태우는 이 구조(ImsGpuEngine)는 앞으로 고속 웨이퍼 검사 모듈의 핵심 엔진이 될 것입니다. 비슷한 대용량 데이터 처리로 고민하시는 분들께 힌트가 되었으면 좋겠네요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/et3w9d/dJMb995XPNl/Pvp4rCS2C31kMe2k9amx0K/ImsGpuEngine.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;ImsGpuEngine.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <category>AffineImage</category>
      <category>cv::cuda</category>
      <category>GPU</category>
      <category>대용량이미지처리</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/46</guid>
      <comments>https://arosyoung.tistory.com/entry/CCUDA-90GB-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%B2%84%ED%8D%BC%ED%92%80%EC%97%90%EC%84%9C-4000%EA%B0%9C-ROI%EB%A7%8C-%EC%8F%99-%EB%BD%91%EC%95%84-%EC%B4%88%EA%B3%A0%EC%86%8D-%EC%96%B4%ED%8C%8C%EC%9D%B8-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0-Zero-Copy-Batch-Assembly#entry46comment</comments>
      <pubDate>Sun, 7 Dec 2025 20:54:12 +0900</pubDate>
    </item>
    <item>
      <title>[Insight] 이미지를 돌릴까, 마스크를 돌릴까? (Image vs Mask Rotation)</title>
      <link>https://arosyoung.tistory.com/entry/Insight-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%8F%8C%EB%A6%B4%EA%B9%8C-%EB%A7%88%EC%8A%A4%ED%81%AC%EB%A5%BC-%EB%8F%8C%EB%A6%B4%EA%B9%8C-Image-vs-Mask-Rotation</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5IFik/dJMcagD1HSl/Ja8wfnMY4yvC7DN1ZwW5I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5IFik/dJMcagD1HSl/Ja8wfnMY4yvC7DN1ZwW5I0/img.png&quot; data-alt=&quot;패턴매칭으로 시작하는 웨이퍼 검사&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5IFik/dJMcagD1HSl/Ja8wfnMY4yvC7DN1ZwW5I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5IFik%2FdJMcagD1HSl%2FJa8wfnMY4yvC7DN1ZwW5I0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;패턴매칭으로 시작하는 웨이퍼 검사&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;웨이퍼 패턴 검사 알고리즘을 최적화하다 보면 필연적으로 마주치는 고민이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Cognex PatMax, CopenCV Templet Matching 등을 이용해 패턴의 틀어진 각도를 알아냈을 때,&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;이미지 회전:&lt;/b&gt; 들어온 영상을 &lt;b&gt;'기본 이미지'&lt;/b&gt;와 똑같이 0도로 펴서 검사할 것인가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;마스크 회전:&lt;/b&gt; 영상은 그대로 두고, 검사 영역(ROI/Mask)을 theta 만큼 돌려서 검사할 것인가?&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 저희는 전자인 &lt;b&gt;'이미지 회전'&lt;/b&gt; 방식을 사용 중입니다. 하지만 &quot;&lt;b&gt;굳이 무거운 영상 변환을 해야 하나? 좌표만 돌리면 더 빠르지 않을까?&quot;&lt;/b&gt;라는 의문은 개발자라면 누구나 가질 수 있는 합리적인 의심입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 글에서는 두 방식의 메커니즘을 비교하고, 왜 웨이퍼 검사(비교 검사)에서는 &lt;b&gt;'이미지 회전'이 정답에 가까운지&lt;/b&gt; 기술적으로 분석해 봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;1. 두 가지 방식의 프로세스 비교&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;A. 현재 방식: 이미지 회전 (Image Warping)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;준비:&lt;/b&gt; 0도 기준의 &lt;b&gt;'기본 이미지'&lt;/b&gt; 와 마스크 생성.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;실행:&lt;/b&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;12,1,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;패턴매칭 알고리즘으로 틀어진 각도 획득.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;입력 영상을 theta만큼 역회전(Affine)시켜 &lt;b&gt;0도 이미지&lt;/b&gt; 생성.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;기본 이미지&lt;/b&gt;의 마스크를 그대로 씌워 비교 검사 수행.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;B. 고민 중인 방식: 마스크 회전 (Mask/Coordinate Transform)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;준비:&lt;/b&gt; 0도 기준의 &lt;b&gt;'기본 이미지&lt;/b&gt;'와 마스크.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;실행:&lt;/b&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;14,1,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PatMax로 틀어진 각도&amp;nbsp;획득.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;입력 영상은 원본 그대로 유지.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;마스크 좌표&lt;/b&gt;를 theta만큼 회전시켜서 입력 영상 위로 가져감.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;회전된 영역 내부의 픽셀 값을 읽어와 검사 수행.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;2. 왜 '이미지 회전'이 유리한가? (비교 검사의 관점)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;단순히 밝기 평균을 내거나 길이를 잰다면 B방식도 나쁘지 않습니다. 하지만 &lt;b&gt;'기본 이미지'와 픽셀 대 픽셀로 비교(Subtraction)해야 하는 검사&lt;/b&gt;에서는 A방식이 압도적으로 유리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이유 1: 픽셀 매칭의 불가능성&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;18&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;A 방식:&lt;/b&gt; 입력 이미지를 0도로 폈기 때문에, (10, 10)&amp;nbsp;좌표의 픽셀은 &lt;b&gt;기본 이미지&lt;/b&gt;의 (10, 10)&amp;nbsp;픽셀과 정확히 1:1로 대응됩니다. 단순히 AbsDiff(Img1, Img2) 함수 한 방이면 끝납니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;B 방식:&lt;/b&gt; 입력 영상은 기울어져 있습니다. 입력 영상의 $(10, 10)$ 좌표는 &lt;b&gt;기본 이미지&lt;/b&gt;의 (10, 10)이 아니라 엉뚱한 곳을 가리킵니다. 비교를 하려면 실시간으로 좌표 역변환 계산을 해서 짝을 맞춰야 하는데, 배보다 배꼽이 더 커집니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;19&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이유 2: 메모리 접근 효율&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;CPU는 메모리를 읽을 때 가로로 연속된 데이터를 읽는 것을 좋아합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;21&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;A 방식:&lt;/b&gt; 이미지를 회전시켜 놓으면 데이터가 메모리에 가지런히 정렬됩니다. CPU가 고속도로를 달리듯 데이터를 퍼올 수 있습니다. (&lt;b&gt;SIMD/AVX 가속 가능&lt;/b&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;B 방식:&lt;/b&gt; 마스크가 회전되면 읽어야 할 메모리 주소가 불연속적으로 널뛰기(Jump)를 합니다. CPU 캐시 미스(Cache Miss)가 폭발하며 연산 속도가 급격히 느려집니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이유 3: 마스크의 알리아싱&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사각형 마스크를 회전시키면, 픽셀 격자(Grid) 특성상 가장자리가 계단처럼 울퉁불퉁해집니다. 이로 인해 검사 영역의 경계면에서 노이즈가 발생하거나 검사 정밀도가 떨어질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;24&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;&lt;br /&gt;3. B 방식(마스크 회전)은 언제 쓰는가?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그렇다면 B 방식은 쓸모가 없을까요? 아닙니다. 다음과 같은 경우에는 B 방식이 더 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;26&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;이미지 변형 절대 금지:&lt;/b&gt; 의료 영상이나 과학 계측처럼, 어파인 변환 시 발생하는 미세한 픽셀 값 변화(보간)조차 허용되지 않을 때.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;단순 측정:&lt;/b&gt; 비교가 아니라, 단순히 특정 영역의 &lt;b&gt;평균 밝기(Mean)&lt;/b&gt;나 &lt;b&gt;최댓값(Max)&lt;/b&gt;만 구할 때.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;GPU 사용 시:&lt;/b&gt; GPU는 텍스처 매핑 하드웨어가 있어서 회전된 좌표를 읽는 비용이 거의 '0'에 가깝습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;결론: 웨이퍼 검사는 '이미지 회전'이 맞다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우리가 하려는 것은 &lt;b&gt;'기본 이미지(Base Image)'와의 정밀 비교&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;입력 영상을 &lt;b&gt;'기본 이미지'와 동일한 0도 좌표계로 변환(Affine)&lt;/b&gt;해 두는 것은, 이후 이어질 복잡한 검사 알고리즘들을 가장 빠르고 단순하게 만드는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;'가장 효율적인 투자'&lt;/span&gt;&lt;/span&gt;입니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Vision &amp;amp; Inspection</category>
      <category>패턴검사방식</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/45</guid>
      <comments>https://arosyoung.tistory.com/entry/Insight-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%8F%8C%EB%A6%B4%EA%B9%8C-%EB%A7%88%EC%8A%A4%ED%81%AC%EB%A5%BC-%EB%8F%8C%EB%A6%B4%EA%B9%8C-Image-vs-Mask-Rotation#entry45comment</comments>
      <pubDate>Sun, 7 Dec 2025 20:35:43 +0900</pubDate>
    </item>
    <item>
      <title>[Insight] 개발의 절반은 '디버깅'이다</title>
      <link>https://arosyoung.tistory.com/entry/Insight-%EA%B0%9C%EB%B0%9C%EC%9D%98-%EC%A0%88%EB%B0%98%EC%9D%80-%EB%94%94%EB%B2%84%EA%B9%85%EC%9D%B4%EB%8B%A4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;unnamed (1).jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvtUPk/dJMcacnZMH6/MMp1eXiZC9S6RMDTU8Ldf1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvtUPk/dJMcacnZMH6/MMp1eXiZC9S6RMDTU8Ldf1/img.jpg&quot; data-alt=&quot;개발의 복잡성과 디버깅의 중요성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvtUPk/dJMcacnZMH6/MMp1eXiZC9S6RMDTU8Ldf1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvtUPk%2FdJMcacnZMH6%2FMMp1eXiZC9S6RMDTU8Ldf1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-filename=&quot;unnamed (1).jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개발의 복잡성과 디버깅의 중요성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개발자로서 연차가 쌓이면서 바뀌는 습관이 하나 있습니다. 예전에는 &quot;어떻게 하면 이 기능을 빨리 구현할까?&quot;를 고민했다면, 이제는 &quot;이 기능이 멈췄을 때, 어떻게 하면 빨리 원인을 찾을 수 있을까?&quot;를 먼저 고민한다는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;특히 하드웨어를 제어하는 장비 개발 분야에서, 코드가 '도는 것'은 시작에 불과합니다. 진짜 싸움은 현장에서 예기치 않은 이유로 '멈췄을 때' 시작됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오늘은 제가 프로젝트를 진행하며 뼈저리게 느낀 '디버깅을 위한 개발 철학'과, 새로 합류하는 팀원들에게 꼭 해주는 이야기를 나누려 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;1. 미래의 나를 구하는 '방어적 코딩'&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우리는 종종 'Happy Path(모든 조건이 완벽한 상황)'만을 가정하고 코드를 짭니다. &quot;센서는 당연히 켜져 있겠지&quot;, &quot;통신 케이블은 연결되어 있겠지&quot;, &quot;파일은 그 경로에 있겠지&quot;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만 현장은 가혹합니다. 센서는 오동작하고, 케이블은 빠지며, 작업자는 엉뚱한 버튼을 누릅니다. 이때 프로그램이 아무런 말 없이 죽어버리거나(Crash), 멍하니 멈춰 있다면(Hang), 그것만큼 개발자를 절망스럽게 하는 것은 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 저는 코드를 짤 때 항상 '최악의 상황'을 먼저 작성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;가정하지 마라:&lt;/b&gt; 리턴 값, 포인터, 하드웨어 상태를 믿지 말고 검증하는 코드를 먼저 넣습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;친절한 Log을 남겨라:&lt;/b&gt; 단순히 return false;로 끝내지 마십시오. &lt;i&gt;왜&lt;/i&gt; 실패했는지, &lt;i&gt;어떤&lt;/i&gt; 데이터가 문제였는지 로그를 남기고 리턴해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;if (Sensor == On)이 아니라 if (Sensor == Off) { Log(&quot;Sensor Timeout&quot;); return Error; }를 먼저 고민하는 것. 이것이 퇴근 시간을 앞당기는 방어적 코딩의 시작입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;2. 백문이 불여일견: '동작의 시각화 (Visualization)'&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;방어적 코딩으로 로그를 남겼다 해도, 수만 줄의 텍스트 로그를 실시간으로 분석하는 것은 불가능에 가깝습니다. 특히 모션이 움직이고 시퀀스가 빠르게 돌아가는 장비 제어에서는 더욱 그렇습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 제가 강조하는 것이 '로직의 시각화'입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b79Q7y/dJMcachdT8C/U7XS8yToUVdEODJ9c5sYDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b79Q7y/dJMcachdT8C/U7XS8yToUVdEODJ9c5sYDK/img.png&quot; data-alt=&quot;실시간 센서 데이터 그래프, 상태 머신(State Machine)의 흐름도, 에러 로그 창이 하나의 대시보드에 시각적으로 표현된 화면.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b79Q7y/dJMcachdT8C/U7XS8yToUVdEODJ9c5sYDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb79Q7y%2FdJMcachdT8C%2FU7XS8yToUVdEODJ9c5sYDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실시간 센서 데이터 그래프, 상태 머신(State Machine)의 흐름도, 에러 로그 창이 하나의 대시보드에 시각적으로 표현된 화면.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드가 내부적으로 어떻게 돌아가고 있는지를 &lt;b&gt;직관적인 UI나 그래픽으로 표현&lt;/b&gt;해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;상태 머신(State Machine)의 시각화:&lt;/b&gt; 현재 장비가 '대기' 상태인지, '이동' 상태인지, '에러' 상태인지 텍스트가 아닌 &lt;b&gt;블록 다이어그램의 색상 변화&lt;/b&gt;로 보여주십시오.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;데이터의 그래프화:&lt;/b&gt; 모터의 속도나 센서 값을 숫자로만 보지 말고, &lt;b&gt;실시간 그래프&lt;/b&gt;로 그리십시오. 튀는 값(Noise)이나 지연(Delay)이 한눈에 보입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;오버레이(Overlay) 활용:&lt;/b&gt; (지난번 SkaldLogger 처럼) 화면 위에 현재 수행 중인 작업의 로그를 투명하게 띄우십시오.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개발 단계에서 이런 시각화 툴을 만드는 것이 시간 낭비처럼 보일 수 있습니다. 하지만 장비가 오동작할 때, &quot;아, 여기서 멈췄구나&quot;라고 1초 만에 파악할 수 있다면, 그 툴을 만드는 데 쓴 시간은 이미 보상받고도 남습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;3. 꼰대 같지만, 신입 개발자에게 꼭 하는 잔소리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;새로운 팀원이 오면 제가 웰컴 키트처럼 꼭 하는 잔소리가 있습니다. 당장은 귀찮게 들릴지 몰라도, 나중에 피가 되고 살이 되는 이야기들입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;23&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&quot;기능 구현이 다가 아니다. 70%는 예외 처리다.&quot;&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;신입 때는 '정상 동작'을 구현하는 데만 온 힘을 쏟습니다. 하지만 프로의 코드는 '비정상 상황'에서도 우아하게 대처해야 합니다. &quot;이게 안 되면 어떡하지?&quot;를 항상 먼저 고민하세요.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&quot;로그는 네가 없을 때 너를 대신하는 목격자다.&quot;&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Log(&quot;Error&quot;); 같이 성의 없는 로그는 범죄 현장에 &quot;누군가 왔다 감&quot;이라고 써놓는 것과 같습니다. 언제, 어디서, 어떤 값을 가지고, 왜 에러가 났는지 육하원칙에 가깝게 남기세요. 나중에 로그 파일만 보고도 상황을 재구성할 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&quot;제발 눈으로만 디버깅하지 마라.&quot;&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드를 뚫어져라 쳐다본다고 버그가 잡히지 않습니다. 브레이크 포인트를 걸고, 변수 값을 확인하고, 제가 앞서 말한 시각화 도구를 적극적으로 활용하세요. &quot;제 생각에는...&quot; 이라는 추측보다는 &quot;데이터를 보니...&quot; 라는 팩트를 가져오세요.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;4. 결론: 디버깅하기 쉬운 코드가 좋은 코드다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;클린 코드, 디자인 패턴, 최적화... 모두 중요합니다. 하지만 현장의 엔지니어에게 가장 좋은 코드는 &lt;b&gt;'문제가 생겼을 때, 스스로 아픈 곳을 말해주는 코드'&lt;/b&gt; 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개발자는 코드를 짜는 시간보다 디버깅하는 시간에 더 많은 인생을 씁니다. 지금 작성하는 그 한 줄의 에러 처리와, 귀찮음을 무릅쓰고 만든 시각화 기능이, 먼 훗날 새벽 2시 현장에 있는 '미래의 나'를 구해줄 유일한 동아줄이 될 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&quot;구현(Implementation)은 기능의 완성이지만, 시각화(Visualization)와 방어적 코딩은 품질의 완성입니다.&quot;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Insight &amp;amp; Philosophy</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/44</guid>
      <comments>https://arosyoung.tistory.com/entry/Insight-%EA%B0%9C%EB%B0%9C%EC%9D%98-%EC%A0%88%EB%B0%98%EC%9D%80-%EB%94%94%EB%B2%84%EA%B9%85%EC%9D%B4%EB%8B%A4#entry44comment</comments>
      <pubDate>Sun, 23 Nov 2025 23:34:32 +0900</pubDate>
    </item>
    <item>
      <title>[Motion Control] 1. 모션 라이브러리의 이해와 제어 보드별(Adlink, ACS, Ajin) 특징</title>
      <link>https://arosyoung.tistory.com/entry/Motion-Control-1-%EB%AA%A8%EC%85%98-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%9D%98-%EC%9D%B4%ED%95%B4%EC%99%80-%EC%A0%9C%EC%96%B4-%EB%B3%B4%EB%93%9C%EB%B3%84Adlink-ACS-Ajin-%ED%8A%B9%EC%A7%95</link>
      <description>&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오랜만에 블로그에 글을 씁니다. 그동안 바쁜 일들도 있었지만, 무엇보다 게으름이 가장 큰 적이었네요. 게으름에서 벗어나고자 다시 키보드를 잡았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오늘은 장비 제어의 핵심인 '모션 제어(Motion Control)'의 기본 개념과, 개발자에게 가장 중요한 '모션 라이브러리'에 대해 이야기해 보겠습니다. 특히 제가 현업에서 직접 다뤄본 &lt;b&gt;Adlink, ACS, Ajin&lt;/b&gt; 보드들의 특징도 가감 없이 비교해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1. 모션 제어(Motion Control)란?&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;반도체, 디스플레이 등 정밀 부품을 조립하거나 검사하는 장비를 보면, 가장 눈에 띄는 것이 바로 실제 움직임이 일어나는 '이송 시스템' 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;모션(Motion):&lt;/b&gt; 물체가 한 점에서 다른 점으로 이동하는 '운동'&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;제어(Control):&lt;/b&gt; 원하는 방향과 위치로 대상물을 움직이게 하는 '조종'&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉, 모션 제어란 &lt;b&gt;&quot;어떠한 물체를 우리가 원하는 속도와 정밀도로, 원하는 위치까지 이동시키는 기술&quot;&lt;/b&gt; 이라고 정의할 수 있습니다. 장비 개발자는 이 '이동'을 얼마나 빠르고 정확하게 수행하느냐를 고민해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;2. PC 기반 모션 제어의 구성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PC를 이용한 제어 시스템은 보통 다음과 같은 흐름으로 구성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;PC 시스템:&lt;/b&gt; 상위 제어 SW가 돌아가는 곳 (Brain)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;모션 제어기 (Motion Controller):&lt;/b&gt; PC의 명령을 받아 모터를 제어할 전기적 신호를 생성 (PCI/PCIe 보드, EtherCAT 마스터 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;전장부 (Wiring/Interface):&lt;/b&gt; 제어기와 드라이브를 연결하는 케이블 및 터미널 블록&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;서보 드라이브 (Servo Drive):&lt;/b&gt; 제어기의 신호를 받아 모터에 전력을 공급하고 정밀하게 제어&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;모터 (Motor):&lt;/b&gt; 회전력을 발생시키는 구동원&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;이송부 (Stage/Actuator):&lt;/b&gt; 모터의 회전 운동을 직선 운동 등으로 바꾸어 실제 물체를 이동시키는 기구부&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;unnamed.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ryOyY/dJMcacuLihs/8JKhf9820SoKfubqLEBbtK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ryOyY/dJMcacuLihs/8JKhf9820SoKfubqLEBbtK/img.jpg&quot; data-alt=&quot;ChatGPT가 그려준 이미지(그림과 달리 실제는 PC와 서보드라이브는 직접연결이 아니라 MotionController에 연결됩니다.)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ryOyY/dJMcacuLihs/8JKhf9820SoKfubqLEBbtK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FryOyY%2FdJMcacuLihs%2F8JKhf9820SoKfubqLEBbtK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-filename=&quot;unnamed.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ChatGPT가 그려준 이미지(그림과 달리 실제는 PC와 서보드라이브는 직접연결이 아니라 MotionController에 연결됩니다.)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3. 모션 라이브러리란 무엇인가?&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;장비 개발자 입장에서 하드웨어(모터, 드라이브)의 깊은 전자공학적 이론을 모두 알기는 어렵습니다. 이때, 복잡한 제어 이론 없이도 모터를 움직일 수 있게 해주는 것이 바로 '모션 라이브러리'입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;최근 장비 제어는 대부분 Windows 환경(C++, C#, Delphi 등)에서 이루어집니다. 모션 제어기 제조사는 개발자가 쉽게 사용할 수 있도록 &lt;b&gt;API(Application Programming Interface)&lt;/b&gt; 형태의 함수 모음을 제공하는데, 이것이 바로 모션 라이브러리입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉, &quot;모션 라이브러리 = 모션 보드 제어용 API 함수 세트&quot;라고 이해하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4. 실전! 현업에서 사용해 본 제어 보드별 특징&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이론도 중요하지만, 실무에서는 &quot;어떤 보드를 쓰느냐&quot;에 따라 개발 난이도와 장비 성능이 천차만별입니다. 제가 현업에서 직접 다뤄본 대표적인 3사(Adlink, ACS, Ajin) 보드의 솔직한 사용 후기를 정리해 봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1) 아진엑스텍 (Ajinextek)&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;국내 반도체/디스플레이 장비 업계에서 가장 흔하게 볼 수 있는 국산 제어기입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;특징:&lt;/b&gt; 한국 엔지니어에게 가장 친숙한 라이브러리 구조를 가지고 있습니다. CAMC 시리즈(펄스 타입)와 RTEX/EtherCAT 타입 등 라인업이 다양합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;접근성:&lt;/b&gt; 한글 매뉴얼과 기술 지원이 매우 잘 되어 있어 초보자가 입문하기 좋습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;범용성:&lt;/b&gt; 삼성/SK하이닉스 등 대기업 라인의 표준 스펙으로 자주 지정되므로, 장비 제어를 한다면 반드시 다룰 줄 알아야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;사용 경험:&lt;/b&gt; 함수명이 직관적이고(예: AxmMovePos), 이슈 대응이 빠릅니다. 구형 보드와 신형 보드 간의 라이브러리 버전 호환성은 꼼꼼히 체크해야 합니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;Tip:&lt;/i&gt; 저는 실제로 아진의 매뉴얼을 제본해서 책처럼 보고 공부했습니다. 기본적인 모션의 특징과 이론적인 내용이 아주 잘 정리되어 있어, &lt;b&gt;모션 제어 입문 시 교과서로 활용하기에도 손색이 없습니다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2) 에이디링크 (Adlink)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;대만계 기업으로, 가성비가 좋고 PC 기반 제어기 시장에서 전 세계적으로 많이 쓰입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;특징:&lt;/b&gt; Motionnet이라는 독자적인 필드버스를 사용하거나, 범용 펄스 카드를 많이 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;안정성:&lt;/b&gt; 하드웨어 내구성이 좋고 API가 매우 안정적입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;확장성:&lt;/b&gt; DAQ(데이터 수집) 보드와 연동이 쉬워, 계측과 제어가 동시에 필요한 장비에 유리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;사용 경험:&lt;/b&gt; 아진과 비슷하지만 API 구조가 조금 더 단순하고 명료하게 구성된 느낌입니다. 제가 처음 장비 회사에 입사했을 때 사용했던 라이브러리이기도 합니다. 매뉴얼이 영문이긴 하지만, 필요한 기능 위주로 굉장히 자세하게 기술되어 있어 개발 시 큰 도움이 되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3) ACS (ACS Motion Control)&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이스라엘(현재는 Phytronix 소속)의 하이엔드 모션 제어기입니다. 초정밀 제어가 필요한 공정에서 주로 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;33&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;특징:&lt;/b&gt; 단순한 API 호출을 넘어, 제어기 내부에 별도의 스크립트 언어(SPiiPlus)를 내장하여 실시간성을 극대화합니다. 제어기와 드라이브가 통합된 형태가 많아 튜닝(Tuning)까지 여기서 수행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;33,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;압도적인 성능:&lt;/b&gt; LCM(Laser Control Module), 갠트리 제어, 고속 트리거 등에서 타의 추종을 불허하는 정밀도를 보여줍니다. 나노 단위 제어나 고속 레이저 가공 장비에서는 거의 표준입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;실시간성:&lt;/b&gt; 다축 동시 제어 등 타이밍이 중요한 작업에서 PC가 아닌 제어기 내부 스크립트로 처리하므로 반응 속도가 즉각적입니다. PC가 멈춰도 모션은 안전하게 제어됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;사용 경험 (솔직 후기):&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;33,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;높은 진입장벽:&lt;/b&gt; API만 쓰는 게 아니라 내부 버퍼 프로그래밍(스크립트)을 할 줄 알아야 하며, 튜닝 파라미터까지 다뤄야 해서 초보자에겐 매우 복잡하고 프로그램 UI도 불친절합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;범용 Homing의 부재:&lt;/b&gt; 가장 인상 깊었던 점은 &lt;b&gt;'범용 원점 검색(Home)' 기능이 없다는 것&lt;/b&gt;입니다. 기술 지원 엔지니어가 직접 짜준 코드를 사용하는데, 여기서 예외 상황이나 실수가 많이 발생하여 신뢰성이 떨어지는 경험을 했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;총평:&lt;/b&gt; 기능이 너무 많아서인지 이슈가 가장 많은 제어기이기도 합니다. 하지만 그만큼 고난도의 실시간 제어를 구현해야 할 때는 대체 불가능한 정답이 되기도 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;마무리하며&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오늘은 모션 라이브러리의 기본 개념과 메이커별 특징에 대해 간략히 소개했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;라이브러리 자체는 API 함수들의 집합이라 코딩 자체는 어렵지 않습니다. 하지만 &quot;어떤 파라미터를 어떻게 설정하느냐&quot;에 따라 장비가 덜덜거릴 수도 있고, 아주 부드럽게 움직일 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다음 시간에는 모션 라이브러리에서 공통적으로 사용되는 핵심 파라미터(속도 프로파일, 기본 접점 등)에 대해 깊이 있게 다뤄보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;39&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Motion &amp;amp; Control</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/43</guid>
      <comments>https://arosyoung.tistory.com/entry/Motion-Control-1-%EB%AA%A8%EC%85%98-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%9D%98-%EC%9D%B4%ED%95%B4%EC%99%80-%EC%A0%9C%EC%96%B4-%EB%B3%B4%EB%93%9C%EB%B3%84Adlink-ACS-Ajin-%ED%8A%B9%EC%A7%95#entry43comment</comments>
      <pubDate>Sun, 23 Nov 2025 23:11:38 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble &amp;amp; Debug Note] 잉크 도팅 튐 현상, '코일부 개조'로 잡았습니다 (feat. 완제품의 배신)</title>
      <link>https://arosyoung.tistory.com/entry/Trouble-Debug-Note-%EC%9E%89%ED%81%AC-%EB%8F%84%ED%8C%85-%ED%8A%90-%ED%98%84%EC%83%81-%EC%BD%94%EC%9D%BC%EB%B6%80-%EA%B0%9C%EC%A1%B0%EB%A1%9C-%EC%9E%A1%EC%95%98%EC%8A%B5%EB%8B%88%EB%8B%A4-feat-%EC%99%84%EC%A0%9C%ED%92%88%EC%9D%98-%EB%B0%B0%EC%8B%A0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;검사장비에서 불량(NG) 제품에 잉크 마킹을 할 때, 잉크가 튀는(Splatter) 현상은 매우 고질적인 문제입니다. 이 글은 잉크 튐 현상을 잡기 위해 소프트웨어(펄스 시간)와 전기적 제어(속도)를 모두 테스트했으나, 결국 액추에이터 자체인 코일부의 물리적 개조에 도달하기까지의 트러블슈팅 기록입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1. 문제 현상 및 목표&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목표:&lt;/b&gt; 아래 사진처럼 깔끔하고 일정한 원형의 잉크 도트를 찍는 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제:&lt;/b&gt; 실제 도팅 시, 잉크가 제자리에 맺히지 않고 주변으로 심하게 튀었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkF9PH/dJMcaaKnYHL/0J7paeI5k7RLNrdYXHx47k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkF9PH/dJMcaaKnYHL/0J7paeI5k7RLNrdYXHx47k/img.png&quot; data-alt=&quot;그림1) 문제의 잉크튐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkF9PH/dJMcaaKnYHL/0J7paeI5k7RLNrdYXHx47k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkF9PH%2FdJMcaaKnYHL%2F0J7paeI5k7RLNrdYXHx47k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;448&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림1) 문제의 잉크튐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2. 문제의 핵심: &quot;분사(Jet)&quot;가 아닌 &quot;스탬프(Stamp)&quot; 방식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트러블슈팅에 앞서, 이 잉크젯(DieMark)의 동작 원리를 파악해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 제품은 잉크를 '분사(Jetting)'하는 방식이 아니었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;**솔레노이드 코일(Coil)**이 전기 신호를 받아 움직입니다.&lt;/li&gt;
&lt;li&gt;이 코일은 &lt;b&gt;'링크(?)'&lt;/b&gt; 부품과 연결되어 있습니다.&lt;/li&gt;
&lt;li&gt;'링크'는 카트리지 내부의 **'바늘 안의 바늘(Needle within a needle)'**처럼 생긴 복잡한 기구를 밀어냅니다.&lt;/li&gt;
&lt;li&gt;이 '내부 바늘'이 잉크를 묻혀, &lt;b&gt;마치 붓이나 도장이 제품 표면에 '찍는(Stamping)'&lt;/b&gt; 듯한 방식이었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이것이 '속도' 제어가 그토록 중요했던 이유입니다.&lt;/b&gt; '붓'이 표면을 너무 빠르고 강하게 '찍으면' 그 충격으로 잉크가 튀고, 너무 느리게 '찍으면' 잉크가 제대로 묻지 않았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1l2zo/dJMcaawQRVi/FG3kcNfzwrCk6Lat2qPtkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1l2zo/dJMcaawQRVi/FG3kcNfzwrCk6Lat2qPtkK/img.png&quot; data-alt=&quot;그림2) 홈페이지에 있는 코일 사양&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1l2zo/dJMcaawQRVi/FG3kcNfzwrCk6Lat2qPtkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1l2zo%2FdJMcaawQRVi%2FFG3kcNfzwrCk6Lat2qPtkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1554&quot; height=&quot;438&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림2) 홈페이지에 있는 코일 사양&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3. 1차 시도: 펄스 시간(On-Time) 조절 [Software]&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 이 '붓'이 표면에 닿아있는 '시간'을 제어하기 위해 펄스 On-Time을 조절했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트:&lt;/b&gt; I/O 카드의 DO 펄스 On-Time을 줄여가며 테스트했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crpfiA/dJMcajURx12/KPPW4QqJqRIainRU7aRwG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crpfiA/dJMcajURx12/KPPW4QqJqRIainRU7aRwG1/img.png&quot; data-alt=&quot;그림3) 수많은 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crpfiA/dJMcajURx12/KPPW4QqJqRIainRU7aRwG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrpfiA%2FdJMcajURx12%2FKPPW4QqJqRIainRU7aRwG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1586&quot; height=&quot;906&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림3) 수많은 테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1차 결론:&lt;/b&gt; 펄스 시간을 15ms로 최적화하여 눈에 띄게 &lt;b&gt;심한 튐 현상은 해결&lt;/b&gt;했습니다. 하지만, 자세히 보면 '붓'이 찍고 떨어지는 순간 발생하는 &lt;b&gt;'미세하게 튀는'&lt;/b&gt; 현상이 여전히 남아있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4. 2차 시도: 전기적 특성 변경의 한계 [Electrical Test]&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'미세 튐'을 잡기 위해, '붓'이 표면을 '찍는 속도' 자체를 제어하려 했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트:&lt;/b&gt; 코일 사양서를 참고하여, Peak-Hold 전압을 인가하는 등 코일의 동작 속도를 변경하는 전기적 테스트를 진행했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과:&lt;/b&gt; &lt;b&gt;&quot;전기로는 한계가 있었습니다.&quot;&lt;/b&gt; 전기 신호를 아무리 미세하게 제어해도, 코일이 '링크'를 때리고, 그 '링크'가 '바늘'을 때리는 그 순간의 &lt;b&gt;물리적인 충격 속도&lt;/b&gt; 자체를 완벽하게 제어할 수는 없었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5. 최종 해결: '코일부'의 물리적 개조 [Hardware Mod]&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어(시간)와 전기적 제어(속도)가 모두 한계에 부딪혔습니다. 결국 '붓'을 움직이는 힘의 근원인 &lt;b&gt;'솔레노이드 코일부'&lt;/b&gt; 자체를 &lt;b&gt;물리적으로 개조&lt;/b&gt;하기로 결정했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(보안상 자세한 방법을 밝힐 수는 없지만)&lt;/b&gt; 코일이 '링크'를 움직이는 방식 자체의 물리적 충격을 줄이고 '찍는 속도'를 안정화시키기 위해 &lt;b&gt;코일 어셈블리(Coil Assembly)를 물리적으로 개조&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 물리적인 개조를 통해 '붓'이 표면을 찍는 기구적 메커니즘을 변경한 후에야, 비로소 '미세한 튐' 현상까지 완벽하게 해결할 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;최종 결론 및 고찰&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 잉크 튐 현상은 3단계에 걸쳐 해결되었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;심한 튐 :&lt;/b&gt; &lt;b&gt;'펄스 시간'&lt;/b&gt;(찍는 시간) 조절로 해결 (소프트웨어)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;미세 튐 :&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;'전기적 속도 제어'&lt;/b&gt; -&amp;gt; &lt;b&gt;실패 (한계 봉착)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;'코일부 물리적 개조'&lt;/b&gt;(찍는 충격 제어) -&amp;gt; &lt;b&gt;최종 해결 (하드웨어)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 이해하기 어려웠던 점은, 이 제품이 **'완제품'**으로 판매되고 있었다는 사실입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정밀 반도체 라인&lt;/b&gt;에서 사용하기 위해 구매한 제품인데, 단순한 파라미터(펄스 시간) 조절이나 전기적 제어만으로는 100% 성능이 나오지 않았습니다. 결국 사용자가 직접 제품을 분해하고 핵심 구동부인 &lt;b&gt;'코일부'를 물리적으로 개조&lt;/b&gt;해야만 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'완제품'의 의미에 대해 다시 생각하게 된, 힘든 트러블슈팅이었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Trouble &amp;amp; Debug Note</category>
      <category>잉크</category>
      <category>잉크 카트리지</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/42</guid>
      <comments>https://arosyoung.tistory.com/entry/Trouble-Debug-Note-%EC%9E%89%ED%81%AC-%EB%8F%84%ED%8C%85-%ED%8A%90-%ED%98%84%EC%83%81-%EC%BD%94%EC%9D%BC%EB%B6%80-%EA%B0%9C%EC%A1%B0%EB%A1%9C-%EC%9E%A1%EC%95%98%EC%8A%B5%EB%8B%88%EB%8B%A4-feat-%EC%99%84%EC%A0%9C%ED%92%88%EC%9D%98-%EB%B0%B0%EC%8B%A0#entry42comment</comments>
      <pubDate>Mon, 10 Nov 2025 00:28:27 +0900</pubDate>
    </item>
    <item>
      <title>SkaldLogger: MFC GUI를 위한 투명 오버레이 로거 개발 기록</title>
      <link>https://arosyoung.tistory.com/entry/SkaldLogger-MFC-GUI%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%88%AC%EB%AA%85-%EC%98%A4%EB%B2%84%EB%A0%88%EC%9D%B4-%EB%A1%9C%EA%B1%B0-%EA%B0%9C%EB%B0%9C-%EA%B8%B0%EB%A1%9D</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Vision &amp;amp; Inspection, Motion &amp;amp; Control 장비의 HMI(제어 GUI)를 개발하다 보면, 수많은 컴포넌트의 상태를 실시간으로 확인해야 할 때가 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1. 프로젝트의 목표: GUI와 로그의 공존&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅 시 로그를 확인하는 기존 방식은 번거롭습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;AfxMessageBox:&lt;/b&gt; 프로그램을 일시 정지시키고 GUI를 가립니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;별도의 로그 다이얼로그:&lt;/b&gt; 메인 GUI의 중요 부분을 가려서, 로그를 볼 때마다 창을 옮겨야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Visual Studio 출력 창:&lt;/b&gt; (OutputDebugString) GUI와 시선이 분리되어 실시간성이 떨어집니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해, &lt;b&gt;&quot;기존 제어 GUI를 가리지 않고, GUI를 조작하는 동시에 실시간 로그를 확인할 수 있는 투명 오버레이&quot;&lt;/b&gt; 로거를 개발하는 것을 목표로 삼았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 영화 '매트릭스(Matrix)'의 투명한 배경 위로 코드가 흐르는 장면에서 영감을 얻어, MFC DLL 기반의 실시간 오버레이 로깅 툴, &lt;b&gt;SkaldLogger&lt;/b&gt; 프로젝트를 시작하게 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2. SkaldLogger: 시스템의 이야기꾼 (컨셉)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(TMI: 저희 팀은 개발 툴에 라그나로크 신화의 이름을 붙이는 전통이 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skald(스칼드)는 북유럽 신화에서 역사를 기록하고 읊던 시인을 의미합니다. SkaldLogger는 복잡한 장비 시스템의 내부 동작과 이벤트를 실시간으로 기록하고 개발자에게 전달하는 '시스템의 이야기꾼' 역할을 수행한다는 컨셉으로 명명되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3. 핵심 구현 내용 (MFC DLL)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SkaldLogger는 어떤 MFC 호스트 애플리케이션에서도 쉽게 로드할 수 있도록 &lt;b&gt;MFC Regular DLL&lt;/b&gt; 형태로 구현을 진행했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;A. 투명 + 스크롤링 윈도우 구현 (Win32 API &amp;amp; DirectX)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 핵심적인 기능입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;투명 윈도우:&lt;/b&gt; CWnd 기반 윈도우 생성 시, 확장 스타일로 &lt;b&gt;WS_EX_LAYERED&lt;/b&gt; 를 적용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스크롤 및 애니메이션:&lt;/b&gt; OnPaint() 이벤트에서 GDI+ 라이브러리를 사용해 텍스트를 직접 그립니다. SetTimer를 이용해 주기적으로 로그 메시지의 Y좌표와 Alpha(투명도) 값을 업데이트하여, 로그가 아래에서 위로 부드럽게 스크롤되며 페이드-인/아웃 되도록 구현했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 버퍼 관리:&lt;/b&gt; &lt;b&gt;std::deque&lt;/b&gt; 컨테이너를 사용하여 최대 100줄의 로그만 유지하고, 새 로그가 추가될 때 가장 오래된 로그는 자동으로 제거(pop)되도록 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;B. API 설계: 퍼사드 패턴 (Facade Pattern)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트 앱에서의 사용 편의성을 위해, 복잡한 내부 로직은 숨기고 간단한 인터페이스만 노출하도록 설계했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해야 할 때가 많습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;859&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;177&quot; height=&quot;23&quot;&gt;클래스&lt;/td&gt;
&lt;td width=&quot;186&quot;&gt;역할&lt;/td&gt;
&lt;td width=&quot;496&quot;&gt;특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;177&quot; height=&quot;23&quot;&gt;CSkaldLog&lt;/td&gt;
&lt;td width=&quot;186&quot;&gt;내부 핵심 로직&lt;/td&gt;
&lt;td width=&quot;496&quot;&gt;(파일 I/O, 윈도우 관리, 싱글톤) 외부 노출 안 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;177&quot; height=&quot;35&quot;&gt;SkaldLogger&lt;/td&gt;
&lt;td width=&quot;186&quot;&gt;외부 인터페이스 (API)&lt;/td&gt;
&lt;td width=&quot;496&quot;&gt;SKALDLOGGER_API 로 노출. 호스트 앱은 이 클래스만 사용.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설계 덕분에 호스트 애플리케이션 개발자는 CSkaldLog의 복잡한 윈도우 핸들링이나 DirectX 구현을 알 필요 없이, SkaldLogger::GetInstance().LogInfo(...) 같은 간단한 호출만으로 모든 기능을 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4. SkaldLogger 간단 사용법 (API)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트 애플리케이션(exe)에서 DLL의 기능을 사용하는 방법은 매우 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762068482169&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. 초기 설정 (App의 OnInitDialog 등)
void CSkaldHostAppDlg::OnInitDialog()
{
    // ...

    // 로그를 수신할 콜백 함수 등록 (필수)
    SkaldLogger::GetInstance().SetLogCallback(MyLogCallbackFunction); 

    // 투명 오버레이 윈도우 생성 및 위치 지정
    // 예: (50, 50) 위치에서 (850, 200) 크기로 메인 GUI 위에 표시
    CRect overlayRect(50, 50, 850, 200);
    SkaldLogger::GetInstance().SetOverlayWindow(this, overlayRect);

    // (선택) DLL 내부 GUI를 통해 파일명 선택
    std::string filename = SkaldLogger::GetInstance().SelectLogFileViaDialog(m_hWnd);

    if (!filename.empty()) {
        SkaldLogger::GetInstance().SetLogFile(filename);
    }
    
    // ...
}

// 2. 로그 기록 (프로그램 어디서든)
void MySystemClass::PerformTask()
{
    // 로그 레벨별 함수 호출
    SkaldLogger::GetInstance().LogInfo(&quot;작업 시작.&quot;);
    
    if (errorCondition) {
        SkaldLogger::GetInstance().LogError(&quot;치명적 오류 발생!&quot;);
    } else {
        SkaldLogger::GetInstance().LogDebug(&quot;디버깅 데이터: [X=10]&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 회고 (맺음말)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SkaldLogger는 비록 실제 장비에 최종 적용되지는 않았지만, 복잡한 제어 GUI 환경에서 디버깅 편의성을 높이기 위한 시도였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 MFC 환경에서 Win32의 고급 기능(WS_EX_LAYERED)과 C++ 표준 기능(std::deque)을 융합하여 실제 현장의 불편함을 해결하려 했던 이 아이디어와 구현 과정을 'Project Archive'에 기록으로 남깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 고민을 하는 다른 개발자분들에게 이 아이디어가 참고가 되기를 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스코드도 함께 남깁니다. 별로 복잡하거나 참고 하실분은 참고 해주시고, 출처만 잘 남겨주세요&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/egwHZK/dJMcaa4EE3P/otu2qLUkX9UYLtCnPxOgwK/SkaldLog.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;SkaldLog.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.16MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uHBzj/dJMcafx7QWZ/zhJ8CIfakgf8HtE6d6jMq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uHBzj/dJMcafx7QWZ/zhJ8CIfakgf8HtE6d6jMq1/img.png&quot; data-alt=&quot;실제 작동 이미지 Dialog 에도 이벤트가 접근됨!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uHBzj/dJMcafx7QWZ/zhJ8CIfakgf8HtE6d6jMq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuHBzj%2FdJMcafx7QWZ%2FzhJ8CIfakgf8HtE6d6jMq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1802&quot; height=&quot;714&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 작동 이미지 Dialog 에도 이벤트가 접근됨!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC./Project Archive</category>
      <category>다이렉트엑스</category>
      <category>로그</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/41</guid>
      <comments>https://arosyoung.tistory.com/entry/SkaldLogger-MFC-GUI%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%88%AC%EB%AA%85-%EC%98%A4%EB%B2%84%EB%A0%88%EC%9D%B4-%EB%A1%9C%EA%B1%B0-%EA%B0%9C%EB%B0%9C-%EA%B8%B0%EB%A1%9D#entry41comment</comments>
      <pubDate>Sun, 2 Nov 2025 16:30:06 +0900</pubDate>
    </item>
    <item>
      <title>왜 나는 고성능 검사장비에 구형 X299 플랫폼을 고집하는가?</title>
      <link>https://arosyoung.tistory.com/entry/%EC%99%9C-%EB%82%98%EB%8A%94-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EA%B2%80%EC%82%AC%EC%9E%A5%EB%B9%84%EC%97%90-%EA%B5%AC%ED%98%95-X299-%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%9D%84-%EA%B3%A0%EC%A7%91%ED%95%98%EB%8A%94%EA%B0%80</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&amp;nbsp; &quot;아니, 지금 X299라고요?&quot;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 고성능 검사 장비 PC를 맞춘다고 할 때, 대부분은 최신 인텔 코어 i9(14세대)이나 AMD 라이젠 9, 혹은 쓰레드 리퍼를 떠올릴 것입니다. 하지만 저는 오늘도 &quot;구형&quot;으로 취급받는 인텔 X299 플랫폼을 장바구니에 담습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;고리타분하다&quot;고 할 수도 있지만, 여기에는 수많은 장비를 조립하고 디버깅하며 얻은 명확한 이유가 있습니다. 이 글은 최신 PC가 오히려 검사장비에서 성능 저하를 일으키는 이유와, HEDT(High-End Desktop) 플랫폼, 특히 &lt;b&gt;X299가 여전히 강력한 현역인 이유&lt;/b&gt;에 대한 기술 리포트입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1RDyh/dJMcabWNmgc/vQJhTDkHXx8WL2hCKpzGck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1RDyh/dJMcabWNmgc/vQJhTDkHXx8WL2hCKpzGck/img.png&quot; data-alt=&quot;[이미지 1: 검사장비 PC 내부의 이미지 ]&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1RDyh/dJMcabWNmgc/vQJhTDkHXx8WL2hCKpzGck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1RDyh%2FdJMcabWNmgc%2FvQJhTDkHXx8WL2hCKpzGck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;733&quot; height=&quot;475&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[이미지 1: 검사장비 PC 내부의 이미지 ]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2. 검사장비 PC의 첫 번째 관문, PCIe Lane&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 게이밍 PC와 검사장비 PC의 가장 큰 차이는 '확장성'입니다. 검사장비에는 GPU 외에도 고속의 프레임 그래버, 다축 모션 컨트롤 보드, 고속 I/O 보드 등 수많은 PCIe 카드가 장착됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 250px;&quot; border=&quot;1&quot; width=&quot;839&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;Mainstream (i9-14900K)&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;HEDT (i9-10900X)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;CPU&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;i9-14900K (Raptor Lake-S)&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;i9-10900X (Cascade Lake-X)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;출시 시기&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;2023년 10월&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;2019년 11월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;코어/스레드&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;24 코어 (8P+16E) / 32 스레드&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;10 코어 / 20 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;최대 클럭&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;최대 6.0 GHz&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;최대 4.5 GHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;CPU 직결 PCIe 레인&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;총 20개 (GPU용 16개 + NVMe용 4개)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;총 48개&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;PCIe 세대&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;PCIe 5.0 / 4.0 지원&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;PCIe 3.0 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;메모리 지원&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;듀얼 채널 DDR5 / DDR4&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;쿼드 채널 DDR4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;칩셋&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;Z790&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;X299&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;205&quot; height=&quot;23&quot;&gt;CPU-칩셋 연결&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;DMI 4.0 x8 (PCIe 4.0 x8 상당)&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; width=&quot;317&quot;&gt;DMI 3.0 x4 (PCIe 3.0 x4 상당)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;[PCIe Lane 비교]&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메인스트림 PC의 한계 (예: 인텔 14세대 i9-14900K)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;최신&lt;/b&gt; CPU인 i9-14900K (Z790 칩셋 보드)를 예로 들어보겠습니다. 스펙상으로는 최고처럼 보이지만, 'CPU가 직접 제공하는 Lane'은 &lt;b&gt;총 20개&lt;/b&gt; (16+4) 뿐입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Lane 부족 및 분할:&lt;/b&gt; 20개의 Lane은 보통 그래픽카드(GPU)에 &lt;b&gt;x16&lt;/b&gt;, 주 저장장치(NVMe SSD)에 &lt;b&gt;x4&lt;/b&gt;로 할당됩니다. &lt;i&gt;이것으로 CPU 직결 Lane은 끝입니다.&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;칩셋의 한계:&lt;/b&gt; &quot;그럼 나머지 슬롯은요?&quot;라고 묻는다면, 그 슬롯들은 &lt;b&gt;Z790 칩셋&lt;/b&gt;을 거쳐서 CPU와 연결됩니다. 칩셋이 CPU와 연결되는 통로(DMI 4.0 x8)는 기껏해야 PCIe 4.0 x8 수준의 대역폭입니다. 즉, &lt;b&gt;모든 추가 장치(모션 보드, 프레임 그래버, 추가 NVMe, LAN 카드 등)가 이 좁은 통로를 나눠 써야 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 저하:&lt;/b&gt; &quot;Lane 수가 적으면 Pcie 8x - 8x 로 동작&quot;하게 되고, 특히 고해상도 이미지를 &quot;그래픽 카드 메모리에 올릴때 속도 저하&quot;가 발생합니다. (Bayer 변환, 균일도 처리 등 GPU 연산 시 치명적)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인식 불가:&lt;/b&gt; 칩셋을 거치는 Lane에 고대역폭 카드를 2개 이상 장착하면, 대역폭 부족으로 &quot;IO 보드, 모션컨트롤등 들어가는 PCIe 가 많아서 인식이 안&quot;되는 최악의 상황도 발생할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비교: HEDT 플랫폼 (예: 인텔 i9-10900X)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반면 제가 사용하는 &lt;b&gt;X299 플랫폼의 i9-10900X&lt;/b&gt;는 CPU 자체에서 &lt;b&gt;48개의 Lane&lt;/b&gt;을 직접 제공합니다.&lt;/li&gt;
&lt;li&gt;GPU(x16), 프레임 그래버(x8), 모션 보드(x8), 고속 I/O(x8)를 모두 꽂아도 칩셋을 거치지 않고 CPU와 직결되며, Lane이 8개나 남습니다. 모든 카드가 제 속도를 낼 수 있는 것입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;물론, 너무 세대가 차이나면 PCIe 세대 차이(예: Gen 3.0 vs 5.0)에서 오는 대역폭 등을 고려했을 때 최신 CPU가 좋을 수도 있습니다.&lt;/b&gt; (하지만 지금 제 환경에선 Lane 개수 확보가 더 중요합니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3. 본문 2: 왜 X299인가? (feat. 쓰레드 리퍼 실패기)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면 CPU에서 직접 풍부한 Lane을 제공하는 HEDT 플랫폼이 필수입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;선택지 1. AMD 쓰레드 리퍼:&lt;/b&gt; 저도 처음엔 더 많은 코어와 Lane을 제공하는 쓰레드 리퍼를 선택했습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문제:&lt;/b&gt; &quot;동작에서, 고른 성능을 가져오지 않았다. 특히 로드가 많이 걸리는 작업시에!&quot; 이유는 명확히 밝히지 못했으나, 24시간 동작하는 장비에서 원인 불명의 속도편차는 치명적이었습니다.&lt;/li&gt;
&lt;li&gt;예상은 Numa 아키텍처에 따른 속도 저하라고 생각 되지만, 현재로 적용하기에는 무리가 있었습니다. 이 문제에 대해서는 다음에 집중적으로 확인해봐야 될듯 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선택지 2. 인텔 X299:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;검증된 안정성:&lt;/b&gt; X299 플랫폼은 수년간 시장에서 검증되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;충분한 Lane:&lt;/b&gt; CPU에 따라 44~48개의 PCIe Lane을 제공, GPU x16, 모션 x8, 프레임 그래버 x8... 모든 카드를 최대 성능으로 구동하고도 Lane이 남습니다.&lt;/li&gt;
&lt;li&gt;이것이 제가 &quot;고성능 컴퓨터를 쓰고 싶지만, 옛날 x299쓰는이유&quot;입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이미지 3: 실제 보드 설치 사진]&lt;/b&gt; (설명: &lt;b&gt;사용자님 실제 사진.&lt;/b&gt; GPU, 모션 보드, 프레임 그래버 등 PCIe 슬롯이 가득 찬 X299 메인보드 사진)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4. 검사 속도의 진실 - '클럭' vs '코어'&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 최신 CPU가 코어도 많고 좋은데, 왜 X299 CPU를 쓰나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lane 문제와는 별도로 생각해볼 부분이 있습니다. 검사 프로그램의 특성을 확인하여, 결정을 해야 될거 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;내 프로그램의 특성:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;S/W:&lt;/b&gt; Cognex 라이브러리 + OpenCV 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작업:&lt;/b&gt; 8~12 멀티쓰레드를 이용한 Frame 검사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPU:&lt;/b&gt; Bayer 변환, 균일도 처리 등 병렬 연산&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가설:&lt;/b&gt; &quot;실제 단순 검사속도의 차이는 세대수가 아닌 클럭속도 와 쓰레드 개수가 중요&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증:&lt;/b&gt; &lt;b&gt;이 가설은 맞습니다.&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클럭 속도:&lt;/b&gt; Cognex 같은 전통적인 Vision 라이브러리는 여전히 **단일 스레드의 처리 속도(클럭)**에 큰 영향을 받습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코어 활용:&lt;/b&gt; 제 프로그램은 &quot;8~12 멀티쓰레드&quot;를 사용합니다. 12코어/4.5GHz CPU와 32코어/3.8GHz CPU가 있다면, 제 환경에선 12코어 CPU가 더 빠를 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;855&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckns0D/dJMcad79ag1/47J529fiT9tMlEk9arLuWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckns0D/dJMcad79ag1/47J529fiT9tMlEk9arLuWk/img.png&quot; data-alt=&quot;[이미지 2: 작업 관리자 스크린샷 ] &amp;amp;nbsp;-&amp;amp;nbsp;8~12개 쓰레드에 부하가 걸리고 있는 Windows 작업 관리자 스크린샷 이해용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckns0D/dJMcad79ag1/47J529fiT9tMlEk9arLuWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckns0D%2FdJMcad79ag1%2F47J529fiT9tMlEk9arLuWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1414&quot; height=&quot;855&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;855&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[이미지 2: 작업 관리자 스크린샷 ] &amp;nbsp;-&amp;nbsp;8~12개 쓰레드에 부하가 걸리고 있는 Windows 작업 관리자 스크린샷 이해용&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;결론:&lt;/b&gt; 무조건 많은 코어보다, '필요한 만큼의 코어 수'와 '높은 단일 코어 클럭'을 가진 X299 CPU(예: i9-10900X, 10920X)가 제 애플리케이션에는 더 효율적이었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5. 나만의 고집? - One-PC 아키텍처&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 전문가가 안정성을 위해 &quot;제어 PC 와 Vision PC를&quot; 나누라고 조언합니다. &lt;b&gt;하지만 저는 하나로 통일합니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PC를 나누는 순간, 두 PC 간의 '통신'이라는 새로운 변수가 생깁니다.&lt;/li&gt;
&lt;li&gt;Ethernet 통신은 그 자체로 지연 시간(Latency)을 유발하며, &quot;다른 라이브러리의 간섭에 의한 속도 저하&quot;보다 더 예측 불가능한 문제를 일으킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;모든 카드가 하나의 메인보드(PCIe 버스) 안에서 직접 데이터를 교환하는 것이 가장 빠르고 확실합니다.&lt;/li&gt;
&lt;li&gt;&quot;물론 고리타분한 나만의 고집일 수 있습니다.&quot; 하지만 실시간 데이터 처리가 생명인 장비에서, 이 '단순함'은 가장 강력한 무기입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6. X299, 여전히 최고의 '작업대'&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 게이밍 PC가 '스포츠카'라면, X299 플랫폼은 '대형 트럭'입니다. 검사장비는 화려한 스포츠카가 아니라, 많은 짐(카드)을 싣고(PCIe Lane) 24시간 안정적으로 달려야 하는(안정성) 트럭이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PCIe Lane, 검증된 안정성, 그리고 내 S/W에 맞는 고클럭 CPU. 이 세 가지 이유로 저는 오늘도 고성능 검사장비를 위해 구형 X299 플랫폼을 고집합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;앞으로도 저의 의견이 항상 맞을 수가 없습니다. 계속 발전하니까요.&amp;nbsp;&lt;br /&gt;이것만 기억해주시면 됩니다.비전 PC를 선정할때기본적인 Lane 수, 검사 프로그램의 특성 등을 확인하여 PC 선정!!!&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SvBBp/dJMcaawOHi4/Ws8qKRzyxuI96VkCkv8j1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SvBBp/dJMcaawOHi4/Ws8qKRzyxuI96VkCkv8j1k/img.png&quot; data-alt=&quot;[이미지 3: 검사장비이미지 ] - 상상도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SvBBp/dJMcaawOHi4/Ws8qKRzyxuI96VkCkv8j1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSvBBp%2FdJMcaawOHi4%2FWs8qKRzyxuI96VkCkv8j1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[이미지 3: 검사장비이미지 ] - 상상도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/blockquote&gt;</description>
      <category>ETC./Tech Journal</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/40</guid>
      <comments>https://arosyoung.tistory.com/entry/%EC%99%9C-%EB%82%98%EB%8A%94-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EA%B2%80%EC%82%AC%EC%9E%A5%EB%B9%84%EC%97%90-%EA%B5%AC%ED%98%95-X299-%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%9D%84-%EA%B3%A0%EC%A7%91%ED%95%98%EB%8A%94%EA%B0%80#entry40comment</comments>
      <pubDate>Sun, 2 Nov 2025 13:25:57 +0900</pubDate>
    </item>
    <item>
      <title>[Insight] 장비 제어 소프트웨어 개발자란?</title>
      <link>https://arosyoung.tistory.com/entry/%EC%9E%A5%EB%B9%84-%EC%A0%9C%EC%96%B4-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%9E%80</link>
      <description>&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;e1955bf9-8dfb-419c-a760-48fb7ade53ce.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcdtAu/btsRa3LCUXM/VNZ2feGZD7rOTOgXpKvNf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcdtAu/btsRa3LCUXM/VNZ2feGZD7rOTOgXpKvNf0/img.png&quot; data-alt=&quot;장비개발자란?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcdtAu/btsRa3LCUXM/VNZ2feGZD7rOTOgXpKvNf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcdtAu%2FbtsRa3LCUXM%2FVNZ2feGZD7rOTOgXpKvNf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-filename=&quot;e1955bf9-8dfb-419c-a760-48fb7ade53ce.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;장비개발자란?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h1&gt;&lt;b&gt;장비 제어 소프트웨어 개발자, 기계의 언어를 설계하는 사람&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 자동화 장비를 만드는 일을 합니다.&lt;br /&gt;그중에서도 &lt;b&gt;장비 제어 소프트웨어(Equipment Control Software)&lt;/b&gt; 개발이 제 역할입니다.&lt;br /&gt;쉽게 말하면,&lt;br /&gt;&quot;기계가 언제 움직이고, 어디로 가며, 어떤 순서로 동작해야 하는가&quot;를&lt;br /&gt;소프트웨어로 설계하고 구현하는 일입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 기계가 움직이기까지&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장비 제어 소프트웨어는 단순히 모터를 돌리고 IO를 제어하는 수준이 아닙니다.&lt;br /&gt;하나의 장비가 움직이기 위해서는 수십, 때로는 수백 개의 센서와 액추에이터가&lt;br /&gt;순서와 조건에 맞춰 &lt;b&gt;정확히 연동&lt;/b&gt;되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 가능하게 하는 게 바로 &lt;b&gt;제어 로직(Sequence Logic)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실린더가 먼저 내려와야 하고,&lt;/li&gt;
&lt;li&gt;그 신호가 감지되면 모터가 이동하고,&lt;/li&gt;
&lt;li&gt;위치가 안정되면 카메라가 촬영하고,&lt;/li&gt;
&lt;li&gt;검사 결과에 따라 다음 공정을 제어합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 과정을 실시간으로 판단하고 관리하는 게&lt;br /&gt;&lt;b&gt;장비 제어 소프트웨어의 핵심 역할&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 제가 다루는 영역&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Motion Control (모션 제어)&lt;/b&gt;&lt;br /&gt;&amp;rarr; 서보 축 이동, 동기 제어, 정밀 위치 보정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IO Control (입출력 제어)&lt;/b&gt;&lt;br /&gt;&amp;rarr; 센서, 실린더, 솔레노이드 등 하드웨어 신호 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Vision Triggering (비전 트리거링)&lt;/b&gt;&lt;br /&gt;&amp;rarr; 카메라 타이밍, 조명 동기, 검사 결과 수집&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sequence Management (시퀀스 제어)&lt;/b&gt;&lt;br /&gt;&amp;rarr; 각 공정의 순서, 조건, 예외 처리 로직&lt;/li&gt;
&lt;li&gt;&lt;b&gt;System Integration (시스템 통합)&lt;/b&gt;&lt;br /&gt;&amp;rarr; 모션보드, 비전보드, PLC, 센서 등 통신 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 가장 중요한 건&lt;br /&gt;&lt;b&gt;기계의 구조를 이해하는 능력&lt;/b&gt;입니다.&lt;br /&gt;기계가 어떤 구조로 움직이는지 모르면&lt;br /&gt;코드가 아니라 &lt;b&gt;혼란을 만드는 시퀀스&lt;/b&gt;를 짜게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. &amp;lsquo;제어&amp;rsquo;는 결국 판단의 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장비 제어 소프트웨어는 단순히 &amp;lsquo;명령을 전달하는 코드&amp;rsquo;가 아닙니다.&lt;br /&gt;수많은 조건과 예외, 안전 규칙을 &lt;b&gt;사람 대신 판단해주는 로직&lt;/b&gt;입니다.&lt;br /&gt;그래서 제어 프로그램을 짠다는 건,&lt;br /&gt;기계에게 &lt;b&gt;판단 기준을 가르치는 일&lt;/b&gt;이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 모터가 멈추지 않는 문제 하나로&lt;br /&gt;하루를 통째로 쓸 때도 있습니다.&lt;br /&gt;하지만 그 한 줄의 코드가&lt;br /&gt;기계의 움직임을 &amp;lsquo;의미 있게&amp;rsquo; 바꾸는 순간이 있습니다.&lt;br /&gt;그게 이 일의 매력입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 꼭 그 일만은 하지 않는다.&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 중소기업이 많기 때문에 단순히 개발만 한다고 생각하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표를 보시면 전반적으로 모든일에 개입하게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UFGus/btsRafFNQxe/OCD8Nv5A5ZoFVSdQfJhv01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UFGus/btsRafFNQxe/OCD8Nv5A5ZoFVSdQfJhv01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UFGus/btsRafFNQxe/OCD8Nv5A5ZoFVSdQfJhv01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUFGus%2FbtsRafFNQxe%2FOCD8Nv5A5ZoFVSdQfJhv01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;979&quot; height=&quot;976&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;977&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash; &lt;b&gt;Young&lt;/b&gt;&lt;br /&gt;&lt;i&gt;Vision &amp;amp; Motion Automation Developer&lt;/i&gt;&lt;br /&gt;&lt;i&gt;Bolts and Pixels&lt;/i&gt;&lt;/p&gt;</description>
      <category>Insight &amp;amp; Philosophy</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/39</guid>
      <comments>https://arosyoung.tistory.com/entry/%EC%9E%A5%EB%B9%84-%EC%A0%9C%EC%96%B4-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%9E%80#entry39comment</comments>
      <pubDate>Wed, 15 Oct 2025 16:53:44 +0900</pubDate>
    </item>
    <item>
      <title>&amp;ldquo;좋은 엔지니어는 빠른 사람이 아니라, 일관된 사람이다&amp;rdquo;</title>
      <link>https://arosyoung.tistory.com/entry/%E2%80%9C%EC%A2%8B%EC%9D%80-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%8A%94-%EB%B9%A0%EB%A5%B8-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%9D%BC%EA%B4%80%EB%90%9C-%EC%82%AC%EB%9E%8C%EC%9D%B4%EB%8B%A4%E2%80%9D</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fdfe2e38-1481-4789-86cc-1f2950a70bc1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fim38/btsQ927oW1F/tqC4CkIIhmJZ1fwunF4BUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fim38/btsQ927oW1F/tqC4CkIIhmJZ1fwunF4BUK/img.png&quot; data-alt=&quot;좋은엔지니어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fim38/btsQ927oW1F/tqC4CkIIhmJZ1fwunF4BUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFim38%2FbtsQ927oW1F%2FtqC4CkIIhmJZ1fwunF4BUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1536&quot; data-filename=&quot;fdfe2e38-1481-4789-86cc-1f2950a70bc1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;좋은엔지니어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;좋은 엔지니어는 빠른 사람이 아니라, 일관된 사람이다&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현장에서 일하다 보면 늘 빠른 사람이 눈에 들어옵니다.&lt;br /&gt;코드를 금방 짜고, 시운전도 남들보다 먼저 끝내는 사람.&lt;br /&gt;처음엔 그런 사람이 부럽습니다.&lt;br /&gt;하지만 시간이 지나면, 결국 믿음이 가는 사람은 따로 있습니다.&lt;br /&gt;&lt;b&gt;일관된 사람&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;일관성이란 게 뭘까&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비전이나 모션 쪽 일을 하다 보면,&lt;br /&gt;하루에도 같은 세팅을 수십 번 반복합니다.&lt;br /&gt;그런데 이상하게도,&lt;br /&gt;어제 잘 되던 게 오늘은 미세하게 다를 때가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 진짜 실력은 속도보다 &lt;b&gt;기준을 지키는 힘&lt;/b&gt;에서 나옵니다.&lt;br /&gt;환경이 조금 달라도 결과가 크게 흔들리지 않게 만드는 사람,&lt;br /&gt;그게 결국 신뢰를 받습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;빠른 것보다 중요한 것&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 사람은 늘 주목받습니다.&lt;br /&gt;그런데 그 속도가 계속 유지되지는 않습니다.&lt;br /&gt;조금만 상황이 바뀌면,&lt;br /&gt;오히려 다른 사람의 시간을 잡아먹을 때도 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 그보단 느리더라도,&lt;br /&gt;&lt;b&gt;실수를 반복하지 않는 사람&lt;/b&gt;이 좋습니다.&lt;br /&gt;빠름보다 중요한 건 &lt;b&gt;재현성&lt;/b&gt;입니다.&lt;br /&gt;오늘 한 결과를 내일 다시 만들 수 있어야 진짜 기술이죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기술보다 태도&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술은 계속 바뀝니다.&lt;br /&gt;렌즈도 바뀌고, 프레임그래버도 바뀌고,&lt;br /&gt;지금 쓰는 보드도 몇 년 뒤면 단종됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 태도는 안 바뀝니다.&lt;br /&gt;꼼꼼하게 검증하고, 기록 남기고,&lt;br /&gt;작은 오차에도 이유를 찾으려는 습관.&lt;br /&gt;그게 결국 오래 가는 사람을 만듭니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리하자면&lt;/b&gt;&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;빠른 엔지니어&lt;/th&gt;
&lt;th&gt;일관된 엔지니어&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;목표&lt;/td&gt;
&lt;td&gt;&amp;nbsp;눈에 띄는 결과&lt;/td&gt;
&lt;td&gt;&amp;nbsp;믿을 수 있는 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기준&lt;/td&gt;
&lt;td&gt;&amp;nbsp;효율&lt;/td&gt;
&lt;td&gt;&amp;nbsp;재현성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특징&lt;/td&gt;
&lt;td&gt;&amp;nbsp;즉흥적 개선&lt;/td&gt;
&lt;td&gt;&amp;nbsp;꾸준한 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과&lt;/td&gt;
&lt;td&gt;&amp;nbsp;불안정한 성과&lt;/td&gt;
&lt;td&gt;&amp;nbsp;예측 가능한 품질&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 엔지니어는 빠른 사람이 아닙니다.&lt;br /&gt;같은 결과를 꾸준히 내는 사람,&lt;br /&gt;기준을 지키는 사람입니다.&lt;br /&gt;기계는 속도로 평가받지만,&lt;br /&gt;사람은 신뢰로 평가받습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기술은 변해도, 태도는 남는다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash; &lt;b&gt;Young&lt;/b&gt;&lt;br /&gt;&lt;i&gt;Vision &amp;amp; Motion Automation Developer&lt;/i&gt;&lt;br /&gt;&lt;i&gt;Bolts and Pixels&lt;/i&gt;&lt;/p&gt;</description>
      <category>Career &amp;amp; Growth</category>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/38</guid>
      <comments>https://arosyoung.tistory.com/entry/%E2%80%9C%EC%A2%8B%EC%9D%80-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%8A%94-%EB%B9%A0%EB%A5%B8-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%9D%BC%EA%B4%80%EB%90%9C-%EC%82%AC%EB%9E%8C%EC%9D%B4%EB%8B%A4%E2%80%9D#entry38comment</comments>
      <pubDate>Wed, 15 Oct 2025 16:32:07 +0900</pubDate>
    </item>
    <item>
      <title>Project Archive</title>
      <link>https://arosyoung.tistory.com/pages/Project-Archive</link>
      <description>&lt;h1&gt;&lt;strong&gt;Project Archive – Bolts and Pixels&lt;/strong&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;em&gt;From bolts to pixels, every project is a story of precision.&lt;/em&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이 페이지는 그동안 수행했던 &lt;strong&gt;자동화 설비 개발 프로젝트&lt;/strong&gt;를&lt;br&gt;간략히 기록하고, 핵심 기술과 배운 점을 정리하는 공간입니다.&lt;br&gt;기밀이나 고객 정보는 제외하고, &lt;strong&gt;기술적 흐름과 방향성 중심&lt;/strong&gt;으로 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;⚙️ &lt;strong&gt;주요 프로젝트 요약&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;1️⃣ LED Wafer Inspection System&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기간:&lt;/strong&gt; 2018–2024  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;역할:&lt;/strong&gt; 비전·모션 통합 개발 / 시스템 구조 설계  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주요 기술:&lt;/strong&gt; Line-scan Vision, Stage Synchronization, Calibration Algorithm  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성과:&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;검사 속도 30% 향상  &lt;/li&gt;
&lt;li&gt;정렬 오차 1.3 µm 수준으로 개선  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비고:&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;GPU 기반 이미지 전처리 및 동시 멀티스레드 구조 설계  &lt;/li&gt;
&lt;li&gt;NUMA-aware Thread Pool 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;2️⃣ PCB 자동 보상 정렬 시스템&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기간:&lt;/strong&gt; 2022–2023  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;역할:&lt;/strong&gt; 보정 알고리즘 및 좌표 보정 모듈 개발  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주요 기술:&lt;/strong&gt; Fiducial Mark Alignment, Polynomial Surface Fitting, Motion Compensation  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성과:&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;기존 대비 정렬 정확도 40% 개선  &lt;/li&gt;
&lt;li&gt;생산 라인 정지율 25% 감소  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비고:&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;MATLAB → C++ 변환 구조로 자동화 속도 향상  &lt;/li&gt;
&lt;li&gt;CSV 기반 오차 맵 관리 기능 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;3️⃣ Inline Vision Calibration Module&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기간:&lt;/strong&gt; 2021  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;역할:&lt;/strong&gt; 비전 모듈 및 보정 알고리즘 개발  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주요 기술:&lt;/strong&gt; Checkerboard Calibration, Camera-Motion Alignment, Error Map Fitting  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성과:&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;오차 데이터 자동 보정 프로세스 완성  &lt;/li&gt;
&lt;li&gt;현장 보정 소요시간 60% 단축&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;  &lt;strong&gt;기술 키워드&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Machine Vision&lt;/code&gt; · &lt;code&gt;Motion Control&lt;/code&gt; · &lt;code&gt;Precision Alignment&lt;/code&gt;&lt;br&gt;&lt;code&gt;Image Processing&lt;/code&gt; · &lt;code&gt;Automation Engineering&lt;/code&gt; · &lt;code&gt;Error Compensation&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;  &lt;strong&gt;현재 진행 중&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;고속 검사 라인용 &lt;strong&gt;Multi-PC Vision Pipeline&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;GPU 기반 &lt;strong&gt;Real-time Image Processing Engine&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;비전-모션 연동 &lt;strong&gt;Dynamic Error Compensation 연구&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;  &lt;strong&gt;이 페이지의 목적&lt;/strong&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;프로젝트를 자랑하기보다,&lt;br&gt;그 속에서 배운 &lt;strong&gt;원리와 교훈&lt;/strong&gt;을 정리하기 위한 기록입니다.&lt;br&gt;기술의 깊이는 결국 &lt;strong&gt;반복된 시행착오 속에서 완성&lt;/strong&gt;된다고 믿습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;— &lt;strong&gt;Young&lt;/strong&gt;&lt;br&gt;&lt;em&gt;Vision &amp;amp; Motion Automation Developer&lt;/em&gt;&lt;br&gt;&lt;em&gt;Bolts and Pixels&lt;/em&gt;&lt;/p&gt;</description>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/pages/Project-Archive</guid>
      <pubDate>Wed, 15 Oct 2025 16:21:30 +0900</pubDate>
    </item>
    <item>
      <title>Contact</title>
      <link>https://arosyoung.tistory.com/pages/Contact</link>
      <description>&lt;h1&gt;&lt;b&gt;Contact &amp;ndash; Bolts and Pixels&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Automation is Human Work&lt;/i&gt;&lt;br /&gt;&lt;i&gt;비전과 모션 사이, 사람의 판단을 기록합니다.&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요.&lt;br /&gt;&lt;b&gt;Bolts and Pixels&lt;/b&gt;를 운영하는 &lt;b&gt;Young&lt;/b&gt;입니다.&lt;br /&gt;이 블로그는 자동화 설비 개발 현장에서 얻은 경험과 통찰을 공유하기 위한 공간입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;문의 / 협업&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화 설비, 비전 검사, 모션 제어 관련 기술적 논의나&lt;br /&gt;협업 제안이 있으시다면 아래 이메일로 연락 부탁드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  Email:&lt;/b&gt; young538@gmail.com&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능한 빠르게 답변드리며,&lt;br /&gt;기술 상담 또는 협업이 필요한 경우엔&lt;br /&gt;주제와 목적을 간단히 함께 적어주시면 더 원활하게 진행됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;인용 / 공유 안내&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bolts and Pixels의 글이나 이미지를 인용하실 경우,&lt;br /&gt;출처 표기와 함께 원문 링크를 명시해 주시기 바랍니다.&lt;br /&gt;무단 전재나 상업적 이용은 삼가 주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처 표기 예시:&lt;/b&gt;&lt;br /&gt;출처: Bolts and Pixels (&lt;a href=&quot;https://helpums.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://helpums.com/&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙️ &lt;b&gt;함께 이야기하고 싶은 주제들&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비전(검사) 알고리즘과 조명 세팅&lt;/li&gt;
&lt;li&gt;모션 제어, 서보 튜닝, 좌표 보정&lt;/li&gt;
&lt;li&gt;장비 정밀도 향상, 트러블슈팅 사례&lt;/li&gt;
&lt;li&gt;자동화 프로젝트 관리, 협업 구조&lt;/li&gt;
&lt;li&gt;엔지니어의 성장과 커리어 방향&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;br /&gt;기술은 결국 사람의 결정을 담는 과정이라고 생각합니다.&lt;br /&gt;이 공간이 그 대화의 시작이 되길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash; &lt;b&gt;Young&lt;/b&gt;&lt;br /&gt;&lt;i&gt;Vision &amp;amp; Motion Automation Developer&lt;/i&gt;&lt;/p&gt;</description>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/pages/Contact</guid>
      <pubDate>Wed, 15 Oct 2025 16:17:53 +0900</pubDate>
    </item>
    <item>
      <title>About</title>
      <link>https://arosyoung.tistory.com/pages/%F0%9F%A7%BE-About-%E2%80%93-Bolts-and-Pixels</link>
      <description>&lt;h1&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;About &amp;ndash; Bolts and Pixels&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;i&gt;&amp;nbsp;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;저는 자동화 설비를 직접 설계하고 제작하는 개발자 &lt;b&gt;Young&lt;/b&gt;입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;비전(검사)과 모션(제어)을 중심으로 장비를 만들고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그 안에서 발생하는 문제를 해결하며 15년을 현장에서 보냈습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;장비는 정밀하고, 센서는 빠르며, 알고리즘은 똑똑해졌지만&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여전히 자동화의 성공을 결정짓는 것은 &lt;b&gt;사람의 판단&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;자동화는 결국 사람의 일이다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 한 문장은 제가 일을 시작한 이후부터 지금까지 변하지 않은 신념입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;볼트 하나의 토크, 픽셀 하나의 밝기.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 작은 차이들이 장비의 성능을 결정합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러나 그보다 더 큰 차이는 언제나 사람의 손끝과 생각 속에서 만들어집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Bolts and Pixels&lt;/b&gt;는 그런 경험을 기록하는 공간입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이곳에서 다루는 주제는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Vision &amp;amp; Inspection&lt;/b&gt; : 카메라, 조명, 이미지 처리, 불량 판정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Motion &amp;amp; Control&lt;/b&gt; : 서보, 스테이지, 위치 보정, 진동 억제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Trouble &amp;amp; Debug Note&lt;/b&gt; : 현장 트러블 해결 과정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Insight &amp;amp; Philosophy&lt;/b&gt; : 기술자의 판단과 태도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Career &amp;amp; Growth&lt;/b&gt; : 엔지니어로서의 성장과 고민&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 블로그는 기술 매뉴얼이 아닙니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;기록을 통해 경험을 나누는 곳&lt;/b&gt;이며,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;누군가의 시행착오를 조금이라도 줄일 수 있는 공간이 되길 바랍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기술은 매뉴얼에 있지만, 해답은 현장에 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 그 현장은 결국, 사람입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;mdash; &lt;b&gt;Young&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;i&gt;Vision &amp;amp; Motion Automation Developer&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <author>PixelMechanic</author>
      <guid isPermaLink="true">https://arosyoung.tistory.com/pages/%F0%9F%A7%BE-About-%E2%80%93-Bolts-and-Pixels</guid>
      <pubDate>Wed, 15 Oct 2025 16:08:32 +0900</pubDate>
    </item>
  </channel>
</rss>