ContourHelper.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Runtime.InteropServices;
  6. using AI.Common;
  7. using AI.Common.Tools;
  8. using AI.Common.Log;
  9. namespace Reconstruction3DHelper
  10. {
  11. /// <summary>
  12. /// 参照opencv里的freeman链码
  13. /// 由于freeman链码里只有0~7,分别表示8个不同方向
  14. /// 但我们的输入点可能不满足上述关系,因此增加了-1和8,分别表示重叠和间距较大的情况
  15. /// </summary>
  16. internal enum EnumChainCode
  17. {
  18. // 参考点和当前点重叠
  19. Overlap = -1,
  20. Up = 0,
  21. UpRight = 1,
  22. Right = 2,
  23. DownRight = 3,
  24. Down = 4,
  25. DownLeft = 5,
  26. Left = 6,
  27. UpLeft = 7,
  28. // 当前点不在参考点8邻域内
  29. Gap = 8,
  30. }
  31. /// <summary>
  32. /// 线的类型
  33. /// </summary>
  34. public enum EnumLineType
  35. {
  36. EightNeighborhood = 0,
  37. FourNeighborhood = 1,
  38. }
  39. /// <summary>
  40. /// 轮廓相关帮助类
  41. /// </summary>
  42. public class ContourHelper
  43. {
  44. /// <summary>
  45. /// float的精度
  46. /// </summary>
  47. public const float FloatPrecision = 0.000001f;
  48. #region
  49. /// <summary>
  50. /// 参照opencv里的cv::DistanceTypes定义的
  51. /// </summary>
  52. public enum EnumDistType
  53. {
  54. User=-1,
  55. L1=1,
  56. L2=2,
  57. C=3,
  58. L12=4,
  59. Fair=5,
  60. Welsch=6,
  61. Huber=7,
  62. }
  63. #endregion
  64. #region dllimport
  65. [DllImport(@"PlaqueProcessingCpp.dll", CallingConvention = CallingConvention.Cdecl)]
  66. public static extern bool FitLine(IntPtr roiVec, int len, float[] matrix, EnumDistType distType,
  67. double param, double reps, double aeps);
  68. #endregion
  69. #region public
  70. /// <summary>
  71. /// 去除轮廓毛刺
  72. /// </summary>
  73. /// <param name="points"></param>
  74. /// <param name="filterRadius"></param>
  75. /// <returns></returns>
  76. public static Point2D[] ContourDeburr(Point2D[] points,int filterRadius=3)
  77. {
  78. // 将轮廓点重采样,使其符合freeman chain code的要求
  79. var resampledPoints = ContourResample(points);
  80. // 重采样后点数过少,直接返回
  81. int resampledPointCount = resampledPoints.Length;
  82. if (resampledPointCount <= 2)
  83. {
  84. return resampledPoints;
  85. }
  86. // 分别计算当前点和前一个点,后一个点的chain code,以此来判断当前点是否为毛刺点
  87. List<int> burrPointIndexes = new List<int>();
  88. for (int ni = 0; ni < resampledPointCount; ni++)
  89. {
  90. int indexPre = ni;
  91. int indexBack = ni;
  92. DecrementInCirculation(ref indexPre, resampledPointCount);
  93. IncrementInCirculation(ref indexBack, resampledPointCount);
  94. if (IsBurrPoint(resampledPoints[indexPre], resampledPoints[ni], resampledPoints[indexBack]))
  95. {
  96. burrPointIndexes.Add(ni);
  97. }
  98. }
  99. // 如果没有毛刺点,可以直接返回
  100. if (burrPointIndexes.Count <= 0)
  101. {
  102. return resampledPoints;
  103. }
  104. // 有毛刺点,需要去除毛刺
  105. // 考虑到毛刺可能突出不止一个像素,因此从每个毛刺点左右分别找与已有轮廓线重合的点
  106. // 只删除与毛刺相邻的重叠点(避免影响其他轮廓交叠位置处)
  107. List<int> indexesToRemove = new List<int>();
  108. foreach (var burrPointIndex in burrPointIndexes)
  109. {
  110. bool preSearch = true;
  111. bool backSearch = true;
  112. int indexPre = burrPointIndex;
  113. int indexBack = burrPointIndex;
  114. int searchedCountPre = 0;
  115. int searchedCountBack = 0;
  116. while (preSearch || backSearch)
  117. {
  118. // 向前搜索
  119. if (preSearch)
  120. {
  121. DecrementInCirculation(ref indexPre, resampledPointCount);
  122. searchedCountPre += 1;
  123. }
  124. // 向后搜索
  125. if (backSearch)
  126. {
  127. IncrementInCirculation(ref indexBack, resampledPointCount);
  128. searchedCountBack += 1;
  129. }
  130. // 判断indexPre处的点,是否和毛刺点后方点相邻,如果相邻,则继续搜索
  131. bool neighborPre = false;
  132. for (int ni = 0; ni <= searchedCountBack; ni++)
  133. {
  134. int index = burrPointIndex;
  135. IncrementInCirculation(ref index, resampledPointCount, ni);
  136. var chainCodePre = ChainCodeToReferencePoint(resampledPoints[indexPre], resampledPoints[index]);
  137. if (chainCodePre != EnumChainCode.Gap)
  138. {
  139. neighborPre = true;
  140. break;
  141. }
  142. }
  143. if (!neighborPre)
  144. {
  145. preSearch = false;
  146. }
  147. // 判断indexBack处的点,是否和毛刺点前方的点相邻,如果相邻,则继续搜索
  148. bool neighborBack = false;
  149. for (int ni = 0; ni <= searchedCountPre; ni++)
  150. {
  151. int index = burrPointIndex;
  152. DecrementInCirculation(ref index, resampledPointCount, ni);
  153. var chainCodeBack = ChainCodeToReferencePoint(resampledPoints[indexBack], resampledPoints[index]);
  154. if (chainCodeBack != EnumChainCode.Gap)
  155. {
  156. neighborBack = true;
  157. break;
  158. }
  159. }
  160. if (!neighborBack)
  161. {
  162. backSearch = false;
  163. }
  164. if (searchedCountPre + searchedCountBack +1 >= resampledPointCount)
  165. {
  166. break;
  167. }
  168. }
  169. // 逐个将当前位置处该删除的点添加到总的indexesToRemove里
  170. int indexStart = burrPointIndex;
  171. int indexEnd = burrPointIndex;
  172. if (searchedCountPre >= 2)
  173. {
  174. DecrementInCirculation(ref indexStart, resampledPointCount, searchedCountPre-1);
  175. }
  176. if (searchedCountBack >= 2)
  177. {
  178. IncrementInCirculation(ref indexEnd, resampledPointCount, searchedCountBack);
  179. }
  180. int indexRemove = indexStart;
  181. while (true)
  182. {
  183. if (!indexesToRemove.Contains(indexRemove))
  184. {
  185. indexesToRemove.Add(indexRemove);
  186. }
  187. IncrementInCirculation(ref indexRemove, resampledPointCount);
  188. if (indexRemove == indexEnd || indexStart == indexEnd)
  189. {
  190. break;
  191. }
  192. }
  193. }
  194. // 没有找到要移除的点,直接返回
  195. if (indexesToRemove.Count <= 0)
  196. {
  197. return resampledPoints;
  198. }
  199. // 将待删除的点的序号,排序,从大到小排
  200. indexesToRemove.Sort();
  201. indexesToRemove.Reverse();
  202. List<Point2D> removedPoints = resampledPoints.ToList();
  203. foreach (var index in indexesToRemove)
  204. {
  205. removedPoints.RemoveAt(index);
  206. }
  207. // 删除后的点,可能会有空缺,补齐
  208. var pointsDeburred = ContourResample(removedPoints.ToArray());
  209. // 最后再对去毛刺后的点进行滤波
  210. int pointCount = pointsDeburred.Length;
  211. if (pointCount <= 2*filterRadius)
  212. {
  213. return pointsDeburred;
  214. }
  215. // 得到要滤波的x和y
  216. int len = pointCount + 2 * filterRadius;
  217. int idx = pointCount - filterRadius;
  218. double[] contourX = new double[len];
  219. double[] contourY = new double[len];
  220. for (int ni = 0; ni < len; ni++)
  221. {
  222. int index = (idx + ni) % pointCount;
  223. contourX[ni] = pointsDeburred[index].X;
  224. contourY[ni] = pointsDeburred[index].Y;
  225. }
  226. // 进行一维滤波
  227. double[] contourXFilt = GaussianBlur(contourX, filterRadius);
  228. double[] contourYFilt = GaussianBlur(contourY, filterRadius);
  229. // 转为输出
  230. Point2D[] pointReturn = new Point2D[pointCount];
  231. for (int ni = 0; ni < pointCount; ni++)
  232. {
  233. pointReturn[ni].X = (int)contourXFilt[ni + filterRadius];
  234. pointReturn[ni].Y = (int)contourYFilt[ni + filterRadius];
  235. }
  236. return pointReturn;
  237. }
  238. /// <summary>
  239. /// 轮廓重采样
  240. /// </summary>
  241. /// <param name="points"></param>
  242. /// <param name="lineType"></param>
  243. /// <returns></returns>
  244. public static Point2D[] ContourResample(Point2D[] points, EnumLineType lineType = EnumLineType.EightNeighborhood)
  245. {
  246. int pointNum = points.Length;
  247. if (pointNum <= 0)
  248. {
  249. return Array.Empty<Point2D>();
  250. }
  251. // 点数过少,直接复制
  252. if (pointNum <= 2)
  253. {
  254. Point2D[] result = new Point2D[pointNum];
  255. Array.Copy(result, points, pointNum);
  256. return result;
  257. }
  258. // 输入的轮廓点应该是逐像素相连的(8邻域),如果有点重叠或间距较大,则应先重采样成所需格式
  259. List<Point2D> pointsResampled = new List<Point2D>();
  260. Point2D pointPre, pointNi;
  261. // 第0个点的前一个点是最后一个点
  262. pointPre = points[pointNum - 1];
  263. for (int ni = 0; ni < pointNum; ni++)
  264. {
  265. pointNi = points[ni];
  266. // 计算当前点到前一个点之间的连线上所有的点
  267. var linePoints = PointsOnTheLine(pointPre, pointNi, lineType);
  268. // 由于前一个点已添加到pointsResampled里,但PointsOnTheLine的返回值是包含起点和终点的,所以从第1个点开始添加
  269. // 当前点和前一个点重叠时,PointsOnTheLine的返回值长度为1,因此不会重复添加重叠点
  270. for (int nj = 1; nj < linePoints.Length; nj++)
  271. {
  272. pointsResampled.Add(linePoints[nj]);
  273. }
  274. pointPre = pointNi;
  275. }
  276. return pointsResampled.ToArray();
  277. }
  278. /// <summary>
  279. /// 循环自减
  280. /// </summary>
  281. /// <param name="index"></param>
  282. /// <param name="circleLen"></param>
  283. /// <returns></returns>
  284. public static void DecrementInCirculation(ref int index, int circleLen, int delta = 1)
  285. {
  286. delta = delta % circleLen;
  287. index -= delta;
  288. if (index < 0)
  289. {
  290. index += circleLen;
  291. }
  292. }
  293. /// <summary>
  294. /// 循环自增
  295. /// </summary>
  296. /// <param name="index"></param>
  297. /// <param name="circleLen"></param>
  298. /// <returns></returns>
  299. public static void IncrementInCirculation(ref int index, int circleLen, int delta = 1)
  300. {
  301. delta = delta % circleLen;
  302. index += delta;
  303. if (index >= circleLen)
  304. {
  305. index -= circleLen;
  306. }
  307. }
  308. /// <summary>
  309. /// 一维高斯滤波
  310. /// </summary>
  311. /// <param name="datas"></param>
  312. /// <param name="radius"></param>
  313. /// <returns></returns>
  314. public static double[] GaussianBlur(double[] datas, int radius)
  315. {
  316. double[] kernel = GaussianKernel(radius);
  317. int len = datas.Length;
  318. double[] datasFilt = new double[len];
  319. Array.Copy(datas, datasFilt, len);
  320. int kernelLen = kernel.Length;
  321. double[] temp = new double[kernelLen];
  322. for (int ni = radius; ni < len - radius; ni++)
  323. {
  324. double sum = 0;
  325. for (int nj = 0; nj < kernelLen; nj++)
  326. {
  327. temp[nj] = datas[ni + nj - radius] * kernel[nj];
  328. sum += temp[nj];
  329. }
  330. datasFilt[ni] = sum;
  331. }
  332. return datasFilt;
  333. }
  334. /// <summary>
  335. /// 获取两点之间的连线
  336. /// 主要参考了opencv的画线算法(该方法用整数的加减计算即可实现,效率较高,属于改进的Bresenham方法)
  337. /// (如果pointStart和pointEnd重叠,返回结果只有一个点)
  338. /// </summary>
  339. /// <param name="pointStart"></param>
  340. /// <param name="pointEnd"></param>
  341. /// <param name="lineType"></param>
  342. /// <returns></returns>
  343. public static Point2D[] PointsOnTheLine(Point2D pointStart, Point2D pointEnd,
  344. EnumLineType lineType = EnumLineType.EightNeighborhood)
  345. {
  346. if (lineType != EnumLineType.EightNeighborhood && lineType != EnumLineType.FourNeighborhood)
  347. {
  348. throw new ArgumentException("lineType should be 'EightNeighborhood' or 'FourNeighborhood'in this function.");
  349. }
  350. // 判断是否需要交换xy或交换起始和结束点
  351. int deltaX = 1;
  352. int deltaY = 1;
  353. int dx = pointEnd.X - pointStart.X;
  354. int dy = pointEnd.Y - pointStart.Y;
  355. int x = pointStart.X;
  356. int y = pointStart.Y;
  357. bool vertStartEnd = dx < 0;
  358. if (vertStartEnd)
  359. {
  360. // 交换开始点和结束点
  361. // 注: 这里用if dx<0 则 dx=-dx 来替代Math.abs
  362. dx = -dx;
  363. dy = -dy;
  364. x = pointEnd.X;
  365. y = pointEnd.Y;
  366. }
  367. if (dy < 0)
  368. {
  369. // 注: 这里用if dy<0 则 dy=-dy 来替代Math.abs
  370. dy = -dy;
  371. deltaY = -1;
  372. }
  373. bool vertXY = dy > dx;
  374. if (vertXY)
  375. {
  376. // 交换x和y
  377. int temp = dx;
  378. dx = dy;
  379. dy = temp;
  380. temp = deltaX;
  381. deltaX = deltaY;
  382. deltaY = temp;
  383. }
  384. // 得到控制循环的一些参数
  385. int error =0, count = 0;
  386. int plusErr =0, plusX =0, plusY =0;
  387. int minusErr = 0, minusX = 0, minusY = 0;
  388. // 注: 这里用 dy+dy 和 dx+dx 代替 2*dy 和 2*dx, 也是为了提高计算效率
  389. switch (lineType)
  390. {
  391. case EnumLineType.EightNeighborhood:
  392. count = dx + 1;
  393. error = dx - (dy + dy);
  394. plusErr = dx + dx;
  395. plusX = 0;
  396. plusY = deltaY;
  397. minusErr = -(dy + dy);
  398. minusX = deltaX;
  399. minusY = 0;
  400. break;
  401. case EnumLineType.FourNeighborhood:
  402. count = dx + dy + 1;
  403. error = 0;
  404. plusErr = (dx + dx) + (dy + dy);
  405. plusX = -deltaX;
  406. plusY = deltaY;
  407. minusErr = -(dy + dy);
  408. minusX = deltaX;
  409. minusY = 0;
  410. break;
  411. }
  412. if (vertXY)
  413. {
  414. // 交换plusX和plusY、minusX和minusY
  415. int temp = plusX;
  416. plusX = plusY;
  417. plusY = temp;
  418. temp = minusX;
  419. minusX = minusY;
  420. minusY = temp;
  421. }
  422. // 循环,得到线上所有点的坐标
  423. Point2D[] points = new Point2D[count];
  424. for (int ni = 0; ni < count; ni++)
  425. {
  426. int index = ni;
  427. if (vertStartEnd)
  428. {
  429. index = count - 1 - ni;
  430. }
  431. points[index].X = x;
  432. points[index].Y = y;
  433. if (error < 0)
  434. {
  435. y += plusY;
  436. error += plusErr;
  437. x += plusX;
  438. }
  439. error += minusErr;
  440. x += minusX;
  441. y += minusY;
  442. }
  443. return points;
  444. }
  445. /// <summary>
  446. /// 获取一段弧线上的所有点
  447. /// 如果 endAngle大于startAngle 则逆时针绘制,如果endAngle小于startAngle则顺时针绘制
  448. /// 用多边形逼近法(该方法虽然计算量相对较大,但可以较为方便地生成任意的圆弧)
  449. /// </summary>
  450. /// <param name="pointCenter"></param>
  451. /// <param name="radius"></param>
  452. /// <param name="startAngle">起始角度(角度,0~360)</param>
  453. /// <param name="angleSpan">角度范围(角度,-360~360,为正则从起始角度逆时针转,为负则顺时针转)</param>
  454. /// <returns></returns>
  455. public static Point2D[] PointsOnTheArc(Point2D pointCenter, float radius, double startAngle, double angleSpan)
  456. {
  457. // 设置多边形逼近的步长,目前暂定边长约为5个像素
  458. int polygonSideLen = 5;
  459. double endAngle = startAngle + angleSpan;
  460. // 判断是否需要交换start和end
  461. bool vertStartEnd = angleSpan < 0;
  462. if (vertStartEnd)
  463. {
  464. double temp = startAngle;
  465. startAngle = endAngle;
  466. endAngle = temp;
  467. }
  468. // 把角度转换到0~360范围内
  469. while (startAngle < 0)
  470. {
  471. startAngle += 360;
  472. endAngle += 360;
  473. }
  474. while (endAngle > 360)
  475. {
  476. endAngle -= 360;
  477. startAngle -= 360;
  478. }
  479. if (endAngle - startAngle > 360)
  480. {
  481. startAngle = 0;
  482. endAngle = 360;
  483. }
  484. // 迭代次数(根据radius和弧度范围,大致确定迭代次数即可)
  485. int count = Math.Max(1, Convert.ToInt32(2 * Math.PI * radius * (endAngle - startAngle) / 360 / polygonSideLen));
  486. double alpha = (endAngle - startAngle) / count;
  487. double alphaRad = MathHelper.DegreeToRadian(alpha);
  488. double startAngleRad = MathHelper.DegreeToRadian(startAngle);
  489. double cos = Math.Cos(alphaRad);
  490. double sin = Math.Sin(alphaRad);
  491. double xi = radius * Math.Cos(startAngleRad);
  492. double yi = radius * Math.Sin(startAngleRad);
  493. double x, y;
  494. List<Point2D> points = new List<Point2D>();
  495. Point2D pointStart = new Point2D();
  496. // 图像坐标上y轴是向下的,所以y应该在pointCenter.Y的基础上减掉yi, 而x方向,因为图像坐标上x轴也是向右,所以是加
  497. pointStart.X = pointCenter.X + (int)xi;
  498. pointStart.Y = pointCenter.Y-(int)yi;
  499. // 将起始点加到points里
  500. points.Add(pointStart);
  501. Point2D pointEnd = new Point2D();
  502. for (int ni = 0; ni < count; ni++)
  503. {
  504. x = (xi * cos - yi * sin);
  505. y = (xi * sin + yi * cos);
  506. pointEnd.X = pointCenter.X +(int)x;
  507. pointEnd.Y = pointCenter.Y-(int)y;
  508. // 两点之间用直线连接
  509. var linePoints = PointsOnTheLine(pointStart, pointEnd);
  510. for (int nj = 1; nj < linePoints.Length; nj++)
  511. {
  512. points.Add(linePoints[nj]);
  513. }
  514. // 递推
  515. xi = x;
  516. yi = y;
  517. pointStart = pointEnd;
  518. }
  519. // 如果交换了起始点和结束点,则翻转一下
  520. if (vertStartEnd)
  521. {
  522. points.Reverse();
  523. }
  524. return points.ToArray();
  525. }
  526. /// <summary>
  527. /// 获取一段椭圆上所有点
  528. /// 如果 endAngle大于startAngle 则逆时针绘制,如果endAngle小于startAngle则顺时针绘制
  529. /// 用多边形逼近法(该方法虽然计算量相对较大,但可以较为方便地生成任意的圆弧)
  530. /// </summary>
  531. /// <param name="pointCenter"></param>
  532. /// <param name="width"></param>
  533. /// <param name="height"></param>
  534. /// <param name="angle"></param>
  535. /// <param name="startAngle"></param>
  536. /// <param name="angleSpan"></param>
  537. /// <returns></returns>
  538. public static Point2D[] PointsOnTheEllipse(Point2D pointCenter, float width, float height, double angle,
  539. double startAngle, double angleSpan)
  540. {
  541. float halfWidth = width / 2;
  542. float halfHeight = height / 2;
  543. // 设置多边形逼近的步长,目前暂定边长约为5个像素
  544. int polygonSideLen = 5;
  545. // 将angle调整到合适的范围
  546. while (angle < 0)
  547. {
  548. angle += 360;
  549. }
  550. while (angle > 360)
  551. {
  552. angle -= 360;
  553. }
  554. double endAngle = startAngle + angleSpan;
  555. // 判断是否需要交换start和end
  556. bool vertStartEnd = angleSpan < 0;
  557. if (vertStartEnd)
  558. {
  559. double temp = startAngle;
  560. startAngle = endAngle;
  561. endAngle = temp;
  562. }
  563. // 把角度转换到0~360范围内
  564. while (startAngle < 0)
  565. {
  566. startAngle += 360;
  567. endAngle += 360;
  568. }
  569. while (endAngle > 360)
  570. {
  571. endAngle -= 360;
  572. startAngle -= 360;
  573. }
  574. if (endAngle - startAngle > 360)
  575. {
  576. startAngle = 0;
  577. endAngle = 360;
  578. }
  579. // 迭代次数(根据radius和弧度范围,大致确定迭代次数即可)
  580. int count = Math.Max(1, Convert.ToInt32(2 * Math.PI * Math.Sqrt(Math.Pow(halfWidth, 2) + Math.Pow(halfHeight, 2)) * (endAngle - startAngle) / 360 / polygonSideLen));
  581. double alpha = (endAngle - startAngle) / count;
  582. double angleRad = MathHelper.DegreeToRadian(angle);
  583. double startAngleRad = MathHelper.DegreeToRadian(startAngle);
  584. double cosAngle = Math.Cos(angleRad);
  585. double sinAngle = Math.Sin(angleRad);
  586. double xi = halfWidth * Math.Cos(startAngleRad);
  587. double yi = halfHeight * Math.Sin(startAngleRad);
  588. double tempAngle,tempRad;
  589. List<Point2D> points = new List<Point2D>();
  590. Point2D pointStart = new Point2D();
  591. // 图像坐标上y轴是向下的,所以y应该在pointCenter.Y的基础上减掉yi, 而x方向,因为图像坐标上x轴也是向右,所以是加
  592. pointStart.X = pointCenter.X+(int)(xi * cosAngle - yi * sinAngle);
  593. pointStart.Y = pointCenter.Y-(int)(xi * sinAngle + yi * cosAngle);
  594. // 将起始点加到points里
  595. points.Add(pointStart);
  596. Point2D pointEnd = new Point2D();
  597. for (int ni = 1; ni <= count; ni++)
  598. {
  599. tempAngle = startAngle + alpha * ni;
  600. tempRad = MathHelper.DegreeToRadian(tempAngle);
  601. xi = halfWidth * Math.Cos(tempRad);
  602. yi = halfHeight * Math.Sin(tempRad);
  603. pointEnd.X = pointCenter.X+(int)(xi * cosAngle - yi * sinAngle);
  604. pointEnd.Y = pointCenter.Y-(int)(xi * sinAngle + yi * cosAngle);
  605. // 两点之间用直线连接
  606. var linePoints = PointsOnTheLine(pointStart, pointEnd);
  607. for (int nj = 1; nj < linePoints.Length; nj++)
  608. {
  609. points.Add(linePoints[nj]);
  610. }
  611. pointStart = pointEnd;
  612. }
  613. // 如果交换了起始点和结束点,则翻转一下
  614. if (vertStartEnd)
  615. {
  616. points.Reverse();
  617. }
  618. return points.ToArray();
  619. }
  620. /// <summary>
  621. /// 计算点到直线的距离
  622. /// </summary>
  623. public static double GetPointToLineDistance(Point2D lineStart, Point2D lineEnd, Point2D point)
  624. {
  625. if (lineStart.X == lineEnd.X)
  626. {
  627. return Math.Abs(point.X - lineStart.X);
  628. }
  629. double slope = (lineEnd.Y - lineStart.Y) / (lineEnd.X - lineStart.X);
  630. double intercept = (lineEnd.X * lineStart.Y - lineStart.X * lineEnd.Y) / (lineEnd.X - lineStart.X);
  631. return Math.Abs(slope * point.X - point.Y + intercept) / (Math.Sqrt(slope * slope + 1));
  632. }
  633. /// <summary>
  634. /// 计算点到直线的距离
  635. /// </summary>
  636. /// <param name="lineParam"></param>
  637. /// <param name="point"></param>
  638. /// <returns></returns>
  639. public static double GetPointToLineDistance(double[] lineParam, Point2D point)
  640. {
  641. if (lineParam[1] == 0)
  642. {
  643. return Math.Abs(point.X - lineParam[2]) / lineParam[0];
  644. }
  645. var A = lineParam[0];
  646. var C = lineParam[2];
  647. var a = point.X;
  648. var b = point.Y;
  649. var n = (A * a + A * A * b + C) / (A * A + 1);
  650. var m = a - A * n + A * b;
  651. if (Math.Abs(m - a) < 0.00001)
  652. {
  653. return 0;
  654. }
  655. var disX = m - a;
  656. var disY = n - b;
  657. return Math.Sqrt(disX * disX + disY * disY);
  658. }
  659. /// <summary>
  660. /// 计算两点间的直线斜率与截距
  661. /// </summary>
  662. public static void GetSlopeAndIntercept(Point2D FirstPoint, Point2D SeconsPoint, out double k, out double b)
  663. {
  664. double x1 = FirstPoint.X;
  665. double y1 = FirstPoint.Y;
  666. double x2 = SeconsPoint.X;
  667. double y2 = SeconsPoint.Y;
  668. if (x1 == x2)
  669. {
  670. k = double.NaN;
  671. b = double.NaN;
  672. return;
  673. }
  674. if (y1 == y2)
  675. {
  676. k = 0.0;
  677. b = y1;
  678. return;
  679. }
  680. k = ((y1 - y2) / (x1 - x2));
  681. b = y1 - x1 * k;
  682. return;
  683. }
  684. /// <summary>
  685. /// 射线法判断点是否在多边形内,即计算射线与多边形各边的交点,如果是奇数,则点在多边形内,否则在多边形外
  686. /// <param name="polygonPointList"></param> 多边形上的点,注意要按照顺序保存
  687. /// <param name="objectPoint"></param> 目标点,判断该点是否在多边形内的
  688. /// </summary>
  689. public static bool IsPointInPolygonRayMethod(Point2D[] polygonPointList, Point2D objectPoint)
  690. {
  691. if (polygonPointList.Length == 0)
  692. {
  693. return false;
  694. }
  695. int num = 0;
  696. // 用当前点(i)和前一个点(j)拉一条直线,计算与objectPoint点发出的射线(指向x轴正方向)的交点的个数
  697. int j = polygonPointList.Length - 1;
  698. for (int i = 0; i < polygonPointList.Length; i++)
  699. {
  700. Point2D start = new Point2D(polygonPointList[i].X, polygonPointList[i].Y);
  701. Point2D end = new Point2D(polygonPointList[j].X, polygonPointList[j].Y);
  702. // 点与多边形的顶点重合
  703. if (((start.X == objectPoint.X) && (start.Y == objectPoint.Y)) || ((end.X == objectPoint.X) && (end.Y == objectPoint.Y)))
  704. {
  705. return true;
  706. }
  707. // 判断线段两端点是否在射线两侧
  708. if (((objectPoint.Y >= start.Y) && (objectPoint.Y < end.Y)) || ((objectPoint.Y >= end.Y) && (objectPoint.Y < start.Y)))
  709. {
  710. // 线段上与射线 Y 坐标相同的点的 X 坐标
  711. var x = start.X + (objectPoint.Y - start.Y) * (end.X - start.X) / (end.Y - start.Y);
  712. // 点在多边形的边上
  713. if (objectPoint.X == x)
  714. {
  715. return true;
  716. }
  717. // 射线穿过多边形的边界
  718. if (x > objectPoint.X)
  719. {
  720. ++num;
  721. }
  722. }
  723. j = i;
  724. }
  725. // 射线穿过多边形边界的次数num为偶数时点在多边形外,返回false,否则返回true
  726. return (num % 2 == 0) ? false : true;
  727. }
  728. /// <summary>
  729. /// 拟合直线
  730. /// </summary>
  731. /// <param name="points"></param>
  732. /// <param name="removeAbnormal">是否移除异常点</param>
  733. /// <returns></returns>
  734. public static bool FitLines(Point2D[] points,out double[] lineParam, bool removeAbnormal = false)
  735. {
  736. lineParam = new double[3];
  737. try
  738. {
  739. // 为了避免直接删除输入数组points里的元素,复制一份出来
  740. var pointsCal = new Point2D[points.Length];
  741. Array.Copy(points, pointsCal,points.Length);
  742. // 移除异常数据
  743. if (removeAbnormal)
  744. {
  745. // 原来颈动脉代码里的意思,应该是反复做三次移除异常数据
  746. var removeTimes = 0;
  747. while (removeTimes < 3)
  748. {
  749. pointsCal = RemoveAbnormalData(pointsCal);
  750. removeTimes++;
  751. }
  752. // 因为这里的Point2D,X,Y里存的分别是Point3D的X和Z,所以这里将Point2D按Y排序,即为将投影点仍旧按照其帧顺序排序
  753. // (因为在RemoveAbnormalData里根据点到直线的距离排序后,投影点的帧顺序会被打乱)
  754. pointsCal = pointsCal.OrderBy(x => x.Y).ToArray();
  755. }
  756. int pointCount = pointsCal.Length;
  757. float[] matrix = new float[4];
  758. GCHandle hObject = GCHandle.Alloc(pointsCal, GCHandleType.Pinned);
  759. IntPtr pObject = hObject.AddrOfPinnedObject();
  760. // ToDo: 是否需要将拟合直线的参数开放出来(之前颈动脉代码里也是写死的)
  761. var ret = FitLine(pObject, pointCount, matrix, EnumDistType.L2, 0, 0.001, 0.001);
  762. float x0 = matrix[0];
  763. float x1 = matrix[1];
  764. float x2 = matrix[2];
  765. float x3 = matrix[3];
  766. if (x0 < FloatPrecision)
  767. {
  768. lineParam[0] = 1;
  769. lineParam[1] = 0;
  770. lineParam[2] = x2;
  771. }
  772. else
  773. {
  774. double k = x1 / x0;
  775. double b = x3 - k * x2;
  776. lineParam[0] = k;
  777. lineParam[1] = -1;
  778. lineParam[2] = b;
  779. }
  780. return true;
  781. }
  782. catch (Exception excep)
  783. {
  784. LogHelper.ErrorLog("ContourHelper.FitLine Failed:"+excep);
  785. return false;
  786. }
  787. }
  788. #endregion
  789. #region private
  790. /// <summary>
  791. /// 计算当前点相对于参考点的chain code
  792. /// </summary>
  793. /// <param name="pointRef"></param>
  794. /// <param name="pointToAdd"></param>
  795. /// <returns></returns>
  796. private static EnumChainCode ChainCodeToReferencePoint(Point2D pointRef, Point2D pointToAdd)
  797. {
  798. int deltaX = pointToAdd.X - pointRef.X;
  799. int deltaY = pointToAdd.Y - pointRef.Y;
  800. if (deltaX == 0 && deltaY == 0)
  801. {
  802. return EnumChainCode.Overlap;
  803. }
  804. if (deltaX == -1 && deltaY == 0)
  805. {
  806. return EnumChainCode.Left;
  807. }
  808. if (deltaX == 1 && deltaY == 0)
  809. {
  810. return EnumChainCode.Right;
  811. }
  812. if (deltaX == -1 && deltaY == -1)
  813. {
  814. return EnumChainCode.UpLeft;
  815. }
  816. if (deltaX == 0 && deltaY == -1)
  817. {
  818. return EnumChainCode.Up;
  819. }
  820. if (deltaX == 1 && deltaY == -1)
  821. {
  822. return EnumChainCode.UpRight;
  823. }
  824. if (deltaX == -1 && deltaY == 1)
  825. {
  826. return EnumChainCode.DownLeft;
  827. }
  828. if (deltaX == 0 && deltaY == 1)
  829. {
  830. return EnumChainCode.Down;
  831. }
  832. if (deltaX == 1 && deltaY == 1)
  833. {
  834. return EnumChainCode.DownRight;
  835. }
  836. return EnumChainCode.Gap;
  837. }
  838. /// <summary>
  839. /// 计算当前点是否为毛刺点
  840. /// </summary>
  841. /// <param name="pointBefore"></param>
  842. /// <param name="pointCur"></param>
  843. /// <param name="pointAfter"></param>
  844. /// <returns></returns>
  845. private static bool IsBurrPoint(Point2D pointBefore, Point2D pointCur, Point2D pointAfter)
  846. {
  847. int chainCodeWithBef = (int)ChainCodeToReferencePoint(pointCur, pointBefore);
  848. int chainCodeWithAft = (int)ChainCodeToReferencePoint(pointCur, pointAfter);
  849. // 只有符合freeman chain code 的相邻点才能这样判断
  850. if (chainCodeWithBef >= 0 && chainCodeWithBef <= 7 && chainCodeWithAft >= 0 && chainCodeWithAft <= 7)
  851. {
  852. int delta = Math.Abs(chainCodeWithBef - chainCodeWithAft);
  853. if (delta<=1 || delta>=7)
  854. {
  855. return true;
  856. }
  857. }
  858. return false;
  859. }
  860. /// <summary>
  861. /// 高斯核
  862. /// </summary>
  863. /// <param name="radius"></param>
  864. /// <returns></returns>
  865. private static double[] GaussianKernel(int radius)
  866. {
  867. int count = radius * 2 + 1;
  868. // sigma 决定高斯核的形态,经验值,选择让核边缘的权重较小,中心权重较大的sigma值
  869. double sigma = radius / 1.5;
  870. double[] kernel = new double[count];
  871. double sum = 0;
  872. for (int ni = 0; ni < count; ni++)
  873. {
  874. kernel[ni] = Math.Exp(-Math.Pow(ni - radius, 2) / (2 * Math.Pow(sigma, 2))) /
  875. (sigma * Math.Sqrt(2 * Math.PI));
  876. sum += kernel[ni];
  877. }
  878. // 除以sum
  879. for (int ni = 0; ni < count; ni++)
  880. {
  881. kernel[ni] /= sum;
  882. }
  883. return kernel;
  884. }
  885. /// <summary>
  886. /// 移除异常数据
  887. /// </summary>
  888. /// <param name="points"></param>
  889. /// <returns></returns>
  890. private static Point2D[] RemoveAbnormalData(Point2D[] points)
  891. {
  892. // 如果点数过少,则直接返回
  893. var count = points.Length;
  894. if (count < 10)
  895. {
  896. return points;
  897. }
  898. if (!FitLines(points, out var line, false))
  899. {
  900. return points;
  901. }
  902. // 计算每个点到直线的距离
  903. var diffs = new double[count];
  904. double sumDiff = 0;
  905. for (int ni = 0; ni < count; ni++)
  906. {
  907. var diff = GetPointToLineDistance(line, points[ni]);
  908. sumDiff += diff;
  909. diffs[ni] = diff;
  910. }
  911. // 计算距离值的平均值和标准差
  912. var averageDiff = sumDiff / count;
  913. double sumStd = 0;
  914. for (int ni = 0; ni < count; ni++)
  915. {
  916. var diff = diffs[ni];
  917. sumStd += Math.Pow(diff - averageDiff, 2);
  918. }
  919. sumStd = Math.Sqrt(sumStd / count);
  920. var minDiff = averageDiff - 3 * sumStd;
  921. var maxDiff = averageDiff + 3 * sumStd;
  922. // 删除距离大于平均值加减3倍的标准差的点
  923. for (var ni = 0; ni < count; ni++)
  924. {
  925. var diff = diffs[ni];
  926. if (diff < minDiff || diff > maxDiff)
  927. {
  928. diffs[ni] = double.MinValue;
  929. }
  930. }
  931. // 去掉10%的数据(注:颈动脉原来代码里是这样写的,这里先直接照搬,后续再考虑优化)
  932. var countRemove = count / 10;
  933. var diffList = new List<KeyValuePair<int, double>>();
  934. for (var ni = 0; ni < count; ni++)
  935. {
  936. diffList.Add(new KeyValuePair<int, double>(ni, diffs[ni]));
  937. }
  938. var sortedDiffList = diffList.OrderByDescending(o => o.Value);
  939. var result = new List<Point2D>();
  940. for (var ni = countRemove; ni < count; ni++)
  941. {
  942. // 在上一步中,距离大于平均值加减3倍标准差的点(被赋为了double.MinValue),不管在不在10%以内,都应该被舍弃
  943. if (sortedDiffList.ElementAt(ni).Value >=0)
  944. {
  945. result.Add(points[sortedDiffList.ElementAt(ni).Key]);
  946. }
  947. }
  948. return result.ToArray();
  949. }
  950. #endregion
  951. }
  952. }