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:
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# ===== ビルド成果物 =====
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# ===== VS / MSBuild 生成物 =====
|
||||||
|
*.user
|
||||||
|
*.aps
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.idb
|
||||||
|
*.ipdb
|
||||||
|
*.iobj
|
||||||
|
*.tlog
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# ===== 自己テスト / デモ 出力(通常は bin 配下=bin/ で除外) =====
|
||||||
|
selftest_*.bmp
|
||||||
|
selftest_*.png
|
||||||
|
selftest_*.txt
|
||||||
|
selftest_*.itr
|
||||||
|
demo_*.png
|
||||||
|
demo_*.bmp
|
||||||
|
diag_*.bmp
|
||||||
|
imgtest_demo.bmp
|
||||||
|
inspect_log.csv
|
||||||
|
result_*.png
|
||||||
|
logtest_out/
|
||||||
|
|
||||||
|
# Claude Code ローカル設定
|
||||||
|
.claude/settings.local.json
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// App.cpp - CWinApp。GDI+ 起動、/selftest の分岐、メインウィンドウ生成。
|
||||||
|
//
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "App.h"
|
||||||
|
#include "MainFrame.h"
|
||||||
|
#include "SelfTest.h"
|
||||||
|
|
||||||
|
CImgTestApp theApp;
|
||||||
|
|
||||||
|
CImgTestApp::CImgTestApp() {}
|
||||||
|
|
||||||
|
BOOL CImgTestApp::InitInstance()
|
||||||
|
{
|
||||||
|
CWinApp::InitInstance();
|
||||||
|
|
||||||
|
// 設定(最近使ったレシピ 等)を HKCU\Software\YFox\ImgTestSystem に保存。
|
||||||
|
SetRegistryKey(L"YFox");
|
||||||
|
|
||||||
|
// GDI+ 起動(CImage/CImageEx が JPEG/PNG 読込に使用)。
|
||||||
|
Gdiplus::GdiplusStartupInput gdiInput;
|
||||||
|
Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiInput, NULL);
|
||||||
|
|
||||||
|
// コマンドライン /selftest はヘッドレス自己検証を実行して終了。
|
||||||
|
CString cmd = m_lpCmdLine;
|
||||||
|
if (cmd.MakeLower().Find(L"/selftest") >= 0)
|
||||||
|
{
|
||||||
|
int rc = selftest::Run();
|
||||||
|
return FALSE; // UI を出さず終了(ExitInstance で GDI+ 停止)
|
||||||
|
(void)rc;
|
||||||
|
}
|
||||||
|
if (cmd.MakeLower().Find(L"/demosave") >= 0)
|
||||||
|
{
|
||||||
|
selftest::SaveDemoResult(L"demo_result.png");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
CMainFrame* pFrame = new CMainFrame;
|
||||||
|
if (!pFrame->Create(NULL, L"ImgTestSystem - ノーコード画像検査",
|
||||||
|
WS_OVERLAPPEDWINDOW, CFrameWnd::rectDefault, NULL, NULL))
|
||||||
|
{
|
||||||
|
delete pFrame;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
m_pMainWnd = pFrame;
|
||||||
|
pFrame->ShowWindow(SW_SHOW);
|
||||||
|
pFrame->UpdateWindow();
|
||||||
|
|
||||||
|
if (cmd.Find(L"/demo") >= 0)
|
||||||
|
pFrame->LoadDemo();
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CImgTestApp::ExitInstance()
|
||||||
|
{
|
||||||
|
if (m_gdiplusToken)
|
||||||
|
Gdiplus::GdiplusShutdown(m_gdiplusToken);
|
||||||
|
return CWinApp::ExitInstance();
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "stdafx.h"
|
||||||
|
|
||||||
|
// アプリケーション本体。
|
||||||
|
class CImgTestApp : public CWinApp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CImgTestApp();
|
||||||
|
virtual BOOL InitInstance();
|
||||||
|
virtual int ExitInstance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ULONG_PTR m_gdiplusToken = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern CImgTestApp theApp;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code when working with code in this repository.
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
**ImgTestSystem** は yfoxlib(`yfoximg` / `yfoxbase`)を使う **ノーコード画像検査アプリ**(純MFC)。
|
||||||
|
パラダイムは隣の `C:\debug\ImgChkSystem`(ブロックをワイヤで繋ぐノードエディタ)とは **意図的に別物**:
|
||||||
|
|
||||||
|
> **検査レシピ = 上から下へ順に実行する「ステップ」の縦リスト**(配線なし)。
|
||||||
|
> 料理のレシピ / 検査チェックリストのメタファ。ステップは2種:
|
||||||
|
> - **処理**(画像を加工): グレー化 / メディアン / 二値化(帯) / 大津 / ガンマ / コントラスト
|
||||||
|
> - **検査**(測って範囲判定→PASS/FAIL): 平均輝度 / 白画素率
|
||||||
|
|
||||||
|
UI: 左=ステップ縦リスト(追加/削除/↑↓)・右上=選択ステップのパラメータ入力・右下=作業画像プレビュー・
|
||||||
|
上部=[画像を開く]/[実行]と総合判定バナー。ノーコード=ドロップダウンと数値入力のみ、コードもワイヤも書かない。
|
||||||
|
|
||||||
|
## ビルド
|
||||||
|
|
||||||
|
VS2022 (v143 / MFC Dynamic / Release|x64 / /utf-8 / stdcpp17)。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$msb="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\amd64\MSBuild.exe"
|
||||||
|
& $msb ImgTestSystem.sln /p:Configuration=Release /p:Platform=x64
|
||||||
|
```
|
||||||
|
|
||||||
|
- リンク: `yfoximg.lib;yfoxbase.lib;gdiplus.lib`(libdir `C:\debug\yfoxlib\x64\$(Configuration)`)。
|
||||||
|
- include: `C:\debug\yfoxlib\yfoximg;...\yfoxbase;...\yfoxbase\include`。
|
||||||
|
- PostBuild で `yfoximg.dll`/`yfoxbase.dll` を出力先へコピー。出力=`bin\x64\<Config>\`。
|
||||||
|
|
||||||
|
## アーキテクチャ
|
||||||
|
|
||||||
|
UI非依存の **core/** と MFC UI層(ルート)に分離。
|
||||||
|
|
||||||
|
### core/(ウィンドウ非依存)
|
||||||
|
- `Recipe.h` — `StepKind`(処理/検査)、`StepParam`(数値パラメータ)、`Step`、`Recipe`(ステップの並び)。
|
||||||
|
- `StepCatalog.h` — 種別→表示名 `StepName` / 検査判定 `IsInspect` / 既定パラメータ生成 `MakeStep` / パレット `AllStepKinds`。
|
||||||
|
**新ステップ追加は `StepKind` に足し、この2関数(`StepName`/`MakeStep`)と `InspectEngine` の `RunStep` に1ケース足すだけ。**
|
||||||
|
- `InspectEngine.h/.cpp` — **唯一 yfoximg に依存する層**。`LoadImage`→`Run(recipe, upTo)` で作業画像を原画像から作り直し、
|
||||||
|
各ステップを `CImgProcessing`(`RGB2BW`/`Threshold`/`Threshold_Otsu`/`Avg`/`Gamma`/`Contrast`/`Median`)で適用。
|
||||||
|
`upTo>=0` でその番号までプレビュー実行、`-1` で全実行し総合 PASS/FAIL を返す。`DrawWork` でプレビュー描画。
|
||||||
|
|
||||||
|
### UI層(ルート)
|
||||||
|
- `App.h/.cpp` — `CImgTestApp:CWinApp`。GDI+ 起動、`/selftest` 分岐、`CMainFrame` 生成。
|
||||||
|
- `MainFrame.h/.cpp` — `CMainFrame:CFrameWnd`。子コントロールは **リソース不使用で OnCreate で動的生成**(.rc なし=RC の cp932 罠を回避)。
|
||||||
|
リスト選択→パラメータ欄ロード+`PreviewUpTo`、[追加/削除/↑↓]でレシピ編集、[実行]で全実行+バナー更新。
|
||||||
|
- `SelfTest.cpp` — `/selftest`(ヘッドレス)。合成画像→レシピ実行→`selftest_result.txt`(UTF-8)。
|
||||||
|
|
||||||
|
## yfoximg 作法(重要)
|
||||||
|
|
||||||
|
- アンブレラ `yfoximg.h`(OpenGL 引き込み)は使わず **`ImgProcessing.h` を直接 include**。
|
||||||
|
- yfoximg は **NOMINMAX を定義しない**(裸の `min`/`max` マクロ前提)。GDI+ も min/max マクロを要求するため、
|
||||||
|
`ImgProcessing.h` を含む TU(`InspectEngine.cpp`/`SelfTest.cpp`)では **`std::min/std::max` を使わない**(手書きクランプ)。
|
||||||
|
- 8bit グレー画像は `SetGrayPalette()` してから `Draw`/`Save`。処理系は R1(入力不変)・R2(出力は新規 `CDiBitmap`)。
|
||||||
|
- 画像は 8/24/32bit を受理。`Ensure8` が 32→24(手動)→`RGB2BW` で 8bit へ正規化してから濃淡系処理を行う。
|
||||||
|
|
||||||
|
## 検証
|
||||||
|
|
||||||
|
- `ImgTestSystem.exe /selftest` → 合成画像(一様輝度128)で グレー化→平均輝度→白画素率 を実行し PASS を確認、
|
||||||
|
`selftest_result.txt` を出力(終了コード 0=PASS)。
|
||||||
|
|
||||||
|
## わかりやすさ強化(3方式を統合)
|
||||||
|
|
||||||
|
- **ビジュアル・パイプライン**(`PipelineView.*`, `CPipelineView:CWnd`): 各ステップをカード化し、
|
||||||
|
「そのステップ適用後のサムネイル」を縦に並べる=画像が上から下へ変化する様子が見える。
|
||||||
|
検査カードは大きな緑✓/赤✗バッジ+測定値。左ストライプ 青=処理/橙=検査。ダブルバッファ描画・ホイール/スクロール対応。
|
||||||
|
**罠**: `CDiBitmap::Draw`(サムネ)は DC の `BkMode` を OPAQUE/暗色へ戻すので、直後の文字描画前に
|
||||||
|
`SetBkMode(TRANSPARENT)` を必ず再設定する(でないと暗地に明字で反転)。
|
||||||
|
- **テンプレート**(`core/Templates.h` + `wizard::ChooseTemplate`): 目的(キズ/欠け/明るさ/文字)を選ぶと完成レシピが入る。
|
||||||
|
- **対話式ウィザード**(`Wizard.*` + `CTaskDialog`): Q&A でレシピ生成。.rc 不要のため `CTaskDialog`(要 Common-Controls v6 マニフェスト=stdafx.h の pragma)。
|
||||||
|
- 右パネル: 選択ステップのパラメータを **CSliderCtrl でライブ調整**(`OnHScroll`→即再実行→サムネ更新)。整数域は実値、
|
||||||
|
ガンマ等の実数域は 0..1000 マッピング。合否バナーは `OnCtlColor` で緑/赤/灰に色分け。
|
||||||
|
- 検査種別に **塊(キズ/穴)の数**(`InspectBlobCount`)を追加(`LabelingInit`/`Labeling`/`m_nLabelCount`)。
|
||||||
|
|
||||||
|
## 検証(追加)
|
||||||
|
|
||||||
|
- `ImgTestSystem.exe /demo` → デモ画像(明るい地に暗い欠陥3点)+ キズ検査テンプレを読み込み実行(画面確認用)。
|
||||||
|
スクリーンショットは `PrintWindow(PW_RENDERFULLCONTENT=2)` で捕捉可(Windows PowerShell 5.1 + System.Drawing)。
|
||||||
|
|
||||||
|
## レシピ保存/ROI/並べ替え/検査ログ(追加実装)
|
||||||
|
|
||||||
|
- **レシピ保存/読込**(`core/RecipeStore.*`): 人が読める UTF-8 テキスト `*.itr`(`STEP <id>` / `PARAM <key> <val>` / `ROI ...`)。
|
||||||
|
`StepKindId`/`StepKindFromId`(`StepCatalog.h`)で enum を安定文字列 ID として永続化。読込は `MakeStep` 既定へキー一致で上書き(前方互換)。メニュー ファイル。
|
||||||
|
- **検査範囲 ROI**(`Recipe::roiEnabled/roi`): 大プレビュー上をドラッグして矩形指定(`CMainFrame::OnLButtonDown/MouseMove/Up`)。
|
||||||
|
描画メソッド `DrawShot/DrawSource` は**実際に描いた矩形**(アスペクト維持=`AspectFit`)を返し、client↔画像座標の相互変換に使う。
|
||||||
|
検査は ROI 内で計測: 明るさ=`RectAvg(roi)` / 白画素率=`Threshold(...,roi)`÷ROI面積 / 塊数=`Labeling(...,roi)`。処理ステップは寸法不変なので全ステップ ROI 同座標系。ROI は大プレビュー+カードサムネに黄枠表示。メニュー 検査範囲。
|
||||||
|
- **カードのドラッグ並べ替え**(`CPipelineView`): 押下→しきい値超で `m_dragging`、青い挿入インジケータ、離すと `WM_PV_MOVESTEP(from,slot)`→`CMainFrame::OnPipeMoveStep` が並べ替え+再実行。
|
||||||
|
- **判定結果画像の保存 / 検査ログ**: `InspectEngine::SaveResultImage`=最終画像を24bit化し ROI枠+PASS/FAIL バナー+検査要約(縁取り文字)を焼き込み保存(拡張子で png/bmp/jpg)。
|
||||||
|
検査ログ=記録フォルダ(`SHBrowseForFolder`)へ `result_<ts>.png` 保存+`inspect_log.csv`(UTF-8 BOM, 日時/総合/画像/詳細)追記、フォルダを開く。メニュー 検査ログ。
|
||||||
|
- **⚠ 罠**: Windows ヘッダの `#define small char`(rpcndr.h)のため、ローカル変数名に `small` を使うと C2628。`fSmall` 等にする。
|
||||||
|
|
||||||
|
## 複数ROI / MRU / CSV拡張(追加実装)
|
||||||
|
|
||||||
|
- **複数ROI**(`Recipe::rois` = `vector<CRect>`。空=画像全体): 大プレビューを**ドラッグで範囲追加**、**右クリックでその範囲を削除**、
|
||||||
|
メニュー 検査範囲=「最後を取り消す」「すべてリセット」。検査は全ROIを合算(明るさ=面積重み付き平均 / 白画素率=合算/合計面積 / 塊数=全ROIマスクを結合→union bbox でラベリング)。
|
||||||
|
大プレビュー+カード+結果画像に黄枠+番号を描画。`.itr` は `ROI l t r b` を範囲ごとに1行(旧 `ROI enabled l t r b` 5値も読める)。
|
||||||
|
- **⚠ 重要バグ修正**: yfoximg `CImgProcessing::Threshold` を**同一バッファへ sub-rect で複数回呼ぶとマスクが壊れる**(2個ROIで塊数0になった)。
|
||||||
|
`MakeBinary` は Threshold をやめ、**画素を直接走査してROI内だけ白く塗る**方式に変更(8bit前提=Ensure8済)。`/selftest` に複数ROI塊カウント検証(全体3/大ROI2/小2個ROI2)を常設。
|
||||||
|
- **最近使ったレシピ(MRU)**: 最大5件を `HKCU\Software\YFox\ImgTestSystem\Recent\File1..5` に保存(`SetRegistryKey(L"YFox")`)。
|
||||||
|
ファイルメニューの「最近使ったレシピ」サブメニュー(`m_hRecentMenu` を保持し `UpdateRecentMenu` で再構築、`ON_COMMAND_RANGE`)。開く/保存時に `AddRecent`。存在しない項目はクリック時に一覧から除去提案。
|
||||||
|
- **検査ログCSV 拡張**: 列= 日時,総合判定,元画像,レシピ,ステップ数,検査数,合格数,不合格数,検査範囲数,結果画像,詳細(UTF-8 BOM)。記録フォルダも `HKCU\...\Log\Folder` に永続化。
|
||||||
|
|
||||||
|
## 現状
|
||||||
|
|
||||||
|
処理6種+検査3種、ビジュアル・パイプライン、テンプレート、ウィザード、スライダー調整、
|
||||||
|
レシピ保存/読込(+MRU)、複数ROI 指定、カードのドラッグ並べ替え、判定結果画像保存/検査ログ(CSV拡張)、/selftest・/demo。
|
||||||
|
`/selftest` は 実行 + レシピ往復一致 + 結果画像保存 + 複数ROI塊カウント を検証(RESULT: PASS)。
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImgTestSystem", "ImgTestSystem.vcxproj", "{7A2B3C4D-5E6F-4A10-9B21-C3D4E5F60718}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{7A2B3C4D-5E6F-4A10-9B21-C3D4E5F60718}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{7A2B3C4D-5E6F-4A10-9B21-C3D4E5F60718}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{7A2B3C4D-5E6F-4A10-9B21-C3D4E5F60718}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{7A2B3C4D-5E6F-4A10-9B21-C3D4E5F60718}.Release|x64.Build.0 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ItemGroup Label="ProjectConfigurations">
|
||||||
|
<ProjectConfiguration Include="Debug|x64">
|
||||||
|
<Configuration>Debug</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|x64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
</ItemGroup>
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>{7A2B3C4D-5E6F-4A10-9B21-C3D4E5F60718}</ProjectGuid>
|
||||||
|
<RootNamespace>ImgTestSystem</RootNamespace>
|
||||||
|
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||||
|
<Keyword>MFCProj</Keyword>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
|
<PropertyGroup Label="Configuration">
|
||||||
|
<ConfigurationType>Application</ConfigurationType>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
<UseOfMfc>Dynamic</UseOfMfc>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<UseDebugLibraries>false</UseDebugLibraries>
|
||||||
|
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<UseDebugLibraries>true</UseDebugLibraries>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutDir>$(ProjectDir)bin\$(Platform)\$(Configuration)\</OutDir>
|
||||||
|
<IntDir>$(ProjectDir)obj\$(Platform)\$(Configuration)\</IntDir>
|
||||||
|
<VcpkgEnabled>false</VcpkgEnabled>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemDefinitionGroup>
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level3</WarningLevel>
|
||||||
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
|
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||||
|
<PrecompiledHeaderFile>stdafx.h</PrecompiledHeaderFile>
|
||||||
|
<AdditionalIncludeDirectories>$(ProjectDir);$(ProjectDir)core;C:\debug\yfoxlib\yfoximg;C:\debug\yfoxlib\yfoxbase;C:\debug\yfoxlib\yfoxbase\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
|
<PreprocessorDefinitions>WINVER=0x0A00;_WIN32_WINNT=0x0A00;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
|
||||||
|
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Windows</SubSystem>
|
||||||
|
<AdditionalLibraryDirectories>C:\debug\yfoxlib\x64\$(Configuration);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||||
|
<AdditionalDependencies>yfoximg.lib;yfoxbase.lib;gdiplus.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
</Link>
|
||||||
|
<PostBuildEvent>
|
||||||
|
<Command>copy /Y "C:\debug\yfoxlib\x64\$(Configuration)\yfoximg.dll" "$(OutDir)"
|
||||||
|
copy /Y "C:\debug\yfoxlib\x64\$(Configuration)\yfoxbase.dll" "$(OutDir)"</Command>
|
||||||
|
<Message>Copy yfoximg.dll / yfoxbase.dll next to ImgTestSystem.exe</Message>
|
||||||
|
</PostBuildEvent>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<ClCompile>
|
||||||
|
<Optimization>MaxSpeed</Optimization>
|
||||||
|
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||||
|
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||||
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
|
</Link>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<ClCompile>
|
||||||
|
<Optimization>Disabled</Optimization>
|
||||||
|
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||||
|
</ClCompile>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="targetver.h" />
|
||||||
|
<ClInclude Include="stdafx.h" />
|
||||||
|
<ClInclude Include="App.h" />
|
||||||
|
<ClInclude Include="MainFrame.h" />
|
||||||
|
<ClInclude Include="PipelineView.h" />
|
||||||
|
<ClInclude Include="Wizard.h" />
|
||||||
|
<ClInclude Include="SelfTest.h" />
|
||||||
|
<ClInclude Include="core\Recipe.h" />
|
||||||
|
<ClInclude Include="core\StepCatalog.h" />
|
||||||
|
<ClInclude Include="core\Templates.h" />
|
||||||
|
<ClInclude Include="core\RecipeStore.h" />
|
||||||
|
<ClInclude Include="core\InspectEngine.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="stdafx.cpp">
|
||||||
|
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="App.cpp" />
|
||||||
|
<ClCompile Include="MainFrame.cpp" />
|
||||||
|
<ClCompile Include="PipelineView.cpp" />
|
||||||
|
<ClCompile Include="Wizard.cpp" />
|
||||||
|
<ClCompile Include="SelfTest.cpp" />
|
||||||
|
<ClCompile Include="core\InspectEngine.cpp" />
|
||||||
|
<ClCompile Include="core\RecipeStore.cpp" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
|
</Project>
|
||||||
+851
@@ -0,0 +1,851 @@
|
|||||||
|
//
|
||||||
|
// MainFrame.cpp - ノーコード画像検査(ビジュアル・パイプライン + テンプレ + ウィザード)。
|
||||||
|
// 左: 各ステップをカード化し適用後サムネイルを縦に並べる(画像が変化する様子が見える)。
|
||||||
|
// 右: 選択カードの設定をスライダーでライブ調整 / ステップ追加パレット。
|
||||||
|
// 上: [画像を開く][テンプレート][ウィザード][実行] と 大きな合否バナー。
|
||||||
|
//
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "MainFrame.h"
|
||||||
|
#include "core/StepCatalog.h"
|
||||||
|
#include "core/Templates.h"
|
||||||
|
#include "core/RecipeStore.h"
|
||||||
|
#include "Wizard.h"
|
||||||
|
#include "SelfTest.h"
|
||||||
|
#include <shlobj.h> // SHBrowseForFolder
|
||||||
|
|
||||||
|
using namespace core;
|
||||||
|
|
||||||
|
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
|
||||||
|
ON_WM_CREATE()
|
||||||
|
ON_WM_SIZE()
|
||||||
|
ON_WM_PAINT()
|
||||||
|
ON_WM_CTLCOLOR()
|
||||||
|
ON_WM_HSCROLL()
|
||||||
|
ON_WM_LBUTTONDOWN()
|
||||||
|
ON_WM_MOUSEMOVE()
|
||||||
|
ON_WM_LBUTTONUP()
|
||||||
|
ON_WM_RBUTTONDOWN()
|
||||||
|
ON_BN_CLICKED(IDC_BTN_OPEN, OnBtnOpen)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_RUN, OnBtnRun)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_ADD, OnBtnAdd)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_DEL, OnBtnDel)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_UP, OnBtnUp)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_DOWN, OnBtnDown)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_TMPL, OnBtnTemplate)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_WIZ, OnBtnWizard)
|
||||||
|
ON_COMMAND(ID_FILE_OPENIMAGE, OnBtnOpen)
|
||||||
|
ON_COMMAND(ID_RECIPE_OPEN, OnRecipeOpen)
|
||||||
|
ON_COMMAND(ID_RECIPE_SAVE, OnRecipeSave)
|
||||||
|
ON_COMMAND(ID_APP_EXIT, OnAppExit)
|
||||||
|
ON_COMMAND(ID_ROI_CLEAR, OnRoiClear)
|
||||||
|
ON_COMMAND(ID_ROI_UNDO, OnRoiUndo)
|
||||||
|
ON_COMMAND(ID_RESULT_SAVE, OnResultSave)
|
||||||
|
ON_COMMAND(ID_LOG_SETFOLDER, OnLogSetFolder)
|
||||||
|
ON_COMMAND(ID_LOG_RECORD, OnLogRecord)
|
||||||
|
ON_COMMAND(ID_LOG_OPENFOLDER, OnLogOpenFolder)
|
||||||
|
ON_MESSAGE(WM_PV_SELCHANGED, OnPipeSelChanged)
|
||||||
|
ON_MESSAGE(WM_PV_MOVESTEP, OnPipeMoveStep)
|
||||||
|
ON_COMMAND_RANGE(ID_RECENT_BASE, ID_RECENT_BASE + RECENT_MAX - 1, OnRecentFile)
|
||||||
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
void CMainFrame::BuildMenu()
|
||||||
|
{
|
||||||
|
m_menu.CreateMenu();
|
||||||
|
|
||||||
|
CMenu file; file.CreatePopupMenu();
|
||||||
|
file.AppendMenu(MF_STRING, ID_FILE_OPENIMAGE, L"画像を開く...\tCtrl+O");
|
||||||
|
file.AppendMenu(MF_SEPARATOR);
|
||||||
|
file.AppendMenu(MF_STRING, ID_RECIPE_OPEN, L"レシピを開く...");
|
||||||
|
file.AppendMenu(MF_STRING, ID_RECIPE_SAVE, L"レシピを保存...\tCtrl+S");
|
||||||
|
{
|
||||||
|
CMenu recent; recent.CreatePopupMenu();
|
||||||
|
m_hRecentMenu = recent.GetSafeHmenu();
|
||||||
|
file.AppendMenu(MF_POPUP, (UINT_PTR)recent.Detach(), L"最近使ったレシピ");
|
||||||
|
}
|
||||||
|
file.AppendMenu(MF_SEPARATOR);
|
||||||
|
file.AppendMenu(MF_STRING, ID_RESULT_SAVE, L"判定結果画像を保存...");
|
||||||
|
file.AppendMenu(MF_SEPARATOR);
|
||||||
|
file.AppendMenu(MF_STRING, ID_APP_EXIT, L"終了");
|
||||||
|
m_menu.AppendMenu(MF_POPUP, (UINT_PTR)file.Detach(), L"ファイル(&F)");
|
||||||
|
|
||||||
|
CMenu roi; roi.CreatePopupMenu();
|
||||||
|
roi.AppendMenu(MF_STRING, ID_ROI_UNDO, L"最後の検査範囲を取り消す");
|
||||||
|
roi.AppendMenu(MF_STRING, ID_ROI_CLEAR, L"すべてリセット(画像全体)");
|
||||||
|
m_menu.AppendMenu(MF_POPUP, (UINT_PTR)roi.Detach(), L"検査範囲(&R)");
|
||||||
|
|
||||||
|
CMenu log; log.CreatePopupMenu();
|
||||||
|
log.AppendMenu(MF_STRING, ID_LOG_SETFOLDER, L"記録フォルダを指定...");
|
||||||
|
log.AppendMenu(MF_STRING, ID_LOG_RECORD, L"今の結果を記録する");
|
||||||
|
log.AppendMenu(MF_STRING, ID_LOG_OPENFOLDER, L"記録フォルダを開く");
|
||||||
|
m_menu.AppendMenu(MF_POPUP, (UINT_PTR)log.Detach(), L"検査ログ(&L)");
|
||||||
|
|
||||||
|
SetMenu(&m_menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
CMainFrame::CMainFrame() {}
|
||||||
|
|
||||||
|
CString CMainFrame::FmtNum(double v)
|
||||||
|
{
|
||||||
|
CString s;
|
||||||
|
if (v == (double)(long)v) s.Format(L"%ld", (long)v);
|
||||||
|
else s.Format(L"%.2f", v);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CMainFrame::OnCreate(LPCREATESTRUCT lpcs)
|
||||||
|
{
|
||||||
|
if (CFrameWnd::OnCreate(lpcs) == -1) return -1;
|
||||||
|
|
||||||
|
m_uiFont.CreatePointFont(100, L"Yu Gothic UI");
|
||||||
|
m_bigFont.CreatePointFont(200, L"Yu Gothic UI");
|
||||||
|
m_brPass.CreateSolidBrush(RGB(40, 170, 80));
|
||||||
|
m_brFail.CreateSolidBrush(RGB(220, 60, 60));
|
||||||
|
m_brNeutral.CreateSolidBrush(RGB(90, 100, 110));
|
||||||
|
|
||||||
|
DWORD bs = WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON;
|
||||||
|
m_btnOpen.Create(L"画像を開く…", bs, CRect(0, 0, 10, 10), this, IDC_BTN_OPEN);
|
||||||
|
m_btnTemplate.Create(L"テンプレートから作る", bs, CRect(0, 0, 10, 10), this, IDC_BTN_TMPL);
|
||||||
|
m_btnWizard.Create(L"ウィザードで作る", bs, CRect(0, 0, 10, 10), this, IDC_BTN_WIZ);
|
||||||
|
m_btnRun.Create(L"実行(検査する)", bs, CRect(0, 0, 10, 10), this, IDC_BTN_RUN);
|
||||||
|
m_result.Create(L"画像を開いて[実行]してください",
|
||||||
|
WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE, CRect(0, 0, 10, 10), this, IDC_RESULT);
|
||||||
|
m_result.SetFont(&m_bigFont);
|
||||||
|
|
||||||
|
for (CButton* b : { &m_btnOpen, &m_btnTemplate, &m_btnWizard, &m_btnRun })
|
||||||
|
b->SetFont(&m_uiFont);
|
||||||
|
|
||||||
|
// 左: パイプライン
|
||||||
|
m_pipe.Create(this, IDC_PIPE);
|
||||||
|
m_pipe.SetData(&m_recipe, &m_engine);
|
||||||
|
|
||||||
|
// 右: 設定
|
||||||
|
m_settingsTitle.Create(L"② 選んだステップの設定", WS_CHILD | WS_VISIBLE | SS_LEFT, CRect(0, 0, 10, 10), this, (UINT)-1);
|
||||||
|
m_settingsTitle.SetFont(&m_uiFont);
|
||||||
|
for (int i = 0; i < PARAM_ROWS; i++)
|
||||||
|
{
|
||||||
|
m_slabel[i].Create(L"", WS_CHILD | SS_LEFT | SS_CENTERIMAGE, CRect(0, 0, 10, 10), this, (UINT)-1);
|
||||||
|
m_slabel[i].SetFont(&m_uiFont);
|
||||||
|
m_slider[i].Create(WS_CHILD | WS_TABSTOP | TBS_HORZ | TBS_AUTOTICKS, CRect(0, 0, 10, 10), this, IDC_SLIDER0 + i);
|
||||||
|
m_sval[i].Create(L"", WS_CHILD | SS_LEFT | SS_CENTERIMAGE, CRect(0, 0, 10, 10), this, (UINT)-1);
|
||||||
|
m_sval[i].SetFont(&m_uiFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右: 追加パレット
|
||||||
|
m_addTitle.Create(L"③ ステップを追加/並べ替え", WS_CHILD | WS_VISIBLE | SS_LEFT, CRect(0, 0, 10, 10), this, (UINT)-1);
|
||||||
|
m_addTitle.SetFont(&m_uiFont);
|
||||||
|
m_combo.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, CRect(0, 0, 10, 240), this, IDC_COMBO);
|
||||||
|
m_combo.SetFont(&m_uiFont);
|
||||||
|
int nk = 0; const StepKind* ks = AllStepKinds(nk);
|
||||||
|
for (int i = 0; i < nk; i++)
|
||||||
|
{
|
||||||
|
int item = m_combo.AddString(StepName(ks[i]));
|
||||||
|
m_combo.SetItemData(item, (DWORD_PTR)ks[i]);
|
||||||
|
}
|
||||||
|
m_combo.SetCurSel(0);
|
||||||
|
m_btnAdd.Create(L"追加", bs, CRect(0, 0, 10, 10), this, IDC_BTN_ADD);
|
||||||
|
m_btnDel.Create(L"削除", bs, CRect(0, 0, 10, 10), this, IDC_BTN_DEL);
|
||||||
|
m_btnUp.Create(L"↑ 上へ", bs, CRect(0, 0, 10, 10), this, IDC_BTN_UP);
|
||||||
|
m_btnDown.Create(L"↓ 下へ", bs, CRect(0, 0, 10, 10), this, IDC_BTN_DOWN);
|
||||||
|
for (CButton* b : { &m_btnAdd, &m_btnDel, &m_btnUp, &m_btnDown })
|
||||||
|
b->SetFont(&m_uiFont);
|
||||||
|
|
||||||
|
// 既定レシピ(明るさ確認の例)
|
||||||
|
m_recipe.steps.push_back(MakeStep(StepKind::Grayscale));
|
||||||
|
{ Step s = MakeStep(StepKind::InspectBrightness); s.SetParam(L"min", 80); s.SetParam(L"max", 180); m_recipe.steps.push_back(s); }
|
||||||
|
|
||||||
|
BuildMenu();
|
||||||
|
LoadRecent();
|
||||||
|
UpdateRecentMenu();
|
||||||
|
m_logFolder = AfxGetApp()->GetProfileString(L"Log", L"Folder", L"");
|
||||||
|
|
||||||
|
m_pipe.SetSel(0);
|
||||||
|
ConfigureSliders(0);
|
||||||
|
|
||||||
|
SetWindowText(L"ImgTestSystem - ノーコード画像検査");
|
||||||
|
SetWindowPos(NULL, 0, 0, 1180, 780, SWP_NOMOVE | SWP_NOZORDER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CMainFrame::Sel() const { return const_cast<CPipelineView&>(m_pipe).GetSel(); }
|
||||||
|
|
||||||
|
void CMainFrame::OnSize(UINT nType, int cx, int cy)
|
||||||
|
{
|
||||||
|
CFrameWnd::OnSize(nType, cx, cy);
|
||||||
|
LayoutControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::LayoutControls()
|
||||||
|
{
|
||||||
|
CRect cr; GetClientRect(&cr);
|
||||||
|
int cx = cr.Width(), cy = cr.Height();
|
||||||
|
if (cx < 100 || cy < 100) return;
|
||||||
|
|
||||||
|
const int M = 10;
|
||||||
|
// 上部バー(2段: ボタン行 + バナー)
|
||||||
|
int bw = 150, bh = 30, bx = M;
|
||||||
|
m_btnOpen.MoveWindow(bx, M, bw, bh); bx += bw + 8;
|
||||||
|
m_btnTemplate.MoveWindow(bx, M, bw + 20, bh); bx += bw + 28;
|
||||||
|
m_btnWizard.MoveWindow(bx, M, bw, bh); bx += bw + 8;
|
||||||
|
m_btnRun.MoveWindow(bx, M, bw, bh); bx += bw + 8;
|
||||||
|
int bannerX = bx + 6;
|
||||||
|
m_result.MoveWindow(bannerX, M, cx - bannerX - M, bh + 8);
|
||||||
|
|
||||||
|
int top = M + bh + 12;
|
||||||
|
|
||||||
|
// 左: パイプライン(右カラム幅を残す)
|
||||||
|
int rightW = 380;
|
||||||
|
if (cx - rightW - M * 3 < 360) rightW = cx - 360 - M * 3;
|
||||||
|
int leftW = cx - rightW - M * 3;
|
||||||
|
m_pipe.MoveWindow(M, top, leftW, cy - top - M);
|
||||||
|
|
||||||
|
// 右カラム
|
||||||
|
int rx = M + leftW + M;
|
||||||
|
int rw = rightW + M; // 使える幅
|
||||||
|
if (rx + rw > cx - M) rw = cx - M - rx;
|
||||||
|
|
||||||
|
// ① 大プレビュー(正方形)
|
||||||
|
int pv = rw < (cy - top - 40) ? rw : 240;
|
||||||
|
if (pv > 300) pv = 300;
|
||||||
|
m_bigPreviewRect = CRect(rx, top + 20, rx + rw, top + 20 + pv);
|
||||||
|
// (プレビュー上のラベルは OnPaint で描く)
|
||||||
|
|
||||||
|
// ② 設定スライダー
|
||||||
|
int sy = m_bigPreviewRect.bottom + 12;
|
||||||
|
m_settingsTitle.MoveWindow(rx, sy, rw, 20); sy += 24;
|
||||||
|
for (int i = 0; i < PARAM_ROWS; i++)
|
||||||
|
{
|
||||||
|
m_slabel[i].MoveWindow(rx, sy, rw - 70, 20);
|
||||||
|
m_sval[i].MoveWindow(rx + rw - 66, sy, 66, 20);
|
||||||
|
m_slider[i].MoveWindow(rx, sy + 20, rw, 26);
|
||||||
|
sy += 52;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ③ 追加パレット(下部)
|
||||||
|
int ay = cy - M - 92;
|
||||||
|
if (ay < sy + 6) ay = sy + 6;
|
||||||
|
m_addTitle.MoveWindow(rx, ay, rw, 20); ay += 24;
|
||||||
|
m_combo.MoveWindow(rx, ay, rw - 76, 240);
|
||||||
|
m_btnAdd.MoveWindow(rx + rw - 70, ay, 70, 26); ay += 32;
|
||||||
|
int q = (rw - 16) / 3;
|
||||||
|
m_btnDel.MoveWindow(rx, ay, q, 26);
|
||||||
|
m_btnUp.MoveWindow(rx + q + 8, ay, q, 26);
|
||||||
|
m_btnDown.MoveWindow(rx + 2 * q + 16, ay, rw - 2 * q - 16, 26);
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnPaint()
|
||||||
|
{
|
||||||
|
CPaintDC dc(this);
|
||||||
|
|
||||||
|
// プレビュー見出し
|
||||||
|
dc.SetBkMode(TRANSPARENT);
|
||||||
|
CFont* of = dc.SelectObject(&m_uiFont);
|
||||||
|
dc.SetTextColor(RGB(60, 70, 80));
|
||||||
|
CRect lbl(m_bigPreviewRect.left, m_bigPreviewRect.top - 20, m_bigPreviewRect.right, m_bigPreviewRect.top);
|
||||||
|
int sel = Sel();
|
||||||
|
CString cap = L"① プレビュー(ドラッグで検査範囲を追加/右クリックで削除)";
|
||||||
|
if (sel >= 0 && sel < (int)m_recipe.steps.size())
|
||||||
|
cap.Format(L"① ステップ%d: %s(ドラッグで範囲追加/右クリックで削除)", sel + 1, (LPCWSTR)StepName(m_recipe.steps[sel].kind));
|
||||||
|
dc.DrawText(cap, lbl, DT_LEFT | DT_BOTTOM | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||||
|
dc.SelectObject(of);
|
||||||
|
|
||||||
|
// プレビュー本体
|
||||||
|
dc.FillSolidRect(m_bigPreviewRect, RGB(30, 30, 30));
|
||||||
|
CRect inner = m_bigPreviewRect; inner.DeflateRect(3, 3);
|
||||||
|
CRect fit(0, 0, 0, 0);
|
||||||
|
if (m_engine.HasImage() && sel >= 0 && m_engine.HasShot(sel))
|
||||||
|
fit = m_engine.DrawShot(&dc, inner, sel);
|
||||||
|
else if (m_engine.HasImage())
|
||||||
|
fit = m_engine.DrawSource(&dc, inner);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dc.SetBkMode(TRANSPARENT);
|
||||||
|
dc.SetTextColor(RGB(200, 200, 200));
|
||||||
|
dc.DrawText(L"[画像を開く]で画像を読み込みます", inner, DT_CENTER | DT_VCENTER | DT_WORDBREAK);
|
||||||
|
}
|
||||||
|
m_previewFitRect = fit;
|
||||||
|
|
||||||
|
// 確定済み ROI(複数)を黄枠+番号で表示
|
||||||
|
if (m_engine.HasImage() && !m_recipe.rois.empty() && !fit.IsRectEmpty())
|
||||||
|
{
|
||||||
|
int sw = m_engine.SrcWidth(), sh = m_engine.SrcHeight();
|
||||||
|
if (sw > 0 && sh > 0)
|
||||||
|
{
|
||||||
|
double kx = (double)fit.Width() / sw, ky = (double)fit.Height() / sh;
|
||||||
|
CPen yp(PS_SOLID, 2, RGB(255, 210, 0));
|
||||||
|
CPen* oyp = dc.SelectObject(&yp);
|
||||||
|
dc.SelectStockObject(NULL_BRUSH);
|
||||||
|
dc.SetBkMode(TRANSPARENT);
|
||||||
|
dc.SetTextColor(RGB(255, 210, 0));
|
||||||
|
dc.SelectObject(&m_uiFont);
|
||||||
|
for (size_t i = 0; i < m_recipe.rois.size(); i++)
|
||||||
|
{
|
||||||
|
const CRect& roi = m_recipe.rois[i];
|
||||||
|
CRect r(fit.left + (int)(roi.left * kx), fit.top + (int)(roi.top * ky),
|
||||||
|
fit.left + (int)(roi.right * kx), fit.top + (int)(roi.bottom * ky));
|
||||||
|
dc.Rectangle(r);
|
||||||
|
CString no; no.Format(L"%d", (int)i + 1);
|
||||||
|
dc.TextOut(r.left + 2, r.top, no);
|
||||||
|
}
|
||||||
|
dc.SelectObject(oyp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドラッグ中のラバーバンド
|
||||||
|
if (m_roiDragging)
|
||||||
|
{
|
||||||
|
CRect rb(m_roiStart, m_roiCur); rb.NormalizeRect();
|
||||||
|
CPen dp(PS_DOT, 1, RGB(255, 255, 0));
|
||||||
|
int obk = dc.SetBkMode(TRANSPARENT);
|
||||||
|
CPen* odp = dc.SelectObject(&dp);
|
||||||
|
dc.SelectStockObject(NULL_BRUSH);
|
||||||
|
dc.Rectangle(rb);
|
||||||
|
dc.SelectObject(odp);
|
||||||
|
dc.SetBkMode(obk);
|
||||||
|
}
|
||||||
|
|
||||||
|
CPen pen(PS_SOLID, 1, RGB(120, 120, 120));
|
||||||
|
CPen* op = dc.SelectObject(&pen);
|
||||||
|
dc.SelectStockObject(NULL_BRUSH);
|
||||||
|
dc.Rectangle(m_bigPreviewRect);
|
||||||
|
dc.SelectObject(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ROI マウス操作(大プレビュー上) ---
|
||||||
|
void CMainFrame::OnLButtonDown(UINT nFlags, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_engine.HasImage() && !m_previewFitRect.IsRectEmpty() && m_previewFitRect.PtInRect(pt))
|
||||||
|
{
|
||||||
|
m_roiDragging = true;
|
||||||
|
m_roiStart = m_roiCur = pt;
|
||||||
|
SetCapture();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CFrameWnd::OnLButtonDown(nFlags, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnMouseMove(UINT nFlags, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_roiDragging)
|
||||||
|
{
|
||||||
|
// プレビュー内にクランプ
|
||||||
|
CPoint p = pt;
|
||||||
|
if (p.x < m_previewFitRect.left) p.x = m_previewFitRect.left;
|
||||||
|
if (p.x > m_previewFitRect.right) p.x = m_previewFitRect.right;
|
||||||
|
if (p.y < m_previewFitRect.top) p.y = m_previewFitRect.top;
|
||||||
|
if (p.y > m_previewFitRect.bottom) p.y = m_previewFitRect.bottom;
|
||||||
|
m_roiCur = p;
|
||||||
|
InvalidateRect(m_bigPreviewRect, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CFrameWnd::OnMouseMove(nFlags, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnLButtonUp(UINT nFlags, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_roiDragging)
|
||||||
|
{
|
||||||
|
m_roiDragging = false;
|
||||||
|
ReleaseCapture();
|
||||||
|
CRect rb(m_roiStart, m_roiCur); rb.NormalizeRect();
|
||||||
|
CRect fit = m_previewFitRect;
|
||||||
|
int sw = m_engine.SrcWidth(), sh = m_engine.SrcHeight();
|
||||||
|
if (rb.Width() >= 6 && rb.Height() >= 6 && sw > 0 && sh > 0 && !fit.IsRectEmpty())
|
||||||
|
{
|
||||||
|
double kx = (double)sw / fit.Width(), ky = (double)sh / fit.Height();
|
||||||
|
CRect r(
|
||||||
|
(int)((rb.left - fit.left) * kx), (int)((rb.top - fit.top) * ky),
|
||||||
|
(int)((rb.right - fit.left) * kx), (int)((rb.bottom - fit.top) * ky));
|
||||||
|
if (r.left < 0) r.left = 0; if (r.top < 0) r.top = 0;
|
||||||
|
if (r.right > sw) r.right = sw; if (r.bottom > sh) r.bottom = sh;
|
||||||
|
m_recipe.rois.push_back(r); // 追加(複数指定)
|
||||||
|
RunAndRefresh(-2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InvalidateRect(m_bigPreviewRect, FALSE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CFrameWnd::OnLButtonUp(nFlags, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右クリック: その位置を含む検査範囲を削除。
|
||||||
|
void CMainFrame::OnRButtonDown(UINT nFlags, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_engine.HasImage() && !m_previewFitRect.IsRectEmpty() && m_previewFitRect.PtInRect(pt) && !m_recipe.rois.empty())
|
||||||
|
{
|
||||||
|
int sw = m_engine.SrcWidth(), sh = m_engine.SrcHeight();
|
||||||
|
CRect fit = m_previewFitRect;
|
||||||
|
if (sw > 0 && sh > 0)
|
||||||
|
{
|
||||||
|
double kx = (double)sw / fit.Width(), ky = (double)sh / fit.Height();
|
||||||
|
int ix = (int)((pt.x - fit.left) * kx), iy = (int)((pt.y - fit.top) * ky);
|
||||||
|
// 後に追加したもの(手前)から探す
|
||||||
|
for (int i = (int)m_recipe.rois.size() - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
CRect rr = m_recipe.rois[i]; rr.NormalizeRect();
|
||||||
|
if (rr.PtInRect(CPoint(ix, iy)))
|
||||||
|
{
|
||||||
|
m_recipe.rois.erase(m_recipe.rois.begin() + i);
|
||||||
|
RunAndRefresh(-2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CFrameWnd::OnRButtonDown(nFlags, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnRoiClear()
|
||||||
|
{
|
||||||
|
m_recipe.rois.clear();
|
||||||
|
RunAndRefresh(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnRoiUndo()
|
||||||
|
{
|
||||||
|
if (m_recipe.rois.empty()) return;
|
||||||
|
m_recipe.rois.pop_back();
|
||||||
|
RunAndRefresh(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 判定結果画像の保存 / 検査ログ ---
|
||||||
|
void CMainFrame::OnResultSave()
|
||||||
|
{
|
||||||
|
if (!m_engine.HasImage()) { AfxMessageBox(L"先に画像を開いてください"); return; }
|
||||||
|
CFileDialog dlg(FALSE, L"png", L"result.png", OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY,
|
||||||
|
L"PNG (*.png)|*.png|BMP (*.bmp)|*.bmp|JPEG (*.jpg)|*.jpg||", this);
|
||||||
|
if (dlg.DoModal() != IDOK) return;
|
||||||
|
RunResult rr = m_engine.Run(m_recipe);
|
||||||
|
if (!m_engine.SaveResultImage(dlg.GetPathName(), m_recipe, rr))
|
||||||
|
{ AfxMessageBox(L"結果画像の保存に失敗しました"); return; }
|
||||||
|
AfxMessageBox(L"判定結果画像を保存しました:\n" + dlg.GetPathName(), MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CString BrowseFolder(CWnd* parent, LPCWSTR title)
|
||||||
|
{
|
||||||
|
BROWSEINFO bi = {};
|
||||||
|
bi.hwndOwner = parent->GetSafeHwnd();
|
||||||
|
bi.lpszTitle = title;
|
||||||
|
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
|
||||||
|
LPITEMIDLIST pidl = SHBrowseForFolder(&bi);
|
||||||
|
if (!pidl) return L"";
|
||||||
|
CString path;
|
||||||
|
SHGetPathFromIDList(pidl, path.GetBuffer(MAX_PATH));
|
||||||
|
path.ReleaseBuffer();
|
||||||
|
CoTaskMemFree(pidl);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CString CsvField(const CString& s)
|
||||||
|
{
|
||||||
|
if (s.Find(L',') < 0 && s.Find(L'"') < 0 && s.Find(L'\n') < 0) return s;
|
||||||
|
CString t = s; t.Replace(L"\"", L"\"\"");
|
||||||
|
return L"\"" + t + L"\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnLogSetFolder()
|
||||||
|
{
|
||||||
|
CString f = BrowseFolder(this, L"検査ログの記録フォルダを選んでください");
|
||||||
|
if (f.IsEmpty()) return;
|
||||||
|
m_logFolder = f;
|
||||||
|
AfxGetApp()->WriteProfileString(L"Log", L"Folder", m_logFolder);
|
||||||
|
AfxMessageBox(L"記録フォルダ:\n" + m_logFolder, MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnLogRecord()
|
||||||
|
{
|
||||||
|
if (!m_engine.HasImage()) { AfxMessageBox(L"先に画像を開いてください"); return; }
|
||||||
|
if (m_logFolder.IsEmpty())
|
||||||
|
{
|
||||||
|
OnLogSetFolder();
|
||||||
|
if (m_logFolder.IsEmpty()) return;
|
||||||
|
}
|
||||||
|
RunResult rr = m_engine.Run(m_recipe);
|
||||||
|
|
||||||
|
CTime now = CTime::GetCurrentTime();
|
||||||
|
CString ts = now.Format(L"%Y%m%d_%H%M%S");
|
||||||
|
CString imgName = L"result_" + ts + L".png";
|
||||||
|
CString imgPath = m_logFolder + L"\\" + imgName;
|
||||||
|
m_engine.SaveResultImage(imgPath, m_recipe, rr);
|
||||||
|
|
||||||
|
// 検査要約テキスト + 集計
|
||||||
|
CString detail;
|
||||||
|
int inspectN = 0, okN = 0, ngN = 0;
|
||||||
|
for (const Step& st : m_recipe.steps)
|
||||||
|
{
|
||||||
|
if (!IsInspect(st.kind)) continue;
|
||||||
|
inspectN++;
|
||||||
|
if (st.result.ok) okN++; else ngN++;
|
||||||
|
if (!detail.IsEmpty()) detail += L" | ";
|
||||||
|
detail += StepName(st.kind) + L": " + st.result.text + (st.result.ok ? L"[OK]" : L"[NG]");
|
||||||
|
}
|
||||||
|
CString overall = rr.error ? L"ERROR" : (rr.hasInspect ? (rr.pass ? L"PASS" : L"FAIL") : L"-");
|
||||||
|
|
||||||
|
// パスからファイル名だけ取り出す。
|
||||||
|
auto fileName = [](const CString& p) -> CString {
|
||||||
|
if (p.IsEmpty()) return L"";
|
||||||
|
int s = p.ReverseFind(L'\\'); return (s >= 0) ? p.Mid(s + 1) : p;
|
||||||
|
};
|
||||||
|
|
||||||
|
// CSV 追記(無ければヘッダ付きで新規)
|
||||||
|
CString csvPath = m_logFolder + L"\\inspect_log.csv";
|
||||||
|
bool exists = (::GetFileAttributes(csvPath) != INVALID_FILE_ATTRIBUTES);
|
||||||
|
CFile f;
|
||||||
|
if (f.Open(csvPath, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite))
|
||||||
|
{
|
||||||
|
f.SeekToEnd();
|
||||||
|
CString line;
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
const unsigned char bom[3] = { 0xEF,0xBB,0xBF };
|
||||||
|
f.Write(bom, 3);
|
||||||
|
line = L"日時,総合判定,元画像,レシピ,ステップ数,検査数,合格数,不合格数,検査範囲数,結果画像,詳細\r\n";
|
||||||
|
}
|
||||||
|
CString stamp = now.Format(L"%Y-%m-%d %H:%M:%S");
|
||||||
|
CString cols;
|
||||||
|
cols.Format(L"%d,%d,%d,%d,%d", (int)m_recipe.steps.size(), inspectN, okN, ngN, (int)m_recipe.rois.size());
|
||||||
|
line += stamp + L"," + overall + L","
|
||||||
|
+ CsvField(fileName(m_imagePath)) + L","
|
||||||
|
+ CsvField(fileName(m_recipePath)) + L","
|
||||||
|
+ cols + L","
|
||||||
|
+ CsvField(imgName) + L","
|
||||||
|
+ CsvField(detail) + L"\r\n";
|
||||||
|
int n = WideCharToMultiByte(CP_UTF8, 0, line, line.GetLength(), NULL, 0, NULL, NULL);
|
||||||
|
CStringA a;
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, line, line.GetLength(), a.GetBuffer(n), n, NULL, NULL);
|
||||||
|
a.ReleaseBuffer(n);
|
||||||
|
f.Write((LPCSTR)a, a.GetLength());
|
||||||
|
f.Close();
|
||||||
|
}
|
||||||
|
ShowResult(rr);
|
||||||
|
AfxMessageBox(L"記録しました:\n" + imgName + L"\n" + overall, MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnLogOpenFolder()
|
||||||
|
{
|
||||||
|
if (m_logFolder.IsEmpty()) { AfxMessageBox(L"先に記録フォルダを指定してください"); return; }
|
||||||
|
ShellExecute(GetSafeHwnd(), L"open", m_logFolder, NULL, NULL, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
HBRUSH CMainFrame::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
|
||||||
|
{
|
||||||
|
if (pWnd->GetDlgCtrlID() == IDC_RESULT)
|
||||||
|
{
|
||||||
|
pDC->SetTextColor(RGB(255, 255, 255));
|
||||||
|
pDC->SetBkMode(TRANSPARENT);
|
||||||
|
switch (m_resultState)
|
||||||
|
{
|
||||||
|
case 1: return (HBRUSH)m_brPass;
|
||||||
|
case 2: return (HBRUSH)m_brFail;
|
||||||
|
default: return (HBRUSH)m_brNeutral;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CFrameWnd::OnCtlColor(pDC, pWnd, nCtlColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::ShowResult(const RunResult& rr)
|
||||||
|
{
|
||||||
|
m_resultState = rr.error ? 2 : (rr.hasInspect ? (rr.pass ? 1 : 2) : 0);
|
||||||
|
m_result.SetWindowText(rr.message);
|
||||||
|
m_result.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::RunAndRefresh(int selectAfter)
|
||||||
|
{
|
||||||
|
RunResult rr = m_engine.Run(m_recipe);
|
||||||
|
if (selectAfter != -2)
|
||||||
|
{
|
||||||
|
int s = selectAfter;
|
||||||
|
if (s < 0) s = 0;
|
||||||
|
if (s >= (int)m_recipe.steps.size()) s = (int)m_recipe.steps.size() - 1;
|
||||||
|
m_pipe.SetSel(s);
|
||||||
|
ConfigureSliders(s);
|
||||||
|
}
|
||||||
|
m_pipe.RefreshView();
|
||||||
|
ShowResult(rr);
|
||||||
|
InvalidateRect(CRect(m_bigPreviewRect.left, m_bigPreviewRect.top - 22, m_bigPreviewRect.right + 2, m_bigPreviewRect.bottom + 2), TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- スライダー構成 ---
|
||||||
|
void CMainFrame::ConfigureSliders(int idx)
|
||||||
|
{
|
||||||
|
m_pCount = 0;
|
||||||
|
if (idx >= 0 && idx < (int)m_recipe.steps.size())
|
||||||
|
{
|
||||||
|
const Step& st = m_recipe.steps[idx];
|
||||||
|
m_pCount = (int)st.params.size();
|
||||||
|
if (m_pCount > PARAM_ROWS) m_pCount = PARAM_ROWS;
|
||||||
|
CString t; t.Format(L"② 「%s」の設定", (LPCWSTR)StepName(st.kind));
|
||||||
|
m_settingsTitle.SetWindowText(t);
|
||||||
|
|
||||||
|
for (int i = 0; i < m_pCount; i++)
|
||||||
|
{
|
||||||
|
const StepParam& p = st.params[i];
|
||||||
|
m_pLo[i] = p.lo; m_pHi[i] = p.hi;
|
||||||
|
bool integral = (p.lo == (double)(long)p.lo) && (p.hi == (double)(long)p.hi) && (p.hi - p.lo <= 1000);
|
||||||
|
m_pFloat[i] = !integral;
|
||||||
|
m_slabel[i].SetWindowText(p.label);
|
||||||
|
if (integral)
|
||||||
|
{
|
||||||
|
m_slider[i].SetRange((int)p.lo, (int)p.hi, TRUE);
|
||||||
|
m_slider[i].SetPos((int)p.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_slider[i].SetRange(0, 1000, TRUE);
|
||||||
|
int pos = (int)((p.value - p.lo) / (p.hi - p.lo) * 1000.0 + 0.5);
|
||||||
|
m_slider[i].SetPos(pos);
|
||||||
|
}
|
||||||
|
m_sval[i].SetWindowText(FmtNum(p.value));
|
||||||
|
m_slabel[i].ShowWindow(SW_SHOW);
|
||||||
|
m_slider[i].ShowWindow(SW_SHOW);
|
||||||
|
m_sval[i].ShowWindow(SW_SHOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_settingsTitle.SetWindowText(L"② 選んだステップの設定");
|
||||||
|
}
|
||||||
|
for (int i = m_pCount; i < PARAM_ROWS; i++)
|
||||||
|
{
|
||||||
|
m_slabel[i].ShowWindow(SW_HIDE);
|
||||||
|
m_slider[i].ShowWindow(SW_HIDE);
|
||||||
|
m_sval[i].ShowWindow(SW_HIDE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double CMainFrame::ReadSlider(int i) const
|
||||||
|
{
|
||||||
|
int pos = const_cast<CSliderCtrl&>(m_slider[i]).GetPos();
|
||||||
|
if (m_pFloat[i]) return m_pLo[i] + (m_pHi[i] - m_pLo[i]) * pos / 1000.0;
|
||||||
|
return (double)pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
|
||||||
|
{
|
||||||
|
int sel = Sel();
|
||||||
|
if (sel < 0 || sel >= (int)m_recipe.steps.size()) { CFrameWnd::OnHScroll(nSBCode, nPos, pBar); return; }
|
||||||
|
for (int i = 0; i < m_pCount; i++)
|
||||||
|
{
|
||||||
|
if (pBar && pBar->GetSafeHwnd() == m_slider[i].GetSafeHwnd())
|
||||||
|
{
|
||||||
|
double v = ReadSlider(i);
|
||||||
|
m_recipe.steps[sel].params[i].value = v;
|
||||||
|
m_sval[i].SetWindowText(FmtNum(v));
|
||||||
|
RunAndRefresh(-2); // 選択維持のままライブ再計算
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CFrameWnd::OnHScroll(nSBCode, nPos, pBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- コマンド ---
|
||||||
|
void CMainFrame::OnBtnOpen()
|
||||||
|
{
|
||||||
|
CFileDialog dlg(TRUE, NULL, NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
|
||||||
|
L"画像 (*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff)|*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff|すべて (*.*)|*.*||", this);
|
||||||
|
if (dlg.DoModal() != IDOK) return;
|
||||||
|
if (!m_engine.LoadImage(dlg.GetPathName())) { AfxMessageBox(L"画像の読み込みに失敗しました"); return; }
|
||||||
|
m_imagePath = dlg.GetPathName();
|
||||||
|
RunAndRefresh(Sel() < 0 ? 0 : Sel());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnBtnRun()
|
||||||
|
{
|
||||||
|
if (!m_engine.HasImage()) { AfxMessageBox(L"先に[画像を開く]で画像を読み込んでください"); return; }
|
||||||
|
RunAndRefresh(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnBtnAdd()
|
||||||
|
{
|
||||||
|
int sel = m_combo.GetCurSel();
|
||||||
|
if (sel < 0) return;
|
||||||
|
StepKind k = (StepKind)m_combo.GetItemData(sel);
|
||||||
|
int at = Sel();
|
||||||
|
int insertAt = (at < 0) ? (int)m_recipe.steps.size() : at + 1;
|
||||||
|
m_recipe.steps.insert(m_recipe.steps.begin() + insertAt, MakeStep(k));
|
||||||
|
RunAndRefresh(insertAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnBtnDel()
|
||||||
|
{
|
||||||
|
int at = Sel();
|
||||||
|
if (at < 0 || at >= (int)m_recipe.steps.size()) return;
|
||||||
|
m_recipe.steps.erase(m_recipe.steps.begin() + at);
|
||||||
|
RunAndRefresh(at);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnBtnUp()
|
||||||
|
{
|
||||||
|
int at = Sel();
|
||||||
|
if (at <= 0) return;
|
||||||
|
std::swap(m_recipe.steps[at], m_recipe.steps[at - 1]);
|
||||||
|
RunAndRefresh(at - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnBtnDown()
|
||||||
|
{
|
||||||
|
int at = Sel();
|
||||||
|
if (at < 0 || at >= (int)m_recipe.steps.size() - 1) return;
|
||||||
|
std::swap(m_recipe.steps[at], m_recipe.steps[at + 1]);
|
||||||
|
RunAndRefresh(at + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnBtnTemplate()
|
||||||
|
{
|
||||||
|
Recipe out;
|
||||||
|
if (!wizard::ChooseTemplate(this, out)) return;
|
||||||
|
m_recipe = out;
|
||||||
|
RunAndRefresh(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnBtnWizard()
|
||||||
|
{
|
||||||
|
Recipe out;
|
||||||
|
if (!wizard::BuildWithWizard(this, out)) return;
|
||||||
|
m_recipe = out;
|
||||||
|
RunAndRefresh(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::LoadDemo()
|
||||||
|
{
|
||||||
|
CString path;
|
||||||
|
::GetTempPath(MAX_PATH, path.GetBuffer(MAX_PATH)); path.ReleaseBuffer();
|
||||||
|
path += L"imgtest_demo.bmp";
|
||||||
|
if (!selftest::MakeDemoImage(path)) return;
|
||||||
|
if (!m_engine.LoadImage(path)) return;
|
||||||
|
m_imagePath = path;
|
||||||
|
auto tps = AllTemplates();
|
||||||
|
if (!tps.empty()) m_recipe = tps[0].recipe; // キズ・汚れ
|
||||||
|
// 例として2つの検査範囲(左上と下中央の欠陥)を設定=複数ROIのデモ(右の欠陥は除外)。
|
||||||
|
m_recipe.rois.clear();
|
||||||
|
m_recipe.rois.push_back(CRect(40, 40, 130, 115));
|
||||||
|
m_recipe.rois.push_back(CRect(105, 140, 200, 225));
|
||||||
|
RunAndRefresh((int)m_recipe.steps.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OpenRecipeFile(const CString& path)
|
||||||
|
{
|
||||||
|
Recipe r;
|
||||||
|
if (!LoadRecipe(path, r)) { AfxMessageBox(L"レシピの読み込みに失敗しました:\n" + path); return; }
|
||||||
|
m_recipe = r;
|
||||||
|
m_recipePath = path;
|
||||||
|
AddRecent(path);
|
||||||
|
RunAndRefresh(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnRecipeOpen()
|
||||||
|
{
|
||||||
|
CFileDialog dlg(TRUE, L"itr", NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
|
||||||
|
L"検査レシピ (*.itr)|*.itr|すべて (*.*)|*.*||", this);
|
||||||
|
if (dlg.DoModal() != IDOK) return;
|
||||||
|
OpenRecipeFile(dlg.GetPathName());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnRecipeSave()
|
||||||
|
{
|
||||||
|
CFileDialog dlg(FALSE, L"itr", m_recipePath.IsEmpty() ? L"recipe.itr" : (LPCWSTR)m_recipePath,
|
||||||
|
OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY,
|
||||||
|
L"検査レシピ (*.itr)|*.itr|すべて (*.*)|*.*||", this);
|
||||||
|
if (dlg.DoModal() != IDOK) return;
|
||||||
|
if (!SaveRecipe(dlg.GetPathName(), m_recipe)) { AfxMessageBox(L"レシピの保存に失敗しました"); return; }
|
||||||
|
m_recipePath = dlg.GetPathName();
|
||||||
|
AddRecent(m_recipePath);
|
||||||
|
AfxMessageBox(L"レシピを保存しました:\n" + m_recipePath, MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 最近使ったレシピ(MRU) ---
|
||||||
|
void CMainFrame::LoadRecent()
|
||||||
|
{
|
||||||
|
m_recent.clear();
|
||||||
|
CWinApp* app = AfxGetApp();
|
||||||
|
for (int i = 0; i < RECENT_MAX; i++)
|
||||||
|
{
|
||||||
|
CString key; key.Format(L"File%d", i + 1);
|
||||||
|
CString v = app->GetProfileString(L"Recent", key, L"");
|
||||||
|
if (!v.IsEmpty()) m_recent.push_back(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::SaveRecent()
|
||||||
|
{
|
||||||
|
CWinApp* app = AfxGetApp();
|
||||||
|
for (int i = 0; i < RECENT_MAX; i++)
|
||||||
|
{
|
||||||
|
CString key; key.Format(L"File%d", i + 1);
|
||||||
|
app->WriteProfileString(L"Recent", key, i < (int)m_recent.size() ? m_recent[i] : L"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::AddRecent(const CString& path)
|
||||||
|
{
|
||||||
|
// 既存重複を除去して先頭へ、最大 RECENT_MAX 件。
|
||||||
|
for (int i = (int)m_recent.size() - 1; i >= 0; i--)
|
||||||
|
if (m_recent[i].CompareNoCase(path) == 0) m_recent.erase(m_recent.begin() + i);
|
||||||
|
m_recent.insert(m_recent.begin(), path);
|
||||||
|
if ((int)m_recent.size() > RECENT_MAX) m_recent.resize(RECENT_MAX);
|
||||||
|
SaveRecent();
|
||||||
|
UpdateRecentMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::UpdateRecentMenu()
|
||||||
|
{
|
||||||
|
CMenu* rm = CMenu::FromHandle(m_hRecentMenu);
|
||||||
|
if (!rm) return;
|
||||||
|
while (rm->GetMenuItemCount() > 0) rm->DeleteMenu(0, MF_BYPOSITION);
|
||||||
|
if (m_recent.empty())
|
||||||
|
rm->AppendMenu(MF_STRING | MF_GRAYED, 0, L"(なし)");
|
||||||
|
else
|
||||||
|
for (int i = 0; i < (int)m_recent.size(); i++)
|
||||||
|
{
|
||||||
|
CString disp; disp.Format(L"&%d %s", i + 1, (LPCWSTR)m_recent[i]);
|
||||||
|
rm->AppendMenu(MF_STRING, ID_RECENT_BASE + i, disp);
|
||||||
|
}
|
||||||
|
DrawMenuBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnRecentFile(UINT id)
|
||||||
|
{
|
||||||
|
int idx = (int)id - ID_RECENT_BASE;
|
||||||
|
if (idx < 0 || idx >= (int)m_recent.size()) return;
|
||||||
|
CString path = m_recent[idx];
|
||||||
|
if (::GetFileAttributes(path) == INVALID_FILE_ATTRIBUTES)
|
||||||
|
{
|
||||||
|
if (AfxMessageBox(L"ファイルが見つかりません。一覧から削除しますか?\n" + path, MB_YESNO | MB_ICONWARNING) == IDYES)
|
||||||
|
{
|
||||||
|
m_recent.erase(m_recent.begin() + idx);
|
||||||
|
SaveRecent();
|
||||||
|
UpdateRecentMenu();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OpenRecipeFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMainFrame::OnAppExit() { PostMessage(WM_CLOSE); }
|
||||||
|
|
||||||
|
LRESULT CMainFrame::OnPipeMoveStep(WPARAM w, LPARAM l)
|
||||||
|
{
|
||||||
|
int from = (int)w, slot = (int)l;
|
||||||
|
int n = (int)m_recipe.steps.size();
|
||||||
|
if (from < 0 || from >= n) return 0;
|
||||||
|
if (slot < 0) slot = 0; if (slot > n) slot = n;
|
||||||
|
Step s = m_recipe.steps[from];
|
||||||
|
m_recipe.steps.erase(m_recipe.steps.begin() + from);
|
||||||
|
int dst = (slot > from) ? slot - 1 : slot;
|
||||||
|
if (dst < 0) dst = 0; if (dst > (int)m_recipe.steps.size()) dst = (int)m_recipe.steps.size();
|
||||||
|
m_recipe.steps.insert(m_recipe.steps.begin() + dst, s);
|
||||||
|
RunAndRefresh(dst);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CMainFrame::OnPipeSelChanged(WPARAM w, LPARAM)
|
||||||
|
{
|
||||||
|
int idx = (int)w;
|
||||||
|
ConfigureSliders(idx);
|
||||||
|
InvalidateRect(CRect(m_bigPreviewRect.left, m_bigPreviewRect.top - 22, m_bigPreviewRect.right + 2, m_bigPreviewRect.bottom + 2), TRUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "core/Recipe.h"
|
||||||
|
#include "core/InspectEngine.h"
|
||||||
|
#include "PipelineView.h"
|
||||||
|
|
||||||
|
#define IDC_BTN_OPEN 2001
|
||||||
|
#define IDC_BTN_RUN 2002
|
||||||
|
#define IDC_BTN_ADD 2003
|
||||||
|
#define IDC_BTN_DEL 2004
|
||||||
|
#define IDC_BTN_UP 2005
|
||||||
|
#define IDC_BTN_DOWN 2006
|
||||||
|
#define IDC_BTN_TMPL 2007
|
||||||
|
#define IDC_BTN_WIZ 2008
|
||||||
|
#define IDC_COMBO 2011
|
||||||
|
#define IDC_PIPE 2020
|
||||||
|
#define IDC_RESULT 2021
|
||||||
|
#define IDC_SLIDER0 3001 // パラメータ スライダー(4本)
|
||||||
|
#define PARAM_ROWS 4
|
||||||
|
|
||||||
|
// メニュー コマンド ID
|
||||||
|
#define ID_FILE_OPENIMAGE 40001
|
||||||
|
#define ID_RECIPE_OPEN 40002
|
||||||
|
#define ID_RECIPE_SAVE 40003
|
||||||
|
#define ID_APP_EXIT 40004
|
||||||
|
#define ID_RESULT_SAVE 40010
|
||||||
|
#define ID_ROI_CLEAR 40020
|
||||||
|
#define ID_ROI_UNDO 40021
|
||||||
|
#define ID_LOG_SETFOLDER 40030
|
||||||
|
#define ID_LOG_RECORD 40031
|
||||||
|
#define ID_LOG_OPENFOLDER 40032
|
||||||
|
#define ID_RECENT_BASE 41000
|
||||||
|
#define RECENT_MAX 5
|
||||||
|
|
||||||
|
// メインウィンドウ。左=ビジュアル・パイプライン / 右=選択ステップの調整(スライダー)+追加。
|
||||||
|
class CMainFrame : public CFrameWnd
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CMainFrame();
|
||||||
|
void LoadDemo(); // /demo: デモ画像 + キズ検査テンプレートを読み込み実行
|
||||||
|
|
||||||
|
protected:
|
||||||
|
afx_msg int OnCreate(LPCREATESTRUCT lpcs);
|
||||||
|
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||||
|
afx_msg void OnPaint();
|
||||||
|
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
|
||||||
|
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar);
|
||||||
|
afx_msg void OnBtnOpen();
|
||||||
|
afx_msg void OnBtnRun();
|
||||||
|
afx_msg void OnBtnAdd();
|
||||||
|
afx_msg void OnBtnDel();
|
||||||
|
afx_msg void OnBtnUp();
|
||||||
|
afx_msg void OnBtnDown();
|
||||||
|
afx_msg void OnBtnTemplate();
|
||||||
|
afx_msg void OnBtnWizard();
|
||||||
|
afx_msg void OnRecipeOpen();
|
||||||
|
afx_msg void OnRecipeSave();
|
||||||
|
afx_msg void OnAppExit();
|
||||||
|
afx_msg void OnRoiClear();
|
||||||
|
afx_msg void OnRoiUndo();
|
||||||
|
afx_msg void OnResultSave();
|
||||||
|
afx_msg void OnLogSetFolder();
|
||||||
|
afx_msg void OnLogRecord();
|
||||||
|
afx_msg void OnLogOpenFolder();
|
||||||
|
afx_msg void OnLButtonDown(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnLButtonUp(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnRButtonDown(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg LRESULT OnPipeSelChanged(WPARAM w, LPARAM l);
|
||||||
|
afx_msg LRESULT OnPipeMoveStep(WPARAM w, LPARAM l);
|
||||||
|
afx_msg void OnRecentFile(UINT id);
|
||||||
|
DECLARE_MESSAGE_MAP()
|
||||||
|
|
||||||
|
private:
|
||||||
|
void BuildMenu();
|
||||||
|
void LoadRecent();
|
||||||
|
void SaveRecent();
|
||||||
|
void AddRecent(const CString& path);
|
||||||
|
void UpdateRecentMenu();
|
||||||
|
void OpenRecipeFile(const CString& path);
|
||||||
|
void LayoutControls();
|
||||||
|
void RunAndRefresh(int selectAfter = -2); // -2=選択維持
|
||||||
|
void ConfigureSliders(int stepIdx); // 選択ステップ→スライダー構成
|
||||||
|
double ReadSlider(int i) const; // スライダー値→パラメータ値
|
||||||
|
void ShowResult(const core::RunResult& rr);
|
||||||
|
int Sel() const;
|
||||||
|
static CString FmtNum(double v);
|
||||||
|
|
||||||
|
// 上部
|
||||||
|
CButton m_btnOpen, m_btnTemplate, m_btnWizard, m_btnRun;
|
||||||
|
CStatic m_result;
|
||||||
|
// 左
|
||||||
|
CPipelineView m_pipe;
|
||||||
|
// 右: 設定スライダー
|
||||||
|
CStatic m_settingsTitle;
|
||||||
|
CStatic m_slabel[PARAM_ROWS];
|
||||||
|
CSliderCtrl m_slider[PARAM_ROWS];
|
||||||
|
CStatic m_sval[PARAM_ROWS];
|
||||||
|
// 右: 追加パレット
|
||||||
|
CStatic m_addTitle;
|
||||||
|
CComboBox m_combo;
|
||||||
|
CButton m_btnAdd, m_btnDel, m_btnUp, m_btnDown;
|
||||||
|
|
||||||
|
CFont m_bigFont, m_uiFont;
|
||||||
|
CBrush m_brPass, m_brFail, m_brNeutral;
|
||||||
|
|
||||||
|
CMenu m_menu;
|
||||||
|
CString m_recipePath;
|
||||||
|
CString m_logFolder;
|
||||||
|
CString m_imagePath; // 最後に開いた画像(CSV用)
|
||||||
|
std::vector<CString> m_recent; // 最近使ったレシピ
|
||||||
|
HMENU m_hRecentMenu = NULL; // 「最近使ったレシピ」サブメニュー
|
||||||
|
|
||||||
|
CRect m_bigPreviewRect;
|
||||||
|
CRect m_previewFitRect; // プレビューで実際に画像が描かれた矩形(ROI 座標変換用)
|
||||||
|
bool m_roiDragging = false;
|
||||||
|
CPoint m_roiStart, m_roiCur;
|
||||||
|
int m_resultState = 0; // 0=neutral 1=pass 2=fail
|
||||||
|
|
||||||
|
// スライダー ↔ パラメータ 変換情報
|
||||||
|
int m_pCount = 0;
|
||||||
|
double m_pLo[PARAM_ROWS] = {};
|
||||||
|
double m_pHi[PARAM_ROWS] = {};
|
||||||
|
bool m_pFloat[PARAM_ROWS] = {};
|
||||||
|
|
||||||
|
core::Recipe m_recipe;
|
||||||
|
core::InspectEngine m_engine;
|
||||||
|
};
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
//
|
||||||
|
// PipelineView.cpp - ビジュアル・パイプライン(カード + サムネイル + 合否)。
|
||||||
|
//
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "PipelineView.h"
|
||||||
|
#include "core/StepCatalog.h"
|
||||||
|
|
||||||
|
using namespace core;
|
||||||
|
|
||||||
|
BEGIN_MESSAGE_MAP(CPipelineView, CWnd)
|
||||||
|
ON_WM_PAINT()
|
||||||
|
ON_WM_ERASEBKGND()
|
||||||
|
ON_WM_LBUTTONDOWN()
|
||||||
|
ON_WM_MOUSEMOVE()
|
||||||
|
ON_WM_LBUTTONUP()
|
||||||
|
ON_WM_VSCROLL()
|
||||||
|
ON_WM_MOUSEWHEEL()
|
||||||
|
ON_WM_SIZE()
|
||||||
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
CPipelineView::CPipelineView() {}
|
||||||
|
|
||||||
|
BOOL CPipelineView::Create(CWnd* parent, UINT id)
|
||||||
|
{
|
||||||
|
// 独自ウィンドウクラス(背景ブラシ・矢印カーソル)。
|
||||||
|
LPCTSTR cls = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,
|
||||||
|
::LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_WINDOW + 1), NULL);
|
||||||
|
if (!CWnd::Create(cls, L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_BORDER,
|
||||||
|
CRect(0, 0, 10, 10), parent, id))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
m_fTitle.CreatePointFont(115, L"Yu Gothic UI");
|
||||||
|
m_fHint.CreatePointFont(90, L"Yu Gothic UI");
|
||||||
|
m_fBadge.CreatePointFont(130, L"Yu Gothic UI");
|
||||||
|
m_fNum.CreatePointFont(120, L"Yu Gothic UI");
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::SetData(Recipe* recipe, InspectEngine* engine)
|
||||||
|
{
|
||||||
|
m_recipe = recipe;
|
||||||
|
m_engine = engine;
|
||||||
|
RefreshView();
|
||||||
|
}
|
||||||
|
|
||||||
|
int CPipelineView::ContentHeight() const
|
||||||
|
{
|
||||||
|
int n = m_recipe ? (int)m_recipe->steps.size() : 0;
|
||||||
|
return 8 + n * (CardHeight() + 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
CRect CPipelineView::CardRect(int i) const
|
||||||
|
{
|
||||||
|
CRect cr; GetClientRect(&cr);
|
||||||
|
int x = 8, w = cr.Width() - 16;
|
||||||
|
int y = 8 + i * (CardHeight() + 8) - m_scrollY;
|
||||||
|
return CRect(x, y, x + w, y + CardHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::UpdateScroll()
|
||||||
|
{
|
||||||
|
CRect cr; GetClientRect(&cr);
|
||||||
|
int content = ContentHeight();
|
||||||
|
SCROLLINFO si = { sizeof(si) };
|
||||||
|
si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
|
||||||
|
si.nMin = 0;
|
||||||
|
si.nMax = content > 0 ? content - 1 : 0;
|
||||||
|
si.nPage = cr.Height();
|
||||||
|
int maxPos = content - cr.Height();
|
||||||
|
if (maxPos < 0) maxPos = 0;
|
||||||
|
if (m_scrollY > maxPos) m_scrollY = maxPos;
|
||||||
|
if (m_scrollY < 0) m_scrollY = 0;
|
||||||
|
si.nPos = m_scrollY;
|
||||||
|
SetScrollInfo(SB_VERT, &si, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::RefreshView()
|
||||||
|
{
|
||||||
|
if (m_sel >= 0 && m_recipe && m_sel >= (int)m_recipe->steps.size())
|
||||||
|
m_sel = (int)m_recipe->steps.size() - 1;
|
||||||
|
UpdateScroll();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::SetSel(int idx)
|
||||||
|
{
|
||||||
|
m_sel = idx;
|
||||||
|
// 選択カードを見えるようにスクロール。
|
||||||
|
if (m_recipe && idx >= 0 && idx < (int)m_recipe->steps.size())
|
||||||
|
{
|
||||||
|
CRect cr; GetClientRect(&cr);
|
||||||
|
int top = 8 + idx * (CardHeight() + 8);
|
||||||
|
int bot = top + CardHeight();
|
||||||
|
if (top - m_scrollY < 0) m_scrollY = top;
|
||||||
|
else if (bot - m_scrollY > cr.Height()) m_scrollY = bot - cr.Height();
|
||||||
|
UpdateScroll();
|
||||||
|
}
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL CPipelineView::OnEraseBkgnd(CDC*) { return TRUE; } // ちらつき防止(OnPaintで塗る)
|
||||||
|
|
||||||
|
void CPipelineView::OnSize(UINT nType, int cx, int cy)
|
||||||
|
{
|
||||||
|
CWnd::OnSize(nType, cx, cy);
|
||||||
|
UpdateScroll();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::OnPaint()
|
||||||
|
{
|
||||||
|
CPaintDC dc(this);
|
||||||
|
CRect cr; GetClientRect(&cr);
|
||||||
|
|
||||||
|
// ダブルバッファ。
|
||||||
|
CDC mem; mem.CreateCompatibleDC(&dc);
|
||||||
|
CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, cr.Width(), cr.Height());
|
||||||
|
CBitmap* pOld = mem.SelectObject(&bmp);
|
||||||
|
|
||||||
|
mem.FillSolidRect(cr, RGB(244, 246, 248));
|
||||||
|
|
||||||
|
if (!m_recipe || m_recipe->steps.empty())
|
||||||
|
{
|
||||||
|
mem.SetBkMode(TRANSPARENT);
|
||||||
|
mem.SetTextColor(RGB(140, 140, 140));
|
||||||
|
CFont* of = mem.SelectObject(&m_fHint);
|
||||||
|
mem.DrawText(L"ステップがありません。\n上の[テンプレート]や[ウィザード]、右の[追加]で作成できます。",
|
||||||
|
cr, DT_CENTER | DT_VCENTER | DT_WORDBREAK);
|
||||||
|
mem.SelectObject(of);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = 0; i < (int)m_recipe->steps.size(); i++)
|
||||||
|
{
|
||||||
|
CRect rc = CardRect(i);
|
||||||
|
if (rc.bottom < 0 || rc.top > cr.Height()) continue; // 画面外
|
||||||
|
DrawCard(&mem, i, rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドラッグ中: 挿入位置インジケータ(青い太線)
|
||||||
|
if (m_dragging && m_dragFrom >= 0)
|
||||||
|
{
|
||||||
|
int slot = InsertSlotAt(m_dragY);
|
||||||
|
int unit = CardHeight() + 8;
|
||||||
|
int y = 8 + slot * unit - 4 - m_scrollY;
|
||||||
|
mem.FillSolidRect(CRect(6, y - 2, cr.Width() - 6, y + 2), RGB(60, 130, 220));
|
||||||
|
// つまんでいるカードの番号を近くに表示
|
||||||
|
mem.SetBkMode(TRANSPARENT);
|
||||||
|
mem.SelectObject(&m_fHint);
|
||||||
|
mem.SetTextColor(RGB(60, 130, 220));
|
||||||
|
CString tip; tip.Format(L"ステップ%d をここへ移動", m_dragFrom + 1);
|
||||||
|
mem.DrawText(tip, CRect(12, y + 4, cr.Width() - 6, y + 24), DT_LEFT | DT_SINGLELINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.BitBlt(0, 0, cr.Width(), cr.Height(), &mem, 0, 0, SRCCOPY);
|
||||||
|
mem.SelectObject(pOld);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::DrawCard(CDC* dc, int i, const CRect& rc)
|
||||||
|
{
|
||||||
|
const Step& st = m_recipe->steps[i];
|
||||||
|
bool inspect = IsInspect(st.kind);
|
||||||
|
bool sel = (i == m_sel);
|
||||||
|
|
||||||
|
// 背景 + 枠
|
||||||
|
COLORREF bg = sel ? RGB(225, 238, 252) : RGB(255, 255, 255);
|
||||||
|
dc->FillSolidRect(rc, bg);
|
||||||
|
CPen penBorder(PS_SOLID, sel ? 2 : 1, sel ? RGB(60, 130, 220) : RGB(210, 214, 218));
|
||||||
|
CPen* op = dc->SelectObject(&penBorder);
|
||||||
|
CBrush* ob = (CBrush*)dc->SelectStockObject(NULL_BRUSH);
|
||||||
|
dc->Rectangle(rc);
|
||||||
|
dc->SelectObject(ob);
|
||||||
|
dc->SelectObject(op);
|
||||||
|
|
||||||
|
// 左の色ストライプ(処理=青 / 検査=橙)
|
||||||
|
CRect stripe(rc.left, rc.top, rc.left + 6, rc.bottom);
|
||||||
|
dc->FillSolidRect(stripe, inspect ? RGB(240, 150, 40) : RGB(70, 130, 190));
|
||||||
|
|
||||||
|
dc->SetBkMode(TRANSPARENT);
|
||||||
|
|
||||||
|
// 番号バッジ
|
||||||
|
int cx = rc.left + 32, cy = rc.top + 26, r = 15;
|
||||||
|
CBrush numBr(inspect ? RGB(240, 150, 40) : RGB(70, 130, 190));
|
||||||
|
CBrush* obn = dc->SelectObject(&numBr);
|
||||||
|
CPen* opn = (CPen*)dc->SelectStockObject(NULL_PEN);
|
||||||
|
dc->Ellipse(cx - r, cy - r, cx + r, cy + r);
|
||||||
|
dc->SelectObject(opn);
|
||||||
|
dc->SelectObject(obn);
|
||||||
|
CString no; no.Format(L"%d", i + 1);
|
||||||
|
CFont* of = dc->SelectObject(&m_fNum);
|
||||||
|
dc->SetTextColor(RGB(255, 255, 255));
|
||||||
|
CRect nr(cx - r, cy - r, cx + r, cy + r);
|
||||||
|
dc->DrawText(no, nr, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
|
// サムネイル域(右)
|
||||||
|
int thumbW = 132, thumbH = CardHeight() - 20;
|
||||||
|
CRect thumb(rc.right - thumbW - 10, rc.top + 10, rc.right - 10, rc.top + 10 + thumbH);
|
||||||
|
dc->FillSolidRect(thumb, RGB(30, 30, 30));
|
||||||
|
if (m_engine && m_engine->HasShot(i))
|
||||||
|
{
|
||||||
|
CRect inner = thumb; inner.DeflateRect(2, 2);
|
||||||
|
CRect fit = m_engine->DrawShot(dc, inner, i);
|
||||||
|
// ROI(複数)を黄枠でオーバーレイ
|
||||||
|
if (m_recipe && !m_recipe->rois.empty() && !fit.IsRectEmpty())
|
||||||
|
{
|
||||||
|
int sw = m_engine->SrcWidth(), sh = m_engine->SrcHeight();
|
||||||
|
if (sw > 0 && sh > 0)
|
||||||
|
{
|
||||||
|
double kx = (double)fit.Width() / sw, ky = (double)fit.Height() / sh;
|
||||||
|
CPen yp(PS_SOLID, 1, RGB(255, 210, 0));
|
||||||
|
CPen* oyp = dc->SelectObject(&yp);
|
||||||
|
dc->SelectStockObject(NULL_BRUSH);
|
||||||
|
for (const CRect& roi : m_recipe->rois)
|
||||||
|
{
|
||||||
|
CRect rr(fit.left + (int)(roi.left * kx), fit.top + (int)(roi.top * ky),
|
||||||
|
fit.left + (int)(roi.right * kx), fit.top + (int)(roi.bottom * ky));
|
||||||
|
dc->Rectangle(rr);
|
||||||
|
}
|
||||||
|
dc->SelectObject(oyp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dc->SelectObject(&m_fHint);
|
||||||
|
dc->SetTextColor(RGB(160, 160, 160));
|
||||||
|
dc->DrawText(L"実行前", thumb, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
}
|
||||||
|
CPen tb(PS_SOLID, 1, RGB(120, 120, 120));
|
||||||
|
CPen* opt = dc->SelectObject(&tb);
|
||||||
|
dc->SelectStockObject(NULL_BRUSH);
|
||||||
|
dc->Rectangle(thumb);
|
||||||
|
dc->SelectObject(opt);
|
||||||
|
|
||||||
|
// テキスト域
|
||||||
|
// ※ CDiBitmap::Draw(サムネイル)が DC の BkMode を OPAQUE/暗色に戻すため、
|
||||||
|
// 文字描画の前に透過へ再設定する(でないと暗地に明字で反転して見える)。
|
||||||
|
dc->SetBkMode(TRANSPARENT);
|
||||||
|
int tx = rc.left + 56;
|
||||||
|
int tw = thumb.left - tx - 10;
|
||||||
|
// タイトル
|
||||||
|
dc->SelectObject(&m_fTitle);
|
||||||
|
dc->SetTextColor(RGB(30, 40, 50));
|
||||||
|
CRect tr(tx, rc.top + 10, tx + tw, rc.top + 34);
|
||||||
|
dc->DrawText(StepName(st.kind), tr, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||||
|
// 説明
|
||||||
|
dc->SelectObject(&m_fHint);
|
||||||
|
dc->SetTextColor(RGB(110, 120, 130));
|
||||||
|
CRect hr(tx, rc.top + 36, tx + tw, rc.top + 74);
|
||||||
|
dc->DrawText(StepHint(st.kind), hr, DT_LEFT | DT_WORDBREAK | DT_END_ELLIPSIS);
|
||||||
|
|
||||||
|
// 検査ステップは合否バッジ + 測定値
|
||||||
|
if (inspect)
|
||||||
|
{
|
||||||
|
bool ran = st.result.ran;
|
||||||
|
bool ok = st.result.ok;
|
||||||
|
CString badge = !ran ? L"未実行" : (ok ? L"✓ 合格" : L"✕ 不合格");
|
||||||
|
COLORREF bc = !ran ? RGB(150, 150, 150) : (ok ? RGB(40, 170, 80) : RGB(220, 60, 60));
|
||||||
|
CRect br(tx, rc.bottom - 30, tx + 96, rc.bottom - 6);
|
||||||
|
dc->FillSolidRect(br, bc);
|
||||||
|
dc->SelectObject(&m_fBadge);
|
||||||
|
dc->SetTextColor(RGB(255, 255, 255));
|
||||||
|
dc->DrawText(badge, br, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
// 測定値テキスト
|
||||||
|
if (ran && !st.result.text.IsEmpty())
|
||||||
|
{
|
||||||
|
dc->SelectObject(&m_fHint);
|
||||||
|
dc->SetTextColor(RGB(70, 80, 90));
|
||||||
|
CRect mr(tx + 104, rc.bottom - 30, thumb.left - 10, rc.bottom - 6);
|
||||||
|
dc->DrawText(st.result.text, mr, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dc->SelectObject(of);
|
||||||
|
}
|
||||||
|
|
||||||
|
// クライアント Y から挿入スロット(0..n)を求める。
|
||||||
|
int CPipelineView::InsertSlotAt(int y) const
|
||||||
|
{
|
||||||
|
int n = m_recipe ? (int)m_recipe->steps.size() : 0;
|
||||||
|
int unit = CardHeight() + 8;
|
||||||
|
int slot = (int)((double)(y + m_scrollY - 8) / unit + 0.5);
|
||||||
|
if (slot < 0) slot = 0;
|
||||||
|
if (slot > n) slot = n;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::OnLButtonDown(UINT, CPoint pt)
|
||||||
|
{
|
||||||
|
if (!m_recipe) return;
|
||||||
|
for (int i = 0; i < (int)m_recipe->steps.size(); i++)
|
||||||
|
{
|
||||||
|
if (CardRect(i).PtInRect(pt))
|
||||||
|
{
|
||||||
|
m_sel = i;
|
||||||
|
Invalidate();
|
||||||
|
GetParent()->SendMessage(WM_PV_SELCHANGED, (WPARAM)i, 0);
|
||||||
|
// ドラッグ開始候補
|
||||||
|
m_dragFrom = i;
|
||||||
|
m_dragging = false;
|
||||||
|
m_dragStart = pt;
|
||||||
|
SetCapture();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::OnMouseMove(UINT, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_dragFrom < 0) return;
|
||||||
|
if (!m_dragging)
|
||||||
|
{
|
||||||
|
if (abs(pt.y - m_dragStart.y) + abs(pt.x - m_dragStart.x) > 5)
|
||||||
|
{
|
||||||
|
m_dragging = true;
|
||||||
|
SetCursor(::LoadCursor(NULL, IDC_SIZENS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_dragging)
|
||||||
|
{
|
||||||
|
m_dragY = pt.y;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::OnLButtonUp(UINT, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_dragFrom < 0) return;
|
||||||
|
int from = m_dragFrom;
|
||||||
|
bool wasDragging = m_dragging;
|
||||||
|
m_dragFrom = -1;
|
||||||
|
m_dragging = false;
|
||||||
|
if (GetCapture() == this) ReleaseCapture();
|
||||||
|
|
||||||
|
if (wasDragging)
|
||||||
|
{
|
||||||
|
int slot = InsertSlotAt(pt.y);
|
||||||
|
// 実質移動が無いケース(同じ位置)は無視
|
||||||
|
if (slot != from && slot != from + 1)
|
||||||
|
GetParent()->SendMessage(WM_PV_MOVESTEP, (WPARAM)from, (LPARAM)slot);
|
||||||
|
else
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPipelineView::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar*)
|
||||||
|
{
|
||||||
|
CRect cr; GetClientRect(&cr);
|
||||||
|
int line = 24, page = cr.Height();
|
||||||
|
int y = m_scrollY;
|
||||||
|
switch (nSBCode)
|
||||||
|
{
|
||||||
|
case SB_LINEUP: y -= line; break;
|
||||||
|
case SB_LINEDOWN: y += line; break;
|
||||||
|
case SB_PAGEUP: y -= page; break;
|
||||||
|
case SB_PAGEDOWN: y += page; break;
|
||||||
|
case SB_THUMBTRACK:
|
||||||
|
case SB_THUMBPOSITION: y = nPos; break;
|
||||||
|
}
|
||||||
|
m_scrollY = y;
|
||||||
|
UpdateScroll();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL CPipelineView::OnMouseWheel(UINT, short zDelta, CPoint)
|
||||||
|
{
|
||||||
|
m_scrollY -= (zDelta / WHEEL_DELTA) * 48;
|
||||||
|
UpdateScroll();
|
||||||
|
Invalidate();
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "core/Recipe.h"
|
||||||
|
#include "core/InspectEngine.h"
|
||||||
|
|
||||||
|
// 親へ「カード選択が変わった」を通知(wParam = 選択index)。
|
||||||
|
#define WM_PV_SELCHANGED (WM_APP + 11)
|
||||||
|
// 親へ「カードを移動した」を通知(wParam = 元index, lParam = 挿入スロット 0..n)。
|
||||||
|
#define WM_PV_MOVESTEP (WM_APP + 12)
|
||||||
|
|
||||||
|
// ビジュアル・パイプライン表示: 各ステップをカード化し、適用後サムネイル + 合否を縦に並べる。
|
||||||
|
class CPipelineView : public CWnd
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CPipelineView();
|
||||||
|
BOOL Create(CWnd* parent, UINT id);
|
||||||
|
|
||||||
|
void SetData(core::Recipe* recipe, core::InspectEngine* engine);
|
||||||
|
void RefreshView(); // スクロール範囲再計算 + 再描画
|
||||||
|
void SetSel(int idx);
|
||||||
|
int GetSel() const { return m_sel; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
afx_msg void OnPaint();
|
||||||
|
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||||
|
afx_msg void OnLButtonDown(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnLButtonUp(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar);
|
||||||
|
afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
|
||||||
|
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||||
|
DECLARE_MESSAGE_MAP()
|
||||||
|
|
||||||
|
private:
|
||||||
|
int CardHeight() const { return 104; }
|
||||||
|
int ContentHeight() const;
|
||||||
|
CRect CardRect(int i) const; // クライアント座標(スクロール込み)
|
||||||
|
void UpdateScroll();
|
||||||
|
void DrawCard(CDC* dc, int i, const CRect& rc);
|
||||||
|
|
||||||
|
core::Recipe* m_recipe = nullptr;
|
||||||
|
core::InspectEngine* m_engine = nullptr;
|
||||||
|
int m_sel = -1;
|
||||||
|
int m_scrollY = 0;
|
||||||
|
// ドラッグ並べ替え
|
||||||
|
int m_dragFrom = -1; // 押下したカード(まだ移動と確定していない)
|
||||||
|
bool m_dragging = false; // しきい値を超えて移動中
|
||||||
|
CPoint m_dragStart;
|
||||||
|
int m_dragY = 0; // 現在のマウスY(挿入位置インジケータ用)
|
||||||
|
int InsertSlotAt(int y) const; // クライアントYから挿入スロット 0..n
|
||||||
|
CFont m_fTitle, m_fHint, m_fBadge, m_fNum;
|
||||||
|
};
|
||||||
+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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
// SelfTest.h - コマンドライン /selftest 用のヘッドレス自己検証。
|
||||||
|
namespace selftest {
|
||||||
|
int Run(); // 0=PASS, 1=FAIL。selftest_result.txt を出力。
|
||||||
|
bool MakeDemoImage(LPCWSTR path); // デモ用: 明るい地に暗い欠陥数点の 24bit BMP を作る。
|
||||||
|
bool SaveDemoResult(LPCWSTR path); // デモ画像+キズ検査+ROI の注釈付き結果画像を保存。
|
||||||
|
}
|
||||||
+148
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "core/Recipe.h"
|
||||||
|
|
||||||
|
namespace wizard {
|
||||||
|
// 対話式ウィザードでレシピを作る。作成したら true。
|
||||||
|
bool BuildWithWizard(CWnd* parent, core::Recipe& out);
|
||||||
|
// 目的テンプレートを選んでレシピを作る。選んだら true。
|
||||||
|
bool ChooseTemplate(CWnd* parent, core::Recipe& out);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
//
|
||||||
|
// stdafx.h - プリコンパイル済みヘッダ。
|
||||||
|
// yfoximg(ImgProcessing.h)はここには入れない。裸の min/max マクロ前提のため、
|
||||||
|
// 必要な TU(InspectEngine.cpp / SelfTest.cpp)だけで include する。
|
||||||
|
//
|
||||||
|
#include "targetver.h"
|
||||||
|
|
||||||
|
#ifndef VC_EXTRALEAN
|
||||||
|
#define VC_EXTRALEAN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <afxwin.h> // MFC コア / 標準
|
||||||
|
#include <afxext.h> // MFC 拡張
|
||||||
|
#include <afxcmn.h> // CSliderCtrl 等 コモンコントロール
|
||||||
|
#include <afxdlgs.h> // CFileDialog
|
||||||
|
#include <afxtaskdialog.h> // CTaskDialog(ウィザード/テンプレート選択)
|
||||||
|
|
||||||
|
// CTaskDialog / 新しい外観のため Common Controls v6 に依存させる。
|
||||||
|
#pragma comment(linker, "\"/manifestdependency:type='win32' \
|
||||||
|
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
|
||||||
|
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <gdiplus.h> // GDI+(CImage/CImageEx が使用)
|
||||||
|
#pragma comment(lib, "gdiplus.lib")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <sdkddkver.h>
|
||||||
Reference in New Issue
Block a user