using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Eventing.Reader;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Aruco;
namespace TIASshot {
internal abstract class CameraBase {
// インターフェース
public abstract bool Connect();
public abstract void Disconnect();
protected abstract void Shot(int numImages=1, int interval=0);
// プロパティ
public string DeviceName { get; protected set; } = "Unknown";
public string SerialNumber { get; protected set; }
public string ErrorMsg { get; protected set; }
// 派生クラスで使用するメンバ
protected Form1 _form;
protected int _calibrating = 0;
protected bool _calibrated = false;
protected Bitmap[] _bmps = new Bitmap[2];
protected int _bmpIndex = 0;
protected bool _isPreview = false;
protected List<Mat> _chartMasks = new List<Mat>();
protected Mat _convRGB2SRGB;
// プライベートメンバ
readonly Dictionary ARDict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50);
readonly Point2f[] PointsDst40 = new Point2f[] {
new Point2f(345, 130),new Point2f(465, 130),new Point2f(465, 250),new Point2f(345, 250),
};
readonly Point2f[] PointsDst41 = new Point2f[]{
new Point2f(345, 1200), new Point2f(465, 1200), new Point2f(465, 1320), new Point2f(345, 1320),
};
readonly Mat TCC_SRGB;
readonly Mat TCC_XYZ;
readonly float UpdateRate;
int _detectionCount = 0;
Point2f _lastPosition = new Point2f(0, 0);
Mat _tccRois; // TCCのROI検出結果画像
string _saveFolder = "";
/// <summary>
/// カメラの基本クラス
/// </summary>
public CameraBase(Form1 form) {
_form = form;
Config.Load();
UpdateRate = Config.GetFloat("UpdateRate");
TCC_SRGB = LoadMatFromCsv(Config.GetString("TccSrgbTableFile"));
TCC_XYZ = LoadMatFromCsv(Config.GetString("TccXyzTableFile"));
}
/// <summary>
/// 起動チェック
/// </summary>
/// <returns></returns>
protected bool BootCheck() {
if (!Config.IsLoaded()) {
ErrorMsg = "設定ファイル(Config.xml)の読み込みに失敗しました.\r\n終了します.";
return false;
}
if (TCC_SRGB is null) {
ErrorMsg = $"ファイル({Config.GetString("TccSrgbTableFile")})の読み込みに失敗しました.\r\n終了します.";
return false;
}
if (TCC_XYZ is null) {
ErrorMsg = $"ファイル({Config.GetString("TccXyzTableFile")})の読み込みに失敗しました.\r\n終了します.";
return false;
}
return true;
}
/// <summary>
/// 画像撮影1枚
/// </summary>
public void ShotOne() {
SetSaveFolder(_form.GetDataName());
Shot();
_form.ShowMessage("1枚撮影 保存しました");
}
/// <summary>
/// 複数画像撮影
/// </summary>
public void ShotMulti() {
SetSaveFolder(_form.GetDataName());
Shot(_form.GetNumMultiShots(), _form.GetMultiShotsInterval());
_form.ShowMessage("連続撮影 保存しました");
}
/// <summary>
/// 画像保存処理
/// </summary>
/// <param name="img"></param>
/// <param name="idx"></param>
protected void SaveImages(Mat img, int idx) {
var filename = Config.GetString("ImageRgbFile");
filename = filename.Replace("0000", $"{idx + 1:0000}");
Cv2.ImWrite(Path.Combine(_saveFolder, filename), img);
using (var converted = ConvertImage(img, _convRGB2SRGB)) {
filename = Config.GetString("ImageSrgbFile");
filename = filename.Replace("0000", $"{idx + 1:0000}");
Cv2.ImWrite(Path.Combine(_saveFolder, filename), converted);
}
}
/// <summary>
/// チャートの検出
/// </summary>
/// <param name="img"></param>
protected void DetectChart(Mat img) {
// ARマーカー検出
CvAruco.DetectMarkers(img, ARDict, out var corners, out var ids,
new DetectorParameters(), out var rejectedImgPoints);
if (ids.Length < 1) return;
// マーカー座標格納
var ptsPict = new List<Point2f>();
var ptsModel = new List<Point2f>();
Point2f position = new Point2f();
float y40 = 0, y41 = 0;
for (int i = 0; i < ids.Length; i++) {
if (ids[i] == 40) {
ptsPict.AddRange(corners[i]);
ptsModel.AddRange(PointsDst40);
position = corners[i][3];
y40 = corners[i][3].Y;
}
if (ids[i] == 41) {
ptsPict.AddRange(corners[i]);
ptsModel.AddRange(PointsDst41);
y41 = corners[i][3].Y;
}
}
if (ptsPict.Count < 8) return;
if (y40 > y41) {
_form.ShowMessage("舌診チャートが上下逆方向です");
return;
}
// チャートの固定判定
_form.ShowMessage("舌診チャートの検出中");
var dist = (float)position.DistanceTo(_lastPosition);
if (dist < Config.GetFloat("ChartSetCriteria")) {
_detectionCount++;
} else {
_detectionCount = 0;
}
_lastPosition = position;
if (_detectionCount < Config.GetInt("ChartSetCount")) return;
// ホモグラフィの計算
var matPtsPict = Mat.FromArray(ptsPict);
var matPtsModel = Mat.FromArray(ptsModel);
var matH = Cv2.FindHomography(matPtsModel, matPtsPict);
var imgF = new Mat(1545, 810, MatType.CV_8UC3);
Cv2.WarpPerspective(img, imgF, matH, imgF.Size());
// チャートマスク作成
_chartMasks.Clear();
var roiSize = ptsPict.Count < 8 ? 60 : 80;
_tccRois = img.Clone();
for (int i = 0; i < 24; i++) {
var row = i % 6;
var col = i / 6;
var x = 581 - col * 144 + (ptsPict.Count < 8 ? 10 : 0);
var y = 318 + row * 144 + (ptsPict.Count < 8 ? 10 : 0);
var roi = new Rect(x, y, roiSize, roiSize);
using (var mask = new Mat(1545, 810, MatType.CV_8U)) {
Cv2.Rectangle(mask, roi, new Scalar(255), Cv2.FILLED);
var maskF = new Mat(_tccRois.Size(), MatType.CV_8U);
Cv2.WarpPerspective(mask, maskF, matH, maskF.Size());
_chartMasks.Add(maskF);
_tccRois.SetTo(new Scalar(0, 200, 0), maskF);
}
}
_form.ShowMessage("舌診チャート検出 校正中");
_calibrating = Config.GetInt("CalibrationFrames");
}
/// <summary>
/// チャートに基づく変換行列の計算
/// </summary>
/// <param name="img"></param>
protected void CalcTcc(Mat img) {
// 変換行列の計算
var tccRgb = GetTccRgb(img);
_convRGB2SRGB = CalcConvertMatrix(tccRgb, TCC_SRGB);
var _convRGB2XYZ = CalcConvertMatrix(tccRgb, TCC_XYZ);
var tccSrgb = ConvertColor(tccRgb, _convRGB2SRGB);
var convSrgb2Xyz = CalcConvertMatrix(tccSrgb, TCC_XYZ);
// 変換精度検証
var diff = Math.Sqrt(Cv2.Norm(TCC_SRGB, tccSrgb, NormTypes.L2));
Debug.WriteLine($"RGB→SRGB 変換行列の誤差 = {diff:.000}");
// データ保存
SetSaveFolder("校正");
Cv2.ImWrite(Path.Combine(_saveFolder, Config.GetString("TccSaveFile")), img);
Cv2.ImWrite(Path.Combine(_saveFolder, Config.GetString("TccRoisFile")), _tccRois);
SaveMatToCsv(Path.Combine(_saveFolder, Config.GetString("ConvSrgbSaveFile")), _convRGB2SRGB);
SaveMatToCsv(Path.Combine(_saveFolder, Config.GetString("ConvXyzSaveFile")), _convRGB2XYZ);
SaveMatToCsv(Path.Combine(_saveFolder, Config.GetString("ConvSrgb2XyzSaveFile")), convSrgb2Xyz);
_form.ShowMessage("自動校正完了");
_form.EnableShots();
_calibrated = true;
}
/// <summary>
/// 画像からチャートのRGB値算出
/// </summary>
/// <param name="img"></param>
/// <returns></returns>
private Mat GetTccRgb(Mat img) {
var arrRGB = new Mat(24, 3, MatType.CV_64FC1);
for (int i = 0; i < _chartMasks.Count; i++) {
var rgb = Cv2.Mean(img, _chartMasks[i]);
arrRGB.At<double>(i, 0) = rgb.Val0;
arrRGB.At<double>(i, 1) = rgb.Val1;
arrRGB.At<double>(i, 2) = rgb.Val2;
}
return arrRGB;
}
/// <summary>
/// 変換行列算出
/// </summary>
/// <param name="img">画像</param>
/// <param name="target">変換目標の24x3行列</param>
private Mat CalcConvertMatrix(Mat src, Mat target) {
var extended = ExtendMat(src);
Mat convMat = new Mat(17, 3, MatType.CV_64FC1);
Cv2.Solve(extended, target, convMat, DecompTypes.SVD);
return convMat;
}
/// <summary>
/// 色変換
/// </summary>
/// <param name="src"></param>
/// <param name="convMat"></param>
/// <returns></returns>
private Mat ConvertColor(Mat src, Mat convMat) {
var extended = ExtendMat(src);
var converted = extended * convMat;
return converted.ToMat();
}
/// <summary>
/// 画像の色変換
/// </summary>
/// <param name="src"></param>
/// <param name="conv"></param>
/// <returns>double型画像</returns>
protected Mat ConvertImage(Mat src, Mat conv) {
if (src.Type() != MatType.CV_64FC3) {
src.ConvertTo(src, MatType.CV_64FC3);
}
var flatten = src.Reshape(3, src.Height * src.Width);
var extended = ExtendMat(flatten);
var converted = (extended * conv).ToMat();
var convertedImage = converted.Reshape(3, src.Height);
return convertedImage;
}
/// <summary>
/// 行列の拡張 3次元→17次元
/// </summary>
/// <param name="src"></param>
/// <returns></returns>
private Mat ExtendMat(Mat src) {
if (src.Cols * src.Channels() != 3) return src;
var dst = new Mat(src.Rows, 17, MatType.CV_64FC1);
for (int row = 0; row < src.Rows; row++) {
var b = src.Cols == 1 ? src.At<Vec3d>(row, 0)[0] : src.At<double>(row, 0);
var g = src.Cols == 1 ? src.At<Vec3d>(row, 0)[1] : src.At<double>(row, 1);
var r = src.Cols == 1 ? src.At<Vec3d>(row, 0)[2] : src.At<double>(row, 2);
dst.At<double>(row, 0) = r;
dst.At<double>(row, 1) = g;
dst.At<double>(row, 2) = b;
dst.At<double>(row, 3) = r * g;
dst.At<double>(row, 4) = r * b;
dst.At<double>(row, 5) = g * b;
dst.At<double>(row, 6) = r * r;
dst.At<double>(row, 7) = g * g;
dst.At<double>(row, 8) = b * b;
dst.At<double>(row, 9) = r * r * b;
dst.At<double>(row, 10) = r * r * g;
dst.At<double>(row, 11) = g * g * r;
dst.At<double>(row, 12) = g * g * b;
dst.At<double>(row, 13) = b * b * r;
dst.At<double>(row, 14) = b * b * g;
dst.At<double>(row, 15) = r * g * b;
dst.At<double>(row, 16) = 1.0;
}
return dst;
}
/// <summary>
/// ゲイン値の更新比率計算
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
protected float GetRatio(float value, float target) {
float ratio = target / value;
ratio = (ratio - 1.0f) * (value == 255.0f ? 1.0f : UpdateRate) + 1.0f;
return ratio;
}
/// <summary>
/// csvファイルからMatを読み込む
/// </summary>
/// <param name="csvFile"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private Mat LoadMatFromCsv(string csvFile) {
try {
var arr = new List<double[]>();
int cols = -1;
using (var reader = new StreamReader(csvFile)) {
while (!reader.EndOfStream) {
var line = reader.ReadLine();
var valStrs = line.Split(',');
if (cols == -1) cols = valStrs.Length;
else if (cols != valStrs.Length) throw new Exception("cols != valStrs.Length");
var vals = valStrs.Select(x => double.Parse(x)).ToArray();
arr.Add(vals);
}
}
var m = new Mat(arr.Count, cols, MatType.CV_64FC1);
for (var row = 0; row < arr.Count; row++) {
for (var col = 0; col < cols; col++) {
m.At<double>(row, col) = arr[row][col];
}
}
return m;
} catch (Exception) {
return null;
}
}
/// <summary>
/// Matをcsvファイルに保存
/// </summary>
/// <param name="m"></param>
/// <param name="csvFile"></param>
private void SaveMatToCsv(string csvFile, Mat m) {
using (var writer = new StreamWriter(csvFile)) {
for (var row = 0; row < m.Rows; row++) {
var line = "";
for (var col = 0; col < m.Cols; col++) {
if (col > 0) line += ",";
line += $"{m.At<double>(row, col):0.0000000}";
}
writer.WriteLine(line);
}
}
}
/// <summary>
/// 保存フォルダの取得
/// </summary>
/// <param name="isSeries"></param>
/// <returns></returns>
protected void SetSaveFolder(string note) {
var dt = DateTime.Now;
var paths = new List<string>() {
Config.GetString("SaveFolder"),
dt.ToString("yyyy-MM-dd"),
dt.ToString("HH_mm_ss") + $"-{note}",
};
_saveFolder = Path.Combine(paths.ToArray());
Directory.CreateDirectory(_saveFolder);
}
}
}