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 { /// /// Interaction logic for CamCalSettingView.xaml /// /// 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 McaFileItems = new ObservableCollection(); 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 _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}"); } } }