ImageBorderRemover.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. using Masuit.Tools.Systems;
  2. using SixLabors.ImageSharp;
  3. using SixLabors.ImageSharp.PixelFormats;
  4. using SixLabors.ImageSharp.Processing;
  5. // ReSharper disable AccessToDisposedClosure
  6. namespace Masuit.Tools.Media;
  7. /// <summary>
  8. /// 图像边框移除器
  9. /// </summary>
  10. public class ImageBorderRemover
  11. {
  12. /// <summary>
  13. /// 检测图片边框信息(支持多色边框)
  14. /// </summary>
  15. /// <param name="imagePath">图片路径</param>
  16. /// <param name="tolerance">颜色容差(0-100),默认10</param>
  17. /// <param name="maxLayers">最大检测边框层数,默认3</param>
  18. /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
  19. /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
  20. /// <returns>边框检测结果</returns>
  21. public static BorderDetectionResult DetectBorders(string imagePath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
  22. {
  23. using (Image<Rgba32> image = Image.Load<Rgba32>(imagePath))
  24. {
  25. return DetectBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
  26. }
  27. }
  28. /// <summary>
  29. /// 检测图片边框信息(从已加载的图像)
  30. /// </summary>
  31. /// <param name="image">已加载的图像</param>
  32. /// <param name="tolerance">颜色容差(0-100),默认10</param>
  33. /// <param name="maxLayers">最大检测边框层数,默认3</param>
  34. /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
  35. /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
  36. /// <returns>边框检测结果</returns>
  37. public static BorderDetectionResult DetectBorders(Image<Rgba32> image, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
  38. {
  39. var result = new BorderDetectionResult
  40. {
  41. ImageWidth = image.Width,
  42. ImageHeight = image.Height,
  43. BorderColors = new List<Rgba32>(),
  44. BorderLayers = 0
  45. };
  46. byte toleranceValue = (byte)(tolerance * 2.55);
  47. // 使用多层边框检测算法
  48. var (top, bottom, left, right, layers, colors) = FindContentBordersWithLayers(image, toleranceValue, maxLayers, useDownscaling, downscaleFactor);
  49. // 设置内容边界
  50. result.ContentTop = top;
  51. result.ContentBottom = bottom;
  52. result.ContentLeft = left;
  53. result.ContentRight = right;
  54. result.BorderLayers = layers;
  55. result.BorderColors = colors;
  56. return result;
  57. }
  58. /// <summary>
  59. /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
  60. /// </summary>
  61. /// <param name="inputPath">输入图片路径</param>
  62. /// <param name="tolerance">颜色容差(0-100),默认10</param>
  63. /// <param name="maxLayers">最大检测边框层数,默认3</param>
  64. /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
  65. /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
  66. /// <returns>是否执行了裁剪操作</returns>
  67. public static void RemoveBorders(string inputPath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
  68. {
  69. RemoveBorders(inputPath, inputPath, tolerance, maxLayers, useDownscaling, downscaleFactor);
  70. }
  71. /// <summary>
  72. /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
  73. /// </summary>
  74. /// <param name="inputPath">输入图片路径</param>
  75. /// <param name="outputPath">输出图片路径</param>
  76. /// <param name="tolerance">颜色容差(0-100),默认10</param>
  77. /// <param name="maxLayers">最大检测边框层数,默认3</param>
  78. /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
  79. /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
  80. /// <returns>是否执行了裁剪操作</returns>
  81. public static void RemoveBorders(string inputPath, string outputPath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
  82. {
  83. using Image<Rgba32> image = Image.Load<Rgba32>(inputPath);
  84. var hasCropped = RemoveBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
  85. // 决定是否保存
  86. if (hasCropped)
  87. {
  88. image.Save(outputPath);
  89. }
  90. }
  91. /// <summary>
  92. /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
  93. /// </summary>
  94. /// <param name="input">输入图片路径</param>
  95. /// <param name="tolerance">颜色容差(0-100),默认10</param>
  96. /// <param name="maxLayers">最大检测边框层数,默认3</param>
  97. /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
  98. /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
  99. /// <returns>是否执行了裁剪操作</returns>
  100. public static PooledMemoryStream RemoveBorders(Stream input, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
  101. {
  102. var format = Image.DetectFormat(input);
  103. input.Seek(0, SeekOrigin.Begin);
  104. Image<Rgba32> image = Image.Load<Rgba32>(input);
  105. RemoveBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
  106. var stream = new PooledMemoryStream();
  107. image.Save(stream, format);
  108. return stream;
  109. }
  110. private static bool RemoveBorders(Image<Rgba32> image, int tolerance, int maxLayers, bool useDownscaling, int downscaleFactor)
  111. {
  112. // 保存原始尺寸用于比较
  113. int originalWidth = image.Width;
  114. int originalHeight = image.Height;
  115. // 使用多层检测方法获取边框信息
  116. var borderInfo = DetectBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
  117. bool hasCropped = false;
  118. if (borderInfo.CanBeCropped)
  119. {
  120. int newWidth = borderInfo.ContentRight - borderInfo.ContentLeft + 1;
  121. int newHeight = borderInfo.ContentBottom - borderInfo.ContentTop + 1;
  122. if (newWidth > 0 && newHeight > 0 && (newWidth != originalWidth || newHeight != originalHeight))
  123. {
  124. image.Mutate(x => x.Crop(new Rectangle(borderInfo.ContentLeft, borderInfo.ContentTop, newWidth, newHeight)));
  125. hasCropped = true;
  126. }
  127. }
  128. return hasCropped;
  129. }
  130. /// <summary>
  131. /// 查找内容边界(支持多层边框检测)
  132. /// </summary>
  133. private static (int top, int bottom, int left, int right, int layers, List<Rgba32> colors) FindContentBordersWithLayers(Image<Rgba32> image, byte tolerance, int maxLayers, bool useDownscaling, int downscaleFactor)
  134. {
  135. // 如果启用缩小采样且图像足够大
  136. Image<Rgba32> workingImage;
  137. float scale = 1f;
  138. bool isDownscaled = false;
  139. if (useDownscaling && image.Width > 500 && image.Height > 500)
  140. {
  141. // 计算缩小尺寸
  142. int newWidth = image.Width / downscaleFactor;
  143. int newHeight = image.Height / downscaleFactor;
  144. scale = (float)image.Width / newWidth;
  145. // 创建缩小版本用于检测
  146. workingImage = image.Clone(ctx => ctx.Resize(newWidth, newHeight));
  147. isDownscaled = true;
  148. }
  149. else
  150. {
  151. workingImage = image;
  152. }
  153. int width = workingImage.Width;
  154. int height = workingImage.Height;
  155. int top = 0;
  156. int bottom = height - 1;
  157. int left = 0;
  158. int right = width - 1;
  159. int layers = 0;
  160. var borderColors = new List<Rgba32>();
  161. // 检测多层边框
  162. for (int layer = 0; layer < maxLayers; layer++)
  163. {
  164. bool borderFound = false;
  165. // 并行检测四个方向的边框层
  166. var results = new (int borderSize, Rgba32? color)[4];
  167. Parallel.Invoke(() =>
  168. {
  169. if (top < height / 2)
  170. {
  171. Rgba32? layerColor = null;
  172. int newTop = DetectLayerBorderTop(workingImage, top, bottom, left, right, tolerance, ref layerColor);
  173. results[0] = (newTop - top, layerColor);
  174. if (newTop > top) borderFound = true;
  175. top = newTop;
  176. }
  177. }, () =>
  178. {
  179. if (bottom > height / 2)
  180. {
  181. Rgba32? layerColor = null;
  182. int newBottom = DetectLayerBorderBottom(workingImage, top, bottom, left, right, tolerance, ref layerColor);
  183. results[1] = (newBottom - bottom, layerColor);
  184. if (newBottom < bottom) borderFound = true;
  185. bottom = newBottom;
  186. }
  187. }, () =>
  188. {
  189. if (left < width / 2)
  190. {
  191. Rgba32? layerColor = null;
  192. int newLeft = DetectLayerBorderLeft(workingImage, top, bottom, left, right, tolerance, ref layerColor);
  193. results[2] = (newLeft - left, layerColor);
  194. if (newLeft > left) borderFound = true;
  195. left = newLeft;
  196. }
  197. }, () =>
  198. {
  199. if (right > width / 2)
  200. {
  201. Rgba32? layerColor = null;
  202. int newRight = DetectLayerBorderRight(workingImage, top, bottom, left, right, tolerance, ref layerColor);
  203. results[3] = (newRight - right, layerColor);
  204. if (newRight < right) borderFound = true;
  205. right = newRight;
  206. }
  207. });
  208. // 收集检测到的边框颜色
  209. foreach (var (borderSize, color) in results)
  210. {
  211. if (color.HasValue && borderSize > 0)
  212. {
  213. borderColors.Add(color.Value);
  214. }
  215. }
  216. if (borderFound)
  217. {
  218. layers++;
  219. }
  220. else
  221. {
  222. break; // 没有检测到更多边框层
  223. }
  224. }
  225. // 如果是缩小采样版本,映射回原图坐标
  226. if (isDownscaled)
  227. {
  228. top = (int)(top * scale);
  229. bottom = (int)(bottom * scale);
  230. left = (int)(left * scale);
  231. right = (int)(right * scale);
  232. // 确保边界在图像范围内
  233. top = Clamp(top, 0, image.Height - 1);
  234. bottom = Clamp(bottom, top, image.Height - 1);
  235. left = Clamp(left, 0, image.Width - 1);
  236. right = Clamp(right, left, image.Width - 1);
  237. // 释放缩小图像
  238. workingImage.Dispose();
  239. }
  240. return (top, bottom, left, right, layers, borderColors);
  241. }
  242. private static int Clamp(int value, int min, int max)
  243. {
  244. return value < min ? min : value > max ? max : value;
  245. }
  246. /// <summary>
  247. /// 检测顶部边框层(优化版)
  248. /// </summary>
  249. private static int DetectLayerBorderTop(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
  250. {
  251. int newTop = currentTop;
  252. Rgba32? detectedColor = null;
  253. // 使用采样检测代替全行扫描
  254. int sampleCount = Math.Min(50, currentRight - currentLeft + 1);
  255. int stepX = Math.Max(1, (currentRight - currentLeft) / sampleCount);
  256. // 从当前顶部开始向下扫描
  257. for (int y = currentTop; y <= currentBottom; y++)
  258. {
  259. Rgba32? rowColor = null;
  260. bool isUniform = true;
  261. // 采样检查行是否统一颜色
  262. for (int x = currentLeft; x <= currentRight; x += stepX)
  263. {
  264. if (!rowColor.HasValue)
  265. {
  266. rowColor = image[x, y];
  267. continue;
  268. }
  269. if (!IsSimilarColor(image[x, y], rowColor.Value, tolerance))
  270. {
  271. isUniform = false;
  272. break;
  273. }
  274. }
  275. // 如果是统一颜色行
  276. if (isUniform && rowColor.HasValue)
  277. {
  278. // 第一行总是被认为是边框
  279. if (y == currentTop)
  280. {
  281. detectedColor = rowColor;
  282. newTop = y + 1;
  283. continue;
  284. }
  285. // 后续行必须与第一行颜色相似
  286. if (detectedColor.HasValue && IsSimilarColor(rowColor.Value, detectedColor.Value, tolerance))
  287. {
  288. newTop = y + 1;
  289. }
  290. else
  291. {
  292. break;
  293. }
  294. }
  295. else
  296. {
  297. break;
  298. }
  299. }
  300. if (newTop > currentTop)
  301. {
  302. borderColor = detectedColor;
  303. return newTop;
  304. }
  305. return currentTop;
  306. }
  307. /// <summary>
  308. /// 检测底部边框层(优化版)
  309. /// </summary>
  310. private static int DetectLayerBorderBottom(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
  311. {
  312. int newBottom = currentBottom;
  313. Rgba32? detectedColor = null;
  314. // 使用采样检测代替全行扫描
  315. int sampleCount = Math.Min(50, currentRight - currentLeft + 1);
  316. int stepX = Math.Max(1, (currentRight - currentLeft) / sampleCount);
  317. // 从当前底部开始向上扫描
  318. for (int y = currentBottom; y >= currentTop; y--)
  319. {
  320. Rgba32? rowColor = null;
  321. bool isUniform = true;
  322. // 采样检查行是否统一颜色
  323. for (int x = currentLeft; x <= currentRight; x += stepX)
  324. {
  325. if (!rowColor.HasValue)
  326. {
  327. rowColor = image[x, y];
  328. continue;
  329. }
  330. if (!IsSimilarColor(image[x, y], rowColor.Value, tolerance))
  331. {
  332. isUniform = false;
  333. break;
  334. }
  335. }
  336. if (isUniform && rowColor.HasValue)
  337. {
  338. if (y == currentBottom)
  339. {
  340. detectedColor = rowColor;
  341. newBottom = y - 1;
  342. continue;
  343. }
  344. if (detectedColor.HasValue && IsSimilarColor(rowColor.Value, detectedColor.Value, tolerance))
  345. {
  346. newBottom = y - 1;
  347. }
  348. else
  349. {
  350. break;
  351. }
  352. }
  353. else
  354. {
  355. break;
  356. }
  357. }
  358. if (newBottom < currentBottom)
  359. {
  360. borderColor = detectedColor;
  361. return newBottom;
  362. }
  363. return currentBottom;
  364. }
  365. /// <summary>
  366. /// 检测左侧边框层(优化版)
  367. /// </summary>
  368. private static int DetectLayerBorderLeft(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
  369. {
  370. int newLeft = currentLeft;
  371. Rgba32? detectedColor = null;
  372. // 使用采样检测代替全列扫描
  373. int sampleCount = Math.Min(50, currentBottom - currentTop + 1);
  374. int stepY = Math.Max(1, (currentBottom - currentTop) / sampleCount);
  375. // 从当前左侧开始向右扫描
  376. for (int x = currentLeft; x <= currentRight; x++)
  377. {
  378. Rgba32? colColor = null;
  379. bool isUniform = true;
  380. // 采样检查列是否统一颜色
  381. for (int y = currentTop; y <= currentBottom; y += stepY)
  382. {
  383. if (!colColor.HasValue)
  384. {
  385. colColor = image[x, y];
  386. continue;
  387. }
  388. if (!IsSimilarColor(image[x, y], colColor.Value, tolerance))
  389. {
  390. isUniform = false;
  391. break;
  392. }
  393. }
  394. if (isUniform && colColor.HasValue)
  395. {
  396. if (x == currentLeft)
  397. {
  398. detectedColor = colColor;
  399. newLeft = x + 1;
  400. continue;
  401. }
  402. if (detectedColor.HasValue && IsSimilarColor(colColor.Value, detectedColor.Value, tolerance))
  403. {
  404. newLeft = x + 1;
  405. }
  406. else
  407. {
  408. break;
  409. }
  410. }
  411. else
  412. {
  413. break;
  414. }
  415. }
  416. if (newLeft > currentLeft)
  417. {
  418. borderColor = detectedColor;
  419. return newLeft;
  420. }
  421. return currentLeft;
  422. }
  423. /// <summary>
  424. /// 检测右侧边框层(优化版)
  425. /// </summary>
  426. private static int DetectLayerBorderRight(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
  427. {
  428. int newRight = currentRight;
  429. Rgba32? detectedColor = null;
  430. // 使用采样检测代替全列扫描
  431. int sampleCount = Math.Min(50, currentBottom - currentTop + 1);
  432. int stepY = Math.Max(1, (currentBottom - currentTop) / sampleCount);
  433. // 从当前右侧开始向左扫描
  434. for (int x = currentRight; x >= currentLeft; x--)
  435. {
  436. Rgba32? colColor = null;
  437. bool isUniform = true;
  438. // 采样检查列是否统一颜色
  439. for (int y = currentTop; y <= currentBottom; y += stepY)
  440. {
  441. if (!colColor.HasValue)
  442. {
  443. colColor = image[x, y];
  444. continue;
  445. }
  446. if (!IsSimilarColor(image[x, y], colColor.Value, tolerance))
  447. {
  448. isUniform = false;
  449. break;
  450. }
  451. }
  452. if (isUniform && colColor.HasValue)
  453. {
  454. if (x == currentRight)
  455. {
  456. detectedColor = colColor;
  457. newRight = x - 1;
  458. continue;
  459. }
  460. if (detectedColor.HasValue && IsSimilarColor(colColor.Value, detectedColor.Value, tolerance))
  461. {
  462. newRight = x - 1;
  463. }
  464. else
  465. {
  466. break;
  467. }
  468. }
  469. else
  470. {
  471. break;
  472. }
  473. }
  474. if (newRight < currentRight)
  475. {
  476. borderColor = detectedColor;
  477. return newRight;
  478. }
  479. return currentRight;
  480. }
  481. /// <summary>
  482. /// 颜色相似度比较(SIMD优化)
  483. /// </summary>
  484. private static bool IsSimilarColor(Rgba32 color1, Rgba32 color2, byte tolerance)
  485. {
  486. // 使用快速比较算法
  487. int diffR = Math.Abs(color1.R - color2.R);
  488. int diffG = Math.Abs(color1.G - color2.G);
  489. int diffB = Math.Abs(color1.B - color2.B);
  490. // 快速路径:如果任一通道差异超过容差
  491. if (diffR > tolerance || diffG > tolerance || diffB > tolerance)
  492. return false;
  493. // 精确比较
  494. return diffR <= tolerance && diffG <= tolerance && diffB <= tolerance;
  495. }
  496. }
  497. /// <summary>
  498. /// 边框检测结果(包含多层边框信息)
  499. /// </summary>
  500. public struct BorderDetectionResult
  501. {
  502. /// <summary>原始图片宽度</summary>
  503. public int ImageWidth { get; set; }
  504. /// <summary>原始图片高度</summary>
  505. public int ImageHeight { get; set; }
  506. /// <summary>内容上边界位置</summary>
  507. public int ContentTop { get; set; }
  508. /// <summary>内容下边界位置</summary>
  509. public int ContentBottom { get; set; }
  510. /// <summary>内容左边界位置</summary>
  511. public int ContentLeft { get; set; }
  512. /// <summary>内容右边界位置</summary>
  513. public int ContentRight { get; set; }
  514. /// <summary>检测到的边框层数</summary>
  515. public int BorderLayers { get; set; }
  516. /// <summary>边框颜色层次(从外到内)</summary>
  517. public List<Rgba32> BorderColors { get; set; }
  518. /// <summary>顶部边框总宽度(像素)</summary>
  519. public int TopBorderWidth => ContentTop;
  520. /// <summary>底部边框总宽度(像素)</summary>
  521. public int BottomBorderWidth => ImageHeight - 1 - ContentBottom;
  522. /// <summary>左侧边框总宽度(像素)</summary>
  523. public int LeftBorderWidth => ContentLeft;
  524. /// <summary>右侧边框总宽度(像素)</summary>
  525. public int RightBorderWidth => ImageWidth - 1 - ContentRight;
  526. /// <summary>是否有顶部边框</summary>
  527. public bool HasTopBorder => TopBorderWidth > 0;
  528. /// <summary>是否有底部边框</summary>
  529. public bool HasBottomBorder => BottomBorderWidth > 0;
  530. /// <summary>是否有左侧边框</summary>
  531. public bool HasLeftBorder => LeftBorderWidth > 0;
  532. /// <summary>是否有右侧边框</summary>
  533. public bool HasRightBorder => RightBorderWidth > 0;
  534. /// <summary>是否有任意边框</summary>
  535. public bool HasAnyBorder => BorderCount > 0;
  536. /// <summary>是否满足裁剪条件(至少两个边)</summary>
  537. public bool CanBeCropped => BorderCount >= 2;
  538. public int BorderCount => (HasTopBorder ? 1 : 0) + (HasBottomBorder ? 1 : 0) + (HasLeftBorder ? 1 : 0) + (HasRightBorder ? 1 : 0);
  539. /// <summary>内容区域宽度</summary>
  540. public int ContentWidth => ContentRight - ContentLeft + 1;
  541. /// <summary>内容区域高度</summary>
  542. public int ContentHeight => ContentBottom - ContentTop + 1;
  543. }