Initial commit: ノーコード画像検査アプリ (MVP + 追加機能一式)
検査レシピ=処理/検査ステップの縦リスト(配線なし)。yfoxlib(yfoximg/yfoxbase)使用。 - ビジュアル・パイプライン(カード+適用後サムネイル+緑/赤 合否) - テンプレート/対話式ウィザードでレシピ生成(CTaskDialog) - パラメータのスライダー・ライブ調整 - レシピ保存/読込(*.itr) + 最近使ったファイル(MRU) - 検査範囲(ROI)を複数マウス指定(ドラッグ追加/右クリック削除) - カードのドラッグ並べ替え - 判定結果画像の保存 + 検査ログCSV(項目拡張) - 処理6種+検査3種(明るさ/白画素率/塊の数) - /selftest(実行+レシピ往復+結果画像+複数ROI塊カウント検証)、/demo Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
//
|
||||
// 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<CDiBitmap>();
|
||||
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<CDiBitmap> Down32To24(CDiBitmap* src)
|
||||
{
|
||||
int W = src->GetWidth(), H = src->GetHeight();
|
||||
auto dst = std::make_unique<CDiBitmap>();
|
||||
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<CDiBitmap>& 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<CDiBitmap>();
|
||||
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<CDiBitmap> MakeBinary(std::unique_ptr<CDiBitmap>& work, CImgProcessing& /*ip*/,
|
||||
int thresh, bool invert, int& whiteCount, const std::vector<CRect>& rois)
|
||||
{
|
||||
int W = work->GetWidth(), H = work->GetHeight();
|
||||
auto bin = std::make_unique<CDiBitmap>();
|
||||
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<CRect>& 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<CDiBitmap>& work, CImgProcessing& ip, const std::vector<CRect>& 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<CDiBitmap>();
|
||||
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<CDiBitmap>();
|
||||
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<CDiBitmap>();
|
||||
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<CDiBitmap>();
|
||||
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<CDiBitmap>();
|
||||
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<CDiBitmap>();
|
||||
m_work->Copy(m_src.get());
|
||||
|
||||
// 有効 ROI 群(画像全体でクランプ)。処理ステップは寸法を変えないので全ステップ共通。
|
||||
const int IW = m_work->GetWidth(), IH = m_work->GetHeight();
|
||||
std::vector<CRect> 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<CDiBitmap>();
|
||||
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
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
//
|
||||
// InspectEngine.h - レシピを yfoximg で実行する検査エンジン。
|
||||
// ここが唯一 yfoximg(CImgProcessing / CDiBitmap)に依存する層。
|
||||
// ImgProcessing.h は .cpp 側だけで include し、UI への漏れを防ぐ(前方宣言でラップ)。
|
||||
//
|
||||
#include "Recipe.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class CDiBitmap; // 前方宣言(ヘッダに yfoximg を持ち込まない)
|
||||
|
||||
namespace core {
|
||||
|
||||
// 検査全体の結果。
|
||||
struct RunResult
|
||||
{
|
||||
bool hasInspect = false; // 検査ステップが1つ以上あった
|
||||
bool pass = false; // 総合 PASS/FAIL
|
||||
bool error = false; // 実行時エラーあり
|
||||
CString message; // サマリ文
|
||||
};
|
||||
|
||||
// レシピ実行器。作業画像(CDiBitmap)と、各ステップ適用後のスナップショットを保持。
|
||||
class InspectEngine
|
||||
{
|
||||
public:
|
||||
InspectEngine();
|
||||
~InspectEngine();
|
||||
|
||||
bool LoadImage(LPCWSTR path);
|
||||
bool HasImage() const;
|
||||
|
||||
// レシピを全ステップ実行。各 Step::result を更新し、ステップごとの
|
||||
// スナップショット画像(サムネイル描画用)を内部に保持する。
|
||||
RunResult Run(Recipe& recipe);
|
||||
|
||||
// 判定結果を注釈画像(最終画像+ROI枠+PASS/FAIL+検査要約)として保存。
|
||||
bool SaveResultImage(LPCWSTR path, const Recipe& recipe, const RunResult& rr);
|
||||
|
||||
// 原画像を DC にアスペクト維持で描画。実際に描いた矩形(オーバーレイ用)を返す。
|
||||
CRect DrawSource(CDC* pDC, const CRect& rc);
|
||||
// ステップ i 適用後のスナップショットを描画。実際に描いた矩形を返す(無ければ空)。
|
||||
CRect DrawShot(CDC* pDC, const CRect& rc, int stepIndex);
|
||||
bool HasShot(int stepIndex) const;
|
||||
|
||||
int SrcWidth() const;
|
||||
int SrcHeight() const;
|
||||
int SrcBpp() const;
|
||||
int WorkWidth() const;
|
||||
int WorkHeight() const;
|
||||
int WorkBpp() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<CDiBitmap> m_src; // 読み込んだ原画像
|
||||
std::unique_ptr<CDiBitmap> m_work; // 作業画像(処理を積み上げる)
|
||||
std::vector<std::shared_ptr<CDiBitmap>> m_shots; // steps[i] 適用後の画像
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
//
|
||||
// Recipe.h - ノーコード「検査レシピ」のデータモデル(UI/画像ライブラリ非依存)。
|
||||
//
|
||||
// パラダイム: ブロックをワイヤで繋ぐノードグラフではなく、
|
||||
// 「上から下へ順に実行するステップの縦リスト」= 料理のレシピ / 検査チェックリスト。
|
||||
// ステップは2種: 処理(画像を加工) と 検査(値を測って範囲判定=PASS/FAIL に寄与)。
|
||||
//
|
||||
#include <afx.h>
|
||||
#include <atltypes.h> // CRect
|
||||
#include <vector>
|
||||
|
||||
namespace core {
|
||||
|
||||
// ステップ種別。process=画像加工 / inspect=測定+判定。
|
||||
enum class StepKind
|
||||
{
|
||||
// --- 処理(画像を加工する) ---
|
||||
Grayscale, // カラー→グレー(24bit→8bit)
|
||||
Median, // 3x3 メディアン(ノイズ除去)
|
||||
Binarize, // 二値化(帯 [L,H])
|
||||
Otsu, // 大津の二値化(自動しきい値)
|
||||
Gamma, // ガンマ補正
|
||||
Contrast, // コントラスト伸長
|
||||
|
||||
// --- 検査(測定して範囲で合否) ---
|
||||
InspectBrightness, // 平均輝度が [min,max] に入れば OK
|
||||
InspectWhiteRatio, // 二値化後の白画素率(%) が [min,max] に入れば OK
|
||||
InspectBlobCount, // 二値化+ラベリングで「塊(キズ/穴)の数」が [min,max] に入れば OK
|
||||
};
|
||||
|
||||
// 1つのパラメータ(数値)。UI はこの記述を見て入力欄を作る。
|
||||
struct StepParam
|
||||
{
|
||||
CString key; // 内部キー
|
||||
CString label; // 画面表示名
|
||||
double value; // 現在値
|
||||
double lo; // 入力下限(UIヒント)
|
||||
double hi; // 入力上限(UIヒント)
|
||||
};
|
||||
|
||||
// 実行後の結果(検査ステップのみ意味を持つ)。
|
||||
struct StepResult
|
||||
{
|
||||
bool ran = false; // 実行された
|
||||
bool ok = true; // 検査OK(処理ステップは常に true)
|
||||
double measured = 0.0; // 測定値(検査ステップ)
|
||||
CString text; // 表示用の説明(測定値/エラー等)
|
||||
};
|
||||
|
||||
// レシピの1ステップ。
|
||||
struct Step
|
||||
{
|
||||
StepKind kind;
|
||||
std::vector<StepParam> params;
|
||||
StepResult result;
|
||||
|
||||
double Param(LPCWSTR key, double def = 0.0) const
|
||||
{
|
||||
for (const auto& p : params)
|
||||
if (p.key == key) return p.value;
|
||||
return def;
|
||||
}
|
||||
void SetParam(LPCWSTR key, double v)
|
||||
{
|
||||
for (auto& p : params)
|
||||
if (p.key == key) { p.value = v; return; }
|
||||
}
|
||||
};
|
||||
|
||||
// 検査レシピ = ステップの並び + 検査範囲(ROI)。
|
||||
struct Recipe
|
||||
{
|
||||
std::vector<Step> steps;
|
||||
|
||||
// 検査範囲(画像ピクセル座標)。空のときは画像全体を検査。複数指定すると各範囲を合算。
|
||||
// 本アプリの処理ステップは寸法を変えないため、ROI は全ステップで同座標系で有効。
|
||||
std::vector<CRect> rois;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// RecipeStore.cpp - レシピの保存/読込。行ベースの UTF-8 テキスト。
|
||||
//
|
||||
// ImgTestSystem Recipe v1
|
||||
// ROI <enabled> <l> <t> <r> <b>
|
||||
// STEP <kindId>
|
||||
// PARAM <key> <value>
|
||||
// ...
|
||||
//
|
||||
#include "stdafx.h"
|
||||
#include "RecipeStore.h"
|
||||
#include "StepCatalog.h"
|
||||
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
|
||||
void WriteUtf8(CFile& f, const CString& s)
|
||||
{
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, s, s.GetLength(), NULL, 0, NULL, NULL);
|
||||
if (n <= 0) return;
|
||||
CStringA a;
|
||||
WideCharToMultiByte(CP_UTF8, 0, s, s.GetLength(), a.GetBuffer(n), n, NULL, NULL);
|
||||
a.ReleaseBuffer(n);
|
||||
f.Write((LPCSTR)a, a.GetLength());
|
||||
}
|
||||
|
||||
CString ReadAllUtf8(LPCWSTR path)
|
||||
{
|
||||
CFile f;
|
||||
if (!f.Open(path, CFile::modeRead | CFile::shareDenyWrite)) return L"";
|
||||
ULONGLONG len = f.GetLength();
|
||||
if (len == 0 || len > (50 * 1024 * 1024)) return L"";
|
||||
CStringA a;
|
||||
UINT got = f.Read(a.GetBuffer((int)len), (UINT)len);
|
||||
a.ReleaseBuffer(got);
|
||||
// BOM 除去
|
||||
if (a.GetLength() >= 3 && (BYTE)a[0] == 0xEF && (BYTE)a[1] == 0xBB && (BYTE)a[2] == 0xBF)
|
||||
a = a.Mid(3);
|
||||
int wn = MultiByteToWideChar(CP_UTF8, 0, a, a.GetLength(), NULL, 0);
|
||||
CString w;
|
||||
MultiByteToWideChar(CP_UTF8, 0, a, a.GetLength(), w.GetBuffer(wn), wn);
|
||||
w.ReleaseBuffer(wn);
|
||||
return w;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool SaveRecipe(LPCWSTR path, const Recipe& r)
|
||||
{
|
||||
CFile f;
|
||||
if (!f.Open(path, CFile::modeCreate | CFile::modeWrite)) return false;
|
||||
// UTF-8 BOM
|
||||
const unsigned char bom[3] = { 0xEF, 0xBB, 0xBF };
|
||||
f.Write(bom, 3);
|
||||
|
||||
WriteUtf8(f, L"ImgTestSystem Recipe v1\r\n");
|
||||
for (const CRect& roi : r.rois)
|
||||
{
|
||||
CString line;
|
||||
line.Format(L"ROI %d %d %d %d\r\n", (int)roi.left, (int)roi.top, (int)roi.right, (int)roi.bottom);
|
||||
WriteUtf8(f, line);
|
||||
}
|
||||
|
||||
for (const Step& s : r.steps)
|
||||
{
|
||||
WriteUtf8(f, L"STEP " + StepKindId(s.kind) + L"\r\n");
|
||||
for (const StepParam& p : s.params)
|
||||
{
|
||||
CString pl;
|
||||
pl.Format(L"PARAM %s %.6g\r\n", (LPCWSTR)p.key, p.value);
|
||||
WriteUtf8(f, pl);
|
||||
}
|
||||
}
|
||||
f.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadRecipe(LPCWSTR path, Recipe& out)
|
||||
{
|
||||
CString text = ReadAllUtf8(path);
|
||||
if (text.IsEmpty()) return false;
|
||||
|
||||
Recipe r;
|
||||
bool sawHeader = false;
|
||||
Step* cur = nullptr;
|
||||
|
||||
int pos = 0;
|
||||
while (pos < text.GetLength())
|
||||
{
|
||||
int nl = text.Find(L'\n', pos);
|
||||
CString line = (nl < 0) ? text.Mid(pos) : text.Mid(pos, nl - pos);
|
||||
pos = (nl < 0) ? text.GetLength() : nl + 1;
|
||||
line.TrimRight(L"\r");
|
||||
line.TrimLeft();
|
||||
if (line.IsEmpty()) continue;
|
||||
|
||||
if (line.Find(L"ImgTestSystem Recipe") == 0) { sawHeader = true; continue; }
|
||||
|
||||
if (line.Left(4) == L"ROI ")
|
||||
{
|
||||
// 新形式: ROI l t r b(4値) / 旧形式: ROI enabled l t r b(5値)
|
||||
int a = 0, bb = 0, c = 0, d = 0, e = 0;
|
||||
int got = swscanf_s((LPCWSTR)line, L"ROI %d %d %d %d %d", &a, &bb, &c, &d, &e);
|
||||
if (got == 5) { if (a != 0) r.rois.push_back(CRect(bb, c, d, e)); }
|
||||
else if (got == 4) { r.rois.push_back(CRect(a, bb, c, d)); }
|
||||
continue;
|
||||
}
|
||||
if (line.Left(5) == L"STEP ")
|
||||
{
|
||||
CString id = line.Mid(5); id.Trim();
|
||||
StepKind k;
|
||||
if (StepKindFromId(id, k)) { r.steps.push_back(MakeStep(k)); cur = &r.steps.back(); }
|
||||
else cur = nullptr;
|
||||
continue;
|
||||
}
|
||||
if (line.Left(6) == L"PARAM " && cur)
|
||||
{
|
||||
CString rest = line.Mid(6); rest.Trim();
|
||||
int sp = rest.Find(L' ');
|
||||
if (sp <= 0) continue;
|
||||
CString key = rest.Left(sp);
|
||||
double val = _wtof(rest.Mid(sp + 1));
|
||||
cur->SetParam(key, val); // 既定パラメータのうち一致キーのみ上書き
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (r.steps.empty() && !sawHeader) return false;
|
||||
out = r;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
//
|
||||
// RecipeStore.h - レシピの保存/読込(人が読める UTF-8 テキスト = *.itr)。
|
||||
//
|
||||
#include "Recipe.h"
|
||||
|
||||
namespace core {
|
||||
bool SaveRecipe(LPCWSTR path, const Recipe& r);
|
||||
bool LoadRecipe(LPCWSTR path, Recipe& r);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
//
|
||||
// StepCatalog.h - ステップ種別のメタデータ(表示名/アイコン/平易な説明/種類/既定パラメータ)。
|
||||
// 新しいステップ種別を足すときは Recipe.h の StepKind と、この各関数、
|
||||
// および InspectEngine::RunStep に1ケース足すだけ。
|
||||
//
|
||||
#include "Recipe.h"
|
||||
|
||||
namespace core {
|
||||
|
||||
// 種別 → 画面表示名(平易な日本語)。
|
||||
inline CString StepName(StepKind k)
|
||||
{
|
||||
switch (k)
|
||||
{
|
||||
case StepKind::Grayscale: return L"白黒にする";
|
||||
case StepKind::Median: return L"ノイズを消す";
|
||||
case StepKind::Binarize: return L"明るさで白黒に分ける";
|
||||
case StepKind::Otsu: return L"自動で白黒に分ける";
|
||||
case StepKind::Gamma: return L"明るさをそろえる";
|
||||
case StepKind::Contrast: return L"コントラストを強める";
|
||||
case StepKind::InspectBrightness: return L"検査:全体の明るさ";
|
||||
case StepKind::InspectWhiteRatio: return L"検査:白い部分の割合";
|
||||
case StepKind::InspectBlobCount: return L"検査:塊(キズ/穴)の数";
|
||||
}
|
||||
return L"?";
|
||||
}
|
||||
|
||||
// 種別 → アイコン(絵文字)。
|
||||
inline CString StepIcon(StepKind k)
|
||||
{
|
||||
switch (k)
|
||||
{
|
||||
case StepKind::Grayscale: return L"⬜";
|
||||
case StepKind::Median: return L"🧹";
|
||||
case StepKind::Binarize: return L"◐";
|
||||
case StepKind::Otsu: return L"🎚";
|
||||
case StepKind::Gamma: return L"💡";
|
||||
case StepKind::Contrast: return L"🌗";
|
||||
case StepKind::InspectBrightness: return L"🔎";
|
||||
case StepKind::InspectWhiteRatio: return L"🔎";
|
||||
case StepKind::InspectBlobCount: return L"🔎";
|
||||
}
|
||||
return L"•";
|
||||
}
|
||||
|
||||
// 種別 → 1行の平易な説明(このステップは何をするか)。
|
||||
inline CString StepHint(StepKind k)
|
||||
{
|
||||
switch (k)
|
||||
{
|
||||
case StepKind::Grayscale: return L"カラー画像を白黒(グレー)にします。検査の前準備。";
|
||||
case StepKind::Median: return L"細かいザラつき(ノイズ)をならして消します。";
|
||||
case StepKind::Binarize: return L"指定した明るさの範囲だけを白にし、他を黒にします。";
|
||||
case StepKind::Otsu: return L"しきい値を自動で決めて白黒に分けます。";
|
||||
case StepKind::Gamma: return L"暗い所/明るい所の見え方を調整します。";
|
||||
case StepKind::Contrast: return L"明暗の差をはっきりさせます。";
|
||||
case StepKind::InspectBrightness: return L"画像全体の平均の明るさが範囲内なら合格。";
|
||||
case StepKind::InspectWhiteRatio: return L"白い部分の割合(%)が範囲内なら合格。";
|
||||
case StepKind::InspectBlobCount: return L"白い塊(キズや穴)の数が範囲内なら合格。";
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
// 種別 → 保存用の安定した文字列ID(enum値は将来変わり得るため文字列で永続化)。
|
||||
inline CString StepKindId(StepKind k)
|
||||
{
|
||||
switch (k)
|
||||
{
|
||||
case StepKind::Grayscale: return L"grayscale";
|
||||
case StepKind::Median: return L"median";
|
||||
case StepKind::Binarize: return L"binarize";
|
||||
case StepKind::Otsu: return L"otsu";
|
||||
case StepKind::Gamma: return L"gamma";
|
||||
case StepKind::Contrast: return L"contrast";
|
||||
case StepKind::InspectBrightness: return L"inspect_brightness";
|
||||
case StepKind::InspectWhiteRatio: return L"inspect_whiteratio";
|
||||
case StepKind::InspectBlobCount: return L"inspect_blobcount";
|
||||
}
|
||||
return L"?";
|
||||
}
|
||||
|
||||
// 文字列ID → 種別。見つかれば true。
|
||||
inline bool StepKindFromId(const CString& id, StepKind& out)
|
||||
{
|
||||
static const StepKind all[] = {
|
||||
StepKind::Grayscale, StepKind::Median, StepKind::Binarize, StepKind::Otsu,
|
||||
StepKind::Gamma, StepKind::Contrast, StepKind::InspectBrightness,
|
||||
StepKind::InspectWhiteRatio, StepKind::InspectBlobCount,
|
||||
};
|
||||
for (StepKind k : all)
|
||||
if (StepKindId(k) == id) { out = k; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// 検査ステップか(合否に寄与するか)。
|
||||
inline bool IsInspect(StepKind k)
|
||||
{
|
||||
return k == StepKind::InspectBrightness
|
||||
|| k == StepKind::InspectWhiteRatio
|
||||
|| k == StepKind::InspectBlobCount;
|
||||
}
|
||||
|
||||
// 種別ごとの既定パラメータを持つ Step を生成。
|
||||
inline Step MakeStep(StepKind k)
|
||||
{
|
||||
Step s;
|
||||
s.kind = k;
|
||||
switch (k)
|
||||
{
|
||||
case StepKind::Grayscale:
|
||||
case StepKind::Median:
|
||||
break;
|
||||
case StepKind::Otsu:
|
||||
s.params.push_back({ L"invert", L"白黒を反転", 0, 0, 1 });
|
||||
break;
|
||||
case StepKind::Binarize:
|
||||
s.params.push_back({ L"lo", L"この明るさ以上", 128, 0, 255 });
|
||||
s.params.push_back({ L"hi", L"この明るさ以下", 255, 0, 255 });
|
||||
s.params.push_back({ L"invert", L"白黒を反転", 0, 0, 1 });
|
||||
break;
|
||||
case StepKind::Gamma:
|
||||
s.params.push_back({ L"gamma", L"明るさ調整(1=そのまま)", 1.0, 0.2, 3.0 });
|
||||
break;
|
||||
case StepKind::Contrast:
|
||||
s.params.push_back({ L"inMin", L"暗side基準", 0, 0, 255 });
|
||||
s.params.push_back({ L"inMax", L"明side基準", 255, 0, 255 });
|
||||
s.params.push_back({ L"outMin", L"出力min", 0, 0, 255 });
|
||||
s.params.push_back({ L"outMax", L"出力max", 255, 0, 255 });
|
||||
break;
|
||||
case StepKind::InspectBrightness:
|
||||
s.params.push_back({ L"min", L"合格の最小(暗い側)", 50, 0, 255 });
|
||||
s.params.push_back({ L"max", L"合格の最大(明るい側)", 200, 0, 255 });
|
||||
break;
|
||||
case StepKind::InspectWhiteRatio:
|
||||
s.params.push_back({ L"thresh", L"白とみなす明るさ", 128, 0, 255 });
|
||||
s.params.push_back({ L"min", L"合格の最小(%)", 0, 0, 100 });
|
||||
s.params.push_back({ L"max", L"合格の最大(%)", 100, 0, 100 });
|
||||
break;
|
||||
case StepKind::InspectBlobCount:
|
||||
s.params.push_back({ L"thresh", L"白とみなす明るさ", 128, 0, 255 });
|
||||
s.params.push_back({ L"invert", L"暗い塊を数える", 0, 0, 1 });
|
||||
s.params.push_back({ L"min", L"合格の最小(個)", 0, 0, 100 });
|
||||
s.params.push_back({ L"max", L"合格の最大(個)", 0, 0, 100 });
|
||||
break;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// パレット(追加できる種別の一覧・並び順)。
|
||||
inline const StepKind* AllStepKinds(int& n)
|
||||
{
|
||||
static const StepKind kinds[] = {
|
||||
StepKind::Grayscale,
|
||||
StepKind::Median,
|
||||
StepKind::Binarize,
|
||||
StepKind::Otsu,
|
||||
StepKind::Gamma,
|
||||
StepKind::Contrast,
|
||||
StepKind::InspectBrightness,
|
||||
StepKind::InspectWhiteRatio,
|
||||
StepKind::InspectBlobCount,
|
||||
};
|
||||
n = (int)(sizeof(kinds) / sizeof(kinds[0]));
|
||||
return kinds;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
//
|
||||
// Templates.h - 「目的から選ぶ」ための完成済みレシピ。
|
||||
// ユーザーは目的を選ぶだけ。あとは右パネルのスライダーで微調整できる。
|
||||
//
|
||||
#include "Recipe.h"
|
||||
#include "StepCatalog.h"
|
||||
#include <vector>
|
||||
|
||||
namespace core {
|
||||
|
||||
struct RecipeTemplate
|
||||
{
|
||||
CString name; // 目的名
|
||||
CString desc; // 説明
|
||||
Recipe recipe; // 中身
|
||||
};
|
||||
|
||||
inline Step Mk(StepKind k) { return MakeStep(k); }
|
||||
|
||||
inline std::vector<RecipeTemplate> AllTemplates()
|
||||
{
|
||||
std::vector<RecipeTemplate> v;
|
||||
|
||||
// 1) キズ・汚れを見つける
|
||||
{
|
||||
RecipeTemplate t;
|
||||
t.name = L"キズ・汚れを見つける";
|
||||
t.desc = L"白黒化→ノイズ除去→自動二値化し、キズ(白い塊)の数が0個なら合格。";
|
||||
t.recipe.steps.push_back(Mk(StepKind::Grayscale));
|
||||
t.recipe.steps.push_back(Mk(StepKind::Median));
|
||||
t.recipe.steps.push_back(Mk(StepKind::Otsu));
|
||||
{ Step s = Mk(StepKind::InspectBlobCount);
|
||||
s.SetParam(L"thresh", 128); s.SetParam(L"invert", 1); // 暗い欠陥を数える
|
||||
s.SetParam(L"min", 0); s.SetParam(L"max", 0);
|
||||
t.recipe.steps.push_back(s); }
|
||||
v.push_back(t);
|
||||
}
|
||||
|
||||
// 2) 欠け・穴を数える
|
||||
{
|
||||
RecipeTemplate t;
|
||||
t.name = L"欠け・穴を数える";
|
||||
t.desc = L"白黒化→自動二値化し、穴(塊)の数が想定範囲(既定1〜4個)なら合格。";
|
||||
t.recipe.steps.push_back(Mk(StepKind::Grayscale));
|
||||
t.recipe.steps.push_back(Mk(StepKind::Otsu));
|
||||
{ Step s = Mk(StepKind::InspectBlobCount);
|
||||
s.SetParam(L"thresh", 128); s.SetParam(L"invert", 1); // 暗い穴を数える
|
||||
s.SetParam(L"min", 1); s.SetParam(L"max", 4);
|
||||
t.recipe.steps.push_back(s); }
|
||||
v.push_back(t);
|
||||
}
|
||||
|
||||
// 3) 明るさ(ムラ)を確認する
|
||||
{
|
||||
RecipeTemplate t;
|
||||
t.name = L"明るさを確認する";
|
||||
t.desc = L"白黒化し、全体の平均の明るさが範囲(既定80〜180)なら合格。";
|
||||
t.recipe.steps.push_back(Mk(StepKind::Grayscale));
|
||||
{ Step s = Mk(StepKind::InspectBrightness);
|
||||
s.SetParam(L"min", 80); s.SetParam(L"max", 180);
|
||||
t.recipe.steps.push_back(s); }
|
||||
v.push_back(t);
|
||||
}
|
||||
|
||||
// 4) 文字・印字の有無を見る
|
||||
{
|
||||
RecipeTemplate t;
|
||||
t.name = L"文字・印字の有無を見る";
|
||||
t.desc = L"白黒化→二値化し、インク(白い部分)の割合が一定以上あれば合格=印字あり。";
|
||||
t.recipe.steps.push_back(Mk(StepKind::Grayscale));
|
||||
{ Step s = Mk(StepKind::Binarize);
|
||||
s.SetParam(L"lo", 128); s.SetParam(L"hi", 255); s.SetParam(L"invert", 1);
|
||||
t.recipe.steps.push_back(s); }
|
||||
{ Step s = Mk(StepKind::InspectWhiteRatio);
|
||||
s.SetParam(L"thresh", 128); s.SetParam(L"min", 2); s.SetParam(L"max", 100);
|
||||
t.recipe.steps.push_back(s); }
|
||||
v.push_back(t);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
Reference in New Issue
Block a user