ArUcoCameraCalibrationExample.cs 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. using UnityEngine;
  2. using UnityEngine.UI;
  3. using UnityEngine.SceneManagement;
  4. using System;
  5. using System.Collections;
  6. using System.Collections.Generic;
  7. using System.Xml.Serialization;
  8. using System.IO;
  9. using System.Linq;
  10. using OpenCVForUnity.ArucoModule;
  11. using OpenCVForUnity.CoreModule;
  12. using OpenCVForUnity.ImgprocModule;
  13. using OpenCVForUnity.Calib3dModule;
  14. using OpenCVForUnity.UnityUtils;
  15. using OpenCVForUnity.ImgcodecsModule;
  16. using OpenCVForUnity.UnityUtils.Helper;
  17. namespace OpenCVForUnityExample
  18. {
  19. /// <summary>
  20. /// ArUco Camera Calibration Example
  21. /// An example of camera calibration using the aruco module.
  22. /// Referring to https://github.com/opencv/opencv_contrib/blob/master/modules/aruco/samples/calibrate_camera.cpp.
  23. /// https://github.com/opencv/opencv/blob/master/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp
  24. /// https://docs.opencv.org/3.2.0/da/d13/tutorial_aruco_calibration.html
  25. /// https://docs.opencv.org/3.4.0/d7/d21/tutorial_interactive_calibration.html
  26. /// </summary>
  27. [RequireComponent (typeof(WebCamTextureToMatHelper))]
  28. public class ArUcoCameraCalibrationExample : MonoBehaviour
  29. {
  30. /// <summary>
  31. /// The marker type.
  32. /// </summary>
  33. public MarkerType markerType = MarkerType.ChArUcoBoard;
  34. /// <summary>
  35. /// The marker type dropdown.
  36. /// </summary>
  37. public Dropdown markerTypeDropdown;
  38. /// <summary>
  39. /// The dictionary identifier.
  40. /// </summary>
  41. public ArUcoDictionary dictionaryId = ArUcoDictionary.DICT_6X6_250;
  42. /// <summary>
  43. /// The dictionary id dropdown.
  44. /// </summary>
  45. public Dropdown dictionaryIdDropdown;
  46. /// <summary>
  47. /// Number of squares in X direction.
  48. /// </summary>
  49. public NumberOfSquaresX squaresX = NumberOfSquaresX.X_5;
  50. /// <summary>
  51. /// The squares X dropdown.
  52. /// </summary>
  53. public Dropdown squaresXDropdown;
  54. /// <summary>
  55. /// Number of squares in X direction.
  56. /// </summary>
  57. public NumberOfSquaresY squaresY = NumberOfSquaresY.Y_7;
  58. /// <summary>
  59. /// The squares X dropdown.
  60. /// </summary>
  61. public Dropdown squaresYDropdown;
  62. /// <summary>
  63. /// The save path input field.
  64. /// </summary>
  65. public InputField savePathInputField;
  66. /// <summary>
  67. /// Determines if refine marker detection. (only valid for ArUco boards)
  68. /// </summary>
  69. public bool refineMarkerDetection = true;
  70. [Header ("Extra Option")]
  71. /// <summary>
  72. /// Determines if calibrates camera using the list of calibration images.
  73. /// </summary>
  74. [TooltipAttribute ("Determines if calibrates camera using the list of calibration images.")]
  75. public bool isImagesInputMode = false;
  76. /// <summary>
  77. /// The calibration images directory path.
  78. /// Set a relative directory path from the starting point of the "StreamingAssets" folder. e.g. "calibration_images/".
  79. /// </summary>
  80. [TooltipAttribute ("Set a relative directory path from the starting point of the \"StreamingAssets\" folder. e.g. \"calibration_images\"")]
  81. public string calibrationImagesDirectory = "calibration_images";
  82. /// <summary>
  83. /// The texture.
  84. /// </summary>
  85. Texture2D texture;
  86. /// <summary>
  87. /// The webcam texture to mat helper.
  88. /// </summary>
  89. WebCamTextureToMatHelper webCamTextureToMatHelper;
  90. /// <summary>
  91. /// The gray mat.
  92. /// </summary>
  93. Mat grayMat;
  94. /// <summary>
  95. /// The bgr mat.
  96. /// </summary>
  97. Mat bgrMat;
  98. /// <summary>
  99. /// The rgba mat.
  100. /// </summary>
  101. Mat rgbaMat;
  102. /// <summary>
  103. /// The cameraparam matrix.
  104. /// </summary>
  105. Mat camMatrix;
  106. /// <summary>
  107. /// The distortion coeffs.
  108. /// </summary>
  109. MatOfDouble distCoeffs;
  110. /// <summary>
  111. /// The identifiers.
  112. /// </summary>
  113. Mat ids;
  114. /// <summary>
  115. /// The corners.
  116. /// </summary>
  117. List<Mat> corners;
  118. /// <summary>
  119. /// The rejected corners.
  120. /// </summary>
  121. List<Mat> rejectedCorners;
  122. /// <summary>
  123. /// The rvecs.
  124. /// </summary>
  125. List<Mat> rvecs;
  126. /// <summary>
  127. /// The tvecs.
  128. /// </summary>
  129. List<Mat> tvecs;
  130. /// <summary>
  131. /// The detector parameters.
  132. /// </summary>
  133. DetectorParameters detectorParams;
  134. /// <summary>
  135. /// The dictionary.
  136. /// </summary>
  137. Dictionary dictionary;
  138. /// <summary>
  139. /// The recovered identifiers.
  140. /// </summary>
  141. Mat recoveredIdxs;
  142. const int calibrationFlags = 0;
  143. // Calib3d.CALIB_FIX_K3 | Calib3d.CALIB_FIX_K4 | Calib3d.CALIB_FIX_K5
  144. double repErr = 0;
  145. bool shouldCaptureFrame = false;
  146. // for ChArUcoBoard.
  147. // chessboard square side length (normally in meters)
  148. const float chArUcoBoradSquareLength = 0.04f;
  149. // marker side length (same unit than squareLength)
  150. const float chArUcoBoradMarkerLength = 0.02f;
  151. const int charucoMinMarkers = 2;
  152. Mat charucoCorners;
  153. Mat charucoIds;
  154. CharucoBoard charucoBoard;
  155. List<List<Mat>> allCorners;
  156. List<Mat> allIds;
  157. List<Mat> allImgs;
  158. // for OthearMarkers.
  159. // square size in some user-defined units (1 by default)
  160. const float squareSize = 1f;
  161. List<Mat> imagePoints;
  162. bool isInitialized = false;
  163. bool isCalibrating = false;
  164. // Use this for initialization
  165. IEnumerator Start ()
  166. {
  167. webCamTextureToMatHelper = gameObject.GetComponent<WebCamTextureToMatHelper> ();
  168. // fix the screen orientation.
  169. Screen.orientation = ScreenOrientation.LandscapeLeft;
  170. // wait for the screen orientation to change.
  171. yield return null;
  172. markerTypeDropdown.value = (int)markerType;
  173. dictionaryIdDropdown.value = (int)dictionaryId;
  174. squaresXDropdown.value = (int)squaresX - 1;
  175. squaresYDropdown.value = (int)squaresY - 1;
  176. dictionaryIdDropdown.interactable = (markerType == MarkerType.ChArUcoBoard);
  177. #if UNITY_WEBGL && !UNITY_EDITOR
  178. isImagesInputMode = false;
  179. #endif
  180. if (isImagesInputMode) {
  181. isImagesInputMode = InitializeImagesInputMode ();
  182. }
  183. if (!isImagesInputMode) {
  184. #if UNITY_ANDROID && !UNITY_EDITOR
  185. // Avoids the front camera low light issue that occurs in only some Android devices (e.g. Google Pixel, Pixel2).
  186. webCamTextureToMatHelper.avoidAndroidFrontCameraLowLightIssue = true;
  187. #endif
  188. webCamTextureToMatHelper.Initialize ();
  189. }
  190. }
  191. /// <summary>
  192. /// Raises the webcam texture to mat helper initialized event.
  193. /// </summary>
  194. public void OnWebCamTextureToMatHelperInitialized ()
  195. {
  196. Debug.Log ("OnWebCamTextureToMatHelperInitialized");
  197. Mat webCamTextureMat = webCamTextureToMatHelper.GetMat ();
  198. InitializeCalibraton (webCamTextureMat);
  199. // if WebCamera is frontFaceing, flip Mat.
  200. if (webCamTextureToMatHelper.GetWebCamDevice ().isFrontFacing) {
  201. webCamTextureToMatHelper.flipHorizontal = true;
  202. }
  203. }
  204. /// <summary>
  205. /// Raises the webcam texture to mat helper disposed event.
  206. /// </summary>
  207. public void OnWebCamTextureToMatHelperDisposed ()
  208. {
  209. Debug.Log ("OnWebCamTextureToMatHelperDisposed");
  210. DisposeCalibraton ();
  211. }
  212. /// <summary>
  213. /// Raises the webcam texture to mat helper error occurred event.
  214. /// </summary>
  215. /// <param name="errorCode">Error code.</param>
  216. public void OnWebCamTextureToMatHelperErrorOccurred (WebCamTextureToMatHelper.ErrorCode errorCode)
  217. {
  218. Debug.Log ("OnWebCamTextureToMatHelperErrorOccurred " + errorCode);
  219. }
  220. // Update is called once per frame
  221. void Update ()
  222. {
  223. if (isImagesInputMode)
  224. return;
  225. if (webCamTextureToMatHelper.IsPlaying () && webCamTextureToMatHelper.DidUpdateThisFrame ()) {
  226. Mat rgbaMat = webCamTextureToMatHelper.GetMat ();
  227. Imgproc.cvtColor (rgbaMat, grayMat, Imgproc.COLOR_RGBA2GRAY);
  228. if (shouldCaptureFrame) {
  229. shouldCaptureFrame = false;
  230. Mat frameMat = grayMat.clone ();
  231. double e = CaptureFrame (frameMat);
  232. if (e > 0)
  233. repErr = e;
  234. }
  235. DrawFrame (grayMat, bgrMat);
  236. Imgproc.cvtColor (bgrMat, rgbaMat, Imgproc.COLOR_BGR2RGBA);
  237. Utils.fastMatToTexture2D (rgbaMat, texture);
  238. }
  239. }
  240. private void InitializeCalibraton (Mat frameMat)
  241. {
  242. texture = new Texture2D (frameMat.cols (), frameMat.rows (), TextureFormat.RGBA32, false);
  243. gameObject.GetComponent<Renderer> ().material.mainTexture = texture;
  244. gameObject.transform.localScale = new Vector3 (frameMat.cols (), frameMat.rows (), 1);
  245. Debug.Log ("Screen.width " + Screen.width + " Screen.height " + Screen.height + " Screen.orientation " + Screen.orientation);
  246. float width = frameMat.width ();
  247. float height = frameMat.height ();
  248. float imageSizeScale = 1.0f;
  249. float widthScale = (float)Screen.width / width;
  250. float heightScale = (float)Screen.height / height;
  251. if (widthScale < heightScale) {
  252. Camera.main.orthographicSize = (width * (float)Screen.height / (float)Screen.width) / 2;
  253. imageSizeScale = (float)Screen.height / (float)Screen.width;
  254. } else {
  255. Camera.main.orthographicSize = height / 2;
  256. }
  257. // set cameraparam.
  258. camMatrix = CreateCameraMatrix (width, height);
  259. Debug.Log ("camMatrix " + camMatrix.dump ());
  260. distCoeffs = new MatOfDouble (0, 0, 0, 0, 0);
  261. Debug.Log ("distCoeffs " + distCoeffs.dump ());
  262. // calibration camera.
  263. Size imageSize = new Size (width * imageSizeScale, height * imageSizeScale);
  264. double apertureWidth = 0;
  265. double apertureHeight = 0;
  266. double[] fovx = new double[1];
  267. double[] fovy = new double[1];
  268. double[] focalLength = new double[1];
  269. Point principalPoint = new Point (0, 0);
  270. double[] aspectratio = new double[1];
  271. Calib3d.calibrationMatrixValues (camMatrix, imageSize, apertureWidth, apertureHeight, fovx, fovy, focalLength, principalPoint, aspectratio);
  272. Debug.Log ("imageSize " + imageSize.ToString ());
  273. Debug.Log ("apertureWidth " + apertureWidth);
  274. Debug.Log ("apertureHeight " + apertureHeight);
  275. Debug.Log ("fovx " + fovx [0]);
  276. Debug.Log ("fovy " + fovy [0]);
  277. Debug.Log ("focalLength " + focalLength [0]);
  278. Debug.Log ("principalPoint " + principalPoint.ToString ());
  279. Debug.Log ("aspectratio " + aspectratio [0]);
  280. grayMat = new Mat (frameMat.rows (), frameMat.cols (), CvType.CV_8UC1);
  281. bgrMat = new Mat (frameMat.rows (), frameMat.cols (), CvType.CV_8UC3);
  282. rgbaMat = new Mat (frameMat.rows (), frameMat.cols (), CvType.CV_8UC4);
  283. ids = new Mat ();
  284. corners = new List<Mat> ();
  285. rejectedCorners = new List<Mat> ();
  286. rvecs = new List<Mat> ();
  287. tvecs = new List<Mat> ();
  288. detectorParams = DetectorParameters.create ();
  289. detectorParams.set_cornerRefinementMethod (1);// do cornerSubPix() of OpenCV.
  290. dictionary = Aruco.getPredefinedDictionary ((int)dictionaryId);
  291. recoveredIdxs = new Mat ();
  292. charucoCorners = new Mat ();
  293. charucoIds = new Mat ();
  294. charucoBoard = CharucoBoard.create ((int)squaresX, (int)squaresY, chArUcoBoradSquareLength, chArUcoBoradMarkerLength, dictionary);
  295. allCorners = new List<List<Mat>> ();
  296. allIds = new List<Mat> ();
  297. allImgs = new List<Mat> ();
  298. imagePoints = new List<Mat> ();
  299. isInitialized = true;
  300. }
  301. private void DisposeCalibraton ()
  302. {
  303. ResetCalibration ();
  304. if (grayMat != null)
  305. grayMat.Dispose ();
  306. if (bgrMat != null)
  307. bgrMat.Dispose ();
  308. if (rgbaMat != null)
  309. rgbaMat.Dispose ();
  310. if (texture != null) {
  311. Texture2D.Destroy (texture);
  312. texture = null;
  313. }
  314. if (ids != null)
  315. ids.Dispose ();
  316. foreach (var item in corners) {
  317. item.Dispose ();
  318. }
  319. corners.Clear ();
  320. foreach (var item in rejectedCorners) {
  321. item.Dispose ();
  322. }
  323. rejectedCorners.Clear ();
  324. foreach (var item in rvecs) {
  325. item.Dispose ();
  326. }
  327. rvecs.Clear ();
  328. foreach (var item in tvecs) {
  329. item.Dispose ();
  330. }
  331. tvecs.Clear ();
  332. if (recoveredIdxs != null)
  333. recoveredIdxs.Dispose ();
  334. if (charucoCorners != null)
  335. charucoCorners.Dispose ();
  336. if (charucoIds != null)
  337. charucoIds.Dispose ();
  338. if (charucoBoard != null)
  339. charucoBoard.Dispose ();
  340. isInitialized = false;
  341. }
  342. private void DrawFrame (Mat grayMat, Mat bgrMat)
  343. {
  344. Imgproc.cvtColor (grayMat, bgrMat, Imgproc.COLOR_GRAY2BGR);
  345. switch (markerType) {
  346. default:
  347. case MarkerType.ChArUcoBoard:
  348. // detect markers.
  349. Aruco.detectMarkers (grayMat, dictionary, corners, ids, detectorParams, rejectedCorners, camMatrix, distCoeffs);
  350. // refine marker detection.
  351. if (refineMarkerDetection) {
  352. Aruco.refineDetectedMarkers (grayMat, charucoBoard, corners, ids, rejectedCorners, camMatrix, distCoeffs, 10f, 3f, true, recoveredIdxs, detectorParams);
  353. }
  354. // if at least one marker detected
  355. if (ids.total () > 0) {
  356. Aruco.interpolateCornersCharuco (corners, ids, grayMat, charucoBoard, charucoCorners, charucoIds, camMatrix, distCoeffs, charucoMinMarkers);
  357. // draw markers.
  358. Aruco.drawDetectedMarkers (bgrMat, corners, ids, new Scalar (0, 255, 0, 255));
  359. // if at least one charuco corner detected
  360. if (charucoIds.total () > 0) {
  361. Aruco.drawDetectedCornersCharuco (bgrMat, charucoCorners, charucoIds, new Scalar (0, 0, 255, 255));
  362. }
  363. }
  364. break;
  365. case MarkerType.ChessBoard:
  366. case MarkerType.CirclesGlid:
  367. case MarkerType.AsymmetricCirclesGlid:
  368. // detect markers.
  369. MatOfPoint2f points = new MatOfPoint2f ();
  370. bool found = false;
  371. switch (markerType) {
  372. default:
  373. case MarkerType.ChessBoard:
  374. found = Calib3d.findChessboardCorners (grayMat, new Size ((int)squaresX, (int)squaresY), points, Calib3d.CALIB_CB_ADAPTIVE_THRESH | Calib3d.CALIB_CB_FAST_CHECK | Calib3d.CALIB_CB_NORMALIZE_IMAGE);
  375. break;
  376. case MarkerType.CirclesGlid:
  377. found = Calib3d.findCirclesGrid (grayMat, new Size ((int)squaresX, (int)squaresY), points, Calib3d.CALIB_CB_SYMMETRIC_GRID);
  378. break;
  379. case MarkerType.AsymmetricCirclesGlid:
  380. found = Calib3d.findCirclesGrid (grayMat, new Size ((int)squaresX, (int)squaresY), points, Calib3d.CALIB_CB_ASYMMETRIC_GRID);
  381. break;
  382. }
  383. if (found) {
  384. if (markerType == MarkerType.ChessBoard)
  385. Imgproc.cornerSubPix (grayMat, points, new Size (5, 5), new Size (-1, -1), new TermCriteria (TermCriteria.EPS + TermCriteria.COUNT, 30, 0.1));
  386. // draw markers.
  387. Calib3d.drawChessboardCorners (bgrMat, new Size ((int)squaresX, (int)squaresY), points, found);
  388. }
  389. break;
  390. }
  391. double[] camMatrixArr = new double[(int)camMatrix.total ()];
  392. camMatrix.get (0, 0, camMatrixArr);
  393. double[] distCoeffsArr = new double[(int)distCoeffs.total ()];
  394. distCoeffs.get (0, 0, distCoeffsArr);
  395. int ff = Imgproc.FONT_HERSHEY_SIMPLEX;
  396. double fs = 0.4;
  397. Scalar c = new Scalar (255, 255, 255, 255);
  398. int t = 0;
  399. int lt = Imgproc.LINE_AA;
  400. bool blo = false;
  401. int frameCount = (markerType == MarkerType.ChArUcoBoard) ? allCorners.Count : imagePoints.Count;
  402. Imgproc.putText (bgrMat, frameCount + " FRAME CAPTURED", new Point (bgrMat.cols () - 300, 20), ff, fs, c, t, lt, blo);
  403. Imgproc.putText (bgrMat, "IMAGE_WIDTH: " + bgrMat.width (), new Point (bgrMat.cols () - 300, 40), ff, fs, c, t, lt, blo);
  404. Imgproc.putText (bgrMat, "IMAGE_HEIGHT: " + bgrMat.height (), new Point (bgrMat.cols () - 300, 60), ff, fs, c, t, lt, blo);
  405. Imgproc.putText (bgrMat, "CALIBRATION_FLAGS: " + calibrationFlags, new Point (bgrMat.cols () - 300, 80), ff, fs, c, t, lt, blo);
  406. Imgproc.putText (bgrMat, "CAMERA_MATRIX: ", new Point (bgrMat.cols () - 300, 100), ff, fs, c, t, lt, blo);
  407. for (int i = 0; i < camMatrixArr.Length; i = i + 3) {
  408. Imgproc.putText (bgrMat, " " + camMatrixArr [i] + ", " + camMatrixArr [i + 1] + ", " + camMatrixArr [i + 2] + ",", new Point (bgrMat.cols () - 300, 120 + 20 * i / 3), ff, fs, c, t, lt, blo);
  409. }
  410. Imgproc.putText (bgrMat, "DISTORTION_COEFFICIENTS: ", new Point (bgrMat.cols () - 300, 180), ff, fs, c, t, lt, blo);
  411. for (int i = 0; i < distCoeffsArr.Length; ++i) {
  412. Imgproc.putText (bgrMat, " " + distCoeffsArr [i] + ",", new Point (bgrMat.cols () - 300, 200 + 20 * i), ff, fs, c, t, lt, blo);
  413. }
  414. Imgproc.putText (bgrMat, "AVG_REPROJECTION_ERROR: " + repErr, new Point (bgrMat.cols () - 300, 300), ff, fs, c, t, lt, blo);
  415. if (frameCount == 0)
  416. Imgproc.putText (bgrMat, "Please press the capture button to start!", new Point (5, bgrMat.rows () - 10), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar (255, 255, 255, 255), 1, Imgproc.LINE_AA, false);
  417. }
  418. private double CaptureFrame (Mat frameMat)
  419. {
  420. double repErr = -1;
  421. switch (markerType) {
  422. default:
  423. case MarkerType.ChArUcoBoard:
  424. List<Mat> corners = new List<Mat> ();
  425. Mat ids = new Mat ();
  426. Aruco.detectMarkers (frameMat, dictionary, corners, ids, detectorParams, rejectedCorners, camMatrix, distCoeffs);
  427. if (refineMarkerDetection) {
  428. Aruco.refineDetectedMarkers (frameMat, charucoBoard, corners, ids, rejectedCorners, camMatrix, distCoeffs, 10f, 3f, true, recoveredIdxs, detectorParams);
  429. }
  430. if (ids.total () > 0) {
  431. Debug.Log ("Frame captured.");
  432. allCorners.Add (corners);
  433. allIds.Add (ids);
  434. allImgs.Add (frameMat);
  435. } else {
  436. Debug.Log ("Invalid frame.");
  437. frameMat.Dispose ();
  438. if (ids != null)
  439. ids.Dispose ();
  440. foreach (var item in corners) {
  441. item.Dispose ();
  442. }
  443. corners.Clear ();
  444. return -1;
  445. }
  446. // calibrate camera using aruco markers
  447. //double arucoRepErr = CalibrateCameraAruco (allCorners, allIds, charucoBoard, frameMat.size(), camMatrix, distCoeffs, rvecs, tvecs, calibrationFlags);
  448. //Debug.Log ("arucoRepErr: " + arucoRepErr);
  449. // calibrate camera using charuco
  450. repErr = CalibrateCameraCharuco (allCorners, allIds, charucoBoard, frameMat.size (), camMatrix, distCoeffs, rvecs, tvecs, calibrationFlags, calibrationFlags);
  451. break;
  452. case MarkerType.ChessBoard:
  453. case MarkerType.CirclesGlid:
  454. case MarkerType.AsymmetricCirclesGlid:
  455. MatOfPoint2f points = new MatOfPoint2f ();
  456. Size patternSize = new Size ((int)squaresX, (int)squaresY);
  457. bool found = false;
  458. switch (markerType) {
  459. default:
  460. case MarkerType.ChessBoard:
  461. found = Calib3d.findChessboardCorners (frameMat, patternSize, points, Calib3d.CALIB_CB_ADAPTIVE_THRESH | Calib3d.CALIB_CB_FAST_CHECK | Calib3d.CALIB_CB_NORMALIZE_IMAGE);
  462. break;
  463. case MarkerType.CirclesGlid:
  464. found = Calib3d.findCirclesGrid (frameMat, patternSize, points, Calib3d.CALIB_CB_SYMMETRIC_GRID);
  465. break;
  466. case MarkerType.AsymmetricCirclesGlid:
  467. found = Calib3d.findCirclesGrid (frameMat, patternSize, points, Calib3d.CALIB_CB_ASYMMETRIC_GRID);
  468. break;
  469. }
  470. if (found) {
  471. Debug.Log ("Frame captured.");
  472. if (markerType == MarkerType.ChessBoard)
  473. Imgproc.cornerSubPix (frameMat, points, new Size (5, 5), new Size (-1, -1), new TermCriteria (TermCriteria.EPS + TermCriteria.COUNT, 30, 0.1));
  474. imagePoints.Add (points);
  475. allImgs.Add (frameMat);
  476. } else {
  477. Debug.Log ("Invalid frame.");
  478. frameMat.Dispose ();
  479. if (points != null)
  480. points.Dispose ();
  481. return -1;
  482. }
  483. if (imagePoints.Count < 1) {
  484. Debug.Log ("Not enough points for calibration.");
  485. repErr = -1;
  486. } else {
  487. MatOfPoint3f objectPoint = new MatOfPoint3f (new Mat (imagePoints [0].rows (), 1, CvType.CV_32FC3));
  488. CalcChessboardCorners (patternSize, squareSize, objectPoint, markerType);
  489. List<Mat> objectPoints = new List<Mat> ();
  490. for (int i = 0; i < imagePoints.Count; ++i) {
  491. objectPoints.Add (objectPoint);
  492. }
  493. repErr = Calib3d.calibrateCamera (objectPoints, imagePoints, frameMat.size (), camMatrix, distCoeffs, rvecs, tvecs, calibrationFlags);
  494. objectPoint.Dispose ();
  495. }
  496. break;
  497. }
  498. Debug.Log ("repErr: " + repErr);
  499. Debug.Log ("camMatrix: " + camMatrix.dump ());
  500. Debug.Log ("distCoeffs: " + distCoeffs.dump ());
  501. return repErr;
  502. }
  503. private double CalibrateCameraAruco (List<List<Mat>> allCorners, List<Mat> allIds, CharucoBoard board, Size imageSize, Mat cameraMatrix, Mat distCoeffs, List<Mat> rvecs = null, List<Mat> tvecs = null, int calibrationFlags = 0)
  504. {
  505. // prepare data for calibration
  506. int nFrames = allCorners.Count;
  507. int allLen = 0;
  508. int[] markerCounterPerFrameArr = new int[allCorners.Count];
  509. for (int i = 0; i < nFrames; ++i) {
  510. markerCounterPerFrameArr [i] = allCorners [i].Count;
  511. allLen += allCorners [i].Count;
  512. }
  513. int[] allIdsConcatenatedArr = new int[allLen];
  514. int index = 0;
  515. for (int j = 0; j < allIds.Count; ++j) {
  516. int[] idsArr = new int[(int)allIds [j].total ()];
  517. allIds [j].get (0, 0, idsArr);
  518. for (int k = 0; k < idsArr.Length; ++k) {
  519. allIdsConcatenatedArr [index + k] = (int)idsArr [k];
  520. }
  521. index += idsArr.Length;
  522. }
  523. using (Mat allIdsConcatenated = new Mat (1, allLen, CvType.CV_32SC1))
  524. using (Mat markerCounterPerFrame = new Mat (1, nFrames, CvType.CV_32SC1)) {
  525. List<Mat> allCornersConcatenated = new List<Mat> ();
  526. foreach (var c in allCorners) {
  527. foreach (var m in c) {
  528. allCornersConcatenated.Add (m);
  529. }
  530. }
  531. allIdsConcatenated.put (0, 0, allIdsConcatenatedArr);
  532. markerCounterPerFrame.put (0, 0, markerCounterPerFrameArr);
  533. if (rvecs == null)
  534. rvecs = new List<Mat> ();
  535. if (tvecs == null)
  536. tvecs = new List<Mat> ();
  537. return Aruco.calibrateCameraAruco (allCornersConcatenated, allIdsConcatenated, markerCounterPerFrame, board, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, calibrationFlags);
  538. }
  539. }
  540. private double CalibrateCameraCharuco (List<List<Mat>> allCorners, List<Mat> allIds, CharucoBoard board, Size imageSize, Mat cameraMatrix, Mat distCoeffs, List<Mat> rvecs = null, List<Mat> tvecs = null, int calibrationFlags = 0, int minMarkers = 2)
  541. {
  542. // prepare data for charuco calibration
  543. int nFrames = allCorners.Count;
  544. List<Mat> allCharucoCorners = new List<Mat> ();
  545. List<Mat> allCharucoIds = new List<Mat> ();
  546. List<Mat> filteredImages = new List<Mat> ();
  547. for (int i = 0; i < nFrames; ++i) {
  548. // interpolate using camera parameters
  549. Mat currentCharucoCorners = new Mat ();
  550. Mat currentCharucoIds = new Mat ();
  551. Aruco.interpolateCornersCharuco (allCorners [i], allIds [i], allImgs [i], board, currentCharucoCorners, currentCharucoIds, cameraMatrix, distCoeffs, minMarkers);
  552. if (charucoIds.total () > 0) {
  553. allCharucoCorners.Add (currentCharucoCorners);
  554. allCharucoIds.Add (currentCharucoIds);
  555. filteredImages.Add (allImgs [i]);
  556. } else {
  557. currentCharucoCorners.Dispose ();
  558. currentCharucoIds.Dispose ();
  559. }
  560. }
  561. if (allCharucoCorners.Count < 1) {
  562. Debug.Log ("Not enough corners for calibration.");
  563. return -1;
  564. }
  565. if (rvecs == null)
  566. rvecs = new List<Mat> ();
  567. if (tvecs == null)
  568. tvecs = new List<Mat> ();
  569. return Aruco.calibrateCameraCharuco (allCharucoCorners, allCharucoIds, board, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, calibrationFlags);
  570. }
  571. private void ResetCalibration ()
  572. {
  573. foreach (var corners in allCorners) {
  574. foreach (var item in corners) {
  575. item.Dispose ();
  576. }
  577. }
  578. allCorners.Clear ();
  579. foreach (var item in allIds) {
  580. item.Dispose ();
  581. }
  582. allIds.Clear ();
  583. foreach (var item in allImgs) {
  584. item.Dispose ();
  585. }
  586. allImgs.Clear ();
  587. repErr = 0;
  588. camMatrix = CreateCameraMatrix (bgrMat.width (), bgrMat.height ());
  589. distCoeffs = new MatOfDouble (0, 0, 0, 0, 0);
  590. foreach (var item in imagePoints) {
  591. item.Dispose ();
  592. }
  593. imagePoints.Clear ();
  594. }
  595. private Mat CreateCameraMatrix (float width, float height)
  596. {
  597. int max_d = (int)Mathf.Max (width, height);
  598. double fx = max_d;
  599. double fy = max_d;
  600. double cx = width / 2.0f;
  601. double cy = height / 2.0f;
  602. Mat camMatrix = new Mat (3, 3, CvType.CV_64FC1);
  603. camMatrix.put (0, 0, fx);
  604. camMatrix.put (0, 1, 0);
  605. camMatrix.put (0, 2, cx);
  606. camMatrix.put (1, 0, 0);
  607. camMatrix.put (1, 1, fy);
  608. camMatrix.put (1, 2, cy);
  609. camMatrix.put (2, 0, 0);
  610. camMatrix.put (2, 1, 0);
  611. camMatrix.put (2, 2, 1.0f);
  612. return camMatrix;
  613. }
  614. private void CalcChessboardCorners (Size patternSize, float squareSize, MatOfPoint3f corners, MarkerType markerType)
  615. {
  616. if ((int)(patternSize.width * patternSize.height) != corners.rows ()) {
  617. Debug.Log ("Invalid corners size.");
  618. corners.create ((int)(patternSize.width * patternSize.height), 1, CvType.CV_32FC3);
  619. }
  620. const int cn = 3;
  621. float[] cornersArr = new float[corners.rows () * cn];
  622. int width = (int)patternSize.width;
  623. int height = (int)patternSize.height;
  624. switch (markerType) {
  625. default:
  626. case MarkerType.ChessBoard:
  627. case MarkerType.CirclesGlid:
  628. for (int i = 0; i < height; ++i) {
  629. for (int j = 0; j < width; ++j) {
  630. cornersArr [(i * width * cn) + (j * cn)] = j * squareSize;
  631. cornersArr [(i * width * cn) + (j * cn) + 1] = i * squareSize;
  632. cornersArr [(i * width * cn) + (j * cn) + 2] = 0;
  633. }
  634. }
  635. corners.put (0, 0, cornersArr);
  636. break;
  637. case MarkerType.AsymmetricCirclesGlid:
  638. for (int i = 0; i < height; ++i) {
  639. for (int j = 0; j < width; ++j) {
  640. cornersArr [(i * width * cn) + (j * cn)] = (2 * j + i % 2) * squareSize;
  641. cornersArr [(i * width * cn) + (j * cn) + 1] = i * squareSize;
  642. cornersArr [(i * width * cn) + (j * cn) + 2] = 0;
  643. }
  644. }
  645. corners.put (0, 0, cornersArr);
  646. break;
  647. }
  648. }
  649. private bool InitializeImagesInputMode ()
  650. {
  651. if (isInitialized)
  652. DisposeCalibraton ();
  653. if (String.IsNullOrEmpty (calibrationImagesDirectory)) {
  654. Debug.LogWarning ("When using the images input mode, please set a calibration images directory path.");
  655. return false;
  656. }
  657. string dirPath = Path.Combine (Application.streamingAssetsPath, calibrationImagesDirectory);
  658. if (!Directory.Exists (dirPath)) {
  659. Debug.LogWarning ("The directory does not exist.");
  660. return false;
  661. }
  662. string[] imageFiles = GetImageFilesInDirectory (dirPath);
  663. if (imageFiles.Length < 1) {
  664. Debug.LogWarning ("The image file does not exist.");
  665. return false;
  666. }
  667. Uri rootPath = new Uri (Application.streamingAssetsPath + System.IO.Path.AltDirectorySeparatorChar);
  668. Uri fullPath = new Uri (imageFiles [0]);
  669. string relativePath = rootPath.MakeRelativeUri (fullPath).ToString ();
  670. using (Mat gray = Imgcodecs.imread (Utils.getFilePath (relativePath), Imgcodecs.IMREAD_GRAYSCALE)) {
  671. if (gray.total () == 0) {
  672. Debug.LogWarning ("Invalid image file.");
  673. return false;
  674. }
  675. using (Mat bgr = new Mat (gray.size (), CvType.CV_8UC3))
  676. using (Mat bgra = new Mat (gray.size (), CvType.CV_8UC4)) {
  677. InitializeCalibraton (gray);
  678. DrawFrame (gray, bgr);
  679. Imgproc.cvtColor (bgr, bgra, Imgproc.COLOR_BGR2RGBA);
  680. Utils.fastMatToTexture2D (bgra, texture);
  681. }
  682. }
  683. return true;
  684. }
  685. private IEnumerator CalibrateCameraUsingImages ()
  686. {
  687. string dirPath = Path.Combine (Application.streamingAssetsPath, calibrationImagesDirectory);
  688. string[] imageFiles = GetImageFilesInDirectory (dirPath);
  689. if (imageFiles.Length < 1)
  690. yield break;
  691. isCalibrating = true;
  692. markerTypeDropdown.interactable = dictionaryIdDropdown.interactable = squaresXDropdown.interactable = squaresYDropdown.interactable = false;
  693. Uri rootPath = new Uri (Application.streamingAssetsPath + System.IO.Path.AltDirectorySeparatorChar);
  694. foreach (var path in imageFiles) {
  695. Uri fullPath = new Uri (path);
  696. string relativePath = rootPath.MakeRelativeUri (fullPath).ToString ();
  697. using (Mat gray = Imgcodecs.imread (Utils.getFilePath (relativePath), Imgcodecs.IMREAD_GRAYSCALE)) {
  698. if (gray.width () != bgrMat.width () || gray.height () != bgrMat.height ())
  699. continue;
  700. Mat frameMat = gray.clone ();
  701. double e = CaptureFrame (frameMat);
  702. if (e > 0)
  703. repErr = e;
  704. DrawFrame (gray, bgrMat);
  705. Imgproc.cvtColor (bgrMat, rgbaMat, Imgproc.COLOR_BGR2RGBA);
  706. Utils.matToTexture2D (rgbaMat, texture);
  707. }
  708. yield return new WaitForSeconds (0.5f);
  709. }
  710. isCalibrating = false;
  711. markerTypeDropdown.interactable = dictionaryIdDropdown.interactable = squaresXDropdown.interactable = squaresYDropdown.interactable = true;
  712. }
  713. private string[] GetImageFilesInDirectory (string dirPath)
  714. {
  715. if (Directory.Exists (dirPath)) {
  716. string[] files = Directory.GetFiles (dirPath, "*.jpg");
  717. files = files.Concat (Directory.GetFiles (dirPath, "*.jpeg")).ToArray ();
  718. files = files.Concat (Directory.GetFiles (dirPath, "*.png")).ToArray ();
  719. files = files.Concat (Directory.GetFiles (dirPath, "*.tiff")).ToArray ();
  720. files = files.Concat (Directory.GetFiles (dirPath, "*.tif")).ToArray ();
  721. return files;
  722. }
  723. return new string[0];
  724. }
  725. /// <summary>
  726. /// Raises the destroy event.
  727. /// </summary>
  728. void OnDestroy ()
  729. {
  730. if (isImagesInputMode) {
  731. DisposeCalibraton ();
  732. } else {
  733. webCamTextureToMatHelper.Dispose ();
  734. }
  735. Screen.orientation = ScreenOrientation.AutoRotation;
  736. }
  737. /// <summary>
  738. /// Raises the back button click event.
  739. /// </summary>
  740. public void OnBackButtonClick ()
  741. {
  742. SceneManager.LoadScene ("OpenCVForUnityExample");
  743. }
  744. /// <summary>
  745. /// Raises the play button click event.
  746. /// </summary>
  747. public void OnPlayButtonClick ()
  748. {
  749. if (isImagesInputMode)
  750. return;
  751. webCamTextureToMatHelper.Play ();
  752. }
  753. /// <summary>
  754. /// Raises the pause button click event.
  755. /// </summary>
  756. public void OnPauseButtonClick ()
  757. {
  758. if (isImagesInputMode)
  759. return;
  760. webCamTextureToMatHelper.Pause ();
  761. }
  762. /// <summary>
  763. /// Raises the stop button click event.
  764. /// </summary>
  765. public void OnStopButtonClick ()
  766. {
  767. if (isImagesInputMode)
  768. return;
  769. webCamTextureToMatHelper.Stop ();
  770. }
  771. /// <summary>
  772. /// Raises the change camera button click event.
  773. /// </summary>
  774. public void OnChangeCameraButtonClick ()
  775. {
  776. if (isImagesInputMode)
  777. return;
  778. webCamTextureToMatHelper.requestedIsFrontFacing = !webCamTextureToMatHelper.IsFrontFacing ();
  779. }
  780. /// <summary>
  781. /// Raises the marker type dropdown value changed event.
  782. /// </summary>
  783. public void OnMarkerTypeDropdownValueChanged (int result)
  784. {
  785. if ((int)markerType != result) {
  786. markerType = (MarkerType)result;
  787. dictionaryIdDropdown.interactable = (markerType == MarkerType.ChArUcoBoard);
  788. if (isImagesInputMode) {
  789. InitializeImagesInputMode ();
  790. } else {
  791. if (webCamTextureToMatHelper.IsInitialized ())
  792. webCamTextureToMatHelper.Initialize ();
  793. }
  794. }
  795. }
  796. /// <summary>
  797. /// Raises the dictionary id dropdown value changed event.
  798. /// </summary>
  799. public void OnDictionaryIdDropdownValueChanged (int result)
  800. {
  801. if ((int)dictionaryId != result) {
  802. dictionaryId = (ArUcoDictionary)result;
  803. dictionary = Aruco.getPredefinedDictionary ((int)dictionaryId);
  804. if (isImagesInputMode) {
  805. InitializeImagesInputMode ();
  806. } else {
  807. if (webCamTextureToMatHelper.IsInitialized ())
  808. webCamTextureToMatHelper.Initialize ();
  809. }
  810. }
  811. }
  812. /// <summary>
  813. /// Raises the squares X dropdown value changed event.
  814. /// </summary>
  815. public void OnSquaresXDropdownValueChanged (int result)
  816. {
  817. if ((int)squaresX != result + 1) {
  818. squaresX = (NumberOfSquaresX)(result + 1);
  819. if (isImagesInputMode) {
  820. InitializeImagesInputMode ();
  821. } else {
  822. if (webCamTextureToMatHelper.IsInitialized ())
  823. webCamTextureToMatHelper.Initialize ();
  824. }
  825. }
  826. }
  827. /// <summary>
  828. /// Raises the squares Y dropdown value changed event.
  829. /// </summary>
  830. public void OnSquaresYDropdownValueChanged (int result)
  831. {
  832. if ((int)squaresY != result + 1) {
  833. squaresY = (NumberOfSquaresY)(result + 1);
  834. if (isImagesInputMode) {
  835. InitializeImagesInputMode ();
  836. } else {
  837. if (webCamTextureToMatHelper.IsInitialized ())
  838. webCamTextureToMatHelper.Initialize ();
  839. }
  840. }
  841. }
  842. /// <summary>
  843. /// Raises the capture button click event.
  844. /// </summary>
  845. public void OnCaptureButtonClick ()
  846. {
  847. if (isImagesInputMode) {
  848. if (!isCalibrating)
  849. InitializeImagesInputMode ();
  850. StartCoroutine ("CalibrateCameraUsingImages");
  851. } else {
  852. shouldCaptureFrame = true;
  853. }
  854. }
  855. /// <summary>
  856. /// Raises the reset button click event.
  857. /// </summary>
  858. public void OnResetButtonClick ()
  859. {
  860. if (isImagesInputMode) {
  861. if (!isCalibrating)
  862. InitializeImagesInputMode ();
  863. } else {
  864. ResetCalibration ();
  865. }
  866. }
  867. /// <summary>
  868. /// Raises the save button click event.
  869. /// </summary>
  870. public void OnSaveButtonClick ()
  871. {
  872. string saveDirectoryPath = Path.Combine (Application.persistentDataPath, "ArUcoCameraCalibrationExample");
  873. if (!Directory.Exists (saveDirectoryPath)) {
  874. Directory.CreateDirectory (saveDirectoryPath);
  875. }
  876. string calibratonDirectoryName = "camera_parameters" + bgrMat.width () + "x" + bgrMat.height ();
  877. string saveCalibratonFileDirectoryPath = Path.Combine (saveDirectoryPath, calibratonDirectoryName);
  878. // Clean up old files.
  879. if (Directory.Exists (saveCalibratonFileDirectoryPath)) {
  880. DirectoryInfo directoryInfo = new DirectoryInfo (saveCalibratonFileDirectoryPath);
  881. foreach (FileInfo fileInfo in directoryInfo.GetFiles()) {
  882. if ((fileInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) {
  883. fileInfo.Attributes = FileAttributes.Normal;
  884. }
  885. }
  886. if ((directoryInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) {
  887. directoryInfo.Attributes = FileAttributes.Directory;
  888. }
  889. directoryInfo.Delete (true);
  890. }
  891. Directory.CreateDirectory (saveCalibratonFileDirectoryPath);
  892. // save the calibraton file.
  893. string savePath = Path.Combine (saveCalibratonFileDirectoryPath, calibratonDirectoryName + ".xml");
  894. int frameCount = (markerType == (int)MarkerType.ChArUcoBoard) ? allCorners.Count : imagePoints.Count;
  895. CameraParameters param = new CameraParameters (frameCount, bgrMat.width (), bgrMat.height (), calibrationFlags, camMatrix, distCoeffs, repErr);
  896. XmlSerializer serializer = new XmlSerializer (typeof(CameraParameters));
  897. using (var stream = new FileStream (savePath, FileMode.Create)) {
  898. serializer.Serialize (stream, param);
  899. }
  900. // save the calibration images.
  901. #if UNITY_WEBGL && !UNITY_EDITOR
  902. string format = "jpg";
  903. MatOfInt compressionParams = new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 100);
  904. #else
  905. string format = "png";
  906. MatOfInt compressionParams = new MatOfInt (Imgcodecs.IMWRITE_PNG_COMPRESSION, 0);
  907. #endif
  908. for (int i = 0; i < allImgs.Count; ++i) {
  909. Imgcodecs.imwrite (Path.Combine (saveCalibratonFileDirectoryPath, calibratonDirectoryName + "_" + i.ToString ("00") + "." + format), allImgs [i], compressionParams);
  910. }
  911. savePathInputField.text = savePath;
  912. Debug.Log ("Saved the CameraParameters to disk in XML file format.");
  913. Debug.Log ("savePath: " + savePath);
  914. }
  915. public enum MarkerType
  916. {
  917. ChArUcoBoard,
  918. ChessBoard,
  919. CirclesGlid,
  920. AsymmetricCirclesGlid
  921. }
  922. public enum ArUcoDictionary
  923. {
  924. DICT_4X4_50 = Aruco.DICT_4X4_50,
  925. DICT_4X4_100 = Aruco.DICT_4X4_100,
  926. DICT_4X4_250 = Aruco.DICT_4X4_250,
  927. DICT_4X4_1000 = Aruco.DICT_4X4_1000,
  928. DICT_5X5_50 = Aruco.DICT_5X5_50,
  929. DICT_5X5_100 = Aruco.DICT_5X5_100,
  930. DICT_5X5_250 = Aruco.DICT_5X5_250,
  931. DICT_5X5_1000 = Aruco.DICT_5X5_1000,
  932. DICT_6X6_50 = Aruco.DICT_6X6_50,
  933. DICT_6X6_100 = Aruco.DICT_6X6_100,
  934. DICT_6X6_250 = Aruco.DICT_6X6_250,
  935. DICT_6X6_1000 = Aruco.DICT_6X6_1000,
  936. DICT_7X7_50 = Aruco.DICT_7X7_50,
  937. DICT_7X7_100 = Aruco.DICT_7X7_100,
  938. DICT_7X7_250 = Aruco.DICT_7X7_250,
  939. DICT_7X7_1000 = Aruco.DICT_7X7_1000,
  940. DICT_ARUCO_ORIGINAL = Aruco.DICT_ARUCO_ORIGINAL,
  941. }
  942. public enum NumberOfSquaresX
  943. {
  944. X_1 = 1,
  945. X_2,
  946. X_3,
  947. X_4,
  948. X_5,
  949. X_6,
  950. X_7,
  951. X_8,
  952. X_9,
  953. X_10,
  954. X_11,
  955. X_12,
  956. X_13,
  957. X_14,
  958. X_15,
  959. }
  960. public enum NumberOfSquaresY
  961. {
  962. Y_1 = 1,
  963. Y_2,
  964. Y_3,
  965. Y_4,
  966. Y_5,
  967. Y_6,
  968. Y_7,
  969. Y_8,
  970. Y_9,
  971. Y_10,
  972. Y_11,
  973. Y_12,
  974. Y_13,
  975. Y_14,
  976. Y_15,
  977. }
  978. }
  979. }