Files
ImgTestSystem/SelfTest.cpp
T
yoshikazuandClaude Opus 4.8 e4eca6b303 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>
2026-07-23 09:26:10 +09:00

209 lines
6.7 KiB
C++

//
// 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;
}