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:
+208
@@ -0,0 +1,208 @@
|
||||
//
|
||||
// SelfTest.cpp - /selftest。合成画像を作り→検査レシピを実行→結果をファイル出力。
|
||||
// ビルドが通り、yfoximg 呼び出し(RGB2BW/Threshold/Avg)が実際に動くことを確認する。
|
||||
//
|
||||
#include "stdafx.h"
|
||||
#include "SelfTest.h"
|
||||
#include "core/InspectEngine.h"
|
||||
#include "core/StepCatalog.h"
|
||||
#include "core/RecipeStore.h"
|
||||
#include "core/Templates.h"
|
||||
#include "ImgProcessing.h" // CDiBitmap 合成用
|
||||
|
||||
using namespace core;
|
||||
|
||||
namespace {
|
||||
|
||||
// UTF-8 でテキストファイルを書き出す。
|
||||
void WriteUtf8(LPCWSTR path, const CString& text)
|
||||
{
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, text, text.GetLength(), NULL, 0, NULL, NULL);
|
||||
CStringA utf8;
|
||||
if (n > 0)
|
||||
{
|
||||
WideCharToMultiByte(CP_UTF8, 0, text, text.GetLength(), utf8.GetBuffer(n), n, NULL, NULL);
|
||||
utf8.ReleaseBuffer(n);
|
||||
}
|
||||
CFile f;
|
||||
if (f.Open(path, CFile::modeCreate | CFile::modeWrite))
|
||||
{
|
||||
if (!utf8.IsEmpty()) f.Write((LPCSTR)utf8, utf8.GetLength());
|
||||
f.Close();
|
||||
}
|
||||
}
|
||||
|
||||
// 一様輝度 val の 24bit 合成画像を作り BMP 保存。
|
||||
bool MakeInputBmp(LPCWSTR path, int val)
|
||||
{
|
||||
CDiBitmap img;
|
||||
if (!img.Create(64, 64, 24)) return false;
|
||||
int W = img.GetWidth(), H = img.GetHeight(), pitch = img.GetPitch();
|
||||
LPBYTE bits = img.GetBits();
|
||||
for (int y = 0; y < H; y++)
|
||||
{
|
||||
BYTE* row = bits + (intptr_t)y * pitch;
|
||||
for (int x = 0; x < W * 3; x++) row[x] = (BYTE)val;
|
||||
}
|
||||
return img.Save(CString(path));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// デモ画像: 320x240 の明るい地(180)に、暗い欠陥(40)を3点描いた 24bit BMP。
|
||||
bool selftest::MakeDemoImage(LPCWSTR path)
|
||||
{
|
||||
CDiBitmap img;
|
||||
if (!img.Create(320, 240, 24)) return false;
|
||||
int W = img.GetWidth(), H = img.GetHeight(), pitch = img.GetPitch();
|
||||
LPBYTE bits = img.GetBits();
|
||||
for (int y = 0; y < H; y++)
|
||||
{
|
||||
BYTE* row = bits + (intptr_t)y * pitch;
|
||||
for (int x = 0; x < W; x++) { row[x * 3] = 180; row[x * 3 + 1] = 180; row[x * 3 + 2] = 180; }
|
||||
}
|
||||
// 暗い欠陥(塗りつぶし円)を3点。
|
||||
struct Spot { int cx, cy, r; } spots[] = { {80,70,14}, {210,120,10}, {150,180,16} };
|
||||
for (const Spot& s : spots)
|
||||
for (int y = s.cy - s.r; y <= s.cy + s.r; y++)
|
||||
{
|
||||
if (y < 0 || y >= H) continue;
|
||||
BYTE* row = bits + (intptr_t)y * pitch;
|
||||
for (int x = s.cx - s.r; x <= s.cx + s.r; x++)
|
||||
{
|
||||
if (x < 0 || x >= W) continue;
|
||||
int dx = x - s.cx, dy = y - s.cy;
|
||||
if (dx * dx + dy * dy <= s.r * s.r) { row[x * 3] = 40; row[x * 3 + 1] = 40; row[x * 3 + 2] = 40; }
|
||||
}
|
||||
}
|
||||
return img.Save(CString(path));
|
||||
}
|
||||
|
||||
bool selftest::SaveDemoResult(LPCWSTR path)
|
||||
{
|
||||
const wchar_t* in = L"demosave_input.bmp";
|
||||
if (!MakeDemoImage(in)) return false;
|
||||
InspectEngine engine;
|
||||
if (!engine.LoadImage(in)) return false;
|
||||
auto tps = AllTemplates();
|
||||
if (tps.empty()) return false;
|
||||
Recipe r = tps[0].recipe; // キズ・汚れ
|
||||
r.rois.push_back(CRect(40, 40, 130, 115)); // 複数の検査範囲
|
||||
r.rois.push_back(CRect(105, 140, 200, 225));
|
||||
RunResult rr = engine.Run(r);
|
||||
return engine.SaveResultImage(path, r, rr);
|
||||
}
|
||||
|
||||
int selftest::Run()
|
||||
{
|
||||
CString log;
|
||||
log += L"=== ImgTestSystem /selftest ===\r\n";
|
||||
|
||||
const wchar_t* inPath = L"selftest_input.bmp";
|
||||
if (!MakeInputBmp(inPath, 128))
|
||||
{
|
||||
log += L"FAIL: 合成画像の作成に失敗\r\n";
|
||||
WriteUtf8(L"selftest_result.txt", log);
|
||||
return 1;
|
||||
}
|
||||
log += L"合成画像 selftest_input.bmp (64x64, 一様輝度128) を作成\r\n";
|
||||
|
||||
InspectEngine engine;
|
||||
if (!engine.LoadImage(inPath))
|
||||
{
|
||||
log += L"FAIL: 画像読込に失敗\r\n";
|
||||
WriteUtf8(L"selftest_result.txt", log);
|
||||
return 1;
|
||||
}
|
||||
log.AppendFormat(L"読込 OK: %dx%d %dbpp\r\n", engine.SrcWidth(), engine.SrcHeight(), engine.SrcBpp());
|
||||
|
||||
// レシピ: グレー化 → 検査(平均輝度 50〜200) → 検査(白画素率 0〜100%, thr100)
|
||||
Recipe recipe;
|
||||
recipe.steps.push_back(MakeStep(StepKind::Grayscale));
|
||||
{
|
||||
Step s = MakeStep(StepKind::InspectBrightness);
|
||||
s.SetParam(L"min", 50); s.SetParam(L"max", 200);
|
||||
recipe.steps.push_back(s);
|
||||
}
|
||||
{
|
||||
Step s = MakeStep(StepKind::InspectWhiteRatio);
|
||||
s.SetParam(L"thresh", 100); s.SetParam(L"min", 0); s.SetParam(L"max", 100);
|
||||
recipe.steps.push_back(s);
|
||||
}
|
||||
|
||||
RunResult rr = engine.Run(recipe);
|
||||
|
||||
for (int i = 0; i < (int)recipe.steps.size(); i++)
|
||||
{
|
||||
const Step& st = recipe.steps[i];
|
||||
log.AppendFormat(L" #%d %s: %s\r\n", i + 1, (LPCWSTR)StepName(st.kind), (LPCWSTR)st.result.text);
|
||||
}
|
||||
log += rr.message + L"\r\n";
|
||||
|
||||
// レシピ保存/読込の往復一致(ステップ数 + パラメータ値)。
|
||||
bool roundtrip = false;
|
||||
{
|
||||
recipe.rois.clear();
|
||||
recipe.rois.push_back(CRect(5, 6, 40, 50));
|
||||
recipe.rois.push_back(CRect(12, 14, 30, 44));
|
||||
if (SaveRecipe(L"selftest_recipe.itr", recipe))
|
||||
{
|
||||
Recipe r2;
|
||||
if (LoadRecipe(L"selftest_recipe.itr", r2)
|
||||
&& r2.steps.size() == recipe.steps.size()
|
||||
&& r2.rois.size() == recipe.rois.size()
|
||||
&& (r2.rois.empty() || r2.rois[0] == recipe.rois[0])
|
||||
&& (r2.rois.size() < 2 || r2.rois[1] == recipe.rois[1]))
|
||||
{
|
||||
roundtrip = true;
|
||||
for (size_t i = 0; i < recipe.steps.size(); i++)
|
||||
{
|
||||
if (r2.steps[i].kind != recipe.steps[i].kind) { roundtrip = false; break; }
|
||||
for (const auto& p : recipe.steps[i].params)
|
||||
if (r2.steps[i].Param(p.key, -999) != p.value) { roundtrip = false; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.AppendFormat(L"レシピ保存/読込 往復一致: %s\r\n", roundtrip ? L"OK" : L"NG");
|
||||
|
||||
// 判定結果画像の保存(注釈付き)。
|
||||
bool imgSaved = engine.SaveResultImage(L"selftest_result.png", recipe, rr);
|
||||
if (imgSaved)
|
||||
{
|
||||
CFileStatus fs;
|
||||
imgSaved = CFile::GetStatus(L"selftest_result.png", fs) && fs.m_size > 0;
|
||||
}
|
||||
log.AppendFormat(L"判定結果画像の保存: %s\r\n", imgSaved ? L"OK" : L"NG");
|
||||
|
||||
// --- 複数ROI 塊カウントの検証 ---
|
||||
bool g_blobOk = false;
|
||||
{
|
||||
MakeDemoImage(L"diag_input.bmp");
|
||||
auto blobCount = [](std::vector<CRect> rois) -> int {
|
||||
InspectEngine e; e.LoadImage(L"diag_input.bmp");
|
||||
Recipe r;
|
||||
r.steps.push_back(MakeStep(StepKind::Grayscale));
|
||||
r.steps.push_back(MakeStep(StepKind::Otsu));
|
||||
{ Step s = MakeStep(StepKind::InspectBlobCount);
|
||||
s.SetParam(L"thresh", 128); s.SetParam(L"invert", 1);
|
||||
s.SetParam(L"min", 0); s.SetParam(L"max", 100); r.steps.push_back(s); }
|
||||
r.rois = rois;
|
||||
e.Run(r);
|
||||
return (int)r.steps.back().result.measured;
|
||||
};
|
||||
int cAll = blobCount({});
|
||||
int cBig = blobCount({ CRect(10,10,185,230) });
|
||||
int cTwo = blobCount({ CRect(40,40,130,115), CRect(105,140,200,225) });
|
||||
g_blobOk = (cAll == 3 && cBig == 2 && cTwo == 2);
|
||||
log.AppendFormat(L"複数ROI 塊カウント: 全体=%d / 大ROI1個=%d / 小ROI2個=%d → %s\r\n",
|
||||
cAll, cBig, cTwo, g_blobOk ? L"OK" : L"NG");
|
||||
}
|
||||
|
||||
// 期待: 輝度128は[50,200]内でPASS。総合PASSかつエラー無しを合格条件とする。
|
||||
bool ok = rr.pass && !rr.error && roundtrip && imgSaved && g_blobOk;
|
||||
log += ok ? L"RESULT: PASS\r\n" : L"RESULT: FAIL\r\n";
|
||||
|
||||
WriteUtf8(L"selftest_result.txt", log);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user