Coverage Report

Created: 2026-06-13 06:51

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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(&center, &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