2026-01-26 18:56:53 +09:00

393 lines
18 KiB
C#

using Aurora;
using Aurora.Api;
using Aurora.Api.Control;
using Hermes.Functional;
using Matrox.MatroxImagingLibrary;
using MilCommon;
using MilPattern;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using VisionCommon;
using VisionCommonUI;
using VisionCommonUI.Resources;
using VisionCommonUI.UserGuide;
namespace MilUI {
/// <summary>
/// Interaction logic for CamCalSettingView.xaml
/// </summary>
///
public class McaFileItem {
public string ImgFileName { get; set; }
public int PointCount { get; set; }
public double AvgError { get; set; }
public string MCAFullPath { get; set; }
}
public class CalibPointPair {
public double RawX, RawY;
public double CalibX, CalibY;
}
public partial class CamCalSettingView : UserControl, IDisposable {
private string _calPath = Path.Combine(ApplicationExt.DataPath, "_config_vision", "CalData");
private string _backupDir;
private ObservableCollection<McaFileItem> McaFileItems = new ObservableCollection<McaFileItem>();
private Logger _Logger;
private CameraBase _CameraDev;
private FovInfo _FovInfo;
private FovRecipe _FovRecipe;
private CameraDisplay _CameraDisplay;
protected PatternAgentBase _PatternAgent = null;
private MIL_ID _SysId;
private MIL_ID MilCalContext = MIL.M_NULL;
public bool IsDeviceRunning { get; set; } = false;
public bool IsCalUsed { get; set; } = false;
public bool IsCalEnabled { get; set; } = false;
public bool IsCalLoaded { get; set; } = false;
List<CalibPointPair> _calibrationPoints;
public CamCalSettingView(FovInfo fovinfo, UserControl camview) {
_FovInfo = fovinfo;
_FovRecipe = VisionRecipeManager.GetFovRecipe(_FovInfo);
_Logger = new Logger(string.Format("CameraCalibration for [{0}]", _FovRecipe.FovName));
_CameraDev = VisionEnv.AuroraEnv.DeviceManager[_FovRecipe.CamRecipe.CamId.ToString()] as CameraBase;
_CameraDisplay = (camview as CameraView).GetDisplay() as CameraDisplay;
_PatternAgent = PatternAgentManager.GetAgent(_FovInfo);
var camConfig = VisionEnv.AuroraEnv.DeviceManager.GetDeviceConfig(_FovRecipe.CamRecipe.CamId.ToString()) as CamConfig;
_SysId = _PatternAgent.GetMilSystemId(_FovRecipe.CamRecipe.CamId);
InitializeComponent();
if (!Directory.Exists(_calPath)) {
Directory.CreateDirectory(_calPath);
}
_backupDir = Path.Combine(_calPath, "Backup", _FovInfo.ToString());
if (!Directory.Exists(_backupDir)) {
Directory.CreateDirectory(_backupDir);
}
SettingPanel.Visibility = Visibility.Collapsed;
Loaded += CameraCalibrationSettingView_Loaded;
}
private void CameraCalibrationSettingView_Loaded(object sender, RoutedEventArgs e) {
UpdateSignalLights();
}
private void SetSignalLight(System.Windows.Shapes.Ellipse light, bool state) {
light.Fill = state ? Brushes.Green : Brushes.Gray;
}
private void UpdateSignalLights() {
IsCalUsed = _FovRecipe.CamCalibrationRecipe.Used;
IsCalEnabled = _FovRecipe.CamCalibrationRecipe.Enabled;
IsCalLoaded = _FovRecipe.CamCalibrationRecipe.Loaded;
SetSignalLight(LightUsed, IsCalUsed);
SetSignalLight(LightEnabled, IsCalEnabled);
SetSignalLight(LightLoaded, IsCalLoaded);
}
private void StartCalibration_Click(object sender, RoutedEventArgs e) {
if (IsDeviceRunning && IsLoaded) {
MessageBoxUtil.ShowMessageBoxWarning("이미 보정 중입니다. 보정기능을 끄기위해 장치를 재시작합니다.");
StopDevice();
_FovRecipe.CamCalibrationRecipe.Used = false;
RecipeFileManager.SaveVisionRecipe();
StartDevice();
}
SettingPanel.Visibility = Visibility.Visible;
UpdateSignalLights();
}
private void BtnSaveCalibration_Click(object sender, RoutedEventArgs e) {
_CameraDisplay.ClearAllGraphics();
MIL_ID image = _CameraDisplay.GetImage();
if (image == null) {
MessageBoxUtil.ShowMessageBoxError(StrTokens.MSG_E_ImageNotReady);
return;
}
int r = (int)TxtRows.Value;
int c = (int)TxtCols.Value;
double sx = (double)TxtSpacingX.Value;
double sy = (double)TxtSpacingY.Value;
if(r * c * sx * sy == 0 ) {
MessageBoxUtil.ShowMessageBoxInfo("입력값이 정확하지 않습니다.");
return;
}
MIL.McalAlloc(_SysId, MIL.M_LINEAR_INTERPOLATION, MIL.M_DEFAULT, ref MilCalContext);
MIL.McalGrid(MilCalContext, image, 0.0, 0.0, 0.0, r, c, sx, sy, MIL.M_FULL_CALIBRATION, MIL.M_CIRCLE_GRID);
//DropBox 분기
//MIL.McalGrid(MilCalContext, image, 0.0, 0.0, 0.0, r, c, sx, sy, MIL.M_FULL_CALIBRATION, MIL.M_CHESSBOARD_GRID);
var (isValid, usedPoints, error) = IsCalValid(MilCalContext, r*c);
if (isValid) {
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string backupMca = Path.Combine(_backupDir, $"backup_{timestamp}.mca");
string backupImg = Path.Combine(_backupDir, $"backup_{timestamp}.jpg");
MIL.McalSave(backupMca, MilCalContext, MIL.M_DEFAULT);
MilEnvironment.SaveImage(backupImg, image);
McaFileItems.Add(new McaFileItem {
MCAFullPath = backupMca,
PointCount = usedPoints,
AvgError = error,
ImgFileName = backupImg
});
McaFileGrid.ItemsSource = null;
McaFileGrid.ItemsSource = McaFileItems;
MessageBoxUtil.ShowMessageBoxInfo("MCA 추출 완료 및 백업 완료");
}
else {
MessageBoxUtil.ShowMessageBoxInfo("MCA 추출 실패");
}
UpdateSignalLights();
}
private (bool, int, double) IsCalValid(MIL_ID calId, int expectedPointCount, double maxAcceptableError = 0.5) {
double error = 0.0;
MIL.McalInquire(calId, MIL.M_AVERAGE_PIXEL_ERROR, ref error);
double usedPoints = 0;
MIL.McalInquire(calId, MIL.M_NUMBER_OF_CALIBRATION_POINTS, ref usedPoints);
bool res = expectedPointCount > 0 && usedPoints >= expectedPointCount * 0.8 && error < maxAcceptableError;
return (res, (int)usedPoints, error);
}
private void McaFileGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) {
MIL.MgraClear(_CameraDisplay.GetGraphicContextId(), _CameraDisplay.GetGraphicListId());
if (McaFileGrid.SelectedItem is McaFileItem selected) {
string jpgPath = selected.ImgFileName;
if (!File.Exists(jpgPath)) {
MessageBoxUtil.ShowMessageBoxInfo("⚠️ 해당 MCA에 대응하는 이미지 파일이 존재하지 않습니다.");
return;
}
// 사용자 알림
MessageBoxUtil.ShowMessageBoxInfo("📷 칼리브레이션 시 사용된 이미지를 불러옵니다.");
MIL_ID imageId = MIL.M_NULL;
MIL.MbufRestore(jpgPath, _SysId, ref imageId);
_CameraDisplay.SetImage(imageId); // display에 연결
MIL.McalRestore(selected.MCAFullPath, _SysId, MIL.M_DEFAULT, ref MilCalContext);
MIL.MgraColor(
_CameraDisplay.GetGraphicContextId(), // 그래픽 컨텍스트
MIL.M_RGB888(255, 0, 0) // 빨간색 (예: R=255, G=0, B=0)
);
MIL.McalDraw(
_CameraDisplay.GetGraphicContextId(), // 🔸 대상 그래픽 리스트 (Display에 연결된)
MilCalContext, // 🔸 Calibration Context
_CameraDisplay.GetGraphicListId(), // 🔸 Drawing 대상: 이 경우 Display 그래픽 오버레이
MIL.M_DRAW_IMAGE_POINTS + MIL.M_DRAW_CALIBRATION_ERROR, // 🔸 원본 이미지 상의 포인트를 그림
MIL.M_DEFAULT,
MIL.M_DEFAULT
);
MIL.MbufFree(imageId); // 이미지 연결 후 해제 가능 (Display 내부에서 유지됨)
}
}
private void BtnComparePreview_Click(object sender, RoutedEventArgs e) {
if (IsCalLoaded) {
MessageBoxUtil.ShowMessageBoxInfo($"MCA파일이 이미 Load되어 있습니다.\nUnload이후 비교해주세요");
return;
}
if (McaFileGrid.SelectedItem is McaFileItem selected) {
MIL_INT result = MIL.McalRestore(selected.MCAFullPath, _SysId, MIL.M_DEFAULT, ref MilCalContext);
MIL_ID image = _CameraDisplay.GetImage();
MIL_ID transformed = MilEnvironment.GetCompatibleGrayImageBuffer(image);
MIL.McalTransformImage(
image,
transformed,
MilCalContext,
MIL.M_BILINEAR,
MIL.M_FULL_CORRECTION,
MIL.M_WARP_IMAGE
);
_CameraDisplay.SetImage(transformed);
MIL.MgraColor(
_CameraDisplay.GetGraphicContextId(),
MIL.M_RGB888(0, 255, 0)
);
MIL.McalDraw(
_CameraDisplay.GetGraphicContextId(),
MilCalContext,
_CameraDisplay.GetGraphicListId(),
MIL.M_DRAW_WORLD_POINTS,
MIL.M_DEFAULT,
MIL.M_DEFAULT
);
}
GenerateClipboard(MilCalContext);
}
private void BtnSetAsRecipe_Click(object sender, RoutedEventArgs e) {
if (McaFileGrid.SelectedItem is McaFileItem selected) {
MIL_INT result = MIL.McalRestore(selected.MCAFullPath, _SysId, MIL.M_DEFAULT, ref MilCalContext);
if (result != MIL.M_NULL) {
var file_name = _calPath + $@"/{_FovInfo}.mca";
MIL.McalSave(file_name, MilCalContext, MIL.M_DEFAULT);
StopDevice();
_FovRecipe.CamCalibrationRecipe.Used = true;
_FovRecipe.CamCalibrationRecipe.Enabled = true;
RecipeFileManager.SaveVisionRecipe();
StartDevice();
MessageBoxUtil.ShowMessageBoxInfo($"⚠️ 선택된 MCA를 레시피에 적용합니다.");
UpdateSignalLights();
}
}
}
private void BtnLoadCalibration_Click(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.OpenFileDialog {
Title = "백업된 MCA 파일 선택",
InitialDirectory = _backupDir,
Filter = "MCA Files (*.mca)|*.mca",
CheckFileExists = true,
Multiselect = false
};
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
FileInfo selectedMcaFile = new FileInfo(dialog.FileName);
string matchingJpgPath = Path.ChangeExtension(selectedMcaFile.FullName, ".jpg");
if (File.Exists(matchingJpgPath)) {
FileInfo matchingJpgFile = new FileInfo(matchingJpgPath);
MIL_INT result = MIL.McalRestore(selectedMcaFile.FullName, _SysId, MIL.M_DEFAULT, ref MilCalContext);
if (result != MIL.M_NULL) {
double calibrationStatus = 0;
MIL.McalInquire(MilCalContext, MIL.M_CALIBRATION_STATUS, ref calibrationStatus);
if (calibrationStatus == MIL.M_CALIBRATED) {
double averagePixelError = 0.0;
MIL.McalInquire(MilCalContext, MIL.M_AVERAGE_PIXEL_ERROR, ref averagePixelError); ;
double nbCalibrationPoints = 0;
MIL.McalInquire(MilCalContext, MIL.M_NUMBER_OF_CALIBRATION_POINTS, ref nbCalibrationPoints);
McaFileItems.Add(new McaFileItem {
MCAFullPath = selectedMcaFile.FullName,
PointCount = (int)nbCalibrationPoints,
AvgError = averagePixelError,
ImgFileName = matchingJpgPath
});
}
else {
MessageBoxUtil.ShowMessageBoxInfo($"⚠️ MCA복원실패");
}
}
}
else {
MessageBoxUtil.ShowMessageBoxInfo($"⚠️ 선택된 MCA에 대응하는 JPG 이미지가 없습니다.");
}
}
McaFileGrid.ItemsSource = null;
McaFileGrid.ItemsSource = McaFileItems;
}
private void StopDevice() {
}
private void StartDevice() {
}
private void BtnHelp_Click(object sender, RoutedEventArgs e) {
var guide = new GuideManager(this);
guide.AddStep(startbutton, "보정을 원하시면 버튼을 눌러 시작하세요!\n이미 칼리브레이션이 동작중이라면 Camera가 자동으로 껏다켜집니다.");
guide.AddStep(AddButton, "기존 백업된 mca파일들을 Load해서 비교 할 수 있습니다.", placement: MessagePlacement.Above);
guide.AddStep(McaFileGrid, "평균 픽셀 오차는(AVG Error), Mil Calibrator가 좌표를 얼마나 잘 복원해냈는가를 평가하는 지표\n즉 정답지(row,col,spacing)기준으로 모델링 했을때 정답지/칼리브레이션결과의 평균 차이를 뜻함\n OpenCV의 ReProjectionError와 같은 맥락(최적화 알고리즘)", placement: MessagePlacement.Above);
guide.Start();
}
private void BtnSetting_Help_Click(object sender, RoutedEventArgs e) {
var guide = new GuideManager(this);
guide.AddStep(SettingPanel, "좌측 화면의 이미지를 기준으로 칼리브레이션을 하게됩니다. 아래와 같은 이미지를 로드하거나 grab하세요", "cal.PNG");
guide.AddStep(TxtRows, "체커보드의 행 수를 입력하세요");
guide.AddStep(TxtCols, "체커보드의 열 수를 입력하세요");
guide.AddStep(SpacingXY, "mm기준의 거리값을 입력합니다.");
guide.AddStep(BtnSaveCalibration, "Calibration이 되는 경우 백업파일을 만들고 아래 리스트박스에 사용대기형태로 추가됩니다.");
guide.AddStep(McaFileGrid, "리스트에 업로드된 파일을 더블클릭하면 칼리브레이션 당시 사용한 이미지를 볼 수 있습니다.", "cal1.png",placement: MessagePlacement.Above);
guide.AddStep(McaFileGrid, "Action의 preview버튼으로 이미지변환 결과를 볼 수 있습니다.", "cal2.png", placement: MessagePlacement.Above);
guide.AddStep(McaFileGrid, "Action의 Recipe버튼을 클릭하면 해당Calibration이 앞으로 현재 FOV에 적용됩니다.", "cal3.png", placement: MessagePlacement.Above);
guide.Start();
}
public void Dispose() {
if (MilCalContext != null) {
MIL.McalFree(MilCalContext);
}
}
public void GenerateClipboard(MIL_ID calContext) {
// 포인트 개수 조회
double numPoints = 0;
MIL.McalInquire(calContext, MIL.M_NUMBER_OF_CALIBRATION_POINTS, ref numPoints);
if (numPoints <= 0) {
MessageBoxUtil.ShowMessageBoxWarning("⚠️ 캘리브레이션 포인트가 없습니다.");
return;
}
// 포인트 배열 선언
double[] measuredXs = new double[(int)numPoints];
double[] measuredYs = new double[(int)numPoints];
double[] worldXs = new double[(int)numPoints];
double[] worldYs = new double[(int)numPoints];
// 포인트 정보 추출
MIL.McalInquire(calContext, MIL.M_CALIBRATION_IMAGE_POINTS_X, measuredXs);
MIL.McalInquire(calContext, MIL.M_CALIBRATION_IMAGE_POINTS_Y, measuredYs);
MIL.McalInquire(calContext, MIL.M_CALIBRATION_WORLD_POINTS_X, worldXs);
MIL.McalInquire(calContext, MIL.M_CALIBRATION_WORLD_POINTS_Y, worldYs);
var sb = new StringBuilder();
sb.AppendLine("Index\tMeasuredX\tMeasuredY\tWorldX\tWorldY\tPredictedX\tPredictedY\tPixelError");
for (int i = 0; i < (int)numPoints; i++) {
double predictedX = 0, predictedY = 0;
MIL.McalTransformCoordinate(
calContext,
MIL.M_WORLD_TO_PIXEL,
worldXs[i],
worldYs[i],
ref predictedX,
ref predictedY
);
double error = Math.Sqrt(
Math.Pow(predictedX - measuredXs[i], 2) +
Math.Pow(predictedY - measuredYs[i], 2)
);
sb.AppendLine($"{i}\t{measuredXs[i]:F3}\t{measuredYs[i]:F3}\t{worldXs[i]:F3}\t{worldYs[i]:F3}\t{predictedX:F3}\t{predictedY:F3}\t{error:F3}");
}
Clipboard.SetText(sb.ToString());
MessageBoxUtil.ShowMessageBoxInfo($"📋 평균 픽셀 오차 리포트가 클립보드에 복사되었습니다.\n총 포인트: {numPoints}");
}
}
}