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:
2026-07-23 09:26:10 +09:00
co-authored by Claude Opus 4.8
commit e4eca6b303
24 changed files with 3120 additions and 0 deletions
+435
View File
@@ -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
+60
View File
@@ -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
+81
View File
@@ -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
+134
View File
@@ -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
+10
View File
@@ -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);
}
+168
View File
@@ -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
+84
View File
@@ -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