// // InspectEngine.cpp - レシピを yfoximg で実行(唯一 yfoximg に依存する実装ファイル)。 // // 注意(yfoxlib の作法): // - アンブレラの yfoximg.h(OpenGL 引き込み)は使わず ImgProcessing.h を直接 include。 // - yfoximg は NOMINMAX を定義しない(裸の min/max マクロ前提)。この TU では // std::min/std::max を使わず、手書きのクランプで済ませる。 // #include "stdafx.h" #include "InspectEngine.h" #include "StepCatalog.h" #include "ImgProcessing.h" // CImgProcessing / CDiBitmap namespace core { static int ClampI(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); } InspectEngine::InspectEngine() {} InspectEngine::~InspectEngine() {} bool InspectEngine::HasImage() const { return m_src && !m_src->IsNull(); } int InspectEngine::SrcWidth() const { return m_src ? m_src->GetWidth() : 0; } int InspectEngine::SrcHeight() const { return m_src ? m_src->GetHeight() : 0; } int InspectEngine::SrcBpp() const { return m_src ? m_src->GetBPP() : 0; } int InspectEngine::WorkWidth() const { return m_work ? m_work->GetWidth() : 0; } int InspectEngine::WorkHeight() const { return m_work ? m_work->GetHeight() : 0; } int InspectEngine::WorkBpp() const { return m_work ? m_work->GetBPP() : 0; } bool InspectEngine::LoadImage(LPCWSTR path) { auto img = std::make_unique(); if (!img->Load(CString(path), 0) || img->IsNull()) return false; m_src = std::move(img); m_shots.clear(); return true; } // 32bit(アルファ付き)を 24bit へ落とす。 static std::unique_ptr Down32To24(CDiBitmap* src) { int W = src->GetWidth(), H = src->GetHeight(); auto dst = std::make_unique(); dst->Create(W, H, 24); int sp = src->GetPitch(), dp = dst->GetPitch(); LPBYTE sb = src->GetBits(), db = dst->GetBits(); for (int y = 0; y < H; y++) { BYTE* s = sb + (intptr_t)y * sp; BYTE* d = db + (intptr_t)y * dp; for (int x = 0; x < W; x++) { d[x * 3 + 0] = s[x * 4 + 0]; d[x * 3 + 1] = s[x * 4 + 1]; d[x * 3 + 2] = s[x * 4 + 2]; } } return dst; } // m_work を 8bit グレーに保証する。 static bool Ensure8(std::unique_ptr& work, CImgProcessing& ip) { if (!work) return false; int bpp = work->GetBPP(); if (bpp == 8) return true; if (bpp == 32) work = Down32To24(work.get()); if (work->GetBPP() != 24) return false; int W = work->GetWidth(), H = work->GetHeight(); auto gray = std::make_unique(); gray->Create(W, H, 8); if (!ip.RGB2BW(gray.get(), work.get())) return false; gray->SetGrayPalette(); work = std::move(gray); return true; } // アスペクト維持で iw×ih を dst 内に収める矩形(std::min を使わず手計算)。 static CRect AspectFit(int iw, int ih, const CRect& dst) { if (iw <= 0 || ih <= 0 || dst.Width() <= 0 || dst.Height() <= 0) return dst; double sx = (double)dst.Width() / iw, sy = (double)dst.Height() / ih; double s = sx < sy ? sx : sy; int w = (int)(iw * s + 0.5), h = (int)(ih * s + 0.5); int x = dst.left + (dst.Width() - w) / 2; int y = dst.top + (dst.Height() - h) / 2; return CRect(x, y, x + w, y + h); } // 二値化した一時画像を作る(白=255)。invert で暗い側を白に。複数 roi の内側のみ白く塗る。 // yfoximg の Threshold を同一バッファへ sub-rect で複数回呼ぶと結果が壊れるため、 // ここは画素を直接走査して確実にマスクを作る(8bit 前提=呼び出し前に Ensure8 済み)。 static std::unique_ptr MakeBinary(std::unique_ptr& work, CImgProcessing& /*ip*/, int thresh, bool invert, int& whiteCount, const std::vector& rois) { int W = work->GetWidth(), H = work->GetHeight(); auto bin = std::make_unique(); bin->Create(W, H, 8); bin->Clear((BYTE)0); LPBYTE wb = work->GetBits(); int wp = work->GetPitch(); LPBYTE bb = bin->GetBits(); int bp = bin->GetPitch(); whiteCount = 0; for (const CRect& roi : rois) { CRect r = roi; r.NormalizeRect(); if (r.left < 0) r.left = 0; if (r.top < 0) r.top = 0; if (r.right > W) r.right = W; if (r.bottom > H) r.bottom = H; for (int y = r.top; y < r.bottom; y++) { const BYTE* ws = wb + (intptr_t)y * wp; BYTE* bs = bb + (intptr_t)y * bp; for (int x = r.left; x < r.right; x++) { BYTE v = ws[x]; bool white = invert ? (v <= thresh) : (v >= thresh); if (white && bs[x] == 0) { bs[x] = 255; whiteCount++; } } } } bin->SetGrayPalette(); return bin; } // 複数 roi の外接矩形(union)。 static CRect UnionRects(const std::vector& rois) { CRect u = rois.empty() ? CRect(0, 0, 0, 0) : rois[0]; for (const CRect& r : rois) u |= r; return u; } static bool RunStep(Step& st, std::unique_ptr& work, CImgProcessing& ip, const std::vector& rois) { st.result = StepResult(); st.result.ran = true; const int W = work->GetWidth(), H = work->GetHeight(); switch (st.kind) { case StepKind::Grayscale: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } st.result.text = L"白黒に変換しました"; return true; } case StepKind::Median: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } auto dst = std::make_unique(); dst->Create(work->GetWidth(), work->GetHeight(), 8); ip.Median(dst.get(), work.get()); dst->SetGrayPalette(); work = std::move(dst); st.result.text = L"ノイズをならしました"; return true; } case StepKind::Binarize: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } int lo = ClampI((int)st.Param(L"lo", 128), 0, 255); int hi = ClampI((int)st.Param(L"hi", 255), 0, 255); bool inBand = (int)st.Param(L"invert", 0) == 0; auto dst = std::make_unique(); dst->Create(W, H, 8); int cnt = ip.Threshold(dst.get(), work.get(), CRect(0, 0, W, H), lo, hi, inBand); dst->SetGrayPalette(); work = std::move(dst); st.result.measured = cnt; st.result.text.Format(L"明るさ %d〜%d を白に(白=%d px)", lo, hi, cnt); return true; } case StepKind::Otsu: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } bool inv = (int)st.Param(L"invert", 0) != 0; auto dst = std::make_unique(); dst->Create(W, H, 8); ip.Threshold_Otsu(dst.get(), work.get(), CRect(0, 0, W, H), inv); dst->SetGrayPalette(); work = std::move(dst); st.result.text = L"自動しきい値で白黒に分けました"; return true; } case StepKind::Gamma: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } double g = st.Param(L"gamma", 1.0); if (g < 0.05) g = 0.05; auto dst = std::make_unique(); dst->Create(work->GetWidth(), work->GetHeight(), 8); ip.CreateGammaLUT(g); ip.Gamma(dst.get(), work.get()); ip.DeleteGammaLUT(); dst->SetGrayPalette(); work = std::move(dst); st.result.text.Format(L"明るさ調整 %.2f", g); return true; } case StepKind::Contrast: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } int inMin = ClampI((int)st.Param(L"inMin", 0), 0, 255); int inMax = ClampI((int)st.Param(L"inMax", 255), 0, 255); int outMin = ClampI((int)st.Param(L"outMin", 0), 0, 255); int outMax = ClampI((int)st.Param(L"outMax", 255), 0, 255); auto dst = std::make_unique(); dst->Create(work->GetWidth(), work->GetHeight(), 8); ip.Contrast(dst.get(), work.get(), inMin, inMax, outMin, outMax); dst->SetGrayPalette(); work = std::move(dst); st.result.text = L"コントラストを強めました"; return true; } case StepKind::InspectBrightness: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } // 全 ROI の面積重み付き平均。 double sumWA = 0, sumA = 0; for (const CRect& r : rois) { int a = ip.RectAvg(work.get(), r); double ar = (double)r.Width() * r.Height(); sumWA += a * ar; sumA += ar; } int avg = (int)(sumA > 0 ? (sumWA / sumA) + 0.5 : 0); double lo = st.Param(L"min", 50), hi = st.Param(L"max", 200); st.result.measured = avg; st.result.ok = (avg >= lo && avg <= hi); st.result.text.Format(L"平均の明るさ=%d(合格 %.0f〜%.0f)", avg, lo, hi); return true; } case StepKind::InspectWhiteRatio: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } int th = ClampI((int)st.Param(L"thresh", 128), 0, 255); int white = 0; auto bin = MakeBinary(work, ip, th, false, white, rois); double area = 0; for (const CRect& r : rois) area += (double)r.Width() * r.Height(); double pct = (area > 0) ? (white * 100.0 / area) : 0.0; double lo = st.Param(L"min", 0), hi = st.Param(L"max", 100); st.result.measured = pct; st.result.ok = (pct >= lo && pct <= hi); st.result.text.Format(L"白い部分=%.1f%%(合格 %.0f〜%.0f%%)", pct, lo, hi); return true; } case StepKind::InspectBlobCount: { if (!Ensure8(work, ip)) { st.result.ok = false; st.result.text = L"白黒化に失敗"; return false; } int th = ClampI((int)st.Param(L"thresh", 128), 0, 255); bool inv = (int)st.Param(L"invert", 0) != 0; int white = 0; auto bin = MakeBinary(work, ip, th, inv, white, rois); CRect uni = UnionRects(rois); ip.LabelingInit(bin.get(), 0, uni); ip.Labeling(bin.get(), uni, CImgProcessing::NORMAL, false); int n = ip.m_nLabelCount; double lo = st.Param(L"min", 0), hi = st.Param(L"max", 0); st.result.measured = n; st.result.ok = (n >= lo && n <= hi); st.result.text.Format(L"塊の数=%d 個(合格 %.0f〜%.0f 個)", n, lo, hi); return true; } } st.result.ok = false; st.result.text = L"未知のステップ"; return false; } RunResult InspectEngine::Run(Recipe& recipe) { RunResult rr; m_shots.assign(recipe.steps.size(), nullptr); if (!HasImage()) { rr.error = true; rr.message = L"画像が読み込まれていません"; return rr; } m_work = std::make_unique(); m_work->Copy(m_src.get()); // 有効 ROI 群(画像全体でクランプ)。処理ステップは寸法を変えないので全ステップ共通。 const int IW = m_work->GetWidth(), IH = m_work->GetHeight(); std::vector eff; for (CRect r : recipe.rois) { r.NormalizeRect(); if (r.left < 0) r.left = 0; if (r.top < 0) r.top = 0; if (r.right > IW) r.right = IW; if (r.bottom > IH) r.bottom = IH; if (r.Width() >= 2 && r.Height() >= 2) eff.push_back(r); } if (eff.empty()) eff.push_back(CRect(0, 0, IW, IH)); // 指定なし=画像全体 CImgProcessing ip; bool allInspectOk = true; int failIdx = -1; for (int i = 0; i < (int)recipe.steps.size(); i++) { Step& st = recipe.steps[i]; bool ok = RunStep(st, m_work, ip, eff); // ステップ適用後のスナップショット(サムネイル用)。 auto shot = std::make_shared(); shot->Copy(m_work.get()); m_shots[i] = shot; if (!ok) { rr.error = true; if (failIdx < 0) failIdx = i; } if (IsInspect(st.kind)) { rr.hasInspect = true; if (!st.result.ok) { allInspectOk = false; if (failIdx < 0) failIdx = i; } } } if (rr.error) { rr.pass = false; rr.message.Format(L"エラー(ステップ#%d)", failIdx + 1); } else if (!rr.hasInspect) { rr.pass = false; rr.message = L"検査ステップがありません"; } else { rr.pass = allInspectOk; rr.message = rr.pass ? L"合格 PASS" : L"不合格 FAIL"; } return rr; } bool InspectEngine::HasShot(int i) const { return i >= 0 && i < (int)m_shots.size() && m_shots[i] && !m_shots[i]->IsNull(); } bool InspectEngine::SaveResultImage(LPCWSTR path, const Recipe& recipe, const RunResult& rr) { // ベース画像 = 最後のスナップショット(無ければ原画像)。 CDiBitmap* base = nullptr; for (int i = (int)m_shots.size() - 1; i >= 0; i--) if (m_shots[i] && !m_shots[i]->IsNull()) { base = m_shots[i].get(); break; } if (!base) base = m_src.get(); if (!base || base->IsNull()) return false; int W = base->GetWidth(), H = base->GetHeight(); CDiBitmap out; if (!out.Create(W, H, 24)) return false; HDC hdc = out.GetDC(); if (!hdc) return false; { CDC* pDC = CDC::FromHandle(hdc); base->Draw(pDC, CRect(0, 0, W, H), false); // 24bit へ最終画像を焼き込む pDC->SetBkMode(TRANSPARENT); // ROI 枠(黄)+ 番号 if (!recipe.rois.empty()) { CPen yp(PS_SOLID, (H > 400 ? 3 : 2), RGB(255, 210, 0)); CPen* op = pDC->SelectObject(&yp); pDC->SelectStockObject(NULL_BRUSH); pDC->SetTextColor(RGB(255, 210, 0)); for (size_t i = 0; i < recipe.rois.size(); i++) { pDC->Rectangle(recipe.rois[i]); CString no; no.Format(L"%d", (int)i + 1); pDC->TextOut(recipe.rois[i].left + 2, recipe.rois[i].top + 1, no); } pDC->SelectObject(op); } // PASS/FAIL バナー(左上) int fh = H / 16; if (fh < 16) fh = 16; if (fh > 40) fh = 40; LOGFONT lf = {}; lf.lfHeight = -fh; lf.lfWeight = FW_BOLD; wcscpy_s(lf.lfFaceName, L"Yu Gothic UI"); CFont big; big.CreateFontIndirect(&lf); CFont* of = pDC->SelectObject(&big); CString banner = rr.error ? L" ERROR " : (rr.hasInspect ? (rr.pass ? L" PASS " : L" FAIL ") : L" (検査なし) "); COLORREF bc = rr.error ? RGB(220, 60, 60) : (rr.hasInspect ? (rr.pass ? RGB(40, 170, 80) : RGB(220, 60, 60)) : RGB(90, 100, 110)); CSize sz = pDC->GetTextExtent(banner); CRect br(6, 6, 6 + sz.cx + 12, 6 + sz.cy + 8); pDC->FillSolidRect(br, bc); pDC->SetTextColor(RGB(255, 255, 255)); pDC->DrawText(banner, br, DT_CENTER | DT_VCENTER | DT_SINGLELINE); // 検査要約(各検査ステップ) LOGFONT lf2 = {}; lf2.lfHeight = -(fh * 3 / 5 < 12 ? 12 : fh * 3 / 5); wcscpy_s(lf2.lfFaceName, L"Yu Gothic UI"); CFont fSmall; fSmall.CreateFontIndirect(&lf2); pDC->SelectObject(&fSmall); int y = br.bottom + 6; for (const Step& st : recipe.steps) { if (!IsInspect(st.kind)) continue; CString line = StepName(st.kind) + L": " + st.result.text + (st.result.ok ? L" [OK]" : L" [NG]"); COLORREF tc = st.result.ok ? RGB(30, 160, 70) : RGB(210, 50, 50); // 縁取り(白背景の代わりに黒縁) pDC->SetTextColor(RGB(0, 0, 0)); for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) pDC->TextOut(8 + dx, y + dy, line); pDC->SetTextColor(tc); pDC->TextOut(8, y, line); y += (fh * 3 / 5) + 6; if (y > H - 20) break; } pDC->SelectObject(of); } out.ReleaseDC(); return out.Save(CString(path)); } CRect InspectEngine::DrawSource(CDC* pDC, const CRect& rc) { if (!m_src || m_src->IsNull()) return CRect(0, 0, 0, 0); CRect fit = AspectFit(m_src->GetWidth(), m_src->GetHeight(), rc); m_src->Draw(pDC, fit, false); // アスペクト維持済みの矩形へ正確に描画 return fit; } CRect InspectEngine::DrawShot(CDC* pDC, const CRect& rc, int i) { if (!HasShot(i)) return CRect(0, 0, 0, 0); CRect fit = AspectFit(m_shots[i]->GetWidth(), m_shots[i]->GetHeight(), rc); m_shots[i]->Draw(pDC, fit, false); return fit; } } // namespace core