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