From 196083df6be6ef1c0e4ea6440c965f89730a1ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kilian=20Sch=C3=BCttler?= Date: Wed, 12 Jul 2023 13:44:43 +0200 Subject: [PATCH] DM-305: port rules to new schema --- .../model/AbstractPageBlock.java | 8 + .../classification/model/Column.java | 14 ++ .../classification/model/ColumnType.java | 6 + .../service/ColumnDetectionService.java | 149 ++++++++++++++++++ .../service/LineDetectionService.java | 115 ++++++++++++++ .../service/PdfSegmentationService.java | 2 + .../RedactManagerBlockificationService.java | 2 +- .../factory/DocumentGraphFactory.java | 3 +- .../document/factory/SectionNodeFactory.java | 39 +++-- .../document/factory/TextBlockFactory.java | 2 +- .../utils/RectangleTransformations.java | 17 +- .../service/ColumnDetectionServiceTest.java | 58 +++++++ .../service/LineDetectionServiceTest.java | 79 ++++++++++ .../Flora/ProblemDocs/S37Struktur.pdf | Bin 0 -> 40306 bytes 14 files changed, 478 insertions(+), 16 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/Column.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/ColumnType.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionService.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionService.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionServiceTest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionServiceTest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/ProblemDocs/S37Struktur.pdf diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/AbstractPageBlock.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/AbstractPageBlock.java index a0d2caef..d4a49133 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/AbstractPageBlock.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/AbstractPageBlock.java @@ -26,6 +26,8 @@ public abstract class AbstractPageBlock { @JsonIgnore protected int page; + int columnIndex; + @JsonIgnore private Orientation orientation = Orientation.NONE; @@ -77,4 +79,10 @@ public abstract class AbstractPageBlock { return this.minY <= atc.getMaxY() && this.maxY >= atc.getMinY(); } + + public boolean intersectsX(AbstractPageBlock atc) { + + return this.minX <= atc.getMaxX() && this.maxX >= atc.getMinX(); + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/Column.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/Column.java new file mode 100644 index 00000000..ffd4a37d --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/Column.java @@ -0,0 +1,14 @@ +package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model; + +import java.awt.geom.Rectangle2D; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class Column { + + int index; + ColumnType columnType; + Rectangle2D bBox; + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/ColumnType.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/ColumnType.java new file mode 100644 index 00000000..d2212c42 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/ColumnType.java @@ -0,0 +1,6 @@ +package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model; + +public enum ColumnType { + RULING, + DISTANCE +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionService.java new file mode 100644 index 00000000..27a15912 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionService.java @@ -0,0 +1,149 @@ +package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service; + +import java.awt.geom.Rectangle2D; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ColumnDetectionService { + + private static final double SPLITTABLE_LINE_PERCENTAGE_THRESHOLD = 0.6; + private static final int MAX_NUMBER_OF_COLUMNS = 4; + + + public List detectColumns(List textPositionSequences, Rectangle2D mainBodyTextFrame) { + + if (textPositionSequences.size() < 2) { + return List.of(mainBodyTextFrame); + } + + List> linesWithGaps = LineDetectionService.findLinesWithGaps(textPositionSequences); + + Map> linesWithMatchingGapIndices = new HashMap<>(); + for (int numberOfColumns = 2; numberOfColumns <= MAX_NUMBER_OF_COLUMNS; numberOfColumns++) { + linesWithMatchingGapIndices.put(numberOfColumns, findConsecutiveLinesWithMatchingGaps(linesWithGaps, mainBodyTextFrame.getWidth(), numberOfColumns)); + } + + int optimalNumberOfColumns = findOptimalNumberOfColumns(linesWithMatchingGapIndices, linesWithGaps.size()); + if (optimalNumberOfColumns == 1) { + return List.of(mainBodyTextFrame); + } + return buildColumns(mainBodyTextFrame, getLinesWithMatchingGaps(linesWithMatchingGapIndices.get(optimalNumberOfColumns), linesWithGaps), optimalNumberOfColumns); + } + + + private static List findConsecutiveLinesWithMatchingGaps(List> linesWithGaps, double width, int numberOfColumns) { + + List booleans = lineHasMatchingGap(linesWithGaps, width, numberOfColumns); + return findConsecutiveTrueIndicesWithMaxLengthRun(booleans); + } + + + private List lineHasMatchingGap(List> linesWithGaps, double width, int numberOfColumns) { + + return linesWithGaps.stream() + .map(blocksWithGaps -> IntStream.range(1, numberOfColumns) + .allMatch(columnIndex -> noBlocksIntersectX(blocksWithGaps, calculateGapLocation(width, numberOfColumns, columnIndex)))) + .toList(); + } + + + private List findConsecutiveTrueIndicesWithMaxLengthRun(List booleans) { + + List maxConsecutiveTrueIndices = new LinkedList<>(); + List currentConsecutiveTrueIndices = new LinkedList<>(); + for (int i = 0; i < booleans.size(); i++) { + if (!booleans.get(i)) { + if (currentConsecutiveTrueIndices.isEmpty()) { + continue; + } + if (currentConsecutiveTrueIndices.size() > maxConsecutiveTrueIndices.size()) { + maxConsecutiveTrueIndices = currentConsecutiveTrueIndices; + } + currentConsecutiveTrueIndices = new LinkedList<>(); + continue; + } + currentConsecutiveTrueIndices.add(i); + } + if (currentConsecutiveTrueIndices.size() > maxConsecutiveTrueIndices.size()) { + return currentConsecutiveTrueIndices; + } + return maxConsecutiveTrueIndices; + } + + + private static int findOptimalNumberOfColumns(Map> linesWithMatchingGapIndices, Integer numberOfLines) { + + return linesWithMatchingGapIndices.entrySet() + .stream() + .max(comparePercentages(numberOfLines)) + .filter(entry -> percentageIsAboveThreshold(entry, numberOfLines)) + .map(Map.Entry::getKey) + .orElse(1); + } + + + private List buildColumns(Rectangle2D mainBodyTextFrame, List rectanglesToMerge, int optimalColumnCount) { + + if (optimalColumnCount == 1 || rectanglesToMerge.isEmpty()) { + return List.of(mainBodyTextFrame); + } + + double maxY = rectanglesToMerge.get(0).getMaxY(); + double minY = rectanglesToMerge.get(rectanglesToMerge.size() - 1).getMinY(); + + List columns = new LinkedList<>(); + double width = mainBodyTextFrame.getWidth() / optimalColumnCount; + double height = maxY - minY; + for (int i = 0; i < optimalColumnCount; i++) { + columns.add(new Rectangle2D.Double(mainBodyTextFrame.getMinY() + i * width, minY, width, height)); + } + return columns; + } + + + private Comparator>> comparePercentages(Integer numberOfLines) { + + return Comparator.comparingDouble(entry -> calculatePercentage(entry.getValue().size(), numberOfLines)); + } + + + private List getLinesWithMatchingGaps(List linesWithMatchingGapIndices, List> linesWithGaps) { + + return linesWithMatchingGapIndices.stream().map(linesWithGaps::get).flatMap(Collection::stream).toList(); + } + + + private boolean percentageIsAboveThreshold(Map.Entry> entry, Integer numberOfLines) { + + return calculatePercentage(entry.getValue().size(), numberOfLines) > SPLITTABLE_LINE_PERCENTAGE_THRESHOLD; + } + + + private double calculatePercentage(Integer numberOfMatchingLines, Integer numberOfLines) { + + return ((double) numberOfMatchingLines) / ((double) numberOfLines); + } + + + private double calculateGapLocation(double pageWidth, int numberOfColumns, int columnIndex) { + + return (pageWidth / numberOfColumns) * columnIndex; + } + + + private Boolean noBlocksIntersectX(List blocksWithGaps, double x) { + + return blocksWithGaps.stream().noneMatch(rect -> rect.getMaxX() > x && rect.getMinX() < x); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionService.java new file mode 100644 index 00000000..758b9890 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionService.java @@ -0,0 +1,115 @@ +package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service; + +import java.awt.geom.Rectangle2D; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RectangleTransformations; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.TextPositionSequenceComparator; + +import lombok.AllArgsConstructor; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class LineDetectionService { + + private static final double X_GAP_FACTOR = 1; // multiplied with average text height, determines the minimum distance of gaps in lines + + + public static List> findLinesWithGaps(List textPositionSequences) { + + if (textPositionSequences.isEmpty()) { + return Collections.emptyList(); + } + + final double avgTextPositionHeight = textPositionSequences.stream().mapToDouble(TextPositionSequence::getHeight).average().orElseThrow(); + + Context context = Context.init(); + + List sortedTextPositionSequence = textPositionSequences.stream().sorted(new TextPositionSequenceComparator()).toList(); + + var previousTextPosition = sortedTextPositionSequence.get(0); + context.textPositionsToMerge.add(previousTextPosition); + for (TextPositionSequence currentTextPosition : sortedTextPositionSequence.subList(1, sortedTextPositionSequence.size())) { + if (isNewLine(currentTextPosition, previousTextPosition, avgTextPositionHeight) || isSplitByOrientation(currentTextPosition, previousTextPosition)) { + addBlockToLine(context); + startNewLine(currentTextPosition, context); + } else if (isXGap(currentTextPosition, previousTextPosition, avgTextPositionHeight)) { + addBlockToLine(context); + startNewBlock(currentTextPosition, context); + } else { + context.textPositionsToMerge.add(currentTextPosition); + } + previousTextPosition = currentTextPosition; + } + addBlockToLine(context); + return context.linesWithGaps; + } + + + private static boolean isXGap(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) { + + return Math.abs(previousTextPosition.getMaxXDirAdj() - currentTextPosition.getMinXDirAdj()) > (avgTextPositionHeight * X_GAP_FACTOR); + } + + + private static boolean isSplitByOrientation(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition) { + + return !previousTextPosition.getDir().equals(currentTextPosition.getDir()); + } + + + private static boolean isNewLine(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) { + + return Math.abs(previousTextPosition.getMinYDirAdj() - currentTextPosition.getMinYDirAdj()) > avgTextPositionHeight; + } + + + private static void startNewBlock(TextPositionSequence currentTextPosition, Context context) { + + context.textPositionsToMerge = new LinkedList<>(); + context.textPositionsToMerge.add(currentTextPosition); + } + + + private static void addBlockToLine(Context context) { + + context.blocksInLine.add(textPositionBBox(context.textPositionsToMerge)); + } + + + private static void startNewLine(TextPositionSequence current, Context context) { + + context.blocksInLine = new LinkedList<>(); + startNewBlock(current, context); + context.linesWithGaps.add(context.blocksInLine); + } + + + private Rectangle2D textPositionBBox(List textPositionSequences) { + + return RectangleTransformations.rectangleBBox(textPositionSequences.stream().map(TextPositionSequence::getRectangle).toList()); + } + + + @AllArgsConstructor + private class Context { + + List> linesWithGaps; + List blocksInLine; + List textPositionsToMerge; + + + public static Context init() { + + List> initialLinesWithGaps = new LinkedList<>(); + List initialBlocksInLine = new LinkedList<>(); + initialLinesWithGaps.add(initialBlocksInLine); + return new Context(initialLinesWithGaps, initialBlocksInLine, new LinkedList<>()); + } + + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/PdfSegmentationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/PdfSegmentationService.java index 913db6c9..94227fbc 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/PdfSegmentationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/PdfSegmentationService.java @@ -29,6 +29,7 @@ import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.mo import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence; import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.parsing.PDFLinesTextStripper; import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.FileUtils; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RectangleTransformations; import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; import lombok.RequiredArgsConstructor; @@ -129,6 +130,7 @@ public class PdfSegmentationService { stripper.getRulings(), stripper.getMinCharWidth(), stripper.getMaxCharHeight()); + // var columns = ColumnDetectionService.detectColumns(stripper.getTextPositionSequences(), RectangleTransformations.toRectangle2D(pdPage.getCropBox())); ClassificationPage page = blockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical()); page.setRotation(rotation); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/RedactManagerBlockificationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/RedactManagerBlockificationService.java index d303e79e..5c4fcf1c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/RedactManagerBlockificationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/RedactManagerBlockificationService.java @@ -23,7 +23,7 @@ import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.ut @Service @SuppressWarnings("all") @ConditionalOnProperty(prefix = "application", name = "type", havingValue = "RedactManager") -public class RedactManagerBlockificationService implements BlockificationService{ +public class RedactManagerBlockificationService implements BlockificationService { static final float THRESHOLD = 1f; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/DocumentGraphFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/DocumentGraphFactory.java index f517a93a..a9701164 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/DocumentGraphFactory.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/DocumentGraphFactory.java @@ -82,7 +82,6 @@ public class DocumentGraphFactory { page.getMainBody().add(node); List textBlocks = new ArrayList<>(textBlocksToMerge); - textBlocks.add(originalTextBlock); AtomicTextBlock textBlock = context.textBlockFactory.fromContext(TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(textBlocks), node, context, page); List treeId = context.documentTree.createNewChildEntryAndReturnId(parentNode, node); node.setLeafTextBlock(textBlock); @@ -181,7 +180,7 @@ public class DocumentGraphFactory { Page page = context.getPage(pageIndex); Header header = Header.builder().documentTree(context.getDocumentTree()).build(); - AtomicTextBlock textBlock = context.textBlockFactory.emptyTextBlock(header, 0, page); + AtomicTextBlock textBlock = context.textBlockFactory.emptyTextBlockFromInteger(header, 0, page); List tocId = context.getDocumentTree().createNewMainEntryAndReturnId(header); header.setTreeId(tocId); header.setLeafTextBlock(textBlock); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java index d162304d..4f675298 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java @@ -9,6 +9,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock; import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage; @@ -80,9 +81,10 @@ public class SectionNodeFactory { remainingBlocks.removeAll(alreadyMerged); if (abstractPageBlock instanceof TextPageBlock) { - List textBlocks = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(abstractPageBlock, remainingBlocks); - alreadyMerged.addAll(textBlocks); - DocumentGraphFactory.addParagraphOrHeadline(section, (TextPageBlock) abstractPageBlock, context, textBlocks); +// List textBlocksToMerge = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientationUntilConvergence((TextPageBlock) abstractPageBlock, remainingBlocks); + List textBlocksToMerge = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(List.of((TextPageBlock) abstractPageBlock), remainingBlocks); + alreadyMerged.addAll(textBlocksToMerge); + DocumentGraphFactory.addParagraphOrHeadline(section, (TextPageBlock) abstractPageBlock, context, textBlocksToMerge); } else if (abstractPageBlock instanceof TablePageBlock tablePageBlock) { List tablesToMerge = TableMergingUtility.findConsecutiveTablesWithSameColCountAndSameHeaders(tablePageBlock, remainingBlocks); alreadyMerged.addAll(tablesToMerge); @@ -162,15 +164,30 @@ public class SectionNodeFactory { } - private List findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(AbstractPageBlock atc, List pageBlocks) { + private List findTextBlocksWithSameClassificationAndAlignsYAndSameOrientationUntilConvergence(TextPageBlock originalTextBlocks, + List pageBlocks) { - return pageBlocks.stream() - .filter(abstractPageBlock -> !abstractPageBlock.equals(atc)) - .filter(abstractPageBlock -> abstractPageBlock.getPage() == atc.getPage()) - .filter(abstractPageBlock -> abstractPageBlock.getOrientation().equals(atc.getOrientation())) - .filter(abstractPageBlock -> abstractPageBlock.intersectsY(atc)) - .filter(abstractPageBlock -> abstractPageBlock instanceof TextPageBlock) - .map(abstractPageBlock -> (TextPageBlock) abstractPageBlock) + int previousCount = 1; + List alignedBlocks = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(List.of(originalTextBlocks), pageBlocks); + while (previousCount < alignedBlocks.size()) { + alignedBlocks = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(alignedBlocks, pageBlocks); + previousCount = alignedBlocks.size(); + } + return alignedBlocks; + } + + + private static List findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(List textBlocksToMerge, List pageBlocks) { + + return Stream.concat(pageBlocks.stream() + .filter(abstractPageBlock -> !textBlocksToMerge.contains(abstractPageBlock)) + .filter(abstractPageBlock -> textBlocksToMerge.stream().allMatch(textBlockToMerge -> abstractPageBlock.getPage() == textBlockToMerge.getPage())) + .filter(abstractPageBlock -> textBlocksToMerge.stream().allMatch(textBlockToMerge -> abstractPageBlock.getOrientation().equals(textBlockToMerge.getOrientation()))) + .filter(abstractPageBlock -> textBlocksToMerge.stream().anyMatch(abstractPageBlock::intersectsY)) + //.filter(abstractPageBlock -> textBlocksToMerge.stream().anyMatch(abstractPageBlock::intersectsX)) + .filter(abstractPageBlock -> abstractPageBlock instanceof TextPageBlock) + .map(abstractPageBlock -> (TextPageBlock) abstractPageBlock), // + textBlocksToMerge.stream())// .toList(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TextBlockFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TextBlockFactory.java index 12c0157f..dd79c6d2 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TextBlockFactory.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TextBlockFactory.java @@ -43,7 +43,7 @@ public class TextBlockFactory { } - public AtomicTextBlock emptyTextBlock(SemanticNode parent, Integer numberOnPage, Page page) { + public AtomicTextBlock emptyTextBlockFromInteger(SemanticNode parent, Integer numberOnPage, Page page) { long idx = textBlockIdx; textBlockIdx++; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RectangleTransformations.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RectangleTransformations.java index 4c101e57..122bfaab 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RectangleTransformations.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RectangleTransformations.java @@ -104,6 +104,12 @@ public class RectangleTransformations { } + public static Rectangle2D toRectangle2D(PDRectangle cropBox) { + + return new Rectangle2D.Double(cropBox.getLowerLeftX(), cropBox.getLowerLeftY(), cropBox.getWidth(), cropBox.getHeight()); + } + + private static class Rectangle2DBBoxCollector implements Collector { @Override @@ -133,7 +139,7 @@ public class RectangleTransformations { @Override public Function finisher() { - return bb -> new Rectangle2D.Double(bb.lowerLeftX, bb.lowerLeftY, bb.upperRightX - bb.lowerLeftX, bb.upperRightY - bb.lowerLeftY); + return BBox::toRectangle2D; } @@ -154,6 +160,15 @@ public class RectangleTransformations { Double upperRightY; + public Rectangle2D toRectangle2D() { + + if (lowerLeftX == null || lowerLeftY == null || upperRightX == null || upperRightY == null) { + return new Rectangle2D.Double(0, 0, 0, 0); + } + return new Rectangle2D.Double(lowerLeftX, lowerLeftY, upperRightX - lowerLeftX, upperRightY - lowerLeftY); + } + + public void addRectangle(Rectangle2D rectangle2D) { double lowerLeftX = Math.min(rectangle2D.getMinX(), rectangle2D.getMaxX()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionServiceTest.java new file mode 100644 index 00000000..0d0712dc --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/ColumnDetectionServiceTest.java @@ -0,0 +1,58 @@ +package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service; + +import java.awt.geom.Rectangle2D; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.parsing.PDFLinesTextStripper; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.PdfVisualisationUtility; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RectangleTransformations; + +import lombok.SneakyThrows; + +class ColumnDetectionServiceTest { + + @Test + @SneakyThrows + public void testColumnDetection() { + + String filename = "files/Documine/Flora/ProblemDocs/S37Struktur.pdf"; + var tmpFileName = "/tmp/" + filename.split("/")[2] + "_COLUMNS.pdf"; + try (InputStream inputStream = new ClassPathResource(filename).getInputStream()) { + + PDDocument pdDocument = PDDocument.load(inputStream); + System.out.println("start column detection"); + long start = System.currentTimeMillis(); + + for (int pageNumber = 1; pageNumber < pdDocument.getNumberOfPages() + 1; pageNumber++) { + + PDFLinesTextStripper stripper = new PDFLinesTextStripper(); + PDPage pdPage = pdDocument.getPage(pageNumber - 1); + stripper.setPageNumber(pageNumber); + stripper.setStartPage(pageNumber); + stripper.setEndPage(pageNumber); + stripper.setPdpage(pdPage); + stripper.getText(pdDocument); + + List columns = ColumnDetectionService.detectColumns(stripper.getTextPositionSequences(), RectangleTransformations.toRectangle2D(pdPage.getCropBox())); + System.out.printf("found %d columns on page %d%n", columns.size(), pageNumber); + PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, columns, PdfVisualisationUtility.Options.builder().stroke(true).build()); + } + + System.out.printf("finished col detection, took %d ms", System.currentTimeMillis() - start); + + try (var out = new FileOutputStream(tmpFileName)) { + pdDocument.save(out); + pdDocument.close(); + } + } + + } + +} \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionServiceTest.java new file mode 100644 index 00000000..b8480776 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/service/LineDetectionServiceTest.java @@ -0,0 +1,79 @@ +package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.parsing.PDFLinesTextStripper; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.PdfVisualisationUtility; + +import lombok.SneakyThrows; + +class LineDetectionServiceTest { + + @Test + @Disabled + @SneakyThrows + public void testLineDetection() { + + String filename = "files/BDR/Plenarprotokoll 1 (keine Druchsache!) (1).pdf"; + var tmpFileName = "/tmp/" + filename.split("/")[2] + "_LINES.pdf"; + try (InputStream inputStream = new ClassPathResource(filename).getInputStream()) { + + PDDocument pdDocument = PDDocument.load(inputStream); + System.out.println("start column detection"); + long start = System.currentTimeMillis(); + + for (int pageNumber = 1; pageNumber < pdDocument.getNumberOfPages() + 1; pageNumber++) { + + PDFLinesTextStripper stripper = new PDFLinesTextStripper(); + PDPage pdPage = pdDocument.getPage(pageNumber - 1); + stripper.setPageNumber(pageNumber); + stripper.setStartPage(pageNumber); + stripper.setEndPage(pageNumber); + stripper.setPdpage(pdPage); + stripper.getText(pdDocument); + + List> linesWithGaps = LineDetectionService.findLinesWithGaps(stripper.getTextPositionSequences()); + System.out.printf("found %d lines on page %d%n", linesWithGaps.size(), pageNumber); + for (int i = 0; i < linesWithGaps.size(); i++) { + PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, linesWithGaps.get(i), PdfVisualisationUtility.Options.builder().stroke(true).build()); + PdfVisualisationUtility.drawText(String.format("%d", i), + pdDocument, + new Point2D.Double(linesWithGaps.get(i).get(0).getX() - (5 + (5 * countNumberOfDigits(i))), linesWithGaps.get(i).get(0).getY() + 2), + pageNumber, + PdfVisualisationUtility.Options.builder().stroke(true).build()); + + } + } + + System.out.printf("finished line detection, took %d ms", System.currentTimeMillis() - start); + + try (var out = new FileOutputStream(tmpFileName)) { + pdDocument.save(out); + pdDocument.close(); + } + } + } + + + private int countNumberOfDigits(int num) { + + if (num == 0) { + return 1; + } + int count = 0; + for (; num != 0; num /= 10, ++count) { + } + return count; + } + +} \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/ProblemDocs/S37Struktur.pdf b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/ProblemDocs/S37Struktur.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8123bf3ce77afbd9ea1f03d96acab053e4fca274 GIT binary patch literal 40306 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

jXEx7~E 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