com.joliciel.jochre.boundaries.RecursiveShapeSplitter.java Source code

Java tutorial

Introduction

Here is the source code for com.joliciel.jochre.boundaries.RecursiveShapeSplitter.java

Source

///////////////////////////////////////////////////////////////////////////////
//Copyright (C) 2012 Assaf Urieli
//
//This file is part of Jochre.
//
//Jochre is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//Jochre is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU Affero General Public License for more details.
//
//You should have received a copy of the GNU Affero General Public License
//along with Jochre.  If not, see <http://www.gnu.org/licenses/>.
//////////////////////////////////////////////////////////////////////////////
package com.joliciel.jochre.boundaries;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.joliciel.jochre.boundaries.features.SplitFeature;
import com.joliciel.jochre.graphics.Shape;
import com.joliciel.talismane.machineLearning.Decision;
import com.joliciel.talismane.machineLearning.DecisionMaker;
import com.joliciel.talismane.machineLearning.features.FeatureResult;
import com.joliciel.talismane.machineLearning.features.FeatureService;
import com.joliciel.talismane.machineLearning.features.RuntimeEnvironment;
import com.joliciel.talismane.utils.PerformanceMonitor;
import com.joliciel.talismane.utils.WeightedOutcome;

/**
 * Splits a shape using a DecisionMaker.
 * Makes it possible to split a shape multiple times, by applying the split algorithm recursively,
 * up to a pre-defined depth, to the sub-shapes resulting from each split.
 * <br/>In the recursive case, the scoring of the various sequences needs to satisfy the following intuition:
 * <br/>Shape A has a 50% probability of being split into A1 and A2.
 * <br/>Shape A1 has a 50% probability of being split into A11 and A12.
 * <br/>Shape A2 has a 50% probability of being split into A21 and A22.
 * <br/>This results in 5 possible sequences:
 * <ol>
 * <li>A</li>
 * <li>A1-A2</li>
 * <li>A11-A12-A2</li>
 * <li>A1-A21-A22</li>
 * <li>A11-A12-A21-A22</li>
 * </ol>
 * We want the five sequences to be returned as being equi-probable.
 * If we simply multiply the probabilities of all the splits applied, we get: 1=50%, 2=25%, 3=12.5%, 4=12.5%, 5=6.25%.
 * Instead, we do the following: at each level of recursion, we set the highest probability option to 1, and set all of the
 * other probabilities proportionally to the highest.
 * This will give equiprobable sequences in the above case, and gives the intuitive response in other cases.
 * @author Assaf Urieli
 *
 */
class RecursiveShapeSplitter implements ShapeSplitter {
    private static final Log LOG = LogFactory.getLog(RecursiveShapeSplitter.class);
    private static final PerformanceMonitor MONITOR = PerformanceMonitor.getMonitor(JochreSplitEventStream.class);

    private SplitCandidateFinder splitCandidateFinder;
    private BoundaryServiceInternal boundaryServiceInternal;
    private FeatureService featureService;

    private Set<SplitFeature<?>> splitFeatures = null;
    private DecisionMaker<SplitOutcome> decisionMaker;
    private BoundaryDecisionFactory boundaryDecisionFactory = new BoundaryDecisionFactory();

    double minWidthRatio = 1.1;
    int beamWidth = 5;
    int maxDepth = 3;

    public RecursiveShapeSplitter(SplitCandidateFinder splitCandidateFinder, Set<SplitFeature<?>> splitFeatures,
            DecisionMaker<SplitOutcome> decisionMaker) {
        super();
        this.splitCandidateFinder = splitCandidateFinder;
        this.splitFeatures = splitFeatures;
        this.decisionMaker = decisionMaker;
    }

    @Override
    public List<ShapeSequence> split(Shape shape) {
        LOG.trace("Splitting shape: " + shape);
        boolean leftToRight = shape.getJochreImage().isLeftToRight();
        List<ShapeSequence> shapeSequences = this.split(shape, 0, shape, leftToRight);
        int i = 0;
        for (ShapeSequence shapeSequence : shapeSequences) {
            LOG.debug("Sequence" + i + ", score=" + shapeSequence.getScore());
            for (ShapeInSequence splitGuess : shapeSequence) {
                LOG.debug(splitGuess.getShape());
            }
            i++;
        }
        return shapeSequences;
    }

    List<ShapeSequence> split(Shape shape, int depth, Shape originalShape, boolean leftToRight) {
        String padding = "-";
        for (int i = 0; i < depth; i++)
            padding += "-";
        padding += " ";
        LOG.trace(padding + "Splitting shape: " + shape.getLeft() + " , " + shape.getRight());
        LOG.trace(padding + "depth: " + depth);
        List<ShapeSequence> shapeSequences = new ArrayList<ShapeSequence>();
        // check if shape is wide enough to bother with
        double widthRatio = (double) shape.getWidth() / (double) shape.getXHeight();
        LOG.trace(padding + "widthRatio: " + widthRatio);

        if (widthRatio < minWidthRatio || depth >= maxDepth) {
            LOG.trace(padding + "too narrow or too deep");
            ShapeSequence shapeSequence = this.boundaryServiceInternal.getEmptyShapeSequence();
            shapeSequence.addShape(shape, originalShape);
            shapeSequences.add(shapeSequence);
        } else {
            List<Split> splitCandidates = this.splitCandidateFinder.findSplitCandidates(shape);
            TreeSet<ShapeSequence> myShapeSequences = new TreeSet<ShapeSequence>();

            TreeSet<WeightedOutcome<Split>> weightedSplits = new TreeSet<WeightedOutcome<Split>>();
            for (Split splitCandidate : splitCandidates) {
                double splitProb = this.shouldSplit(splitCandidate);
                WeightedOutcome<Split> weightedSplit = new WeightedOutcome<Split>(splitCandidate, splitProb);
                weightedSplits.add(weightedSplit);
            }

            double maxSplitProb = 0.0;
            if (weightedSplits.size() > 0)
                maxSplitProb = weightedSplits.first().getWeight();

            double noSplitProb = 1 - maxSplitProb;
            if (noSplitProb > maxSplitProb)
                maxSplitProb = noSplitProb;

            Split noSplit = boundaryServiceInternal.getEmptySplit(shape);
            noSplit.setPosition(-1);
            WeightedOutcome<Split> weightedNoSplit = new WeightedOutcome<Split>(noSplit, noSplitProb);
            weightedSplits.add(weightedNoSplit);

            boolean topCandidate = true;
            double topCandidateWeight = 1.0;
            for (WeightedOutcome<Split> weightedSplit : weightedSplits) {
                Split splitCandidate = weightedSplit.getOutcome();
                double splitProb = weightedSplit.getWeight();
                LOG.trace(padding + "splitCandidate: left=" + splitCandidate.getShape().getLeft() + ", pos="
                        + splitCandidate.getPosition() + ", initial prob: " + splitProb);

                if (topCandidate) {
                    LOG.trace(padding + "topCandidate");
                }
                if (splitCandidate.getPosition() < 0) {
                    // This is the no-split candidate
                    if (topCandidate)
                        topCandidateWeight = 1.0;

                    ShapeSequence shapeSequence = boundaryServiceInternal.getEmptyShapeSequence();
                    shapeSequence.addShape(shape, originalShape);
                    double prob = (splitProb / maxSplitProb) * topCandidateWeight;
                    LOG.trace(padding + "noSplit prob=(" + splitProb + " / " + maxSplitProb + ") * "
                            + topCandidateWeight + " = " + prob);

                    Decision<SplitMergeOutcome> decision = boundaryDecisionFactory
                            .createDecision(SplitOutcome.DO_NOT_SPLIT.getCode(), prob);
                    shapeSequence.addDecision(decision);
                    myShapeSequences.add(shapeSequence);
                } else {
                    // a proper split
                    Shape leftShape = shape.getJochreImage().getShape(shape.getLeft(), shape.getTop(),
                            shape.getLeft() + splitCandidate.getPosition(), shape.getBottom());
                    Shape rightShape = shape.getJochreImage().getShape(
                            shape.getLeft() + splitCandidate.getPosition() + 1, shape.getTop(), shape.getRight(),
                            shape.getBottom());

                    // for each split recursively try to split it again up to depth of m
                    // Note: m=2 is probably enough, since we're not expecting more than 4 letters per shape (3 splits)
                    List<ShapeSequence> leftShapeSequences = this.split(leftShape, depth + 1, originalShape,
                            leftToRight);
                    List<ShapeSequence> rightShapeSequences = this.split(rightShape, depth + 1, originalShape,
                            leftToRight);

                    if (topCandidate) {
                        // find the no-split sequence in each sub-sequence
                        ShapeSequence noSplitLeft = null;
                        for (ShapeSequence leftShapeSequence : leftShapeSequences) {
                            if (leftShapeSequence.size() == 1) {
                                noSplitLeft = leftShapeSequence;
                                break;
                            }
                        }

                        ShapeSequence noSplitRight = null;
                        for (ShapeSequence rightShapeSequence : rightShapeSequences) {
                            if (rightShapeSequence.size() == 1) {
                                noSplitRight = rightShapeSequence;
                                break;
                            }
                        }

                        // we should be guaranteed to find a noSplitLeft and noSplitRight
                        // since a no-split candidate is always returned
                        topCandidateWeight = noSplitLeft.getScore() * noSplitRight.getScore();
                        LOG.trace(padding + "topCandidateWeight=" + noSplitLeft.getScore() + " *"
                                + noSplitRight.getScore() + " = " + topCandidateWeight);
                    }

                    for (ShapeSequence leftShapeSequence : leftShapeSequences) {
                        for (ShapeSequence rightShapeSequence : rightShapeSequences) {
                            ShapeSequence newSequence = null;
                            if (leftToRight)
                                newSequence = boundaryServiceInternal.getShapeSequence(leftShapeSequence,
                                        rightShapeSequence);
                            else
                                newSequence = boundaryServiceInternal.getShapeSequence(rightShapeSequence,
                                        leftShapeSequence);
                            if (LOG.isTraceEnabled()) {
                                StringBuilder sb = new StringBuilder();
                                for (ShapeInSequence splitShape : newSequence) {
                                    sb.append("(" + splitShape.getShape().getLeft() + ","
                                            + splitShape.getShape().getRight() + ") ");
                                }
                                LOG.trace(padding + sb.toString());
                            }
                            double totalProb = 1.0;
                            for (Decision<SplitMergeOutcome> decision : newSequence.getDecisions()) {
                                totalProb = totalProb * decision.getProbability();
                            }
                            newSequence.getDecisions().clear();
                            double prob = 0.0;
                            if (topCandidate) {
                                prob = totalProb * (splitProb / maxSplitProb);
                                LOG.trace(padding + "prob=" + totalProb + " * (" + splitProb + " / " + maxSplitProb
                                        + ") = " + prob);
                            } else {
                                prob = totalProb * (splitProb / maxSplitProb) * topCandidateWeight;
                                LOG.trace(padding + "prob=" + totalProb + " * (" + splitProb + " / " + maxSplitProb
                                        + ") * " + topCandidateWeight + " = " + prob);
                            }

                            Decision<SplitMergeOutcome> decision = this.boundaryDecisionFactory
                                    .createDecision(SplitOutcome.DO_SPLIT.getCode(), prob);
                            newSequence.addDecision(decision);
                            myShapeSequences.add(newSequence);
                        }
                    }
                }

                topCandidate = false;
            }

            int i = 0;
            for (ShapeSequence shapeSequence : myShapeSequences) {
                // Note: we always return the no-split option, even it it's very low probability
                if (shapeSequence.size() == 1 || i < beamWidth) {
                    shapeSequences.add(shapeSequence);
                }
                i++;
            }
        }

        return shapeSequences;
    }

    public double shouldSplit(Split splitCandidate) {
        MONITOR.startTask("shouldSplit");
        try {

            List<FeatureResult<?>> featureResults = new ArrayList<FeatureResult<?>>();

            MONITOR.startTask("analyse features");
            try {
                for (SplitFeature<?> feature : splitFeatures) {
                    MONITOR.startTask(feature.getName());
                    try {
                        RuntimeEnvironment env = this.featureService.getRuntimeEnvironment();
                        FeatureResult<?> featureResult = feature.check(splitCandidate, env);
                        if (featureResult != null) {
                            featureResults.add(featureResult);
                            if (LOG.isTraceEnabled()) {
                                LOG.trace(featureResult.toString());
                            }
                        }
                    } finally {
                        MONITOR.endTask();
                    }
                }
            } finally {
                MONITOR.endTask();
            }

            List<Decision<SplitOutcome>> decisions = null;
            MONITOR.startTask("decision maker");
            try {
                decisions = decisionMaker.decide(featureResults);
            } finally {
                MONITOR.endTask();
            }

            double yesProb = 0.0;
            for (Decision<SplitOutcome> decision : decisions) {
                if (decision.getOutcome().equals(SplitOutcome.DO_SPLIT)) {
                    yesProb = decision.getProbability();
                    break;
                }
            }

            if (LOG.isTraceEnabled()) {
                LOG.trace("splitCandidate: left=" + splitCandidate.getShape().getLeft() + ", pos="
                        + splitCandidate.getPosition());
                LOG.trace("yesProb: " + yesProb);
            }

            return yesProb;
        } finally {
            MONITOR.endTask();
        }
    }

    public SplitCandidateFinder getSplitCandidateFinder() {
        return splitCandidateFinder;
    }

    public void setSplitCandidateFinder(SplitCandidateFinder splitCandidateFinder) {
        this.splitCandidateFinder = splitCandidateFinder;
    }

    public BoundaryServiceInternal getBoundaryServiceInternal() {
        return boundaryServiceInternal;
    }

    public void setBoundaryServiceInternal(BoundaryServiceInternal boundaryServiceInternal) {
        this.boundaryServiceInternal = boundaryServiceInternal;
    }

    /**
     * The minimum ratio between the shape's width and it's x-height
     * for the shape to even be considered for splitting.
     * @return
     */
    public double getMinWidthRatio() {
        return minWidthRatio;
    }

    public void setMinWidthRatio(double minWidthRatio) {
        this.minWidthRatio = minWidthRatio;
    }

    /**
     * The beam width indicating the maximum possible decisions to return for each
     * shape (applies recursively as well).
     * @return
     */
    public int getBeamWidth() {
        return beamWidth;
    }

    public void setBeamWidth(int beamWidth) {
        this.beamWidth = beamWidth;
    }

    /**
     * The maximum recursive depth to search for splits in a single shape.
     * The maximum number of splits = 2^maxDepth.
     * @return
     */
    public int getMaxDepth() {
        return maxDepth;
    }

    public void setMaxDepth(int maxDepth) {
        this.maxDepth = maxDepth;
    }

    public FeatureService getFeatureService() {
        return featureService;
    }

    public void setFeatureService(FeatureService featureService) {
        this.featureService = featureService;
    }

}