View Javadoc

1   /**
2    * Copyright (c) 2011, The University of Southampton and the individual contributors.
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without modification,
6    * are permitted provided that the following conditions are met:
7    *
8    *   * 	Redistributions of source code must retain the above copyright notice,
9    * 	this list of conditions and the following disclaimer.
10   *
11   *   *	Redistributions in binary form must reproduce the above copyright notice,
12   * 	this list of conditions and the following disclaimer in the documentation
13   * 	and/or other materials provided with the distribution.
14   *
15   *   *	Neither the name of the University of Southampton nor the names of its
16   * 	contributors may be used to endorse or promote products derived from this
17   * 	software without specific prior written permission.
18   *
19   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package org.openimaj.image.text.extraction.swt;
31  
32  import java.util.ArrayList;
33  import java.util.Comparator;
34  import java.util.HashSet;
35  import java.util.List;
36  import java.util.Set;
37  
38  import org.openimaj.image.pixel.Pixel;
39  import org.openimaj.math.geometry.shape.Rectangle;
40  import org.openimaj.util.pair.Pair;
41  import org.openimaj.util.set.DisjointSetForest;
42  
43  /**
44   * This class models a candidate line of text, with one of more word candidates
45   * within it, from the {@link SWTTextDetector}.
46   * 
47   * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
48   * 
49   */
50  public class LineCandidate extends Candidate {
51  	protected List<LetterCandidate> letters = new ArrayList<LetterCandidate>();
52  	protected List<WordCandidate> words;
53  
54  	protected LineCandidate() {
55  	}
56  
57  	/**
58  	 * Computes lines of text by merging pairs of characters that have similar
59  	 * directions.
60  	 * 
61  	 * @param letters
62  	 * @param options
63  	 * @return
64  	 */
65  	protected static List<LineCandidate> extractLines(List<LetterCandidate> letters, SWTTextDetector.Options options) {
66  		final List<Pair<LetterCandidate>> pairs = createLetterPairs(letters, options);
67  
68  		final Set<Set<Pair<LetterCandidate>>> sets = DisjointSetForest.partitionSubsets(pairs,
69  				new Comparator<Pair<LetterCandidate>>() {
70  					@Override
71  					public int compare(Pair<LetterCandidate> pair1, Pair<LetterCandidate> pair2) {
72  						final Pixel pair1d = computeDelta(pair1.firstObject(), pair1.secondObject());
73  						final Pixel pair2d = computeDelta(pair2.firstObject(), pair2.secondObject());
74  
75  						if (pair1.firstObject() == pair2.firstObject() || pair1.secondObject() == pair2.secondObject())
76  						{
77  							final int tn = pair1d.y * pair2d.x - pair1d.x * pair2d.y;
78  							final int td = pair1d.x * pair2d.x + pair1d.y * pair2d.y;
79  							// share the same end, opposite direction
80  							if (tn * 7 < -td * 4 && tn * 7 > td * 4)
81  								return 0;
82  						} else if (pair1.firstObject() == pair2.secondObject()
83  								|| pair1.secondObject() == pair2.firstObject())
84  						{
85  							final int tn = pair1d.y * pair2d.x - pair1d.x * pair2d.y;
86  							final int td = pair1d.x * pair2d.x + pair1d.y * pair2d.y;
87  							// share the other end, same direction
88  							if (tn * 7 < td * 4 && tn * 7 > -td * 4)
89  								return 0;
90  						}
91  
92  						return 1;
93  					}
94  
95  					private Pixel computeDelta(LetterCandidate firstObject, LetterCandidate secondObject) {
96  						final Rectangle frect = firstObject.regularBoundingBox;
97  						final Rectangle srect = secondObject.regularBoundingBox;
98  
99  						final int dx = (int) (frect.x - srect.x + (frect.width - srect.width) / 2);
100 						final int dy = (int) (frect.y - srect.y + (frect.height - srect.height) / 2);
101 						return new Pixel(dx, dy);
102 					}
103 				});
104 
105 		final List<LineCandidate> chains = new ArrayList<LineCandidate>();
106 		for (final Set<Pair<LetterCandidate>> line : sets) {
107 			final Set<LetterCandidate> lcs = new HashSet<LetterCandidate>();
108 
109 			for (final Pair<LetterCandidate> p : line) {
110 				lcs.add(p.firstObject());
111 				lcs.add(p.secondObject());
112 			}
113 
114 			if (lcs.size() < options.minLettersPerLine)
115 				continue;
116 
117 			final LineCandidate lc = new LineCandidate();
118 			lc.letters = new ArrayList<LetterCandidate>(lcs);
119 
120 			// set the line
121 			for (final LetterCandidate letter : lc.letters)
122 				letter.line = lc;
123 
124 			lc.regularBoundingBox = LetterCandidate.computeBounds(lc.letters);
125 			lc.words = WordCandidate.extractWords(lc, options);
126 
127 			chains.add(lc);
128 		}
129 
130 		return chains;
131 	}
132 
133 	/**
134 	 * Compute all likely pairs of letters on the basis that they close
135 	 * together, have similar stroke widths & similar heights.
136 	 * 
137 	 * @param letters
138 	 *            the candidate letters
139 	 * @param options
140 	 *            the options
141 	 * @return a list of potentially valid pairs
142 	 */
143 	private static List<Pair<LetterCandidate>> createLetterPairs(List<LetterCandidate> letters,
144 			SWTTextDetector.Options options)
145 	{
146 		final List<Pair<LetterCandidate>> pairs = new ArrayList<Pair<LetterCandidate>>();
147 
148 		final int numLetters = letters.size();
149 
150 		for (int j = 0; j < numLetters; j++) {
151 			final LetterCandidate l1 = letters.get(j);
152 
153 			for (int i = j + 1; i < numLetters; i++) {
154 				final LetterCandidate l2 = letters.get(i);
155 
156 				// similar stroke width (median ratio < 2)
157 				if (Math.max(l1.medianStrokeWidth, l2.medianStrokeWidth)
158 						/ Math.min(l1.medianStrokeWidth, l2.medianStrokeWidth) > options.medianStrokeWidthRatio)
159 					continue;
160 
161 				// similar height
162 				if (Math.max(l1.regularBoundingBox.height, l2.regularBoundingBox.height)
163 						/ Math.min(l1.regularBoundingBox.height, l2.regularBoundingBox.height) > options.letterHeightRatio)
164 					continue;
165 
166 				// similar color (technically intensity)
167 				if (Math.abs(l1.averageBrightness - l2.averageBrightness) > options.intensityThreshold)
168 					continue;
169 
170 				// small distance between
171 				final double distance = l1.centroid.x - l2.centroid.x;
172 				if (Math.abs(distance) > options.widthMultiplier
173 						* Math.max(l1.regularBoundingBox.width, l2.regularBoundingBox.width))
174 					continue;
175 
176 				// approximately level
177 				// FIXME: vertical text...
178 				final int oy = (int) (Math.min(l1.regularBoundingBox.y +
179 						l1.regularBoundingBox.height,
180 						l2.regularBoundingBox.y + l2.regularBoundingBox.height) -
181 						Math.max(l1.regularBoundingBox.y,
182 								l2.regularBoundingBox.y));
183 				if (oy * options.intersectRatio < Math.min(l1.regularBoundingBox.height,
184 						l2.regularBoundingBox.height))
185 					continue;
186 
187 				// tests passed... merge
188 				pairs.add(new Pair<LetterCandidate>(l1, l2));
189 			}
190 		}
191 
192 		return pairs;
193 	}
194 
195 	/**
196 	 * Get the letters corresponding to this line.
197 	 * 
198 	 * @return the letters
199 	 */
200 	public List<LetterCandidate> getLetters() {
201 		return this.letters;
202 	}
203 
204 	/**
205 	 * Get the words corresponding to this line
206 	 * 
207 	 * @return the words
208 	 */
209 	public List<WordCandidate> getWords() {
210 		return words;
211 	}
212 }