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
+148
View File
@@ -0,0 +1,148 @@
//
// Wizard.cpp - 対話式ウィザード & 目的テンプレート選択(CTaskDialog ベース、.rc 不要)。
//
#include "stdafx.h"
#include "Wizard.h"
#include "core/Templates.h"
#include "core/StepCatalog.h"
using namespace core;
namespace {
// コマンドリンク付き質問を1つ出し、選ばれた ID を返す(キャンセル=0)。
struct Choice { int id; LPCWSTR caption; };
int AskChoices(CWnd* parent, LPCWSTR title, LPCWSTR main, LPCWSTR content,
const Choice* choices, int n)
{
if (!CTaskDialog::IsSupported())
{
AfxMessageBox(L"この環境ではウィザードを表示できません。");
return 0;
}
CTaskDialog dlg(content, main, title,
TDCBF_CANCEL_BUTTON,
TDF_USE_COMMAND_LINKS | TDF_ALLOW_DIALOG_CANCELLATION);
for (int i = 0; i < n; i++)
dlg.AddCommandControl(choices[i].id, choices[i].caption);
INT_PTR r = dlg.DoModal(parent ? parent->GetSafeHwnd() : NULL);
for (int i = 0; i < n; i++)
if (r == choices[i].id) return choices[i].id;
return 0; // キャンセル
}
// 検査ステップ(末尾想定)の min/max を書き換える。
void SetInspectRange(Recipe& rc, double lo, double hi)
{
for (auto it = rc.steps.rbegin(); it != rc.steps.rend(); ++it)
{
if (IsInspect(it->kind)) { it->SetParam(L"min", lo); it->SetParam(L"max", hi); return; }
}
}
} // namespace
bool wizard::ChooseTemplate(CWnd* parent, Recipe& out)
{
auto tps = AllTemplates();
std::vector<Choice> ch;
std::vector<CString> caps; caps.reserve(tps.size());
for (size_t i = 0; i < tps.size(); i++)
{
caps.push_back(tps[i].name + L"\n" + tps[i].desc);
}
for (size_t i = 0; i < tps.size(); i++)
ch.push_back({ (int)(2001 + i), (LPCWSTR)caps[i] });
int r = AskChoices(parent, L"テンプレートから作る",
L"検査の目的を選んでください",
L"選ぶと、その目的に合った検査レシピが自動で入ります。あとでスライダーで微調整できます。",
ch.data(), (int)ch.size());
if (r == 0) return false;
int idx = r - 2001;
if (idx < 0 || idx >= (int)tps.size()) return false;
out = tps[idx].recipe;
return true;
}
bool wizard::BuildWithWizard(CWnd* parent, Recipe& out)
{
// Q1: 何を検査?
Choice q1[] = {
{ 1, L"キズ・汚れを見つける\n表面に傷や汚れが無いか調べます" },
{ 2, L"欠け・穴の数を数える\n穴やパターンの数が想定どおりか調べます" },
{ 3, L"明るさを確認する\n全体の明るさが基準内か調べます" },
{ 4, L"文字・印字の有無を見る\n印字・マークがあるか(無いか)調べます" },
};
int a1 = AskChoices(parent, L"検査ウィザード (1/2)",
L"何を検査したいですか?", L"目的に合わせて質問します。", q1, 4);
if (a1 == 0) return false;
auto tps = AllTemplates(); // [0]キズ [1]欠け [2]明るさ [3]文字
if (a1 == 1) // キズ・汚れ
{
out = tps[0].recipe;
Choice q2[] = {
{ 10, L"厳しい:キズは1個も許さない(0個で合格)" },
{ 11, L"ふつう:小さなキズ1個までは許容(1個まで合格)" },
{ 12, L"ゆるい:3個まで許容(3個まで合格)" },
};
int a2 = AskChoices(parent, L"検査ウィザード (2/2)",
L"どのくらい厳しく判定しますか?", L"許容するキズの数を選びます。", q2, 3);
if (a2 == 0) return false;
double mx = (a2 == 10) ? 0 : (a2 == 11) ? 1 : 3;
SetInspectRange(out, 0, mx);
return true;
}
if (a1 == 2) // 欠け・穴の数
{
out = tps[1].recipe;
Choice q2[] = {
{ 20, L"ちょうど1個なら合格" },
{ 21, L"1〜2個なら合格" },
{ 22, L"1〜4個なら合格" },
{ 23, L"あとで自分で決める(1〜4個で仮設定)" },
};
int a2 = AskChoices(parent, L"検査ウィザード (2/2)",
L"いくつなら合格にしますか?", L"想定される穴/塊の数を選びます。", q2, 4);
if (a2 == 0) return false;
double lo = 1, hi = 4;
if (a2 == 20) { lo = 1; hi = 1; }
else if (a2 == 21) { lo = 1; hi = 2; }
else { lo = 1; hi = 4; }
SetInspectRange(out, lo, hi);
return true;
}
if (a1 == 3) // 明るさ
{
out = tps[2].recipe;
Choice q2[] = {
{ 30, L"暗すぎ・明るすぎを弾く(80〜180)" },
{ 31, L"明るめ重視(120〜220)" },
{ 32, L"暗め重視(40〜140)" },
};
int a2 = AskChoices(parent, L"検査ウィザード (2/2)",
L"どの明るさなら合格ですか?", L"合格とする平均の明るさの範囲を選びます。", q2, 3);
if (a2 == 0) return false;
if (a2 == 30) SetInspectRange(out, 80, 180);
else if (a2 == 31) SetInspectRange(out, 120, 220);
else SetInspectRange(out, 40, 140);
return true;
}
// a1 == 4 文字・印字
{
out = tps[3].recipe;
Choice q2[] = {
{ 40, L"印字が「ある」ことを確認したい(あれば合格)" },
{ 41, L"印字が「ない」ことを確認したい(無ければ合格)" },
};
int a2 = AskChoices(parent, L"検査ウィザード (2/2)",
L"どちらを合格にしますか?", L"印字の有無どちらを合格とするか選びます。", q2, 2);
if (a2 == 0) return false;
if (a2 == 40) SetInspectRange(out, 2, 100); // 2%以上インクあり
else SetInspectRange(out, 0, 1); // ほぼ無し
return true;
}
}