/src/tesseract/src/classify/intfx.cpp
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | ** Filename: intfx.c |
3 | | ** Purpose: Integer character normalization & feature extraction |
4 | | ** Author: Robert Moss, rays@google.com (Ray Smith) |
5 | | ** |
6 | | ** (c) Copyright Hewlett-Packard Company, 1988. |
7 | | ** Licensed under the Apache License, Version 2.0 (the "License"); |
8 | | ** you may not use this file except in compliance with the License. |
9 | | ** You may obtain a copy of the License at |
10 | | ** http://www.apache.org/licenses/LICENSE-2.0 |
11 | | ** Unless required by applicable law or agreed to in writing, software |
12 | | ** distributed under the License is distributed on an "AS IS" BASIS, |
13 | | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 | | ** See the License for the specific language governing permissions and |
15 | | ** limitations under the License. |
16 | | *****************************************************************************/ |
17 | | /**---------------------------------------------------------------------------- |
18 | | Include Files and Type Defines |
19 | | ----------------------------------------------------------------------------**/ |
20 | | |
21 | | #define _USE_MATH_DEFINES // for M_PI |
22 | | |
23 | | #include "intfx.h" |
24 | | |
25 | | #include "classify.h" |
26 | | #include "intmatcher.h" |
27 | | #include "linlsq.h" |
28 | | #include "normalis.h" |
29 | | #include "statistc.h" |
30 | | #include "trainingsample.h" |
31 | | |
32 | | #include "helpers.h" |
33 | | |
34 | | #include <allheaders.h> |
35 | | |
36 | | #include <cmath> // for M_PI |
37 | | #include <mutex> // for std::mutex |
38 | | |
39 | | namespace tesseract { |
40 | | |
41 | | /**---------------------------------------------------------------------------- |
42 | | Global Data Definitions and Declarations |
43 | | ----------------------------------------------------------------------------**/ |
44 | | // Look up table for cos and sin to turn the intfx feature angle to a vector. |
45 | | // Protected by atan_table_mutex. |
46 | | // The entries are in binary degrees where a full circle is 256 binary degrees. |
47 | | static float cos_table[INT_CHAR_NORM_RANGE]; |
48 | | static float sin_table[INT_CHAR_NORM_RANGE]; |
49 | | |
50 | | /**---------------------------------------------------------------------------- |
51 | | Public Code |
52 | | ----------------------------------------------------------------------------**/ |
53 | | |
54 | 4 | void InitIntegerFX() { |
55 | | // Guards write access to AtanTable so we don't create it more than once. |
56 | 4 | static std::mutex atan_table_mutex; |
57 | 4 | static bool atan_table_init = false; |
58 | 4 | std::lock_guard<std::mutex> guard(atan_table_mutex); |
59 | 4 | if (!atan_table_init) { |
60 | 1.02k | for (int i = 0; i < INT_CHAR_NORM_RANGE; ++i) { |
61 | 1.02k | cos_table[i] = cos(i * 2 * M_PI / INT_CHAR_NORM_RANGE + M_PI); |
62 | 1.02k | sin_table[i] = sin(i * 2 * M_PI / INT_CHAR_NORM_RANGE + M_PI); |
63 | 1.02k | } |
64 | 4 | atan_table_init = true; |
65 | 4 | } |
66 | 4 | } |
67 | | |
68 | | // Returns a vector representing the direction of a feature with the given |
69 | | // theta direction in an INT_FEATURE_STRUCT. |
70 | 0 | FCOORD FeatureDirection(uint8_t theta) { |
71 | 0 | return FCOORD(cos_table[theta], sin_table[theta]); |
72 | 0 | } |
73 | | |
74 | | // Generates a TrainingSample from a TBLOB. Extracts features and sets |
75 | | // the bounding box, so classifiers that operate on the image can work. |
76 | | // TODO(rays) Make BlobToTrainingSample a member of Classify now that |
77 | | // the FlexFx and FeatureDescription code have been removed and LearnBlob |
78 | | // is now a member of Classify. |
79 | | TrainingSample *BlobToTrainingSample(const TBLOB &blob, bool nonlinear_norm, |
80 | | INT_FX_RESULT_STRUCT *fx_info, |
81 | 1.79M | std::vector<INT_FEATURE_STRUCT> *bl_features) { |
82 | 1.79M | std::vector<INT_FEATURE_STRUCT> cn_features; |
83 | 1.79M | Classify::ExtractFeatures(blob, nonlinear_norm, bl_features, &cn_features, fx_info, nullptr); |
84 | | // TODO(rays) Use blob->PreciseBoundingBox() instead. |
85 | 1.79M | TBOX box = blob.bounding_box(); |
86 | 1.79M | TrainingSample *sample = nullptr; |
87 | 1.79M | int num_features = fx_info->NumCN; |
88 | 1.79M | if (num_features > 0) { |
89 | 1.79M | sample = TrainingSample::CopyFromFeatures(*fx_info, box, &cn_features[0], num_features); |
90 | 1.79M | } |
91 | 1.79M | if (sample != nullptr) { |
92 | | // Set the bounding box (in original image coordinates) in the sample. |
93 | 1.79M | TPOINT topleft, botright; |
94 | 1.79M | topleft.x = box.left(); |
95 | 1.79M | topleft.y = box.top(); |
96 | 1.79M | botright.x = box.right(); |
97 | 1.79M | botright.y = box.bottom(); |
98 | 1.79M | TPOINT original_topleft, original_botright; |
99 | 1.79M | blob.denorm().DenormTransform(nullptr, topleft, &original_topleft); |
100 | 1.79M | blob.denorm().DenormTransform(nullptr, botright, &original_botright); |
101 | 1.79M | sample->set_bounding_box( |
102 | 1.79M | TBOX(original_topleft.x, original_botright.y, original_botright.x, original_topleft.y)); |
103 | 1.79M | } |
104 | 1.79M | return sample; |
105 | 1.79M | } |
106 | | |
107 | | // Computes the DENORMS for bl(baseline) and cn(character) normalization |
108 | | // during feature extraction. The input denorm describes the current state |
109 | | // of the blob, which is usually a baseline-normalized word. |
110 | | // The Transforms setup are as follows: |
111 | | // Baseline Normalized (bl) Output: |
112 | | // We center the grapheme by aligning the x-coordinate of its centroid with |
113 | | // x=128 and leaving the already-baseline-normalized y as-is. |
114 | | // |
115 | | // Character Normalized (cn) Output: |
116 | | // We align the grapheme's centroid at the origin and scale it |
117 | | // asymmetrically in x and y so that the 2nd moments are a standard value |
118 | | // (51.2) ie the result is vaguely square. |
119 | | // If classify_nonlinear_norm is true: |
120 | | // A non-linear normalization is setup that attempts to evenly distribute |
121 | | // edges across x and y. |
122 | | // |
123 | | // Some of the fields of fx_info are also setup: |
124 | | // Length: Total length of outline. |
125 | | // Rx: Rounded y second moment. (Reversed by convention.) |
126 | | // Ry: rounded x second moment. |
127 | | // Xmean: Rounded x center of mass of the blob. |
128 | | // Ymean: Rounded y center of mass of the blob. |
129 | | void Classify::SetupBLCNDenorms(const TBLOB &blob, bool nonlinear_norm, DENORM *bl_denorm, |
130 | 1.79M | DENORM *cn_denorm, INT_FX_RESULT_STRUCT *fx_info) { |
131 | | // Compute 1st and 2nd moments of the original outline. |
132 | 1.79M | FCOORD center, second_moments; |
133 | 1.79M | int length = blob.ComputeMoments(¢er, &second_moments); |
134 | 1.79M | if (fx_info != nullptr) { |
135 | 1.79M | fx_info->Length = length; |
136 | 1.79M | fx_info->Rx = IntCastRounded(second_moments.y()); |
137 | 1.79M | fx_info->Ry = IntCastRounded(second_moments.x()); |
138 | | |
139 | 1.79M | fx_info->Xmean = IntCastRounded(center.x()); |
140 | 1.79M | fx_info->Ymean = IntCastRounded(center.y()); |
141 | 1.79M | } |
142 | | // Setup the denorm for Baseline normalization. |
143 | 1.79M | bl_denorm->SetupNormalization(nullptr, nullptr, &blob.denorm(), center.x(), 128.0f, 1.0f, 1.0f, |
144 | 1.79M | 128.0f, 128.0f); |
145 | | // Setup the denorm for character normalization. |
146 | 1.79M | if (nonlinear_norm) { |
147 | 0 | std::vector<std::vector<int>> x_coords; |
148 | 0 | std::vector<std::vector<int>> y_coords; |
149 | 0 | TBOX box; |
150 | 0 | blob.GetPreciseBoundingBox(&box); |
151 | 0 | box.pad(1, 1); |
152 | 0 | blob.GetEdgeCoords(box, x_coords, y_coords); |
153 | 0 | cn_denorm->SetupNonLinear(&blob.denorm(), box, UINT8_MAX, UINT8_MAX, 0.0f, 0.0f, x_coords, |
154 | 0 | y_coords); |
155 | 1.79M | } else { |
156 | 1.79M | cn_denorm->SetupNormalization(nullptr, nullptr, &blob.denorm(), center.x(), center.y(), |
157 | 1.79M | 51.2f / second_moments.x(), 51.2f / second_moments.y(), 128.0f, |
158 | 1.79M | 128.0f); |
159 | 1.79M | } |
160 | 1.79M | } |
161 | | |
162 | | // Helper normalizes the direction, assuming that it is at the given |
163 | | // unnormed_pos, using the given denorm, starting at the root_denorm. |
164 | | static uint8_t NormalizeDirection(uint8_t dir, const FCOORD &unnormed_pos, const DENORM &denorm, |
165 | 0 | const DENORM *root_denorm) { |
166 | | // Convert direction to a vector. |
167 | 0 | FCOORD unnormed_end; |
168 | 0 | unnormed_end.from_direction(dir); |
169 | 0 | unnormed_end += unnormed_pos; |
170 | 0 | FCOORD normed_pos, normed_end; |
171 | 0 | denorm.NormTransform(root_denorm, unnormed_pos, &normed_pos); |
172 | 0 | denorm.NormTransform(root_denorm, unnormed_end, &normed_end); |
173 | 0 | normed_end -= normed_pos; |
174 | 0 | return normed_end.to_direction(); |
175 | 0 | } |
176 | | |
177 | | // Helper returns the mean direction vector from the given stats. Use the |
178 | | // mean direction from dirs if there is information available, otherwise, use |
179 | | // the fit_vector from point_diffs. |
180 | | static FCOORD MeanDirectionVector(const LLSQ &point_diffs, const LLSQ &dirs, const FCOORD &start_pt, |
181 | 0 | const FCOORD &end_pt) { |
182 | 0 | FCOORD fit_vector; |
183 | 0 | if (dirs.count() > 0) { |
184 | | // There were directions, so use them. To avoid wrap-around problems, we |
185 | | // have 2 accumulators in dirs: x for normal directions and y for |
186 | | // directions offset by 128. We will use the one with the least variance. |
187 | 0 | FCOORD mean_pt = dirs.mean_point(); |
188 | 0 | double mean_dir = 0.0; |
189 | 0 | if (dirs.x_variance() <= dirs.y_variance()) { |
190 | 0 | mean_dir = mean_pt.x(); |
191 | 0 | } else { |
192 | 0 | mean_dir = mean_pt.y() + 128; |
193 | 0 | } |
194 | 0 | fit_vector.from_direction(Modulo(IntCastRounded(mean_dir), 256)); |
195 | 0 | } else { |
196 | | // There were no directions, so we rely on the vector_fit to the points. |
197 | | // Since the vector_fit is 180 degrees ambiguous, we align with the |
198 | | // supplied feature_dir by making the scalar product non-negative. |
199 | 0 | FCOORD feature_dir(end_pt - start_pt); |
200 | 0 | fit_vector = point_diffs.vector_fit(); |
201 | 0 | if (fit_vector.x() == 0.0f && fit_vector.y() == 0.0f) { |
202 | | // There was only a single point. Use feature_dir directly. |
203 | 0 | fit_vector = feature_dir; |
204 | 0 | } else { |
205 | | // Sometimes the least mean squares fit is wrong, due to the small sample |
206 | | // of points and scaling. Use a 90 degree rotated vector if that matches |
207 | | // feature_dir better. |
208 | 0 | FCOORD fit_vector2 = !fit_vector; |
209 | | // The fit_vector is 180 degrees ambiguous, so resolve the ambiguity by |
210 | | // insisting that the scalar product with the feature_dir should be +ve. |
211 | 0 | if (fit_vector % feature_dir < 0.0) { |
212 | 0 | fit_vector = -fit_vector; |
213 | 0 | } |
214 | 0 | if (fit_vector2 % feature_dir < 0.0) { |
215 | 0 | fit_vector2 = -fit_vector2; |
216 | 0 | } |
217 | | // Even though fit_vector2 has a higher mean squared error, it might be |
218 | | // a better fit, so use it if the dot product with feature_dir is bigger. |
219 | 0 | if (fit_vector2 % feature_dir > fit_vector % feature_dir) { |
220 | 0 | fit_vector = fit_vector2; |
221 | 0 | } |
222 | 0 | } |
223 | 0 | } |
224 | 0 | return fit_vector; |
225 | 0 | } |
226 | | |
227 | | // Helper computes one or more features corresponding to the given points. |
228 | | // Emitted features are on the line defined by: |
229 | | // start_pt + lambda * (end_pt - start_pt) for scalar lambda. |
230 | | // Features are spaced at feature_length intervals. |
231 | | static int ComputeFeatures(const FCOORD &start_pt, const FCOORD &end_pt, double feature_length, |
232 | 49.3M | std::vector<INT_FEATURE_STRUCT> *features) { |
233 | 49.3M | FCOORD feature_vector(end_pt - start_pt); |
234 | 49.3M | if (feature_vector.x() == 0.0f && feature_vector.y() == 0.0f) { |
235 | 28.8k | return 0; |
236 | 28.8k | } |
237 | | // Compute theta for the feature based on its direction. |
238 | 49.3M | uint8_t theta = feature_vector.to_direction(); |
239 | | // Compute the number of features and lambda_step. |
240 | 49.3M | double target_length = feature_vector.length(); |
241 | 49.3M | int num_features = IntCastRounded(target_length / feature_length); |
242 | 49.3M | if (num_features == 0) { |
243 | 652k | return 0; |
244 | 652k | } |
245 | | // Divide the length evenly into num_features pieces. |
246 | 48.6M | double lambda_step = 1.0 / num_features; |
247 | 48.6M | double lambda = lambda_step / 2.0; |
248 | 261M | for (int f = 0; f < num_features; ++f, lambda += lambda_step) { |
249 | 213M | FCOORD feature_pt(start_pt); |
250 | 213M | feature_pt += feature_vector * lambda; |
251 | 213M | INT_FEATURE_STRUCT feature(feature_pt, theta); |
252 | 213M | features->push_back(feature); |
253 | 213M | } |
254 | 48.6M | return num_features; |
255 | 49.3M | } |
256 | | |
257 | | // Gathers outline points and their directions from start_index into dirs by |
258 | | // stepping along the outline and normalizing the coordinates until the |
259 | | // required feature_length has been collected or end_index is reached. |
260 | | // On input pos must point to the position corresponding to start_index and on |
261 | | // return pos is updated to the current raw position, and pos_normed is set to |
262 | | // the normed version of pos. |
263 | | // Since directions wrap-around, they need special treatment to get the mean. |
264 | | // Provided the cluster of directions doesn't straddle the wrap-around point, |
265 | | // the simple mean works. If they do, then, unless the directions are wildly |
266 | | // varying, the cluster rotated by 180 degrees will not straddle the wrap- |
267 | | // around point, so mean(dir + 180 degrees) - 180 degrees will work. Since |
268 | | // LLSQ conveniently stores the mean of 2 variables, we use it to store |
269 | | // dir and dir+128 (128 is 180 degrees) and then use the resulting mean |
270 | | // with the least variance. |
271 | | static int GatherPoints(const C_OUTLINE *outline, double feature_length, const DENORM &denorm, |
272 | | const DENORM *root_denorm, int start_index, int end_index, ICOORD *pos, |
273 | 0 | FCOORD *pos_normed, LLSQ *points, LLSQ *dirs) { |
274 | 0 | int step_length = outline->pathlength(); |
275 | 0 | ICOORD step = outline->step(start_index % step_length); |
276 | | // Prev_normed is the start point of this collection and will be set on the |
277 | | // first iteration, and on later iterations used to determine the length |
278 | | // that has been collected. |
279 | 0 | FCOORD prev_normed; |
280 | 0 | points->clear(); |
281 | 0 | dirs->clear(); |
282 | 0 | int num_points = 0; |
283 | 0 | int index; |
284 | 0 | for (index = start_index; index <= end_index; ++index, *pos += step) { |
285 | 0 | step = outline->step(index % step_length); |
286 | 0 | int edge_weight = outline->edge_strength_at_index(index % step_length); |
287 | 0 | if (edge_weight == 0) { |
288 | | // This point has conflicting gradient and step direction, so ignore it. |
289 | 0 | continue; |
290 | 0 | } |
291 | | // Get the sub-pixel precise location and normalize. |
292 | 0 | FCOORD f_pos = outline->sub_pixel_pos_at_index(*pos, index % step_length); |
293 | 0 | denorm.NormTransform(root_denorm, f_pos, pos_normed); |
294 | 0 | if (num_points == 0) { |
295 | | // The start of this segment. |
296 | 0 | prev_normed = *pos_normed; |
297 | 0 | } else { |
298 | 0 | FCOORD offset = *pos_normed - prev_normed; |
299 | 0 | float length = offset.length(); |
300 | 0 | if (length > feature_length) { |
301 | | // We have gone far enough from the start. We will use this point in |
302 | | // the next set so return what we have so far. |
303 | 0 | return index; |
304 | 0 | } |
305 | 0 | } |
306 | 0 | points->add(pos_normed->x(), pos_normed->y(), edge_weight); |
307 | 0 | int direction = outline->direction_at_index(index % step_length); |
308 | 0 | if (direction >= 0) { |
309 | 0 | direction = NormalizeDirection(direction, f_pos, denorm, root_denorm); |
310 | | // Use both the direction and direction +128 so we are not trying to |
311 | | // take the mean of something straddling the wrap-around point. |
312 | 0 | dirs->add(direction, Modulo(direction + 128, 256)); |
313 | 0 | } |
314 | 0 | ++num_points; |
315 | 0 | } |
316 | 0 | return index; |
317 | 0 | } |
318 | | |
319 | | // Extracts Tesseract features and appends them to the features vector. |
320 | | // Startpt to lastpt, inclusive, MUST have the same src_outline member, |
321 | | // which may be nullptr. The vector from lastpt to its next is included in |
322 | | // the feature extraction. Hidden edges should be excluded by the caller. |
323 | | // If force_poly is true, the features will be extracted from the polygonal |
324 | | // approximation even if more accurate data is available. |
325 | | static void ExtractFeaturesFromRun(const EDGEPT *startpt, const EDGEPT *lastpt, |
326 | | const DENORM &denorm, double feature_length, bool force_poly, |
327 | 10.7M | std::vector<INT_FEATURE_STRUCT> *features) { |
328 | 10.7M | const EDGEPT *endpt = lastpt->next; |
329 | 10.7M | const C_OUTLINE *outline = startpt->src_outline; |
330 | 10.7M | if (outline != nullptr && !force_poly) { |
331 | | // Detailed information is available. We have to normalize only from |
332 | | // the root_denorm to denorm. |
333 | 0 | const DENORM *root_denorm = denorm.RootDenorm(); |
334 | 0 | int total_features = 0; |
335 | | // Get the features from the outline. |
336 | 0 | int step_length = outline->pathlength(); |
337 | 0 | int start_index = startpt->start_step; |
338 | | // pos is the integer coordinates of the binary image steps. |
339 | 0 | ICOORD pos = outline->position_at_index(start_index); |
340 | | // We use an end_index that allows us to use a positive increment, but that |
341 | | // may be beyond the bounds of the outline steps/ due to wrap-around, to |
342 | | // so we use % step_length everywhere, except for start_index. |
343 | 0 | int end_index = lastpt->start_step + lastpt->step_count; |
344 | 0 | if (end_index <= start_index) { |
345 | 0 | end_index += step_length; |
346 | 0 | } |
347 | 0 | LLSQ prev_points; |
348 | 0 | LLSQ prev_dirs; |
349 | 0 | FCOORD prev_normed_pos = outline->sub_pixel_pos_at_index(pos, start_index); |
350 | 0 | denorm.NormTransform(root_denorm, prev_normed_pos, &prev_normed_pos); |
351 | 0 | LLSQ points; |
352 | 0 | LLSQ dirs; |
353 | 0 | FCOORD normed_pos(0.0f, 0.0f); |
354 | 0 | int index = GatherPoints(outline, feature_length, denorm, root_denorm, start_index, end_index, |
355 | 0 | &pos, &normed_pos, &points, &dirs); |
356 | 0 | while (index <= end_index) { |
357 | | // At each iteration we nominally have 3 accumulated sets of points and |
358 | | // dirs: prev_points/dirs, points/dirs, next_points/dirs and sum them |
359 | | // into sum_points/dirs, but we don't necessarily get any features out, |
360 | | // so if that is the case, we keep accumulating instead of rotating the |
361 | | // accumulators. |
362 | 0 | LLSQ next_points; |
363 | 0 | LLSQ next_dirs; |
364 | 0 | FCOORD next_normed_pos(0.0f, 0.0f); |
365 | 0 | index = GatherPoints(outline, feature_length, denorm, root_denorm, index, end_index, &pos, |
366 | 0 | &next_normed_pos, &next_points, &next_dirs); |
367 | 0 | LLSQ sum_points(prev_points); |
368 | | // TODO(rays) find out why it is better to use just dirs and next_dirs |
369 | | // in sum_dirs, instead of using prev_dirs as well. |
370 | 0 | LLSQ sum_dirs(dirs); |
371 | 0 | sum_points.add(points); |
372 | 0 | sum_points.add(next_points); |
373 | 0 | sum_dirs.add(next_dirs); |
374 | 0 | bool made_features = false; |
375 | | // If we have some points, we can try making some features. |
376 | 0 | if (sum_points.count() > 0) { |
377 | | // We have gone far enough from the start. Make a feature and restart. |
378 | 0 | FCOORD fit_pt = sum_points.mean_point(); |
379 | 0 | FCOORD fit_vector = MeanDirectionVector(sum_points, sum_dirs, prev_normed_pos, normed_pos); |
380 | | // The segment to which we fit features is the line passing through |
381 | | // fit_pt in direction of fit_vector that starts nearest to |
382 | | // prev_normed_pos and ends nearest to normed_pos. |
383 | 0 | FCOORD start_pos = prev_normed_pos.nearest_pt_on_line(fit_pt, fit_vector); |
384 | 0 | FCOORD end_pos = normed_pos.nearest_pt_on_line(fit_pt, fit_vector); |
385 | | // Possible correction to match the adjacent polygon segment. |
386 | 0 | if (total_features == 0 && startpt != endpt) { |
387 | 0 | FCOORD poly_pos(startpt->pos.x, startpt->pos.y); |
388 | 0 | denorm.LocalNormTransform(poly_pos, &start_pos); |
389 | 0 | } |
390 | 0 | if (index > end_index && startpt != endpt) { |
391 | 0 | FCOORD poly_pos(endpt->pos.x, endpt->pos.y); |
392 | 0 | denorm.LocalNormTransform(poly_pos, &end_pos); |
393 | 0 | } |
394 | 0 | int num_features = ComputeFeatures(start_pos, end_pos, feature_length, features); |
395 | 0 | if (num_features > 0) { |
396 | | // We made some features so shuffle the accumulators. |
397 | 0 | prev_points = points; |
398 | 0 | prev_dirs = dirs; |
399 | 0 | prev_normed_pos = normed_pos; |
400 | 0 | points = next_points; |
401 | 0 | dirs = next_dirs; |
402 | 0 | made_features = true; |
403 | 0 | total_features += num_features; |
404 | 0 | } |
405 | | // The end of the next set becomes the end next time around. |
406 | 0 | normed_pos = next_normed_pos; |
407 | 0 | } |
408 | 0 | if (!made_features) { |
409 | | // We didn't make any features, so keep the prev accumulators and |
410 | | // add the next ones into the current. |
411 | 0 | points.add(next_points); |
412 | 0 | dirs.add(next_dirs); |
413 | 0 | } |
414 | 0 | } |
415 | 10.7M | } else { |
416 | | // There is no outline, so we are forced to use the polygonal approximation. |
417 | 10.7M | const EDGEPT *pt = startpt; |
418 | 49.3M | do { |
419 | 49.3M | FCOORD start_pos(pt->pos.x, pt->pos.y); |
420 | 49.3M | FCOORD end_pos(pt->next->pos.x, pt->next->pos.y); |
421 | 49.3M | denorm.LocalNormTransform(start_pos, &start_pos); |
422 | 49.3M | denorm.LocalNormTransform(end_pos, &end_pos); |
423 | 49.3M | ComputeFeatures(start_pos, end_pos, feature_length, features); |
424 | 49.3M | } while ((pt = pt->next) != endpt); |
425 | 10.7M | } |
426 | 10.7M | } |
427 | | |
428 | | // Extracts sets of 3-D features of length kStandardFeatureLength (=12.8), as |
429 | | // (x,y) position and angle as measured counterclockwise from the vector |
430 | | // <-1, 0>, from blob using two normalizations defined by bl_denorm and |
431 | | // cn_denorm. See SetpuBLCNDenorms for definitions. |
432 | | // If outline_cn_counts is not nullptr, on return it contains the cumulative |
433 | | // number of cn features generated for each outline in the blob (in order). |
434 | | // Thus after the first outline, there were (*outline_cn_counts)[0] features, |
435 | | // after the second outline, there were (*outline_cn_counts)[1] features etc. |
436 | | void Classify::ExtractFeatures(const TBLOB &blob, bool nonlinear_norm, |
437 | | std::vector<INT_FEATURE_STRUCT> *bl_features, |
438 | | std::vector<INT_FEATURE_STRUCT> *cn_features, |
439 | | INT_FX_RESULT_STRUCT *results, |
440 | 1.79M | std::vector<int> *outline_cn_counts) { |
441 | 1.79M | DENORM bl_denorm, cn_denorm; |
442 | 1.79M | tesseract::Classify::SetupBLCNDenorms(blob, nonlinear_norm, &bl_denorm, &cn_denorm, results); |
443 | 1.79M | if (outline_cn_counts != nullptr) { |
444 | 0 | outline_cn_counts->clear(); |
445 | 0 | } |
446 | | // Iterate the outlines. |
447 | 6.99M | for (TESSLINE *ol = blob.outlines; ol != nullptr; ol = ol->next) { |
448 | | // Iterate the polygon. |
449 | 5.19M | EDGEPT *loop_pt = ol->FindBestStartPt(); |
450 | 5.19M | EDGEPT *pt = loop_pt; |
451 | 5.19M | if (pt == nullptr) { |
452 | 0 | continue; |
453 | 0 | } |
454 | 5.95M | do { |
455 | 5.95M | if (pt->IsHidden()) { |
456 | 578k | continue; |
457 | 578k | } |
458 | | // Find a run of equal src_outline. |
459 | 5.38M | EDGEPT *last_pt = pt; |
460 | 24.6M | do { |
461 | 24.6M | last_pt = last_pt->next; |
462 | 24.6M | } while (last_pt != loop_pt && !last_pt->IsHidden() && |
463 | 19.2M | last_pt->src_outline == pt->src_outline); |
464 | 5.38M | last_pt = last_pt->prev; |
465 | | // Until the adaptive classifier can be weaned off polygon segments, |
466 | | // we have to force extraction from the polygon for the bl_features. |
467 | 5.38M | ExtractFeaturesFromRun(pt, last_pt, bl_denorm, kStandardFeatureLength, true, bl_features); |
468 | 5.38M | ExtractFeaturesFromRun(pt, last_pt, cn_denorm, kStandardFeatureLength, false, cn_features); |
469 | 5.38M | pt = last_pt; |
470 | 5.95M | } while ((pt = pt->next) != loop_pt); |
471 | 5.19M | if (outline_cn_counts != nullptr) { |
472 | 0 | outline_cn_counts->push_back(cn_features->size()); |
473 | 0 | } |
474 | 5.19M | } |
475 | 1.79M | results->NumBL = bl_features->size(); |
476 | 1.79M | results->NumCN = cn_features->size(); |
477 | 1.79M | results->YBottom = blob.bounding_box().bottom(); |
478 | 1.79M | results->YTop = blob.bounding_box().top(); |
479 | 1.79M | results->Width = blob.bounding_box().width(); |
480 | 1.79M | } |
481 | | |
482 | | } // namespace tesseract |