First working docstrum
This commit is contained in:
parent
3c9049dc8a
commit
4afa8daafa
@ -10,6 +10,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
@ -26,6 +27,7 @@ import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsi
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationSection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
@ -47,6 +49,8 @@ import com.knecon.fforesight.service.layoutparser.processor.services.blockificat
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.classification.DocuMineClassificationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.classification.RedactManagerClassificationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.classification.TaasClassificationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.DocstrumSegmenter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.HierarchicalReadingOrderResolver;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.mapper.DocumentDataMapper;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.mapper.TaasDocumentDataMapper;
|
||||
@ -86,6 +90,8 @@ public class LayoutParsingPipeline {
|
||||
RedactManagerBlockificationService redactManagerBlockificationService;
|
||||
LayoutGridService layoutGridService;
|
||||
ObservationRegistry observationRegistry;
|
||||
// DocstrumSegmenter docstrumSegmenter;
|
||||
HierarchicalReadingOrderResolver hierarchicalReadingOrderResolver;
|
||||
|
||||
|
||||
public LayoutParsingFinishedEvent parseLayoutAndSaveFilesToStorage(LayoutParsingRequest layoutParsingRequest) throws IOException {
|
||||
@ -243,11 +249,38 @@ public class LayoutParsingPipeline {
|
||||
PDRectangle cropbox = pdPage.getCropBox();
|
||||
CleanRulings cleanRulings = rulingCleaningService.getCleanRulings(pdfTableCells.get(pageNumber), stripper.getRulings());
|
||||
|
||||
ClassificationPage classificationPage = switch (layoutParsingType) {
|
||||
case REDACT_MANAGER -> redactManagerBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
case TAAS -> taasBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
case DOCUMINE -> docuMineBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
};
|
||||
// Docstrum
|
||||
AtomicInteger num = new AtomicInteger(pageNumber);
|
||||
var zones = new DocstrumSegmenter().segmentPage(stripper.getTextPositionSequences());
|
||||
zones = hierarchicalReadingOrderResolver.resolve(zones);
|
||||
|
||||
List<AbstractPageBlock> pageBlocks = new ArrayList<>();
|
||||
AtomicInteger numOnPage = new AtomicInteger(1);
|
||||
// List<TextPositionSequence> textPositionSequences = new ArrayList<>();
|
||||
zones.forEach(zone -> {
|
||||
|
||||
List<TextPositionSequence> textPositionSequences = new ArrayList<>();
|
||||
zone.getLines().forEach(line -> {
|
||||
line.getWords().forEach(word -> {
|
||||
textPositionSequences.add(new TextPositionSequence(word.getTextPositions(), num.get()));
|
||||
});
|
||||
});
|
||||
|
||||
var cps = redactManagerBlockificationService.blockify(textPositionSequences, cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
|
||||
cps.getTextBlocks().forEach(cp -> {
|
||||
pageBlocks.add(redactManagerBlockificationService.buildTextBlock(((TextPageBlock) cp).getSequences(), numOnPage.getAndIncrement()));
|
||||
});
|
||||
});
|
||||
|
||||
// ClassificationPage classificationPage = switch (layoutParsingType) {
|
||||
// case REDACT_MANAGER -> redactManagerBlockificationService.blockify(textPositionSequences, cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
// case TAAS -> taasBlockificationService.blockify(textPositionSequences, cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
// case DOCUMINE -> docuMineBlockificationService.blockify(textPositionSequences, cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
// };
|
||||
|
||||
ClassificationPage classificationPage = new ClassificationPage(pageBlocks);
|
||||
|
||||
classificationPage.setCleanRulings(cleanRulings);
|
||||
classificationPage.setRotation(rotation);
|
||||
classificationPage.setLandscape(isLandscape);
|
||||
@ -283,9 +316,19 @@ public class LayoutParsingPipeline {
|
||||
case REDACT_MANAGER -> redactManagerClassificationService.classifyDocument(classificationDocument);
|
||||
}
|
||||
|
||||
List<ClassificationSection> sections = new ArrayList<>();
|
||||
for (var page : classificationPages) {
|
||||
page.getTextBlocks().forEach(block -> {
|
||||
block.setPage(page.getPageNumber());
|
||||
var section = sectionsBuilderService.buildTextBlock(List.of(block), "a");
|
||||
sections.add(section);
|
||||
});
|
||||
}
|
||||
classificationDocument.setSections(sections);
|
||||
|
||||
log.info("Building Sections for {}", identifier);
|
||||
sectionsBuilderService.buildSections(classificationDocument);
|
||||
sectionsBuilderService.addImagesToSections(classificationDocument);
|
||||
// sectionsBuilderService.buildSections(classificationDocument);
|
||||
// sectionsBuilderService.addImagesToSections(classificationDocument);
|
||||
return classificationDocument;
|
||||
}
|
||||
|
||||
|
||||
@ -12,15 +12,16 @@ import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRul
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class ClassificationPage {
|
||||
|
||||
@NonNull
|
||||
@Getter
|
||||
private List<AbstractPageBlock> textBlocks;
|
||||
|
||||
private List<ClassifiedImage> images = new ArrayList<>();
|
||||
|
||||
@ -45,6 +45,9 @@ public class RedTextPosition {
|
||||
@JsonIgnore
|
||||
private String fontName;
|
||||
|
||||
@JsonIgnore
|
||||
private RedTextPosition parent;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public static RedTextPosition fromTextPosition(TextPosition textPosition) {
|
||||
|
||||
@ -17,6 +17,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ -27,6 +28,7 @@ import lombok.NoArgsConstructor;
|
||||
public class TextPageBlock extends AbstractPageBlock {
|
||||
|
||||
@Builder.Default
|
||||
@Getter
|
||||
private List<TextPositionSequence> sequences = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
@ -73,7 +75,7 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
|
||||
return sequences.get(0).getPageWidth();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static TextPageBlock merge(List<TextPageBlock> textBlocksToMerge) {
|
||||
|
||||
@ -82,6 +84,7 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
return fromTextPositionSequences(sequences);
|
||||
}
|
||||
|
||||
|
||||
public static TextPageBlock fromTextPositionSequences(List<TextPositionSequence> wordBlockList) {
|
||||
|
||||
TextPageBlock textBlock = null;
|
||||
@ -133,7 +136,6 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the minX value in pdf coordinate system.
|
||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
||||
|
||||
@ -55,6 +55,18 @@ public class TextPositionSequence implements CharSequence {
|
||||
}
|
||||
|
||||
|
||||
public TextPositionSequence(List<RedTextPosition> textPositions, int page) {
|
||||
|
||||
this.textPositions = textPositions;
|
||||
this.page = page;
|
||||
this.dir = TextDirection.fromDegrees(textPositions.get(0).getDir());
|
||||
this.rotation = textPositions.get(0).getRotation();
|
||||
this.pageHeight = textPositions.get(0).getPageHeight();
|
||||
this.pageWidth = textPositions.get(0).getPageWidth();
|
||||
this.isParagraphStart = false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
|
||||
|
||||
@ -240,7 +240,7 @@ public class SectionsBuilderService {
|
||||
}
|
||||
|
||||
|
||||
private ClassificationSection buildTextBlock(List<AbstractPageBlock> wordBlockList, String lastHeadline) {
|
||||
public ClassificationSection buildTextBlock(List<AbstractPageBlock> wordBlockList, String lastHeadline) {
|
||||
|
||||
ClassificationSection section = new ClassificationSection();
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ public class RedactManagerBlockificationService {
|
||||
boolean isSplitByRuling = isSplitByRuling(minX, minY, maxX, maxY, word, horizontalRulingLines, verticalRulingLines);
|
||||
boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());
|
||||
|
||||
if (prev != null && (lineSeparation || startFromTop || splitByX || splitByDir || isSplitByRuling)) {
|
||||
if (prev != null && (splitByDir || isSplitByRuling)) {
|
||||
|
||||
Orientation prevOrientation = null;
|
||||
if (!chunkBlockList.isEmpty()) {
|
||||
@ -167,7 +167,7 @@ public class RedactManagerBlockificationService {
|
||||
}
|
||||
|
||||
|
||||
private TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
|
||||
public TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
|
||||
|
||||
TextPageBlock textBlock = null;
|
||||
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.docstrum.Neighbor;
|
||||
|
||||
/**
|
||||
* Filter class for neighbor objects that checks if the angle of the
|
||||
* neighbor is within specified range.
|
||||
*/
|
||||
public abstract class AngleFilter {
|
||||
|
||||
private final double lowerAngle;
|
||||
private final double upperAngle;
|
||||
|
||||
|
||||
private AngleFilter(double lowerAngle, double upperAngle) {
|
||||
|
||||
this.lowerAngle = lowerAngle;
|
||||
this.upperAngle = upperAngle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs new angle filter.
|
||||
*
|
||||
* @param lowerAngle minimum angle in range [-3*pi/2, pi/2)
|
||||
* @param upperAngle maximum angle in range [-pi/2, 3*pi/2)
|
||||
* @return newly constructed angle filter
|
||||
*/
|
||||
public static AngleFilter newInstance(double lowerAngle, double upperAngle) {
|
||||
|
||||
if (lowerAngle < -Math.PI / 2) {
|
||||
lowerAngle += Math.PI;
|
||||
}
|
||||
if (upperAngle >= Math.PI / 2) {
|
||||
upperAngle -= Math.PI;
|
||||
}
|
||||
if (lowerAngle <= upperAngle) {
|
||||
return new AndFilter(lowerAngle, upperAngle);
|
||||
} else {
|
||||
return new OrFilter(lowerAngle, upperAngle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public double getLowerAngle() {
|
||||
|
||||
return lowerAngle;
|
||||
}
|
||||
|
||||
|
||||
public double getUpperAngle() {
|
||||
|
||||
return upperAngle;
|
||||
}
|
||||
|
||||
|
||||
public abstract boolean matches(Neighbor neighbor);
|
||||
|
||||
|
||||
public static final class AndFilter extends AngleFilter {
|
||||
|
||||
private AndFilter(double lowerAngle, double upperAngle) {
|
||||
|
||||
super(lowerAngle, upperAngle);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean matches(Neighbor neighbor) {
|
||||
|
||||
return getLowerAngle() <= neighbor.getAngle() && neighbor.getAngle() < getUpperAngle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class OrFilter extends AngleFilter {
|
||||
|
||||
private OrFilter(double lowerAngle, double upperAngle) {
|
||||
|
||||
super(lowerAngle, upperAngle);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean matches(Neighbor neighbor) {
|
||||
|
||||
return getLowerAngle() <= neighbor.getAngle() || neighbor.getAngle() < getUpperAngle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,767 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.DisjointSets;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.Histogram;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Line;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Word;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Zone;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.docstrum.Character;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.docstrum.Neighbor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.utils.BoundingBoxBuilder;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.utils.ZoneUtils;
|
||||
|
||||
@Service
|
||||
public class DocstrumSegmenter {
|
||||
|
||||
public static final int MAX_ZONES_PER_PAGE = 300;
|
||||
public static final double ORIENTATION_MARGIN = 0.2;
|
||||
public static final int LINES_PER_PAGE_MARGIN = 100;
|
||||
|
||||
private static final double DISTANCE_STEP = 16.0;
|
||||
|
||||
/**
|
||||
* Angle histogram resolution in radians per bin.
|
||||
*/
|
||||
private static final double ANGLE_HIST_RESOLUTION = Math.toRadians(0.5);
|
||||
|
||||
/**
|
||||
* Angle histogram smoothing window length in radians.
|
||||
* Length of angle histogram is equal to pi.
|
||||
*/
|
||||
private static final double ANGLE_HIST_SMOOTHING_LEN = 0.25 * Math.PI;
|
||||
|
||||
/**
|
||||
* Angle histogram gaussian smoothing window standard deviation in radians.
|
||||
*/
|
||||
private static final double ANGLE_HIST_SMOOTHING_STDDEV = 0.0625 * Math.PI;
|
||||
|
||||
/**
|
||||
* Spacing histogram resolution per bin.
|
||||
*/
|
||||
private static final double SPACING_HIST_RESOLUTION = 0.5;
|
||||
|
||||
/**
|
||||
* Spacing histogram smoothing window length.
|
||||
*/
|
||||
private static final double SPACING_HIST_SMOOTHING_LEN = 2.5;
|
||||
|
||||
/**
|
||||
* Spacing histogram gaussian smoothing window standard deviation.
|
||||
*/
|
||||
private static final double SPACING_HIST_SMOOTHING_STDDEV = 0.5;
|
||||
|
||||
/**
|
||||
* Maximum vertical component distance multiplier used during line
|
||||
* determination.
|
||||
* <p>
|
||||
* Maximum vertical distance between components (characters) that belong
|
||||
* to the same line is equal to the product of this value and estimated
|
||||
* between-line spacing.
|
||||
*/
|
||||
private static final double MAX_VERTICAL_COMP_DIST = 0.67;
|
||||
|
||||
/**
|
||||
* Minimum line size scale value.
|
||||
* <p>
|
||||
* During zone determination (merging lines into zones) line height is
|
||||
* taken into account. To achieve this, line size scale is estimated and
|
||||
* limited to range [minLineSizeScale, maxLineSizeScale].
|
||||
*/
|
||||
private static final double MIN_LINE_SIZE_SCALE = 0.9;
|
||||
|
||||
/**
|
||||
* Maximum line size scale value.
|
||||
* <p>
|
||||
* See minLineSizeScale for more information.
|
||||
*/
|
||||
private static final double MAX_LINE_SIZE_SCALE = 2.5;
|
||||
|
||||
/**
|
||||
* Minimum horizontal line distance multiplier.
|
||||
* <p>
|
||||
* Minimum horizontal distance between lines that belong to the same zone
|
||||
* is equal to the product of this value and estimated within-line spacing.
|
||||
*/
|
||||
private static final double MIN_HORIZONTAL_DIST = -0.5;
|
||||
|
||||
/**
|
||||
* Minimum vertical line distance multiplier.
|
||||
* <p>
|
||||
* Minimum vertical distance between lines that belong to the same zone
|
||||
* is equal to the product of this value and estimated between-line spacing.
|
||||
*/
|
||||
private static final double MIN_VERTICAL_DIST = 0.0;
|
||||
|
||||
/**
|
||||
* Maximum vertical line distance multiplier.
|
||||
* <p>
|
||||
* Maximum vertical distance between lines that belong to the same zone
|
||||
* is equal to the product of this value and estimated between-line spacing.
|
||||
*/
|
||||
private static final double MAX_VERTICAL_DIST = 1.2;
|
||||
|
||||
/**
|
||||
* Component distance character spacing multiplier.
|
||||
* <p>
|
||||
* Maximum distance between components that belong to the same line is
|
||||
* equal to (lineSpacing * componentDistanceLineMultiplier +
|
||||
* characterSpacing * componentDistanceCharacterMultiplier), where
|
||||
* lineSpacing and characterSpacing are estimated between-line and
|
||||
* within-line spacing, respectively.
|
||||
*/
|
||||
private static final double COMP_DIST_CHAR = 3.5;
|
||||
|
||||
/**
|
||||
* Word distance multiplier.
|
||||
* <p>
|
||||
* Maximum distance between components that belong to the same word is
|
||||
* equal to the product of this value and estimated within-line spacing.
|
||||
*/
|
||||
private static final double WORD_DIST_MULT = 0.2;
|
||||
|
||||
/**
|
||||
* Minimum horizontal line merge distance multiplier.
|
||||
* <p>
|
||||
* Minimum horizontal distance between lines that should be merged is equal
|
||||
* to the product of this value and estimated within-line spacing.
|
||||
* <p>
|
||||
* Because split lines do not overlap this value should be negative.
|
||||
*/
|
||||
|
||||
private static final double MIN_HORIZONTAL_MERGE_DIST = -3.0;
|
||||
|
||||
/**
|
||||
* Maximum vertical line merge distance multiplier.
|
||||
* <p>
|
||||
* Maximum vertical distance between lines that should be merged is equal
|
||||
* to the product of this value and estimated between-line spacing.
|
||||
*/
|
||||
|
||||
private static final double MAX_VERTICAL_MERGE_DIST = 0.5;
|
||||
|
||||
/**
|
||||
* Angle tolerance for comparisons of angles between components and angles
|
||||
* between lines.
|
||||
*/
|
||||
private static final double ANGLE_TOLERANCE = Math.PI / 6;
|
||||
|
||||
/**
|
||||
* Number of nearest-neighbors found per component.
|
||||
*/
|
||||
private static final int NEIGHBOUR_COUNT = 8;
|
||||
|
||||
|
||||
public List<Zone> segmentPage(List<TextPositionSequence> textPositions) {
|
||||
|
||||
var positions = textPositions.stream().map(t -> t.getTextPositions()).flatMap(List::stream).collect(Collectors.toList());
|
||||
|
||||
var components = positions.stream().map(chunk -> new Character(chunk)).collect(Collectors.toList());
|
||||
|
||||
Character[] componentsArray = new Character[positions.size()];
|
||||
components.toArray(componentsArray);
|
||||
|
||||
Arrays.sort(componentsArray, Character.CharacterXComparator.getInstance());
|
||||
findNeighbors(componentsArray);
|
||||
|
||||
double orientation = computeInitialOrientation(components);
|
||||
|
||||
double characterSpacing = computeCharacterSpacing(components, orientation);
|
||||
double lineSpacing = computeLineSpacing(components, orientation);
|
||||
|
||||
List<ComponentLine> lines = determineLines(components, orientation, characterSpacing * COMP_DIST_CHAR, lineSpacing * MAX_VERTICAL_COMP_DIST);
|
||||
|
||||
if (Math.abs(orientation) > ORIENTATION_MARGIN) {
|
||||
List<ComponentLine> linesZero = determineLines(components, 0, characterSpacing * COMP_DIST_CHAR, lineSpacing * MAX_VERTICAL_COMP_DIST);
|
||||
|
||||
if (Math.abs(lines.size() - LINES_PER_PAGE_MARGIN) > Math.abs(linesZero.size() - LINES_PER_PAGE_MARGIN)) {
|
||||
orientation = 0;
|
||||
lines = linesZero;
|
||||
}
|
||||
}
|
||||
|
||||
double lineOrientation = computeOrientation(lines);
|
||||
if (!Double.isNaN(lineOrientation)) {
|
||||
orientation = lineOrientation;
|
||||
}
|
||||
|
||||
List<List<ComponentLine>> zones = determineZones(lines,
|
||||
orientation,
|
||||
characterSpacing * MIN_HORIZONTAL_DIST,
|
||||
Double.POSITIVE_INFINITY,
|
||||
lineSpacing * MIN_VERTICAL_DIST,
|
||||
lineSpacing * MAX_VERTICAL_DIST,
|
||||
characterSpacing * MIN_HORIZONTAL_MERGE_DIST,
|
||||
0.0,
|
||||
0.0,
|
||||
lineSpacing * MAX_VERTICAL_MERGE_DIST);
|
||||
zones = mergeZones(zones, characterSpacing * 0.5);
|
||||
zones = mergeLines(zones, orientation, Double.NEGATIVE_INFINITY, 0.0, 0.0, lineSpacing * MAX_VERTICAL_MERGE_DIST);
|
||||
return convertToBxModel(zones, WORD_DIST_MULT * characterSpacing);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs for each component search for nearest-neighbors and stores the
|
||||
* result in component's neighbors attribute.
|
||||
*
|
||||
* @param components array of components
|
||||
* equal to the number of nearest-neighbors per component.
|
||||
*/
|
||||
private void findNeighbors(Character[] components) {
|
||||
|
||||
if (components.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (components.length == 1) {
|
||||
components[0].setNeighbors(new ArrayList<Neighbor>());
|
||||
return;
|
||||
}
|
||||
int pageNeighborCount = NEIGHBOUR_COUNT;
|
||||
if (components.length <= NEIGHBOUR_COUNT) {
|
||||
pageNeighborCount = components.length - 1;
|
||||
}
|
||||
|
||||
List<Neighbor> candidates = new ArrayList<Neighbor>();
|
||||
for (int i = 0; i < components.length; i++) {
|
||||
int start = i, end = i + 1;
|
||||
// Contains components from components array
|
||||
// from ranges [start, i) and [i+1, end)
|
||||
double dist = Double.POSITIVE_INFINITY;
|
||||
for (double searchDist = 0; searchDist < dist; ) {
|
||||
searchDist += DISTANCE_STEP;
|
||||
boolean newCandidatesFound = false;
|
||||
|
||||
while (start > 0 && components[i].getX() - components[start - 1].getX() < searchDist) {
|
||||
start--;
|
||||
candidates.add(new Neighbor(components[start], components[i]));
|
||||
if (candidates.size() > pageNeighborCount) {
|
||||
Collections.sort(candidates, NeighborDistanceComparator.getInstance());
|
||||
candidates.subList(pageNeighborCount, candidates.size()).clear();
|
||||
}
|
||||
newCandidatesFound = true;
|
||||
}
|
||||
while (end < components.length && components[end].getX() - components[i].getX() < searchDist) {
|
||||
candidates.add(new Neighbor(components[end], components[i]));
|
||||
if (candidates.size() > pageNeighborCount) {
|
||||
Collections.sort(candidates, NeighborDistanceComparator.getInstance());
|
||||
candidates.subList(pageNeighborCount, candidates.size()).clear();
|
||||
}
|
||||
end++;
|
||||
newCandidatesFound = true;
|
||||
}
|
||||
|
||||
if (newCandidatesFound && candidates.size() >= pageNeighborCount) {
|
||||
Collections.sort(candidates, NeighborDistanceComparator.getInstance());
|
||||
dist = candidates.get(pageNeighborCount - 1).getDistance();
|
||||
}
|
||||
}
|
||||
candidates.subList(pageNeighborCount, candidates.size()).clear();
|
||||
components[i].setNeighbors(new ArrayList<Neighbor>(candidates));
|
||||
candidates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Computes initial orientation estimation based on nearest-neighbors' angles.
|
||||
*
|
||||
* @param components
|
||||
* @return initial orientation estimation
|
||||
*/
|
||||
private double computeInitialOrientation(List<Character> components) {
|
||||
|
||||
Histogram histogram = new Histogram(-Math.PI / 2, Math.PI / 2, ANGLE_HIST_RESOLUTION);
|
||||
for (Character component : components) {
|
||||
for (Neighbor neighbor : component.getNeighbors()) {
|
||||
histogram.add(neighbor.getAngle());
|
||||
}
|
||||
}
|
||||
// Rectangular smoothing window has been replaced with gaussian smoothing window
|
||||
histogram.circularGaussianSmooth(ANGLE_HIST_SMOOTHING_LEN, ANGLE_HIST_SMOOTHING_STDDEV);
|
||||
return histogram.getPeakValue();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Computes within-line spacing based on nearest-neighbors distances.
|
||||
*
|
||||
* @param components
|
||||
* @param orientation estimated text orientation
|
||||
* @return estimated within-line spacing
|
||||
*/
|
||||
private double computeCharacterSpacing(List<Character> components, double orientation) {
|
||||
|
||||
return computeSpacing(components, orientation);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Computes between-line spacing based on nearest-neighbors distances.
|
||||
*
|
||||
* @param components
|
||||
* @param orientation estimated text orientation
|
||||
* @return estimated between-line spacing
|
||||
*/
|
||||
private double computeLineSpacing(List<Character> components, double orientation) {
|
||||
|
||||
if (orientation >= 0) {
|
||||
return computeSpacing(components, orientation - Math.PI / 2);
|
||||
} else {
|
||||
return computeSpacing(components, orientation + Math.PI / 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double computeSpacing(List<Character> components, double angle) {
|
||||
|
||||
double maxDistance = Double.NEGATIVE_INFINITY;
|
||||
for (Character component : components) {
|
||||
for (Neighbor neighbor : component.getNeighbors()) {
|
||||
maxDistance = Math.max(maxDistance, neighbor.getDistance());
|
||||
}
|
||||
}
|
||||
Histogram histogram = new Histogram(0, maxDistance, SPACING_HIST_RESOLUTION);
|
||||
AngleFilter filter = AngleFilter.newInstance(angle - ANGLE_TOLERANCE, angle + ANGLE_TOLERANCE);
|
||||
for (Character component : components) {
|
||||
for (Neighbor neighbor : component.getNeighbors()) {
|
||||
if (filter.matches(neighbor)) {
|
||||
histogram.add(neighbor.getDistance());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rectangular smoothing window has been replaced with gaussian smoothing window
|
||||
histogram.gaussianSmooth(SPACING_HIST_SMOOTHING_LEN, SPACING_HIST_SMOOTHING_STDDEV);
|
||||
return histogram.getPeakValue();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Groups components into text lines.
|
||||
*
|
||||
* @param components component list
|
||||
* @param orientation - estimated text orientation
|
||||
* @param maxHorizontalDistance - maximum horizontal distance between components
|
||||
* @param maxVerticalDistance - maximum vertical distance between components
|
||||
* @return lines of components
|
||||
*/
|
||||
private List<ComponentLine> determineLines(List<Character> components, double orientation, double maxHorizontalDistance, double maxVerticalDistance) {
|
||||
|
||||
DisjointSets<Character> sets = new DisjointSets<Character>(components);
|
||||
AngleFilter filter = AngleFilter.newInstance(orientation - ANGLE_TOLERANCE, orientation + ANGLE_TOLERANCE);
|
||||
for (Character component : components) {
|
||||
for (Neighbor neighbor : component.getNeighbors()) {
|
||||
double x = neighbor.getHorizontalDistance() / maxHorizontalDistance;
|
||||
double y = neighbor.getVerticalDistance() / maxVerticalDistance;
|
||||
if (filter.matches(neighbor) && x * x + y * y <= 1) {
|
||||
sets.union(component, neighbor.getCharacter());
|
||||
}
|
||||
}
|
||||
}
|
||||
List<ComponentLine> lines = new ArrayList<ComponentLine>();
|
||||
for (Set<Character> group : sets) {
|
||||
List<Character> lineComponents = new ArrayList<Character>(group);
|
||||
Collections.sort(lineComponents, Character.CharacterXComparator.getInstance());
|
||||
lines.add(new ComponentLine(lineComponents, orientation));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
private double computeOrientation(List<ComponentLine> lines) {
|
||||
// Compute weighted mean of line angles
|
||||
double valueSum = 0.0;
|
||||
double weightSum = 0.0;
|
||||
for (ComponentLine line : lines) {
|
||||
valueSum += line.getAngle() * line.getLength();
|
||||
weightSum += line.getLength();
|
||||
}
|
||||
return valueSum / weightSum;
|
||||
}
|
||||
|
||||
|
||||
private List<List<ComponentLine>> determineZones(List<ComponentLine> lines,
|
||||
double orientation,
|
||||
double minHorizontalDistance,
|
||||
double maxHorizontalDistance,
|
||||
double minVerticalDistance,
|
||||
double maxVerticalDistance,
|
||||
double minHorizontalMergeDistance,
|
||||
double maxHorizontalMergeDistance,
|
||||
double minVerticalMergeDistance,
|
||||
double maxVerticalMergeDistance) {
|
||||
|
||||
DisjointSets<ComponentLine> sets = new DisjointSets<ComponentLine>(lines);
|
||||
// Mean height is computed so that all distances can be scaled
|
||||
// relative to the line height
|
||||
double meanHeight = 0.0, weights = 0.0;
|
||||
for (ComponentLine line : lines) {
|
||||
double weight = line.getLength();
|
||||
meanHeight += line.getHeight() * weight;
|
||||
weights += weight;
|
||||
}
|
||||
meanHeight /= weights;
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
ComponentLine li = lines.get(i);
|
||||
for (int j = i + 1; j < lines.size(); j++) {
|
||||
ComponentLine lj = lines.get(j);
|
||||
double scale = Math.min(li.getHeight(), lj.getHeight()) / meanHeight;
|
||||
scale = Math.max(MIN_LINE_SIZE_SCALE, Math.min(scale, MAX_LINE_SIZE_SCALE));
|
||||
// "<=" is used instead of "<" for consistency and to allow setting minVertical(Merge)Distance
|
||||
// to 0.0 with meaning "no minimal distance required"
|
||||
if (!sets.areTogether(li, lj) && li.angularDifference(lj) <= ANGLE_TOLERANCE) {
|
||||
double hDist = li.horizontalDistance(lj, orientation) / scale;
|
||||
double vDist = li.verticalDistance(lj, orientation) / scale;
|
||||
// Line over or above
|
||||
if (minHorizontalDistance <= hDist && hDist <= maxHorizontalDistance && minVerticalDistance <= vDist && vDist <= maxVerticalDistance) {
|
||||
sets.union(li, lj);
|
||||
}
|
||||
// Split line that needs later merging
|
||||
else if (minHorizontalMergeDistance <= hDist && hDist <= maxHorizontalMergeDistance && minVerticalMergeDistance <= vDist && vDist <= maxVerticalMergeDistance) {
|
||||
sets.union(li, lj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
List<List<ComponentLine>> zones = new ArrayList<List<ComponentLine>>();
|
||||
for (Set<ComponentLine> group : sets) {
|
||||
zones.add(new ArrayList<ComponentLine>(group));
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
|
||||
private List<List<ComponentLine>> mergeZones(List<List<ComponentLine>> zones, double tolerance) {
|
||||
|
||||
List<BoundingBox> bounds = new ArrayList<BoundingBox>(zones.size());
|
||||
for (List<ComponentLine> zone : zones) {
|
||||
BoundingBoxBuilder builder = new BoundingBoxBuilder();
|
||||
for (ComponentLine line : zone) {
|
||||
for (Character component : line.getComponents()) {
|
||||
builder.expand(component.getChunk());
|
||||
}
|
||||
}
|
||||
bounds.add(builder.getBounds());
|
||||
}
|
||||
|
||||
List<List<ComponentLine>> outputZones = new ArrayList<List<ComponentLine>>();
|
||||
mainFor:
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
for (int j = 0; j < zones.size(); j++) {
|
||||
if (i == j || bounds.get(j) == null || bounds.get(i) == null) {
|
||||
continue;
|
||||
}
|
||||
if (bounds.get(j).contains(bounds.get(i), tolerance)) {
|
||||
zones.get(j).addAll(zones.get(i));
|
||||
bounds.set(i, null);
|
||||
continue mainFor;
|
||||
}
|
||||
}
|
||||
outputZones.add(zones.get(i));
|
||||
}
|
||||
return outputZones;
|
||||
}
|
||||
|
||||
|
||||
private List<List<ComponentLine>> mergeLines(List<List<ComponentLine>> zones,
|
||||
double orientation,
|
||||
double minHorizontalDistance,
|
||||
double maxHorizontalDistance,
|
||||
double minVerticalDistance,
|
||||
double maxVerticalDistance) {
|
||||
|
||||
List<List<ComponentLine>> outputZones = new ArrayList<List<ComponentLine>>(zones.size());
|
||||
for (List<ComponentLine> zone : zones) {
|
||||
outputZones.add(mergeLinesInZone(zone, orientation, minHorizontalDistance, maxHorizontalDistance, minVerticalDistance, maxVerticalDistance));
|
||||
}
|
||||
return outputZones;
|
||||
}
|
||||
|
||||
|
||||
private List<ComponentLine> mergeLinesInZone(List<ComponentLine> lines,
|
||||
double orientation,
|
||||
double minHorizontalDistance,
|
||||
double maxHorizontalDistance,
|
||||
double minVerticalDistance,
|
||||
double maxVerticalDistance) {
|
||||
|
||||
DisjointSets<ComponentLine> sets = new DisjointSets<ComponentLine>(lines);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
ComponentLine li = lines.get(i);
|
||||
for (int j = i + 1; j < lines.size(); j++) {
|
||||
ComponentLine lj = lines.get(j);
|
||||
double hDist = li.horizontalDistance(lj, orientation);
|
||||
double vDist = li.verticalDistance(lj, orientation);
|
||||
if (minHorizontalDistance <= hDist && hDist <= maxHorizontalDistance && minVerticalDistance <= vDist && vDist <= maxVerticalDistance) {
|
||||
sets.union(li, lj);
|
||||
} else if (minVerticalDistance <= vDist && vDist <= maxVerticalDistance && Math.abs(hDist - Math.min(li.getLength(), lj.getLength())) < 0.1) {
|
||||
boolean componentOverlap = false;
|
||||
int overlappingCount = 0;
|
||||
for (Character ci : li.getComponents()) {
|
||||
for (Character cj : lj.getComponents()) {
|
||||
double dist = ci.overlappingDistance(cj, orientation);
|
||||
if (dist > 2) {
|
||||
componentOverlap = true;
|
||||
}
|
||||
if (dist > 0) {
|
||||
overlappingCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!componentOverlap && overlappingCount <= 2) {
|
||||
sets.union(li, lj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
List<ComponentLine> outputZone = new ArrayList<ComponentLine>();
|
||||
for (Set<ComponentLine> group : sets) {
|
||||
List<Character> components = new ArrayList<Character>();
|
||||
for (ComponentLine line : group) {
|
||||
components.addAll(line.getComponents());
|
||||
}
|
||||
Collections.sort(components, Character.CharacterXComparator.getInstance());
|
||||
outputZone.add(new ComponentLine(components, orientation));
|
||||
}
|
||||
return outputZone;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts list of zones from internal format (using components and
|
||||
* component lines) to BxModel.
|
||||
*
|
||||
* @param zones zones in internal format
|
||||
* @param wordSpacing - maximum allowed distance between components that
|
||||
* belong to one word
|
||||
* @return BxModel page
|
||||
*/
|
||||
private List<Zone> convertToBxModel(List<List<ComponentLine>> zones, double wordSpacing) {
|
||||
|
||||
List<Zone> zoneList = new ArrayList<>();
|
||||
if (zones.size() > MAX_ZONES_PER_PAGE) {
|
||||
List<ComponentLine> oneZone = new ArrayList<ComponentLine>();
|
||||
for (List<ComponentLine> zone : zones) {
|
||||
oneZone.addAll(zone);
|
||||
}
|
||||
zones = new ArrayList<>();
|
||||
zones.add(oneZone);
|
||||
}
|
||||
|
||||
for (List<ComponentLine> lines : zones) {
|
||||
Zone zone = new Zone();
|
||||
for (ComponentLine line : lines) {
|
||||
zone.addLine(line.convertToBxLine(wordSpacing));
|
||||
}
|
||||
List<Line> zLines = Lists.newArrayList(zone.getLines());
|
||||
Collections.sort(zLines, new Comparator<Line>() {
|
||||
|
||||
@Override
|
||||
public int compare(Line o1, Line o2) {
|
||||
|
||||
return Double.compare(o1.getbBox().getY(), o2.getbBox().getY());
|
||||
}
|
||||
|
||||
});
|
||||
zone.setLines(zLines);
|
||||
BoundingBoxBuilder.setBounds(zone);
|
||||
zoneList.add(zone);
|
||||
}
|
||||
ZoneUtils.sortZonesYX(zoneList);
|
||||
return zoneList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Neighbor distance comparator based on the distance.
|
||||
* <p>
|
||||
* The ordering is not consistent with equals.
|
||||
*/
|
||||
protected static final class NeighborDistanceComparator implements Comparator<Neighbor> {
|
||||
|
||||
private NeighborDistanceComparator() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compare(Neighbor o1, Neighbor o2) {
|
||||
|
||||
return Double.compare(o1.getDistance(), o2.getDistance());
|
||||
}
|
||||
|
||||
|
||||
private static final NeighborDistanceComparator instance = new NeighborDistanceComparator();
|
||||
|
||||
|
||||
public static NeighborDistanceComparator getInstance() {
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal representation of the text line.
|
||||
*/
|
||||
protected static class ComponentLine {
|
||||
|
||||
private final double x0;
|
||||
private final double y0;
|
||||
|
||||
private final double x1;
|
||||
private final double y1;
|
||||
|
||||
private final double height;
|
||||
|
||||
private final List<Character> components;
|
||||
|
||||
|
||||
public ComponentLine(List<Character> components, double orientation) {
|
||||
|
||||
this.components = components;
|
||||
|
||||
if (components.size() >= 2) {
|
||||
// Simple linear regression
|
||||
double sx = 0.0, sxx = 0.0, sxy = 0.0, sy = 0.0;
|
||||
for (Character component : components) {
|
||||
sx += component.getX();
|
||||
sxx += component.getX() * component.getX();
|
||||
sxy += component.getX() * component.getY();
|
||||
sy += component.getY();
|
||||
}
|
||||
double b = (components.size() * sxy - sx * sy) / (components.size() * sxx - sx * sx);
|
||||
double a = (sy - b * sx) / components.size();
|
||||
|
||||
this.x0 = components.get(0).getX();
|
||||
this.y0 = a + b * this.x0;
|
||||
this.x1 = components.get(components.size() - 1).getX();
|
||||
this.y1 = a + b * this.x1;
|
||||
} else if (!components.isEmpty()) {
|
||||
Character component = components.get(0);
|
||||
double dx = component.getChunk().getWidthDirAdj() / 3;
|
||||
double dy = dx * Math.tan(orientation);
|
||||
this.x0 = component.getX() - dx;
|
||||
this.x1 = component.getX() + dx;
|
||||
this.y0 = component.getY() - dy;
|
||||
this.y1 = component.getY() + dy;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Component list must not be empty");
|
||||
}
|
||||
height = computeHeight();
|
||||
}
|
||||
|
||||
|
||||
public double getAngle() {
|
||||
|
||||
return Math.atan2(y1 - y0, x1 - x0);
|
||||
}
|
||||
|
||||
|
||||
public double getSlope() {
|
||||
|
||||
return (y1 - y0) / (x1 - x0);
|
||||
}
|
||||
|
||||
|
||||
public double getLength() {
|
||||
|
||||
return Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1));
|
||||
}
|
||||
|
||||
|
||||
private double computeHeight() {
|
||||
|
||||
double sum = 0.0;
|
||||
for (Character component : components) {
|
||||
sum += component.getHeight();
|
||||
}
|
||||
return sum / components.size();
|
||||
}
|
||||
|
||||
|
||||
public double getHeight() {
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
|
||||
public List<Character> getComponents() {
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
|
||||
public double angularDifference(ComponentLine j) {
|
||||
|
||||
double diff = Math.abs(getAngle() - j.getAngle());
|
||||
if (diff <= Math.PI / 2) {
|
||||
return diff;
|
||||
} else {
|
||||
return Math.PI - diff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public double horizontalDistance(ComponentLine other, double orientation) {
|
||||
|
||||
double[] xs = new double[4];
|
||||
double s = Math.sin(-orientation), c = Math.cos(-orientation);
|
||||
xs[0] = c * x0 - s * y0;
|
||||
xs[1] = c * x1 - s * y1;
|
||||
xs[2] = c * other.x0 - s * other.y0;
|
||||
xs[3] = c * other.x1 - s * other.y1;
|
||||
boolean overlapping = xs[1] >= xs[2] && xs[3] >= xs[0];
|
||||
Arrays.sort(xs);
|
||||
return Math.abs(xs[2] - xs[1]) * (overlapping ? 1 : -1);
|
||||
}
|
||||
|
||||
|
||||
public double verticalDistance(ComponentLine other, double orientation) {
|
||||
|
||||
double xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
|
||||
double xn = (other.x0 + other.x1) / 2, yn = (other.y0 + other.y1) / 2;
|
||||
double a = Math.tan(orientation);
|
||||
return Math.abs(a * (xn - xm) + ym - yn) / Math.sqrt(a * a + 1);
|
||||
}
|
||||
|
||||
|
||||
public Line convertToBxLine(double wordSpacing) {
|
||||
|
||||
Line line = new Line();
|
||||
Word word = new Word();
|
||||
Character previousComponent = null;
|
||||
for (Character component : components) {
|
||||
if (previousComponent != null) {
|
||||
double dist = component.getChunk().getXDirAdj() - previousComponent.getChunk().getXDirAdj() - previousComponent.getChunk().getWidthDirAdj();
|
||||
if (dist > wordSpacing) {
|
||||
BoundingBoxBuilder.setBounds(word);
|
||||
line.addWord(word);
|
||||
word = new Word();
|
||||
}
|
||||
}
|
||||
word.addChunk(component.getChunk());
|
||||
previousComponent = component;
|
||||
}
|
||||
BoundingBoxBuilder.setBounds(word);
|
||||
line.addWord(word);
|
||||
BoundingBoxBuilder.setBounds(line);
|
||||
return line;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,272 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.BBoxObject;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Line;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Word;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Zone;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.utils.DoubleUtils;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder.BBoxZoneGroup;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder.DistElem;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder.DocumentPlane;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder.TreeToListConverter;
|
||||
|
||||
@Service
|
||||
public class HierarchicalReadingOrderResolver {
|
||||
|
||||
static final int GRIDSIZE = 50;
|
||||
static final double BOXES_FLOW = 0.5;
|
||||
static final double EPS = 0.01;
|
||||
static final int MAX_ZONES = 1000;
|
||||
static final Comparator<BBoxObject> Y_ASCENDING_ORDER = new Comparator<BBoxObject>() {
|
||||
|
||||
@Override
|
||||
public int compare(BBoxObject o1, BBoxObject o2) {
|
||||
|
||||
return DoubleUtils.compareDouble(o1.getY(), o2.getY(), EPS);
|
||||
}
|
||||
};
|
||||
|
||||
static final Comparator<BBoxObject> X_ASCENDING_ORDER = new Comparator<BBoxObject>() {
|
||||
|
||||
@Override
|
||||
public int compare(BBoxObject o1, BBoxObject o2) {
|
||||
|
||||
return DoubleUtils.compareDouble(o1.getX(), o2.getX(), EPS);
|
||||
}
|
||||
};
|
||||
|
||||
static final Comparator<RedTextPosition> TP_X_ASCENDING_ORDER = new Comparator<RedTextPosition>() {
|
||||
|
||||
@Override
|
||||
public int compare(RedTextPosition o1, RedTextPosition o2) {
|
||||
|
||||
return DoubleUtils.compareDouble(o1.getXDirAdj(), o2.getXDirAdj(), EPS);
|
||||
}
|
||||
};
|
||||
|
||||
static final Comparator<BBoxObject> YX_ASCENDING_ORDER = new Comparator<BBoxObject>() {
|
||||
|
||||
@Override
|
||||
public int compare(BBoxObject o1, BBoxObject o2) {
|
||||
|
||||
int yCompare = Y_ASCENDING_ORDER.compare(o1, o2);
|
||||
return yCompare == 0 ? X_ASCENDING_ORDER.compare(o1, o2) : yCompare;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public List<Zone> resolve(List<Zone> zones) {
|
||||
|
||||
for (Zone zone : zones) {
|
||||
List<Line> lines = Lists.newArrayList(zone.getLines());
|
||||
for (Line line : lines) {
|
||||
List<Word> words = Lists.newArrayList(line.getWords());
|
||||
for (Word word : words) {
|
||||
List<RedTextPosition> chunks = Lists.newArrayList(word.getTextPositions());
|
||||
Collections.sort(chunks, TP_X_ASCENDING_ORDER);
|
||||
word.setTextPositions(chunks);
|
||||
}
|
||||
Collections.sort(words, X_ASCENDING_ORDER);
|
||||
line.setWords(words);
|
||||
}
|
||||
Collections.sort(lines, YX_ASCENDING_ORDER);
|
||||
zone.setLines(lines);
|
||||
}
|
||||
List<Zone> orderedZones;
|
||||
if (zones.size() > MAX_ZONES) {
|
||||
orderedZones = new ArrayList<Zone>(zones);
|
||||
Collections.sort(orderedZones, YX_ASCENDING_ORDER);
|
||||
} else {
|
||||
orderedZones = reorderZones(zones);
|
||||
}
|
||||
return orderedZones;
|
||||
}
|
||||
|
||||
|
||||
private List<Zone> reorderZones(List<Zone> unorderedZones) {
|
||||
|
||||
if (unorderedZones.isEmpty()) {
|
||||
return new ArrayList<Zone>();
|
||||
} else if (unorderedZones.size() == 1) {
|
||||
List<Zone> ret = new ArrayList<Zone>(1);
|
||||
ret.add(unorderedZones.get(0));
|
||||
return ret;
|
||||
} else {
|
||||
BBoxZoneGroup bxZonesTree = groupZonesHierarchically(unorderedZones);
|
||||
sortGroupedZones(bxZonesTree);
|
||||
TreeToListConverter treeConverter = new TreeToListConverter();
|
||||
List<Zone> orderedZones = treeConverter.convertToList(bxZonesTree);
|
||||
assert unorderedZones.size() == orderedZones.size();
|
||||
return orderedZones;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a binary tree of zones and groups of zones from a list of unordered zones. This is done in hierarchical
|
||||
* clustering by joining two least distant nodes. Distance is calculated in the distance() method.
|
||||
*
|
||||
* @param zones is a list of unordered zones
|
||||
* @return root of the zones clustered in a tree
|
||||
*/
|
||||
private BBoxZoneGroup groupZonesHierarchically(List<Zone> zones) {
|
||||
/*
|
||||
* Distance tuples are stored sorted by ascending distance value
|
||||
*/
|
||||
List<DistElem<BBoxObject>> dists = new ArrayList<DistElem<BBoxObject>>(zones.size() * zones.size() / 2);
|
||||
for (int idx1 = 0; idx1 < zones.size(); ++idx1) {
|
||||
for (int idx2 = idx1 + 1; idx2 < zones.size(); ++idx2) {
|
||||
Zone zone1 = zones.get(idx1);
|
||||
Zone zone2 = zones.get(idx2);
|
||||
dists.add(new DistElem<BBoxObject>(false, distance(zone1, zone2), zone1, zone2));
|
||||
}
|
||||
}
|
||||
Collections.sort(dists);
|
||||
DocumentPlane plane = new DocumentPlane(zones, GRIDSIZE);
|
||||
while (!dists.isEmpty()) {
|
||||
DistElem<BBoxObject> distElem = dists.get(0);
|
||||
dists.remove(0);
|
||||
if (!distElem.isC() && plane.anyObjectsBetween(distElem.getObj1(), distElem.getObj2())) {
|
||||
dists.add(new DistElem<BBoxObject>(true, distElem.getDist(), distElem.getObj1(), distElem.getObj2()));
|
||||
continue;
|
||||
}
|
||||
BBoxZoneGroup newGroup = new BBoxZoneGroup(distElem.getObj1(), distElem.getObj2());
|
||||
plane.remove(distElem.getObj1()).remove(distElem.getObj2());
|
||||
dists = removeDistElementsContainingObject(dists, distElem.getObj1());
|
||||
dists = removeDistElementsContainingObject(dists, distElem.getObj2());
|
||||
for (BBoxObject other : plane.getObjects()) {
|
||||
dists.add(new DistElem<BBoxObject>(false, distance(other, newGroup), newGroup, other));
|
||||
}
|
||||
Collections.sort(dists);
|
||||
plane.add(newGroup);
|
||||
}
|
||||
|
||||
assert plane.getObjects().size() == 1 : "There should be one object left at the plane after grouping";
|
||||
return (BBoxZoneGroup) plane.getObjects().get(0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all distance tuples containing obj
|
||||
*/
|
||||
private List<DistElem<BBoxObject>> removeDistElementsContainingObject(Collection<DistElem<BBoxObject>> list, BBoxObject obj) {
|
||||
|
||||
List<DistElem<BBoxObject>> ret = new ArrayList<DistElem<BBoxObject>>();
|
||||
for (DistElem<BBoxObject> distElem : list) {
|
||||
if (distElem.getObj1() != obj && distElem.getObj2() != obj) {
|
||||
ret.add(distElem);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Swaps children of BxZoneGroup if necessary. A group with smaller sort factor is placed to the left (leftChild).
|
||||
* An object with greater sort factor is placed on the right (rightChild). This plays an important role when
|
||||
* traversing the tree in conversion to a one dimensional list.
|
||||
*
|
||||
* @param group
|
||||
*/
|
||||
private void sortGroupedZones(BBoxZoneGroup group) {
|
||||
|
||||
BBoxObject leftChild = group.getLeftChild();
|
||||
BBoxObject rightChild = group.getRightChild();
|
||||
if (shouldBeSwapped(leftChild, rightChild)) {
|
||||
// swap
|
||||
group.setLeftChild(rightChild);
|
||||
group.setRightChild(leftChild);
|
||||
}
|
||||
|
||||
if (leftChild instanceof BBoxZoneGroup) // if the child is a tree node, then recurse
|
||||
{
|
||||
sortGroupedZones((BBoxZoneGroup) leftChild);
|
||||
}
|
||||
if (rightChild instanceof BBoxZoneGroup) // as above - recurse
|
||||
{
|
||||
sortGroupedZones((BBoxZoneGroup) rightChild);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean shouldBeSwapped(BBoxObject first, BBoxObject second) {
|
||||
|
||||
double cx, cy, cw, ch, ox, oy, ow, oh;
|
||||
cx = first.getbBox().getX();
|
||||
cy = first.getbBox().getY();
|
||||
cw = first.getbBox().getWidth();
|
||||
ch = first.getbBox().getHeight();
|
||||
|
||||
ox = second.getbBox().getX();
|
||||
oy = second.getbBox().getY();
|
||||
ow = second.getbBox().getWidth();
|
||||
oh = second.getbBox().getHeight();
|
||||
|
||||
// Determine Octant
|
||||
//
|
||||
// 0 | 1 | 2
|
||||
// __|___|__
|
||||
// 7 | 9 | 3 First is placed in 9th square
|
||||
// __|___|__
|
||||
// 6 | 5 | 4
|
||||
|
||||
if (cx + cw <= ox) { //2,3,4
|
||||
return false;
|
||||
} else if (ox + ow <= cx) { //0,6,7
|
||||
return true; //6
|
||||
} else if (cy + ch <= oy) {
|
||||
return false; //5
|
||||
} else if (oy + oh <= cy) {
|
||||
return true; //1
|
||||
} else { //two zones
|
||||
double xdiff = ox + ow / 2 - cx - cw / 2;
|
||||
double ydiff = oy + oh / 2 - cy - ch / 2;
|
||||
return xdiff + ydiff < 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A distance function between two TextBoxes.
|
||||
* <p>
|
||||
* Consider the bounding rectangle for obj1 and obj2. Return its area minus the areas of obj1 and obj2, shown as
|
||||
* 'www' below. This value may be negative. (x0,y0) +------+..........+ | obj1 |wwwwwwwwww: +------+www+------+
|
||||
* :wwwwwwwwww| obj2 | +..........+------+ (x1,y1)
|
||||
*
|
||||
* @return distance value based on objects' coordinates and physical size on a plane
|
||||
*/
|
||||
private double distance(BBoxObject obj1, BBoxObject obj2) {
|
||||
|
||||
double x0 = Math.min(obj1.getX(), obj2.getX());
|
||||
double y0 = Math.min(obj1.getY(), obj2.getY());
|
||||
double x1 = Math.max(obj1.getX() + obj1.getWidth(), obj2.getX() + obj2.getWidth());
|
||||
double y1 = Math.max(obj1.getY() + obj1.getHeight(), obj2.getY() + obj2.getHeight());
|
||||
double dist = ((x1 - x0) * (y1 - y0) - obj1.getArea() - obj2.getArea());
|
||||
|
||||
double obj1X = obj1.getX();
|
||||
double obj1CenterX = obj1.getX() + obj1.getWidth() / 2;
|
||||
double obj1CenterY = obj1.getY() + obj1.getHeight() / 2;
|
||||
double obj2X = obj2.getX();
|
||||
double obj2CenterX = obj2.getX() + obj2.getWidth() / 2;
|
||||
double obj2CenterY = obj2.getY() + obj2.getHeight() / 2;
|
||||
|
||||
double obj1obj2VectorCosineAbsLeft = Math.abs((obj2X - obj1X) / Math.sqrt((obj2X - obj1X) * (obj2X - obj1X) + (obj2CenterY - obj1CenterY) * (obj2CenterY - obj1CenterY)));
|
||||
double obj1obj2VectorCosineAbsCenter = Math.abs((obj2CenterX - obj1CenterX) / Math.sqrt((obj2CenterX - obj1CenterX) * (obj2CenterX - obj1CenterX) + (obj2CenterY - obj1CenterY) * (obj2CenterY - obj1CenterY)));
|
||||
|
||||
double cosine = Math.min(obj1obj2VectorCosineAbsLeft, obj1obj2VectorCosineAbsCenter);
|
||||
|
||||
final double MAGIC_COEFF = 0.5;
|
||||
return dist * (MAGIC_COEFF + cosine);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model;
|
||||
|
||||
import java.util.AbstractSet;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
|
||||
public class DisjointSets<E> implements Iterable<Set<E>> {
|
||||
|
||||
private final Map<E, Entry<E>> map = new HashMap<E, Entry<E>>();
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new set of singletons.
|
||||
*
|
||||
* @param c elements of singleton sets
|
||||
*/
|
||||
public DisjointSets(Collection<? extends E> c) {
|
||||
|
||||
for (E element : c) {
|
||||
map.put(element, new Entry<E>(element));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if elements are in the same subsets.
|
||||
*
|
||||
* @param e1 element from a subset
|
||||
* @param e2 element from a subset
|
||||
* @return true if elements are in the same subset; false otherwise
|
||||
*/
|
||||
public boolean areTogether(E e1, E e2) {
|
||||
|
||||
return map.get(e1).findRepresentative() == map.get(e2).findRepresentative();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Merges subsets which elements e1 and e2 belong to.
|
||||
*
|
||||
* @param e1 element from a subset
|
||||
* @param e2 element from a subset
|
||||
*/
|
||||
public void union(E e1, E e2) {
|
||||
|
||||
Entry<E> r1 = map.get(e1).findRepresentative();
|
||||
Entry<E> r2 = map.get(e2).findRepresentative();
|
||||
if (r1 != r2) {
|
||||
if (r1.size <= r2.size) {
|
||||
r2.mergeWith(r1);
|
||||
} else {
|
||||
r1.mergeWith(r2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterator<Set<E>> iterator() {
|
||||
|
||||
return new Iterator<Set<E>>() {
|
||||
|
||||
private final Iterator<Entry<E>> iterator = map.values().iterator();
|
||||
private Entry<E> nextRepresentative;
|
||||
|
||||
{
|
||||
findNextRepresentative();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
|
||||
return nextRepresentative != null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Set<E> next() {
|
||||
|
||||
if (nextRepresentative == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
Set<E> result = nextRepresentative.asSet();
|
||||
findNextRepresentative();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private void findNextRepresentative() {
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Entry<E> candidate = iterator.next();
|
||||
if (candidate.isRepresentative()) {
|
||||
nextRepresentative = candidate;
|
||||
return;
|
||||
}
|
||||
}
|
||||
nextRepresentative = null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static class Entry<E> {
|
||||
|
||||
private int size = 1;
|
||||
private final E value;
|
||||
private Entry<E> parent = this;
|
||||
private Entry<E> next = null;
|
||||
private Entry<E> last = this;
|
||||
|
||||
|
||||
Entry(E value) {
|
||||
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
void mergeWith(Entry<E> otherRepresentative) {
|
||||
|
||||
size += otherRepresentative.size;
|
||||
last.next = otherRepresentative;
|
||||
last = otherRepresentative.last;
|
||||
otherRepresentative.parent = this;
|
||||
}
|
||||
|
||||
|
||||
Entry<E> findRepresentative() {
|
||||
|
||||
Entry<E> representative = parent;
|
||||
while (representative.parent != representative) {
|
||||
representative = representative.parent;
|
||||
}
|
||||
for (Entry<E> entry = this; entry != representative; ) {
|
||||
Entry<E> nextEntry = entry.parent;
|
||||
entry.parent = representative;
|
||||
entry = nextEntry;
|
||||
}
|
||||
return representative;
|
||||
}
|
||||
|
||||
|
||||
boolean isRepresentative() {
|
||||
|
||||
return parent == this;
|
||||
}
|
||||
|
||||
|
||||
Set<E> asSet() {
|
||||
|
||||
return new AbstractSet<E>() {
|
||||
|
||||
@Override
|
||||
public Iterator<E> iterator() {
|
||||
|
||||
return new Iterator<E>() {
|
||||
|
||||
private Entry<E> nextEntry = findRepresentative();
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
|
||||
return nextEntry != null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public E next() {
|
||||
|
||||
if (nextEntry == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
E result = nextEntry.value;
|
||||
nextEntry = nextEntry.next;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
|
||||
return findRepresentative().size;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,199 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public class Histogram implements Iterable<Histogram.Bin> {
|
||||
|
||||
private static final double EPSILON = 1.0e-6;
|
||||
|
||||
private final double min;
|
||||
private final double delta;
|
||||
private final double resolution;
|
||||
|
||||
private double[] frequencies;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new histogram for values in range [minValue, maxValue] with
|
||||
* given resolution.
|
||||
*
|
||||
* @param minValue - minimum allowed value
|
||||
* @param maxValue - maximum allowed value
|
||||
* @param resolution - histogram's resolution
|
||||
*/
|
||||
public Histogram(double minValue, double maxValue, double resolution) {
|
||||
|
||||
this.min = minValue - EPSILON;
|
||||
this.delta = maxValue - minValue + 2 * EPSILON;
|
||||
int size = Math.max(1, (int) Math.round((maxValue - minValue) / resolution));
|
||||
this.resolution = this.delta / size;
|
||||
this.frequencies = new double[size];
|
||||
}
|
||||
|
||||
|
||||
public void kernelSmooth(double[] kernel) {
|
||||
|
||||
double[] newFrequencies = new double[frequencies.length];
|
||||
int shift = (kernel.length - 1) / 2;
|
||||
for (int i = 0; i < kernel.length; i++) {
|
||||
int jStart = Math.max(0, i - shift);
|
||||
int jEnd = Math.min(frequencies.length, frequencies.length + i - shift);
|
||||
for (int j = jStart; j < jEnd; j++) {
|
||||
newFrequencies[j - i + shift] += kernel[i] * frequencies[j];
|
||||
}
|
||||
}
|
||||
frequencies = newFrequencies;
|
||||
}
|
||||
|
||||
|
||||
public void circularKernelSmooth(double[] kernel) {
|
||||
|
||||
double[] newFrequencies = new double[frequencies.length];
|
||||
int shift = (kernel.length - 1) / 2;
|
||||
for (int i = 0; i < frequencies.length; i++) {
|
||||
for (int d = 0; d < kernel.length; d++) {
|
||||
int j = i + d - shift;
|
||||
if (j < 0) {
|
||||
j += frequencies.length;
|
||||
} else if (j >= frequencies.length) {
|
||||
j -= frequencies.length;
|
||||
}
|
||||
newFrequencies[i] += kernel[d] * frequencies[j];
|
||||
}
|
||||
}
|
||||
frequencies = newFrequencies;
|
||||
}
|
||||
|
||||
|
||||
public double[] createGaussianKernel(double length, double stdDeviation) {
|
||||
|
||||
int r = (int) Math.round(length / resolution) / 2;
|
||||
stdDeviation /= resolution;
|
||||
|
||||
int size = 2 * r + 1;
|
||||
double[] kernel = new double[size];
|
||||
double sum = 0;
|
||||
double b = 2 * stdDeviation * stdDeviation;
|
||||
double a = 1 / Math.sqrt(Math.PI * b);
|
||||
for (int i = 0; i < size; i++) {
|
||||
kernel[i] = a * Math.exp(-(i - r) * (i - r) / b);
|
||||
sum += kernel[i];
|
||||
}
|
||||
for (int i = 0; i < size; i++) {
|
||||
kernel[i] /= sum;
|
||||
}
|
||||
return kernel;
|
||||
}
|
||||
|
||||
|
||||
public void circularGaussianSmooth(double windowLength, double stdDeviation) {
|
||||
|
||||
circularKernelSmooth(createGaussianKernel(windowLength, stdDeviation));
|
||||
}
|
||||
|
||||
|
||||
public void gaussianSmooth(double windowLength, double stdDeviation) {
|
||||
|
||||
kernelSmooth(createGaussianKernel(windowLength, stdDeviation));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds single occurrence of given value to the histogram.
|
||||
*
|
||||
* @param value inserted values
|
||||
*/
|
||||
public void add(double value) {
|
||||
|
||||
frequencies[(int) ((value - min) / resolution)] += 1.0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns histogram's number of bins.
|
||||
*
|
||||
* @return number of bins
|
||||
*/
|
||||
public int getSize() {
|
||||
|
||||
return frequencies.length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the histogram's peak value.
|
||||
*
|
||||
* @return peak value
|
||||
*/
|
||||
public double getPeakValue() {
|
||||
|
||||
int peakIndex = 0;
|
||||
for (int i = 1; i < frequencies.length; i++) {
|
||||
if (frequencies[i] > frequencies[peakIndex]) {
|
||||
peakIndex = i;
|
||||
}
|
||||
}
|
||||
int peakEndIndex = peakIndex + 1;
|
||||
final double EPS = 0.0001;
|
||||
while (peakEndIndex < frequencies.length && Math.abs(frequencies[peakEndIndex] - frequencies[peakIndex]) < EPS) {
|
||||
peakEndIndex++;
|
||||
}
|
||||
return ((double) peakIndex + peakEndIndex) / 2 * resolution + min;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterator<Bin> iterator() {
|
||||
|
||||
return new Iterator() {
|
||||
|
||||
private int index = 0;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
|
||||
return index < frequencies.length;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object next() {
|
||||
|
||||
if (index >= frequencies.length) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return new Bin(index++);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public final class Bin {
|
||||
|
||||
private final int index;
|
||||
|
||||
|
||||
private Bin(int index) {
|
||||
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
|
||||
public double getValue() {
|
||||
|
||||
return (index + 0.5) * resolution + min;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor;
|
||||
|
||||
public abstract class BBoxObject {
|
||||
|
||||
private BoundingBox bBox;
|
||||
|
||||
|
||||
public double getArea() {
|
||||
|
||||
return (bBox.getHeight() * bBox.getWidth());
|
||||
}
|
||||
|
||||
|
||||
public BoundingBox getbBox() {
|
||||
|
||||
return bBox;
|
||||
}
|
||||
|
||||
|
||||
public void setbBox(BoundingBox bBox) {
|
||||
|
||||
this.bBox = bBox;
|
||||
}
|
||||
|
||||
|
||||
public double getX() {
|
||||
|
||||
return bBox.getX();
|
||||
}
|
||||
|
||||
|
||||
public double getY() {
|
||||
|
||||
return bBox.getY();
|
||||
}
|
||||
|
||||
|
||||
public double getWidth() {
|
||||
|
||||
return bBox.getWidth();
|
||||
}
|
||||
|
||||
|
||||
public double getHeight() {
|
||||
|
||||
return bBox.getHeight();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public final class BoundingBox {
|
||||
|
||||
private final double x;
|
||||
private final double y;
|
||||
private final double width;
|
||||
private final double height;
|
||||
|
||||
|
||||
public boolean contains(BoundingBox contained, double tolerance) {
|
||||
|
||||
return x <= contained.getX() + tolerance && y <= contained.getY() + tolerance && x + width >= contained.getX() + contained.getWidth() - tolerance && y + height >= contained.getY() + contained.getHeight() - tolerance;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class Line extends BBoxObject {
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private List<Word> words = new ArrayList<>();
|
||||
|
||||
|
||||
public void addWord(Word word) {
|
||||
|
||||
this.words.add(word);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class Word extends BBoxObject {
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private List<RedTextPosition> textPositions = new ArrayList<>();
|
||||
|
||||
|
||||
public void addChunk(RedTextPosition chunk) {
|
||||
|
||||
this.textPositions.add(chunk);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class Zone extends BBoxObject {
|
||||
|
||||
private List<Line> lines = new ArrayList<>();
|
||||
|
||||
|
||||
public void addLine(Line line) {
|
||||
|
||||
this.lines.add(line);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.docstrum;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
|
||||
@Data
|
||||
public class Character {
|
||||
|
||||
private final double x;
|
||||
private final double y;
|
||||
private final RedTextPosition chunk;
|
||||
|
||||
private List<Neighbor> neighbors;
|
||||
|
||||
|
||||
public Character(RedTextPosition chunk) {
|
||||
|
||||
this.x = chunk.getXDirAdj() + chunk.getWidthDirAdj() / 2;
|
||||
this.y = chunk.getYDirAdj() + chunk.getHeightDir() / 2;
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
|
||||
public double getHeight() {
|
||||
|
||||
return chunk.getHeightDir();
|
||||
}
|
||||
|
||||
|
||||
public double distance(Character character) {
|
||||
|
||||
double dx = getX() - character.getX();
|
||||
double dy = getY() - character.getY();
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
|
||||
public double horizontalDistance(Character character) {
|
||||
|
||||
return Math.abs(getX() - character.getX());
|
||||
}
|
||||
|
||||
|
||||
public double verticalDistance(Character character) {
|
||||
|
||||
return Math.abs(getY() - character.getY());
|
||||
}
|
||||
|
||||
|
||||
public void setNeighbors(List<Neighbor> neighbors) {
|
||||
|
||||
this.neighbors = neighbors;
|
||||
}
|
||||
|
||||
|
||||
public double angle(Character character) {
|
||||
|
||||
if (getX() > character.getX()) {
|
||||
return Math.atan2(getY() - character.getY(), getX() - character.getX());
|
||||
} else {
|
||||
return Math.atan2(character.getY() - getY(), character.getX() - getX());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public double overlappingDistance(Character other, double orientation) {
|
||||
|
||||
double[] xs = new double[4];
|
||||
double s = Math.sin(-orientation), c = Math.cos(-orientation);
|
||||
xs[0] = c * x - s * y;
|
||||
xs[1] = c * (x + chunk.getWidthDirAdj()) - s * (y + chunk.getHeightDir());
|
||||
xs[2] = c * other.x - s * other.y;
|
||||
xs[3] = c * (other.x + other.chunk.getWidthDirAdj()) - s * (other.y + other.chunk.getHeightDir());
|
||||
boolean overlapping = xs[1] >= xs[2] && xs[3] >= xs[0];
|
||||
Arrays.sort(xs);
|
||||
return Math.abs(xs[2] - xs[1]) * (overlapping ? 1 : -1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component comparator based on x coordinate of the centroid of component.
|
||||
* <p>
|
||||
* The ordering is not consistent with equals.
|
||||
*/
|
||||
public static final class CharacterXComparator implements Comparator<Character> {
|
||||
|
||||
private CharacterXComparator() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compare(Character o1, Character o2) {
|
||||
|
||||
return Double.compare(o1.getX(), o2.getX());
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
private static final CharacterXComparator instance = new CharacterXComparator();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.docstrum;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class Neighbor {
|
||||
|
||||
@Getter
|
||||
private final double distance;
|
||||
@Getter
|
||||
private final double angle;
|
||||
private final Character originCharacter;
|
||||
@Getter
|
||||
private final Character character;
|
||||
|
||||
|
||||
public Neighbor(Character neighbor, Character origin) {
|
||||
|
||||
this.distance = neighbor.distance(origin);
|
||||
this.angle = neighbor.angle(origin);
|
||||
this.character = neighbor;
|
||||
this.originCharacter = origin;
|
||||
}
|
||||
|
||||
|
||||
public double getHorizontalDistance() {
|
||||
|
||||
return character.horizontalDistance(originCharacter);
|
||||
}
|
||||
|
||||
|
||||
public double getVerticalDistance() {
|
||||
|
||||
return character.verticalDistance(originCharacter);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.utils;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Line;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Word;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Zone;
|
||||
|
||||
public class BoundingBoxBuilder {
|
||||
|
||||
private double minX = Double.POSITIVE_INFINITY;
|
||||
private double minY = Double.POSITIVE_INFINITY;
|
||||
private double maxX = Double.NEGATIVE_INFINITY;
|
||||
private double maxY = Double.NEGATIVE_INFINITY;
|
||||
|
||||
|
||||
public void expandByLines(Zone zone) {
|
||||
|
||||
for (Line line : zone.getLines()) {
|
||||
expand(line.getbBox());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void expandByWords(Line line) {
|
||||
|
||||
for (Word word : line.getWords()) {
|
||||
expand(word.getbBox());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void expandByChunks(Word word) {
|
||||
|
||||
for (RedTextPosition chunk : word.getTextPositions()) {
|
||||
expand(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void expand(BoundingBox bounds) {
|
||||
|
||||
if (bounds != null) {
|
||||
minX = Math.min(minX, bounds.getX());
|
||||
minY = Math.min(minY, bounds.getY());
|
||||
maxX = Math.max(maxX, bounds.getX() + bounds.getWidth());
|
||||
maxY = Math.max(maxY, bounds.getY() + bounds.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void expand(RedTextPosition bounds) {
|
||||
|
||||
if (bounds != null) {
|
||||
minX = Math.min(minX, bounds.getXDirAdj());
|
||||
minY = Math.min(minY, bounds.getYDirAdj());
|
||||
maxX = Math.max(maxX, bounds.getXDirAdj() + bounds.getWidthDirAdj());
|
||||
maxY = Math.max(maxY, bounds.getYDirAdj() + bounds.getHeightDir());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public BoundingBox getBounds() {
|
||||
|
||||
if (minX <= maxX && minY <= maxY) {
|
||||
return new BoundingBox(minX, minY, maxX - minX, maxY - minY);
|
||||
} else {
|
||||
return new BoundingBox(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void setBounds(Zone zone) {
|
||||
|
||||
BoundingBoxBuilder builder = new BoundingBoxBuilder();
|
||||
builder.expandByLines(zone);
|
||||
zone.setbBox(builder.getBounds());
|
||||
}
|
||||
|
||||
|
||||
public static void setBounds(Line line) {
|
||||
|
||||
BoundingBoxBuilder builder = new BoundingBoxBuilder();
|
||||
builder.expandByWords(line);
|
||||
line.setbBox(builder.getBounds());
|
||||
}
|
||||
|
||||
|
||||
public static void setBounds(Word word) {
|
||||
|
||||
BoundingBoxBuilder builder = new BoundingBoxBuilder();
|
||||
builder.expandByChunks(word);
|
||||
word.setbBox(builder.getBounds());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.utils;
|
||||
|
||||
public class DoubleUtils {
|
||||
|
||||
public static int compareDouble(double d1, double d2, double precision) {
|
||||
|
||||
if (Double.isNaN(d1) || Double.isNaN(d2)) {
|
||||
return Double.compare(d1, d2);
|
||||
}
|
||||
if (precision == 0) {
|
||||
precision = 1;
|
||||
}
|
||||
long i1 = Math.round(d1 / precision);
|
||||
long i2 = Math.round(d2 / precision);
|
||||
return Long.valueOf(i1).compareTo(i2);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.utils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Zone;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class ZoneUtils {
|
||||
|
||||
public void sortZonesYX(List<Zone> zones) {
|
||||
|
||||
sortZonesYX(zones, 0);
|
||||
}
|
||||
|
||||
|
||||
public void sortZonesYX(List<Zone> zones, final double tolerance) {
|
||||
|
||||
Collections.sort(zones, new Comparator<Zone>() {
|
||||
|
||||
@Override
|
||||
public int compare(Zone o1, Zone o2) {
|
||||
|
||||
int cmp = DoubleUtils.compareDouble(o1.getbBox().getY(), o2.getbBox().getY(), tolerance);
|
||||
if (cmp == 0) {
|
||||
return DoubleUtils.compareDouble(o1.getbBox().getX(), o2.getbBox().getX(), tolerance);
|
||||
}
|
||||
return cmp;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.BBoxObject;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.BoundingBox;
|
||||
|
||||
public class BBoxZoneGroup extends BBoxObject {
|
||||
|
||||
private BBoxObject leftChild;
|
||||
private BBoxObject rightChild;
|
||||
|
||||
|
||||
public BBoxZoneGroup(BBoxObject child1, BBoxObject child2) {
|
||||
|
||||
this.leftChild = child1;
|
||||
this.rightChild = child2;
|
||||
setBounds(Math.min(child1.getX(), child2.getX()),
|
||||
Math.min(child1.getY(), child2.getY()),
|
||||
Math.max(child1.getX() + child1.getWidth(), child2.getX() + child2.getWidth()),
|
||||
Math.max(child1.getY() + child1.getHeight(), child2.getY() + child2.getHeight()));
|
||||
}
|
||||
|
||||
|
||||
public void setbBox(BoundingBox bBox) {
|
||||
|
||||
super.setbBox(bBox);
|
||||
}
|
||||
|
||||
|
||||
public BBoxObject getLeftChild() {
|
||||
|
||||
return leftChild;
|
||||
}
|
||||
|
||||
|
||||
public BBoxObject getRightChild() {
|
||||
|
||||
return rightChild;
|
||||
}
|
||||
|
||||
|
||||
public BBoxZoneGroup setLeftChild(BBoxObject obj) {
|
||||
|
||||
this.leftChild = obj;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public BBoxZoneGroup setRightChild(BBoxObject obj) {
|
||||
|
||||
this.rightChild = obj;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public BBoxZoneGroup setBounds(double x0, double y0, double x1, double y1) {
|
||||
|
||||
assert x1 >= x0;
|
||||
assert y1 >= y0;
|
||||
this.setbBox(new BoundingBox(x0, y0, x1 - x0, y1 - y0));
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.utils.DoubleUtils;
|
||||
|
||||
public class DistElem<E> implements Comparable<DistElem<E>> {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (c ? 1231 : 1237);
|
||||
long temp;
|
||||
temp = Double.doubleToLongBits(dist);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
result = prime * result + ((obj1 == null) ? 0 : obj1.hashCode());
|
||||
result = prime * result + ((obj2 == null) ? 0 : obj2.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DistElem other = (DistElem) obj;
|
||||
if (c != other.c) {
|
||||
return false;
|
||||
}
|
||||
if (Double.doubleToLongBits(dist) != Double.doubleToLongBits(other.dist)) {
|
||||
return false;
|
||||
}
|
||||
if (obj1 == null) {
|
||||
if (other.obj1 != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!obj1.equals(other.obj1)) {
|
||||
return false;
|
||||
}
|
||||
if (obj2 == null) {
|
||||
if (other.obj2 != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!obj2.equals(other.obj2)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
boolean c;
|
||||
double dist;
|
||||
E obj1;
|
||||
E obj2;
|
||||
|
||||
|
||||
public boolean isC() {
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
public void setC(boolean c) {
|
||||
|
||||
this.c = c;
|
||||
}
|
||||
|
||||
|
||||
public double getDist() {
|
||||
|
||||
return dist;
|
||||
}
|
||||
|
||||
|
||||
public E getObj1() {
|
||||
|
||||
return obj1;
|
||||
}
|
||||
|
||||
|
||||
public E getObj2() {
|
||||
|
||||
return obj2;
|
||||
}
|
||||
|
||||
|
||||
public DistElem(boolean c, double dist, E obj1, E obj2) {
|
||||
|
||||
this.c = c;
|
||||
this.dist = dist;
|
||||
this.obj1 = obj1;
|
||||
this.obj2 = obj2;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(DistElem<E> compareObject) {
|
||||
|
||||
double eps = 1E-3;
|
||||
if (c == compareObject.c) {
|
||||
return DoubleUtils.compareDouble(dist, compareObject.dist, eps);
|
||||
} else {
|
||||
return c ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,280 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.BBoxObject;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Zone;
|
||||
|
||||
/**
|
||||
* A set-like data structure for objects placed on a plane. Can efficiently find objects in a certain rectangular area.
|
||||
* It maintains two parallel lists of objects, each of which is sorted by its x or y coordinate.
|
||||
*
|
||||
* @author Pawel Szostek
|
||||
*/
|
||||
public class DocumentPlane {
|
||||
|
||||
/**
|
||||
* List of objects on the plane. Stored in a random order
|
||||
*/
|
||||
private final List<BBoxObject> objs;
|
||||
/**
|
||||
* Size of a grid square. If gridSize=50, then the plane is divided into squares of size 50. Each square contains
|
||||
* objects placed in a 50x50 area
|
||||
*/
|
||||
private final int gridSize;
|
||||
/**
|
||||
* Redundant dictionary of objects on the plane. Allows efficient 2D space search. Keys are X-Y coordinates of a
|
||||
* grid square. Single object can be stored under several keys (depending on its physical size). Grid squares are
|
||||
* lazy-initialized.
|
||||
*/
|
||||
private final Map<GridXY, List<BBoxObject>> grid;
|
||||
|
||||
/**
|
||||
* Representation of XY coordinates
|
||||
*/
|
||||
private static class GridXY {
|
||||
|
||||
public int x;
|
||||
public int y;
|
||||
|
||||
|
||||
public GridXY(int x, int y) {
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
return x * y;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
GridXY comparedObj = (GridXY) obj;
|
||||
return x == comparedObj.x && y == comparedObj.y;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "(" + x + "," + y + ")";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public List<BBoxObject> getObjects() {
|
||||
|
||||
return objs;
|
||||
}
|
||||
|
||||
|
||||
public DocumentPlane(List<Zone> objectList, int gridSize) {
|
||||
|
||||
this.grid = new HashMap<GridXY, List<BBoxObject>>();
|
||||
this.objs = new ArrayList<BBoxObject>();
|
||||
this.gridSize = gridSize;
|
||||
for (Zone obj : objectList) {
|
||||
add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Looks for objects placed between obj1 and obj2 excluding them
|
||||
*
|
||||
* @param obj1 object
|
||||
* @param obj2 object
|
||||
* @return object list
|
||||
*/
|
||||
public List<BBoxObject> findObjectsBetween(BBoxObject obj1, BBoxObject obj2) {
|
||||
|
||||
double x0 = Math.min(obj1.getX(), obj2.getX());
|
||||
double y0 = Math.min(obj1.getY(), obj2.getY());
|
||||
double x1 = Math.max(obj1.getX() + obj1.getWidth(), obj2.getX() + obj2.getWidth());
|
||||
double y1 = Math.max(obj1.getY() + obj1.getHeight(), obj2.getY() + obj2.getHeight());
|
||||
assert x1 >= x0 && y1 >= y0;
|
||||
BoundingBox searchBounds = new BoundingBox(x0, y0, x1 - x0, y1 - y0);
|
||||
List<BBoxObject> objsBetween = find(searchBounds);
|
||||
/*
|
||||
* the rectangle area must contain at least obj1 and obj2
|
||||
*/
|
||||
objsBetween.remove(obj1);
|
||||
objsBetween.remove(obj2);
|
||||
return objsBetween;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if there is any object placed between obj1 and obj2
|
||||
*
|
||||
* @param obj1 object
|
||||
* @param obj2 object
|
||||
* @return true if anything is placed between, false otherwise
|
||||
*/
|
||||
public boolean anyObjectsBetween(BBoxObject obj1, BBoxObject obj2) {
|
||||
|
||||
List<BBoxObject> lObjs = findObjectsBetween(obj1, obj2);
|
||||
return !(lObjs.isEmpty());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds object to the plane
|
||||
*
|
||||
* @param obj object
|
||||
* @return document plane
|
||||
*/
|
||||
public DocumentPlane add(BBoxObject obj) {
|
||||
|
||||
int objsBefore = this.objs.size();
|
||||
/*
|
||||
* iterate over grid squares
|
||||
*/
|
||||
for (int y = ((int) obj.getY()) / gridSize; y <= ((int) (obj.getY() + obj.getHeight() + gridSize - 1)) / gridSize; ++y) {
|
||||
for (int x = ((int) obj.getX()) / gridSize; x <= ((int) (obj.getX() + obj.getWidth() + gridSize - 1)) / gridSize; ++x) {
|
||||
GridXY xy = new GridXY(x, y);
|
||||
if (!grid.keySet().contains(xy)) {
|
||||
/*
|
||||
* add the non-existing key
|
||||
*/
|
||||
grid.put(xy, new ArrayList<BBoxObject>());
|
||||
grid.get(xy).add(obj);
|
||||
assert grid.get(xy).size() == 1;
|
||||
} else {
|
||||
grid.get(xy).add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
objs.add(obj);
|
||||
/*
|
||||
* size of the object list should be incremented
|
||||
*/
|
||||
assert objsBefore + 1 == objs.size();
|
||||
/*
|
||||
* object list must contain the same number of objects as object dictionary
|
||||
*/
|
||||
assert objs.size() == elementsInGrid();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public DocumentPlane remove(BBoxObject obj) {
|
||||
/*
|
||||
* iterate over grid squares
|
||||
*/
|
||||
for (int y = ((int) obj.getY()) / gridSize; y <= ((int) (obj.getY() + obj.getHeight() + gridSize - 1)) / gridSize; ++y) {
|
||||
for (int x = ((int) obj.getX()) / gridSize; x <= ((int) (obj.getX() + obj.getWidth() + gridSize - 1)) / gridSize; ++x) {
|
||||
GridXY xy = new GridXY(x, y);
|
||||
if (grid.get(xy).contains(obj)) {
|
||||
grid.get(xy).remove(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
objs.remove(obj);
|
||||
assert objs.size() == elementsInGrid();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find objects within search bounds
|
||||
*
|
||||
* @param searchBounds is a search rectangle
|
||||
* @return list of objects in!side search rectangle
|
||||
*/
|
||||
public List<BBoxObject> find(BoundingBox searchBounds) {
|
||||
|
||||
List<BBoxObject> done = new ArrayList<BBoxObject>(); //contains already considered objects (wrt. optimization)
|
||||
List<BBoxObject> ret = new ArrayList<BBoxObject>();
|
||||
double x0 = searchBounds.getX();
|
||||
double y0 = searchBounds.getY();
|
||||
double y1 = searchBounds.getY() + searchBounds.getHeight();
|
||||
double x1 = searchBounds.getX() + searchBounds.getWidth();
|
||||
/*
|
||||
* iterate over grid squares
|
||||
*/
|
||||
for (int y = (int) y0 / gridSize; y < ((int) (y1 + gridSize - 1)) / gridSize; ++y) {
|
||||
for (int x = (int) x0 / gridSize; x < ((int) (x1 + gridSize - 1)) / gridSize; ++x) {
|
||||
GridXY xy = new GridXY(x, y);
|
||||
if (!grid.containsKey(xy)) {
|
||||
continue;
|
||||
}
|
||||
for (BBoxObject obj : grid.get(xy)) {
|
||||
if (done.contains(obj)) /*
|
||||
* omit if already checked
|
||||
*/ {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* add to the checked objects
|
||||
*/
|
||||
done.add(obj);
|
||||
/*
|
||||
* check if two objects overlap
|
||||
*/
|
||||
if (obj.getX() + obj.getWidth() <= x0 || x1 <= obj.getX() || obj.getY() + obj.getHeight() <= y0 || y1 <= obj.getY()) {
|
||||
continue;
|
||||
}
|
||||
ret.add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Count objects stored in objects dictionary
|
||||
*
|
||||
* @return number of elements
|
||||
*/
|
||||
protected int elementsInGrid() {
|
||||
|
||||
List<BBoxObject> objs_ = new ArrayList<BBoxObject>();
|
||||
for (GridXY coord : grid.keySet()) {
|
||||
for (BBoxObject obj : grid.get(coord)) {
|
||||
if (!objs_.contains(obj)) {
|
||||
objs_.add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
return objs_.size();
|
||||
}
|
||||
|
||||
|
||||
public String dump() {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (GridXY iter : grid.keySet()) {
|
||||
sb.append(iter.toString()).append(" [");
|
||||
for (BBoxObject obj : grid.get(iter)) {
|
||||
if (obj instanceof BBoxZoneGroup) {
|
||||
BBoxZoneGroup group = (BBoxZoneGroup) obj;
|
||||
sb.append(group.getLeftChild());
|
||||
sb.append(group.getRightChild());
|
||||
} else if (obj instanceof Zone) {
|
||||
Zone zone = (Zone) obj;
|
||||
sb.append(zone);
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append("]\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.docstrum.readingorder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.docstrum.model.refactor.Zone;
|
||||
|
||||
/**
|
||||
* @author Pawel Szostek
|
||||
*/
|
||||
public class TreeToListConverter {
|
||||
|
||||
public List<Zone> convertToList(BBoxZoneGroup obj) {
|
||||
|
||||
List<Zone> ret = new ArrayList<Zone>();
|
||||
if (obj.getLeftChild() instanceof Zone) {
|
||||
Zone zone = (Zone) obj.getLeftChild();
|
||||
ret.add(zone);
|
||||
} else { // obj.getLeftChild() instanceof BxZoneGroup
|
||||
ret.addAll(convertToList((BBoxZoneGroup) obj.getLeftChild()));
|
||||
}
|
||||
|
||||
if (obj.getRightChild() instanceof Zone) {
|
||||
Zone zone = (Zone) obj.getRightChild();
|
||||
ret.add(zone);
|
||||
} else { // obj.getRightChild() instanceof BxZoneGroup
|
||||
ret.addAll(convertToList((BBoxZoneGroup) obj.getRightChild()));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
@ -25,7 +25,9 @@ public class LayoutparserEnd2EndTest extends AbstractTest {
|
||||
@SneakyThrows
|
||||
public void testLayoutParserEndToEnd() {
|
||||
|
||||
prepareStorage("files/bdr/Wie weiter bei Kristeneinrichtungen.pdf");
|
||||
String s = "(";
|
||||
String s1 = ")";
|
||||
prepareStorage("files/Minimal Examples/WrongOrderPage1.pdf");
|
||||
LayoutParsingRequest layoutParsingRequest = buildDefaultLayoutParsingRequest(LayoutParsingType.REDACT_MANAGER);
|
||||
LayoutParsingFinishedEvent finishedEvent = layoutParsingPipeline.parseLayoutAndSaveFilesToStorage(layoutParsingRequest);
|
||||
Arrays.stream(finishedEvent.message().split("\n")).forEach(log::info);
|
||||
|
||||
@ -25,7 +25,7 @@ public class ViewerDocumentTest extends BuildDocumentTest {
|
||||
@SneakyThrows
|
||||
public void testViewerDocument() {
|
||||
|
||||
String fileName = "files/S-Metolachlor_RAR_01_Volume_1_2018-09-06.pdf";
|
||||
String fileName = "files/new/agb1.pdf";
|
||||
String tmpFileName = "/tmp/" + Path.of(fileName).getFileName() + "_VIEWER.pdf";
|
||||
|
||||
var documentFile = new ClassPathResource(fileName).getFile();
|
||||
@ -35,9 +35,10 @@ public class ViewerDocumentTest extends BuildDocumentTest {
|
||||
Document document = buildGraph(fileName, LayoutParsingType.REDACT_MANAGER);
|
||||
long start = System.currentTimeMillis();
|
||||
layoutGridService.addLayoutGrid(documentFile, document, new File(tmpFileName), true);
|
||||
System.out.printf("Total time: %.2fs%n", ((float) (System.currentTimeMillis() - start)) / 1000);
|
||||
System.out.printf("Total time: %.2fs%n", ((float) (System.currentTimeMillis() - start)) / 1000);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
@SneakyThrows
|
||||
@ -51,7 +52,11 @@ public class ViewerDocumentTest extends BuildDocumentTest {
|
||||
var tableResponse = mapper.readValue(new ClassPathResource(tableFileName).getInputStream(), TableServiceResponse.class);
|
||||
var documentFile = new ClassPathResource(fileName).getFile();
|
||||
|
||||
var classificationDocument = layoutParsingPipeline.parseLayout(LayoutParsingType.DOCUMINE, documentFile, new ImageServiceResponse(), tableResponse, Path.of(fileName).getFileName().toFile().toString());
|
||||
var classificationDocument = layoutParsingPipeline.parseLayout(LayoutParsingType.DOCUMINE,
|
||||
documentFile,
|
||||
new ImageServiceResponse(),
|
||||
tableResponse,
|
||||
Path.of(fileName).getFileName().toFile().toString());
|
||||
ViewerDocumentService viewerDocumentService = new ViewerDocumentService(null);
|
||||
LayoutGridService layoutGridService = new LayoutGridService(viewerDocumentService);
|
||||
Document document = DocumentGraphFactory.buildDocumentGraph(classificationDocument);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user