ResultTable.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Drawing; 8 using System.Linq; 9 using System.Text; 10 using System.Windows; 11 using System.Windows.Controls; 12 using System.Windows.Media; 13 14 using PowerOCR.Helpers; 15 using Windows.Media.Ocr; 16 17 using Rect = System.Windows.Rect; 18 19 namespace PowerOCR.Models; 20 21 public class ResultTable 22 { 23 public List<ResultColumn> Columns { get; set; } = new(); 24 25 public List<ResultRow> Rows { get; set; } = new(); 26 27 public Rect BoundingRect { get; set; } 28 29 public List<int> ColumnLines { get; set; } = new(); 30 31 public List<int> RowLines { get; set; } = new(); 32 33 public Canvas? TableLines { get; set; } 34 35 public ResultTable(ref List<WordBorder> wordBorders, DpiScale dpiScale) 36 { 37 int borderBuffer = 3; 38 var leftsMin = wordBorders.Select(x => x.Left).Min(); 39 var topsMin = wordBorders.Select(x => x.Top).Min(); 40 var rightsMax = wordBorders.Select(x => x.Right).Max(); 41 var bottomsMax = wordBorders.Select(x => x.Bottom).Max(); 42 43 Rectangle bordersBorder = new() 44 { 45 X = (int)leftsMin - borderBuffer, 46 Y = (int)topsMin - borderBuffer, 47 Width = (int)(rightsMax + borderBuffer), 48 Height = (int)(bottomsMax + borderBuffer), 49 }; 50 51 bordersBorder.Width = (int)(bordersBorder.Width * dpiScale.DpiScaleX); 52 bordersBorder.Height = (int)(bordersBorder.Height * dpiScale.DpiScaleY); 53 54 AnalyzeAsTable(wordBorders, bordersBorder); 55 } 56 57 private void ParseRowAndColumnLines() 58 { 59 // Draw Bounding Rect 60 int topBound = 0; 61 int bottomBound = topBound; 62 int leftBound = 0; 63 int rightBound = leftBound; 64 65 if (Rows.Count >= 1) 66 { 67 topBound = (int)Rows[0].Top; 68 bottomBound = (int)Rows[Rows.Count - 1].Bottom; 69 } 70 71 if (Columns.Count >= 1) 72 { 73 leftBound = (int)Columns[0].Left; 74 rightBound = (int)Columns[Columns.Count - 1].Right; 75 } 76 77 BoundingRect = new() 78 { 79 Width = (rightBound - leftBound) + 10, 80 Height = (bottomBound - topBound) + 10, 81 X = leftBound - 5, 82 Y = topBound - 5, 83 }; 84 85 // parse columns 86 ColumnLines = new(); 87 88 for (int i = 0; i < Columns.Count - 1; i++) 89 { 90 int columnMid = (int)(Columns[i].Right + Columns[i + 1].Left) / 2; 91 ColumnLines.Add(columnMid); 92 } 93 94 // parse rows 95 RowLines = new(); 96 97 for (int i = 0; i < Rows.Count - 1; i++) 98 { 99 int rowMid = (int)(Rows[i].Bottom + Rows[i + 1].Top) / 2; 100 RowLines.Add(rowMid); 101 } 102 } 103 104 public static List<WordBorder> ParseOcrResultIntoWordBorders(OcrResult ocrResult, DpiScale dpi) 105 { 106 List<WordBorder> wordBorders = new(); 107 int lineNumber = 0; 108 109 foreach (OcrLine ocrLine in ocrResult.Lines) 110 { 111 double top = ocrLine.Words.Select(x => x.BoundingRect.Top).Min(); 112 double bottom = ocrLine.Words.Select(x => x.BoundingRect.Bottom).Max(); 113 double left = ocrLine.Words.Select(x => x.BoundingRect.Left).Min(); 114 double right = ocrLine.Words.Select(x => x.BoundingRect.Right).Max(); 115 116 Rect lineRect = new() 117 { 118 X = left, 119 Y = top, 120 Width = Math.Abs(right - left), 121 Height = Math.Abs(bottom - top), 122 }; 123 124 StringBuilder lineText = new(); 125 ocrLine.GetTextFromOcrLine(true, lineText); 126 127 WordBorder wordBorderBox = new() 128 { 129 Width = lineRect.Width / dpi.DpiScaleX, 130 Height = lineRect.Height / dpi.DpiScaleY, 131 Top = lineRect.Y, 132 Left = lineRect.X, 133 Word = lineText.ToString().Trim(), 134 LineNumber = lineNumber, 135 }; 136 wordBorders.Add(wordBorderBox); 137 138 lineNumber++; 139 } 140 141 return wordBorders; 142 } 143 144 public void AnalyzeAsTable(ICollection<WordBorder> wordBorders, Rectangle rectCanvasSize) 145 { 146 int hitGridSpacing = 3; 147 148 int numberOfVerticalLines = rectCanvasSize.Width / hitGridSpacing; 149 int numberOfHorizontalLines = rectCanvasSize.Height / hitGridSpacing; 150 151 Canvas tableIntersectionCanvas = new(); 152 153 List<int> rowAreas = CalculateRowAreas(rectCanvasSize, hitGridSpacing, numberOfHorizontalLines, tableIntersectionCanvas, wordBorders); 154 List<ResultRow> resultRows = CalculateResultRows(hitGridSpacing, rowAreas); 155 156 List<int> columnAreas = CalculateColumnAreas(rectCanvasSize, hitGridSpacing, numberOfVerticalLines, tableIntersectionCanvas, wordBorders); 157 List<ResultColumn> resultColumns = CalculateResultColumns(hitGridSpacing, columnAreas); 158 159 Rect tableBoundingRect = new() 160 { 161 X = columnAreas.FirstOrDefault(), 162 Y = rowAreas.FirstOrDefault(), 163 Width = columnAreas.LastOrDefault() - columnAreas.FirstOrDefault(), 164 Height = rowAreas.LastOrDefault() - rowAreas.FirstOrDefault(), 165 }; 166 167 CombineOutliers(wordBorders, resultRows, tableIntersectionCanvas, resultColumns, tableBoundingRect); 168 169 Rows.Clear(); 170 Rows.AddRange(resultRows); 171 Columns.Clear(); 172 Columns.AddRange(resultColumns); 173 174 ParseRowAndColumnLines(); 175 DrawTable(); 176 } 177 178 private static List<ResultRow> CalculateResultRows(int hitGridSpacing, List<int> rowAreas) 179 { 180 List<ResultRow> resultRows = new(); 181 int rowTop = 0; 182 int rowCount = 0; 183 for (int i = 0; i < rowAreas.Count; i++) 184 { 185 int thisLine = rowAreas[i]; 186 187 // check if should set this as top 188 if (i == 0) 189 { 190 rowTop = thisLine; 191 } 192 else if (i - 1 > 0) 193 { 194 int prevRow = rowAreas[i - 1]; 195 if (thisLine - prevRow != hitGridSpacing) 196 { 197 rowTop = thisLine; 198 } 199 } 200 201 // check to see if at bottom of row 202 if (i == rowAreas.Count - 1) 203 { 204 resultRows.Add(new ResultRow { Top = rowTop, Bottom = thisLine, ID = rowCount }); 205 rowCount++; 206 } 207 else if (i + 1 < rowAreas.Count) 208 { 209 int nextRow = rowAreas[i + 1]; 210 if (nextRow - thisLine != hitGridSpacing) 211 { 212 resultRows.Add(new ResultRow { Top = rowTop, Bottom = thisLine, ID = rowCount }); 213 rowCount++; 214 } 215 } 216 } 217 218 return resultRows; 219 } 220 221 private static List<int> CalculateRowAreas(Rectangle rectCanvasSize, int hitGridSpacing, int numberOfHorizontalLines, Canvas tableIntersectionCanvas, ICollection<WordBorder> wordBorders) 222 { 223 List<int> rowAreas = new(); 224 225 for (int i = 0; i < numberOfHorizontalLines; i++) 226 { 227 Border horizontalLine = new() 228 { 229 Height = 1, 230 Width = rectCanvasSize.Width, 231 Opacity = 0, 232 Background = new SolidColorBrush(Colors.Gray), 233 }; 234 Rect horizontalLineRect = new(0, i * hitGridSpacing, horizontalLine.Width, horizontalLine.Height); 235 _ = tableIntersectionCanvas.Children.Add(horizontalLine); 236 Canvas.SetTop(horizontalLine, i * 3); 237 238 CheckIntersectionsWithWordBorders(hitGridSpacing, wordBorders, rowAreas, i, horizontalLineRect); 239 } 240 241 return rowAreas; 242 } 243 244 private static void CheckIntersectionsWithWordBorders(int hitGridSpacing, ICollection<WordBorder> wordBorders, List<int> rowAreas, int i, Rect horizontalLineRect) 245 { 246 foreach (WordBorder wb in wordBorders) 247 { 248 if (wb.IntersectsWith(horizontalLineRect)) 249 { 250 rowAreas.Add(i * hitGridSpacing); 251 break; 252 } 253 } 254 } 255 256 private static void CombineOutliers(ICollection<WordBorder> wordBorders, List<ResultRow> resultRows, Canvas tableIntersectionCanvas, List<ResultColumn> resultColumns, Rect tableBoundingRect) 257 { 258 // try 4 times to refine the rows and columns for outliers 259 // on the fifth time set the word boundary properties 260 for (int r = 0; r < 5; r++) 261 { 262 int outlierThreshold = 2; 263 List<int> outlierRowIDs = FindOutlierRowIds(wordBorders, resultRows, tableIntersectionCanvas, tableBoundingRect, r, outlierThreshold); 264 265 if (outlierRowIDs.Count > 0) 266 { 267 MergeTheseRowIDs(resultRows, outlierRowIDs); 268 } 269 270 List<int> outlierColumnIDs = FindOutlierColumnIds(wordBorders, tableIntersectionCanvas, resultColumns, tableBoundingRect, outlierThreshold); 271 272 if (outlierColumnIDs.Count > 0 && r != 4) 273 { 274 MergeTheseColumnIDs(resultColumns, outlierColumnIDs); 275 } 276 } 277 } 278 279 private static List<int> FindOutlierRowIds( 280 ICollection<WordBorder> wordBorders, 281 ICollection<ResultRow> resultRows, 282 Canvas tableIntersectionCanvas, 283 Rect tableBoundingRect, 284 int r, 285 int outlierThreshold) 286 { 287 List<int> outlierRowIDs = new(); 288 289 foreach (ResultRow row in resultRows) 290 { 291 int numberOfIntersectingWords = 0; 292 Border rowBorder = new() 293 { 294 Height = row.Bottom - row.Top, 295 Width = tableBoundingRect.Width, 296 Background = new SolidColorBrush(Colors.Red), 297 Tag = row.ID, 298 }; 299 tableIntersectionCanvas.Children.Add(rowBorder); 300 Canvas.SetLeft(rowBorder, tableBoundingRect.X); 301 Canvas.SetTop(rowBorder, row.Top); 302 303 Rect rowRect = new(tableBoundingRect.X, row.Top, rowBorder.Width, rowBorder.Height); 304 305 foreach (WordBorder wb in wordBorders) 306 { 307 if (wb.IntersectsWith(rowRect)) 308 { 309 numberOfIntersectingWords++; 310 wb.ResultRowID = row.ID; 311 } 312 } 313 314 if (numberOfIntersectingWords <= outlierThreshold && r != 4) 315 { 316 outlierRowIDs.Add(row.ID); 317 } 318 } 319 320 return outlierRowIDs; 321 } 322 323 private static List<int> FindOutlierColumnIds( 324 ICollection<WordBorder> wordBorders, 325 Canvas tableIntersectionCanvas, 326 List<ResultColumn> resultColumns, 327 Rect tableBoundingRect, 328 int outlierThreshold) 329 { 330 List<int> outlierColumnIDs = new(); 331 332 foreach (ResultColumn column in resultColumns) 333 { 334 int numberOfIntersectingWords = 0; 335 Border columnBorder = new() 336 { 337 Height = tableBoundingRect.Height, 338 Width = column.Right - column.Left, 339 Background = new SolidColorBrush(Colors.Blue), 340 Opacity = 0.2, 341 Tag = column.ID, 342 }; 343 tableIntersectionCanvas.Children.Add(columnBorder); 344 Canvas.SetLeft(columnBorder, column.Left); 345 Canvas.SetTop(columnBorder, tableBoundingRect.Y); 346 347 Rect columnRect = new(column.Left, tableBoundingRect.Y, columnBorder.Width, columnBorder.Height); 348 foreach (WordBorder wb in wordBorders) 349 { 350 if (wb.IntersectsWith(columnRect)) 351 { 352 numberOfIntersectingWords++; 353 wb.ResultColumnID = column.ID; 354 } 355 } 356 357 if (numberOfIntersectingWords <= outlierThreshold) 358 { 359 outlierColumnIDs.Add(column.ID); 360 } 361 } 362 363 return outlierColumnIDs; 364 } 365 366 private static List<ResultColumn> CalculateResultColumns(int hitGridSpacing, List<int> columnAreas) 367 { 368 List<ResultColumn> resultColumns = new(); 369 int columnLeft = 0; 370 int columnCount = 0; 371 for (int i = 0; i < columnAreas.Count; i++) 372 { 373 int thisLine = columnAreas[i]; 374 375 // check if should set this as top 376 if (i == 0) 377 { 378 columnLeft = thisLine; 379 } 380 else if (i - 1 > 0) 381 { 382 int prevColumn = columnAreas[i - 1]; 383 if (thisLine - prevColumn != hitGridSpacing) 384 { 385 columnLeft = thisLine; 386 } 387 } 388 389 // check to see if at last Column 390 if (i == columnAreas.Count - 1) 391 { 392 resultColumns.Add(new ResultColumn { Left = columnLeft, Right = thisLine, ID = columnCount }); 393 columnCount++; 394 } 395 else if (i + 1 < columnAreas.Count) 396 { 397 int nextColumn = columnAreas[i + 1]; 398 if (nextColumn - thisLine != hitGridSpacing) 399 { 400 resultColumns.Add(new ResultColumn { Left = columnLeft, Right = thisLine, ID = columnCount }); 401 columnCount++; 402 } 403 } 404 } 405 406 return resultColumns; 407 } 408 409 private static List<int> CalculateColumnAreas(Rectangle rectCanvasSize, int hitGridSpacing, int numberOfVerticalLines, Canvas tableIntersectionCanvas, ICollection<WordBorder> wordBorders) 410 { 411 List<int> columnAreas = new(); 412 for (int i = 0; i < numberOfVerticalLines; i++) 413 { 414 Border vertLine = new() 415 { 416 Height = rectCanvasSize.Height, 417 Width = 1, 418 Opacity = 0, 419 Background = new SolidColorBrush(Colors.Gray), 420 }; 421 _ = tableIntersectionCanvas.Children.Add(vertLine); 422 Canvas.SetLeft(vertLine, i * hitGridSpacing); 423 424 Rect vertLineRect = new(i * hitGridSpacing, 0, vertLine.Width, vertLine.Height); 425 426 foreach (WordBorder wb in wordBorders) 427 { 428 if (wb.IntersectsWith(vertLineRect)) 429 { 430 columnAreas.Add(i * hitGridSpacing); 431 break; 432 } 433 } 434 } 435 436 return columnAreas; 437 } 438 439 private static void MergeTheseColumnIDs(List<ResultColumn> resultColumns, List<int> outlierColumnIDs) 440 { 441 for (int i = 0; i < outlierColumnIDs.Count; i++) 442 { 443 for (int j = 0; j < resultColumns.Count; j++) 444 { 445 ResultColumn column = resultColumns[j]; 446 if (column.ID == outlierColumnIDs[i]) 447 { 448 if (j == 0) 449 { 450 // merge with next column if possible 451 if (j + 1 < resultColumns.Count) 452 { 453 ResultColumn nextColumn = resultColumns[j + 1]; 454 nextColumn.Left = column.Left; 455 } 456 } 457 else if (j == resultColumns.Count - 1) 458 { 459 // merge with previous column 460 if (j - 1 >= 0) 461 { 462 ResultColumn prevColumn = resultColumns[j - 1]; 463 prevColumn.Right = column.Right; 464 } 465 } 466 else 467 { 468 // merge with closet column 469 ResultColumn prevColumn = resultColumns[j - 1]; 470 ResultColumn nextColumn = resultColumns[j + 1]; 471 int distanceToPrev = (int)(column.Left - prevColumn.Right); 472 int distanceToNext = (int)(nextColumn.Left - column.Right); 473 474 if (distanceToNext < distanceToPrev) 475 { 476 // merge with next column 477 nextColumn.Left = column.Left; 478 } 479 else 480 { 481 // merge with prev column 482 prevColumn.Right = column.Right; 483 } 484 } 485 486 resultColumns.RemoveAt(j); 487 } 488 } 489 } 490 } 491 492 public static void GetTextFromTabledWordBorders(StringBuilder stringBuilder, List<WordBorder> wordBorders, bool isSpaceJoining) 493 { 494 List<WordBorder>? selectedBorders = wordBorders.Where(w => w.IsSelected).ToList(); 495 496 if (selectedBorders.Count == 0) 497 { 498 selectedBorders.AddRange(wordBorders); 499 } 500 501 List<string> lineList = new(); 502 int? lastLineNum = 0; 503 int lastColumnNum = 0; 504 505 if (selectedBorders.FirstOrDefault() != null) 506 { 507 lastLineNum = selectedBorders.FirstOrDefault()!.LineNumber; 508 } 509 510 selectedBorders = selectedBorders.OrderBy(x => x.ResultColumnID).ToList(); 511 selectedBorders = selectedBorders.OrderBy(x => x.ResultRowID).ToList(); 512 513 int numberOfDistinctRows = selectedBorders.Select(x => x.ResultRowID).Distinct().Count(); 514 515 foreach (WordBorder border in selectedBorders) 516 { 517 if (lineList.Count == 0) 518 { 519 lastLineNum = border.ResultRowID; 520 } 521 522 if (border.ResultRowID != lastLineNum) 523 { 524 if (isSpaceJoining) 525 { 526 stringBuilder.Append(string.Join(' ', lineList)); 527 } 528 else 529 { 530 stringBuilder.Append(string.Join(string.Empty, lineList)); 531 } 532 533 stringBuilder.Replace(" \t ", "\t"); 534 stringBuilder.Replace("\t ", "\t"); 535 stringBuilder.Replace(" \t", "\t"); 536 stringBuilder.Append(Environment.NewLine); 537 lineList.Clear(); 538 lastLineNum = border.ResultRowID; 539 } 540 541 if (border.ResultColumnID != lastColumnNum && numberOfDistinctRows > 1) 542 { 543 string borderWord = border.Word; 544 int numberOfOffColumns = border.ResultColumnID - lastColumnNum; 545 if (numberOfOffColumns < 0) 546 { 547 lastColumnNum = 0; 548 } 549 550 numberOfOffColumns = border.ResultColumnID - lastColumnNum; 551 552 if (numberOfOffColumns > 0) 553 { 554 lineList.Add(new string('\t', numberOfOffColumns)); 555 } 556 } 557 558 lastColumnNum = border.ResultColumnID; 559 560 lineList.Add(border.Word); 561 } 562 563 stringBuilder.Append(string.Join(string.Empty, lineList)); 564 } 565 566 private static void MergeTheseRowIDs(List<ResultRow> resultRows, List<int> outlierRowIDs) 567 { 568 } 569 570 private void DrawTable() 571 { 572 // Draw the lines and bounds of the table 573 SolidColorBrush tableColor = new(System.Windows.Media.Color.FromArgb(255, 40, 118, 126)); 574 575 TableLines = new Canvas() 576 { 577 Tag = "TableLines", 578 }; 579 580 Border tableOutline = new() 581 { 582 Width = this.BoundingRect.Width, 583 Height = this.BoundingRect.Height, 584 BorderThickness = new Thickness(3), 585 BorderBrush = tableColor, 586 }; 587 TableLines.Children.Add(tableOutline); 588 Canvas.SetTop(tableOutline, this.BoundingRect.Y); 589 Canvas.SetLeft(tableOutline, this.BoundingRect.X); 590 591 foreach (int columnLine in this.ColumnLines) 592 { 593 Border vertLine = new() 594 { 595 Width = 2, 596 Height = this.BoundingRect.Height, 597 Background = tableColor, 598 }; 599 TableLines.Children.Add(vertLine); 600 Canvas.SetTop(vertLine, this.BoundingRect.Y); 601 Canvas.SetLeft(vertLine, columnLine); 602 } 603 604 foreach (int rowLine in this.RowLines) 605 { 606 Border horizontalLine = new() 607 { 608 Height = 2, 609 Width = this.BoundingRect.Width, 610 Background = tableColor, 611 }; 612 TableLines.Children.Add(horizontalLine); 613 Canvas.SetTop(horizontalLine, rowLine); 614 Canvas.SetLeft(horizontalLine, this.BoundingRect.X); 615 } 616 } 617 618 public static string GetWordsAsTable(List<WordBorder> wordBorders, DpiScale dpiScale, bool isSpaceJoining) 619 { 620 List<WordBorder> smallerBorders = new(); 621 foreach (WordBorder originalWB in wordBorders) 622 { 623 WordBorder newWB = new() 624 { 625 Word = originalWB.Word, 626 Left = originalWB.Left, 627 Top = originalWB.Top, 628 Width = originalWB.Width > 10 ? originalWB.Width - 6 : originalWB.Width, 629 Height = originalWB.Height > 10 ? originalWB.Height - 6 : originalWB.Height, 630 ResultRowID = originalWB.ResultRowID, 631 ResultColumnID = originalWB.ResultColumnID, 632 }; 633 smallerBorders.Add(newWB); 634 } 635 636 ResultTable resultTable = new(ref smallerBorders, dpiScale); 637 StringBuilder stringBuilder = new(); 638 GetTextFromTabledWordBorders( 639 stringBuilder, 640 smallerBorders, 641 isSpaceJoining); 642 return stringBuilder.ToString(); 643 } 644 }