/**********************************************************************
 * File:        fixxht.cpp  (Formerly fixxht.c)
 * Description: Improve x_ht and look out for case inconsistencies
 * Author:		Phil Cheatle
 * Created:		Thu Aug  5 14:11:08 BST 1993
 *
 * (C) Copyright 1992, Hewlett-Packard Ltd.
 ** Licensed under the Apache License, Version 2.0 (the "License");
 ** you may not use this file except in compliance with the License.
 ** You may obtain a copy of the License at
 ** http://www.apache.org/licenses/LICENSE-2.0
 ** Unless required by applicable law or agreed to in writing, software
 ** distributed under the License is distributed on an "AS IS" BASIS,
 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ** See the License for the specific language governing permissions and
 ** limitations under the License.
 *
 **********************************************************************/

#include "mfcpch.h"
#include          <string.h>
#include          <ctype.h>
#include          "varable.h"
#include          "tessvars.h"
#include          "control.h"
#include          "reject.h"
#include          "fixxht.h"
#include          "secname.h"

#define EXTERN

EXTERN double_VAR (x_ht_fraction_of_caps_ht, 0.7,
"Fract of cps ht est of xht");
EXTERN double_VAR (x_ht_variation, 0.35,
"Err band as fract of caps/xht dist");
EXTERN double_VAR (x_ht_sub_variation, 0.5,
"Err band as fract of caps/xht dist");
EXTERN BOOL_VAR (rej_trial_ambigs, TRUE,
"reject x-ht ambigs when under trial");
EXTERN BOOL_VAR (x_ht_conservative_ambigs, FALSE,
"Dont rely on ambigs + maxht");
EXTERN BOOL_VAR (x_ht_check_est, TRUE, "Cross check estimates");
EXTERN BOOL_VAR (x_ht_case_flip, FALSE, "Flip or reject suspect case");
EXTERN BOOL_VAR (x_ht_include_dodgy_blobs, TRUE,
"Include blobs with possible noise?");
EXTERN BOOL_VAR (x_ht_limit_flip_trials, TRUE,
"Dont do trial flips when ambigs are close to xht?");
EXTERN BOOL_VAR (rej_use_check_block_occ, TRUE,
"Analyse rejection behaviour");

EXTERN STRING_VAR (chs_non_ambig_caps_ht,
"!#$%&()/12346789?ABDEFGHIKLNQRT[]\\bdfhkl",
"Reliable ascenders");
EXTERN STRING_VAR (chs_x_ht, "acegmnopqrsuvwxyz", "X height chars");
EXTERN STRING_VAR (chs_non_ambig_x_ht, "aenqr", "reliable X height chars");
EXTERN STRING_VAR (chs_ambig_caps_x, "cCmMoO05sSuUvVwWxXzZ",
"X ht or caps ht chars");
EXTERN STRING_VAR (chs_bl_ambig_caps_x, "pPyY", " Caps or descender ambigs");

/* The following arent used in this module but are used in applybox.c */
EXTERN STRING_VAR (chs_caps_ht,
"!#$%&()/0123456789?ABCDEFGHIJKLMNOPQRSTUVWXYZ[]\\bdfhkl{|}",
"Ascender chars");
EXTERN STRING_VAR (chs_desc, "gjpqy", "Descender chars");
EXTERN STRING_VAR (chs_non_ambig_bl,
"!#$%&01246789?ABCDEFGHIKLMNORSTUVWXYZabcdehiklmnorstuvwxz",
"Reliable baseline chars");
EXTERN STRING_VAR (chs_odd_top, "ijt", "Chars with funny ascender region");
EXTERN STRING_VAR (chs_odd_bot, "()35JQ[]\\/{}|", "Chars with funny base");

/* The following arent used but are defined for completeness */
EXTERN STRING_VAR (chs_bl,
"!#$%&()/01246789?ABCDEFGHIJKLMNOPRSTUVWXYZ[]\\abcdefhiklmnorstuvwxz{}",
"Baseline chars");
EXTERN STRING_VAR (chs_non_ambig_desc, "gq", "Reliable descender chars");

/*************************************************************************
 * re_estimate_x_ht()
 *
 * Walk the blobs in the word together with the text string and reject map.
 * NOTE: All evaluation is done on the baseline normalised word. This is so that
 * the BOX class can be used (integer). The reasons for this are:
 *   a) We must use the outword - ie the Tess result
 *   b) The outword is always converted to integer representation as that is how
 *      Tess works
 *   c) We would like to use the BOX class, cos its there - this is integer
 *      precision.
 *   d) If we de-normed the outword we would get rounding errors and would find
 *      that integers are too imprecise (x-height around 15 pixels instead of a
 *      scale of 128 in bln form.
 *   CONVINCED?
 *
 * A) Try to re-estimatate x-ht and caps ht from confirmed pts in word.
 *
 *    FOR each non reject blob
 *       IF char is baseline posn ambiguous
 *			Remove ambiguity by comparing its posn with respect to baseline.
 *		IF char is a confirmed x-ht char
 *			Add x-ht posn to confirmed_x_ht pts for word
 *    IF char is a confirmed caps-ht char
 *			Add blob_ht to caps ht pts for word
 *
 *    IF Std Dev of caps hts < 2  (AND # samples > 0)
 *		Use mean as caps ht estimate (Dont use median as we can expect a
 *			fair variation between the heights of the NON_AMBIG_CAPS_HT_CHS)
 *    IF Std Dev of caps hts >= 2  (AND # samples > 0)
 *			Suspect small caps font.
 *			Look for 2 clusters,	each with Std Dev < 2.
 *			IF 2 clusters found
 *			Pick the smaller median as the caps ht estimate of the smallcaps.
 *
 *    IF failed to estimate a caps ht
 *       Use the median caps ht if there is one,
 *		ELSE use the caps ht estimate of the previous word. NO!!!
 *
 *
 *    IF there are confirmed x-height chars
 *			Estimate confirmed x-height as the median value
 *    ELSE IF there is a confirmed caps ht
 *			Estimate confirmed x-height as a fraction of confirmed caps ht value
 *		ELSE
 *			Use the value for the previous word or the row value if this is the
 *			first word in the block. NO!!!
 *
 * B) Add in case ambiguous blobs based on confirmed x-ht/caps ht, changing case
 *    as necessary. Reestimate caps ht and x-ht as in A, using the extended
 *    clusters.
 *
 * C) If word contains rejects, and x-ht estimate significantly differs from
 *    original estimate, return TRUE so that the word can be rematched
 *************************************************************************/

void re_estimate_x_ht(                     //improve for 1 word
                      WERD_RES *word_res,  //word to do
                      float *trial_x_ht    //new match value
                     ) {
  PBLOB_IT blob_it;
  INT16 blob_ht_above_baseline;

  const char *word_str;
  INT16 i;
  INT16 offset;

  STATS all_blobs_ht (0, 300);   //every blob in word
  STATS x_ht (0, 300);           //confirmed pts in wd
  STATS caps_ht (0, 300);        //confirmed pts in wd
  STATS case_ambig (0, 300);     //lower case ambigs

  INT16 rej_blobs_count = 0;
  INT16 rej_blobs_max_height = 0;
  INT32 rej_blobs_max_area = 0;
  float x_ht_ok_variation;
  float max_blob_ht;
  float marginally_above_x_ht;

  BOX blob_box;                  //blob bounding box
  float est_x_ht = 0.0;          //word estimate
  float est_caps_ht = 0.0;       //word estimate
                                 //based on hard data?
  BOOL8 est_caps_ht_certain = FALSE;
  BOOL8 est_x_ht_certain = FALSE;//based on hard data?
  BOOL8 trial = FALSE;           //Sepeculative values?
  BOOL8 no_comment = FALSE;      //No change in xht
  float ambig_lc_x_est;
  float ambig_uc_caps_est;
  INT16 x_ht_ambigs = 0;
  INT16 caps_ht_ambigs = 0;

  /* Calculate default variation of blob x_ht from bln x_ht for bln word */
  x_ht_ok_variation =
    (bln_x_height / x_ht_fraction_of_caps_ht - bln_x_height) * x_ht_variation;

  word_str = word_res->best_choice->string ().string ();
  /*
    Cycle blobs, allocating to one of the stats sets when possible.
  */
  blob_it.set_to_list (word_res->outword->blob_list ());
  for (blob_it.mark_cycle_pt (), i = 0, offset = 0;
  !blob_it.cycled_list (); blob_it.forward (),
           offset += word_res->best_choice->lengths()[i++]) {
    if (!dodgy_blob (blob_it.data ())) {
      blob_box = blob_it.data ()->bounding_box ();
      blob_ht_above_baseline = blob_box.top () - bln_baseline_offset;
      all_blobs_ht.add (blob_ht_above_baseline, 1);

      if (word_res->reject_map[i].rejected ()) {
        rej_blobs_count++;
        if (blob_box.height () > rej_blobs_max_height)
          rej_blobs_max_height = blob_box.height ();
        if (blob_box.area () > rej_blobs_max_area)
          rej_blobs_max_area = blob_box.area ();
      }
      else {
        if (STRING (chs_non_ambig_x_ht).contains (word_str[offset]))
          x_ht.add (blob_ht_above_baseline, 1);

        if (STRING (chs_non_ambig_caps_ht).contains (word_str[offset]))
          caps_ht.add (blob_ht_above_baseline, 1);

        if (STRING (chs_ambig_caps_x).contains (word_str[offset])) {
          case_ambig.add (blob_ht_above_baseline, 1);
          if (STRING (chs_x_ht).contains (word_str[offset]))
            x_ht_ambigs++;
          else
            caps_ht_ambigs++;
        }

        if (STRING (chs_bl_ambig_caps_x).contains (word_str[offset])) {
          if (STRING (chs_x_ht).contains (word_str[offset])) {
            /* confirm x_height provided > 15% total height below baseline */
            if ((bln_baseline_offset - blob_box.bottom ()) /
              (float) blob_box.height () > 0.15)
              x_ht.add (blob_ht_above_baseline, 1);
          }
          else {
            /* confirm caps_height provided < 5% total height below baseline */
            if ((bln_baseline_offset - blob_box.bottom ()) /
              (float) blob_box.height () < 0.05)
              caps_ht.add (blob_ht_above_baseline, 1);
          }
        }
      }
    }
  }
  est_caps_ht = estimate_from_stats (caps_ht);
  est_x_ht = estimate_from_stats (x_ht);
  est_ambigs(word_res, case_ambig, &ambig_lc_x_est, &ambig_uc_caps_est);
  max_blob_ht = all_blobs_ht.ile (0.9999);

  #ifndef SECURE_NAMES
  if (debug_x_ht_level >= 20) {
    tprintf ("Mode20:A: %s ", word_str);
    word_res->reject_map.print (debug_fp);
    tprintf (" XHT:%f CAP:%f MAX:%f AMBIG X:%f CAP:%f\n",
      est_x_ht, est_caps_ht, max_blob_ht,
      ambig_lc_x_est, ambig_uc_caps_est);
  }
  #endif
  if (!x_ht_conservative_ambigs &&
    (ambig_lc_x_est > 0) &&
    (ambig_lc_x_est == ambig_uc_caps_est) &&
  (max_blob_ht > ambig_lc_x_est + x_ht_ok_variation)) {
                                 //may be zero but believe xht
    ambig_uc_caps_est = est_caps_ht;
    #ifndef SECURE_NAMES
    if (debug_x_ht_level >= 20)
      tprintf ("Mode20:B: Fiddle ambig_uc_caps_est to %f\n",
        ambig_lc_x_est);
    #endif
  }

  /* Now make some estimates */

  if ((est_x_ht > 0) ||
    (est_caps_ht > 0) ||
  ((ambig_lc_x_est > 0) && (ambig_lc_x_est != ambig_uc_caps_est))) {
    /* There is some sensible data to go on so make the most of it. */
    if (debug_x_ht_level >= 20)
      tprintf ("Mode20:C: Sensible Data\n", ambig_lc_x_est);
    if (est_x_ht > 0) {
      est_x_ht_certain = TRUE;
      if (est_caps_ht == 0) {
        if ((ambig_uc_caps_est > ambig_lc_x_est) &&
          (ambig_uc_caps_est > est_x_ht + x_ht_ok_variation))
          est_caps_ht = ambig_uc_caps_est;
        else
          est_caps_ht = est_x_ht / x_ht_fraction_of_caps_ht;
      }
      if (case_ambig.get_total () > 0)
        improve_estimate(word_res, est_x_ht, est_caps_ht, x_ht, caps_ht);
      est_caps_ht_certain = caps_ht.get_total () > 0;
      #ifndef SECURE_NAMES
      if (debug_x_ht_level >= 20)
        tprintf ("Mode20:D: Est from xht XHT:%f CAP:%f\n",
          est_x_ht, est_caps_ht);
      #endif
    }
    else if (est_caps_ht > 0) {
      est_caps_ht_certain = TRUE;
      if ((ambig_lc_x_est > 0) &&
        (ambig_lc_x_est < est_caps_ht - x_ht_ok_variation))
        est_x_ht = ambig_lc_x_est;
      else
        est_x_ht = est_caps_ht * x_ht_fraction_of_caps_ht;
      if (ambig_lc_x_est + ambig_uc_caps_est > 0)
        improve_estimate(word_res, est_x_ht, est_caps_ht, x_ht, caps_ht);
      est_x_ht_certain = x_ht.get_total () > 0;
      #ifndef SECURE_NAMES
      if (debug_x_ht_level >= 20)
        tprintf ("Mode20:E: Est from caps XHT:%f CAP:%f\n",
          est_x_ht, est_caps_ht);
      #endif
    }
    else {
      /* Do something based on case ambig chars alone - we have guessed that the
        ambigs are lower case. */
      est_x_ht = ambig_lc_x_est;
      est_x_ht_certain = TRUE;
      if (ambig_uc_caps_est > ambig_lc_x_est) {
        est_caps_ht = ambig_uc_caps_est;
        est_caps_ht_certain = TRUE;
      }
      else
        est_caps_ht = est_x_ht / x_ht_fraction_of_caps_ht;

      #ifndef SECURE_NAMES
      if (debug_x_ht_level >= 20)
        tprintf ("Mode20:F: Est from ambigs XHT:%f CAP:%f\n",
          est_x_ht, est_caps_ht);
      #endif
    }
    /* Check for sane interpretation of evidence:
      Try shifting caps ht if min certain caps ht is not significantly greater
      than the estimated x ht or the max certain x ht is not significantly less
      than the estimated caps ht. */
    if (x_ht_check_est) {
      if ((caps_ht.get_total () > 0) &&
      (est_x_ht + x_ht_ok_variation >= caps_ht.ile (0.0001))) {
        trial = TRUE;
        est_caps_ht = est_x_ht;
        est_x_ht = x_ht_fraction_of_caps_ht * est_caps_ht;

        #ifndef SECURE_NAMES
        if (debug_x_ht_level >= 20)
          tprintf ("Mode20:G: Trial XHT:%f CAP:%f\n",
            est_x_ht, est_caps_ht);
        #endif
      }
      else if ((x_ht.get_total () > 0) &&
      (est_caps_ht - x_ht_ok_variation <= x_ht.ile (0.9999))) {
        trial = TRUE;
        est_x_ht = est_caps_ht;
        est_caps_ht = est_x_ht / x_ht_fraction_of_caps_ht;
        #ifndef SECURE_NAMES
        if (debug_x_ht_level >= 20)
          tprintf ("Mode20:H: Trial XHT:%f CAP:%f\n",
            est_x_ht, est_caps_ht);
        #endif
      }
    }
  }

  else {
    /* There is no sensible data so we're in the dark. */

    marginally_above_x_ht = bln_x_height +
      x_ht_ok_variation * x_ht_sub_variation;
    /*
      If there are no rejects, or the only rejects have a narrow height, or have
      a small area compared to a normal char, then estimate the x-height as the
      original one. (I.e dont fiddle about if the only rejects look like
      punctuation) - we use max height as mean or median will be too low if
      there are only two blobs - Eg "F."
    */

    if (debug_x_ht_level >= 20)
      tprintf ("Mode20:I: In the dark\n");

    if ((rej_blobs_count == 0) ||
      (rej_blobs_max_height < 0.3 * max_blob_ht) ||
    (rej_blobs_max_area < 0.3 * max_blob_ht * max_blob_ht)) {
      no_comment = TRUE;
      if (debug_x_ht_level >= 20)
        tprintf ("Mode20:J: No comment due to no rejects\n");
    }
    else if (x_ht_limit_flip_trials &&
      ((max_blob_ht < marginally_above_x_ht) ||
      ((ambig_lc_x_est > 0) &&
      (ambig_lc_x_est == ambig_uc_caps_est) &&
    (ambig_lc_x_est < marginally_above_x_ht)))) {
      no_comment = TRUE;
      if (debug_x_ht_level >= 20)
        tprintf ("Mode20:K: No comment as close to xht %f < %f\n",
          ambig_lc_x_est, marginally_above_x_ht);
    }
    else if (x_ht_conservative_ambigs && (ambig_uc_caps_est > 0)) {
      trial = TRUE;
      est_caps_ht = ambig_lc_x_est;
      est_x_ht = x_ht_fraction_of_caps_ht * est_caps_ht;

      #ifndef SECURE_NAMES
      if (debug_x_ht_level >= 20)
        tprintf ("Mode20:L: Trial XHT:%f CAP:%f\n",
          est_x_ht, est_caps_ht);
      #endif
    }
    /*
      If the top of the word is nowhere near where we expect ascenders to be
      (less than half the x_ht -> caps_ht distance) - suspect an all caps word
      at the x-ht. Estimate x-ht accordingly - but only as a TRIAL!
      NOTE we do NOT check location of baseline. Commas can descend as much as
      real descenders so we would need to do something to make sure that any
      disqualifying descenders were not at the end.
    */
    else {
      if (max_blob_ht <
      (bln_x_height + bln_x_height / x_ht_fraction_of_caps_ht) / 2.0) {
        trial = TRUE;
        est_x_ht = x_ht_fraction_of_caps_ht * max_blob_ht;
        est_caps_ht = max_blob_ht;

        #ifndef SECURE_NAMES
        if (debug_x_ht_level >= 20)
          tprintf ("Mode20:M: Trial XHT:%f CAP:%f\n",
            est_x_ht, est_caps_ht);
        #endif
      }
      else {
        no_comment = TRUE;
        if (debug_x_ht_level >= 20)
          tprintf ("Mode20:N: No comment as nothing else matched\n");
      }
    }
  }

  /* Sanity check - reject word if fails */

  if (!no_comment &&
    ((est_x_ht > 2 * bln_x_height) ||
    (est_x_ht / word_res->denorm.scale () <= min_sane_x_ht_pixels) ||
  (est_caps_ht <= est_x_ht) || (est_caps_ht >= 2.5 * est_x_ht))) {
    no_comment = TRUE;
    if (!trial && rej_use_xht) {
      if (debug_x_ht_level >= 2) {
        tprintf ("Sanity check rejecting %s ", word_str);
        word_res->reject_map.print (debug_fp);
        tprintf ("\n");
      }
      word_res->reject_map.rej_word_xht_fixup ();

    }
    if (debug_x_ht_level >= 20)
      tprintf ("Mode20:O: No comment as nothing else matched\n");
  }

  if (no_comment || trial) {
    word_res->x_height = bln_x_height / word_res->denorm.scale ();
    word_res->guessed_x_ht = TRUE;
    word_res->caps_height = (bln_x_height / x_ht_fraction_of_caps_ht) /
      word_res->denorm.scale ();
    word_res->guessed_caps_ht = TRUE;
    /*
    Reject ambigs in the current word if we are uncertain and:
        there are rejects OR
        there is only one char which is an ambig OR
        there is conflict between the case of the ambigs even though there is
        no height separation Eg "Ms" recognised from "MS"
    */
    if (rej_trial_ambigs &&
      ((word_res->reject_map.reject_count () > 0) ||
      (word_res->reject_map.length () == 1) ||
    ((x_ht_ambigs > 0) && (caps_ht_ambigs > 0)))) {
      #ifndef SECURE_NAMES
      if (debug_x_ht_level >= 2) {
        tprintf ("TRIAL Rej Ambigs %s ", word_str);
        word_res->reject_map.print (debug_fp);
      }
      #endif
      reject_ambigs(word_res);
      if (debug_x_ht_level >= 2) {
        tprintf (" ");
        word_res->reject_map.print (debug_fp);
        tprintf ("\n");
      }
    }
  }
  else {
    word_res->x_height = est_x_ht / word_res->denorm.scale ();
    word_res->guessed_x_ht = !est_x_ht_certain;
    word_res->caps_height = est_caps_ht / word_res->denorm.scale ();
    word_res->guessed_caps_ht = !est_caps_ht_certain;
  }

  if (!no_comment && (fabs (est_x_ht - bln_x_height) > x_ht_ok_variation))
    *trial_x_ht = est_x_ht / word_res->denorm.scale ();
  else
    *trial_x_ht = 0.0;

  #ifndef SECURE_NAMES
  if (((*trial_x_ht > 0) && (debug_x_ht_level >= 3)) ||
  (debug_x_ht_level >= 5)) {
    tprintf ("%s ", word_str);
    word_res->reject_map.print (debug_fp);
    tprintf
      (" X:%0.2f Cps:%0.2f Mxht:%0.2f RJ MxHt:%d MxAr:%d Rematch:%c\n",
      est_x_ht, est_caps_ht, max_blob_ht, rej_blobs_max_height,
      rej_blobs_max_area, *trial_x_ht > 0 ? '*' : ' ');
  }
  #endif

}


/*************************************************************************
 * check_block_occ()
 * Checks word for coarse block occupancy, rejecting more chars and flipping
 * case of case ambiguous chars as required.
 *************************************************************************/

void check_block_occ(WERD_RES *word_res) {
  PBLOB_IT blob_it;
  STRING new_string;
  STRING new_string_lengths(word_res->best_choice->lengths());
//  char new_string_lengths[word_res->best_choice->lengths().length() + 1];
  REJMAP new_map = word_res->reject_map;
  WERD_CHOICE *new_choice;

  const char *word_str = word_res->best_choice->string ().string ();
  INT16 i;
  INT16 offset;
  INT16 reject_count = 0;
  char confirmed_char[UNICHAR_LEN + 1];
  char temp_char[UNICHAR_LEN + 1];
  float x_ht;
  float caps_ht;

  new_string_lengths[0] = 0;

  if (word_res->x_height > 0)
    x_ht = word_res->x_height * word_res->denorm.scale ();
  else
    x_ht = bln_x_height;

  if (word_res->caps_height > 0)
    caps_ht = word_res->caps_height * word_res->denorm.scale ();
  else
    caps_ht = x_ht / x_ht_fraction_of_caps_ht;

  blob_it.set_to_list (word_res->outword->blob_list ());

  for (blob_it.mark_cycle_pt (), i = 0, offset = 0;
  !blob_it.cycled_list (); blob_it.forward (),
           offset += word_res->best_choice->lengths()[i++]) {
    strncpy(temp_char, word_str + offset,
            word_res->best_choice->lengths()[i]); //default copy
    temp_char[word_res->best_choice->lengths()[i]] = '\0';
    if (word_res->reject_map[i].accepted ()) {
      check_blob_occ (temp_char,
                      blob_it.data ()->bounding_box ().
                      top () - bln_baseline_offset, x_ht,
                      caps_ht, confirmed_char);

      if (strcmp(confirmed_char, "") == 0) {
        if (rej_use_check_block_occ) {
          new_map[i].setrej_xht_fixup ();
          reject_count++;
        }
      }
      else
        strcpy(temp_char, confirmed_char);
    }
    new_string += temp_char;
    new_string_lengths[i] = strlen(temp_char);
    new_string_lengths[i + 1] = 0;

  }
  if ((reject_count > 0) || (new_string != word_str)) {
    if (debug_x_ht_level >= 2) {
      tprintf ("Shape Verification: %s ", word_str);
      word_res->reject_map.print (debug_fp);
      tprintf (" -> %s ", new_string.string ());
      new_map.print (debug_fp);
      tprintf ("\n");
    }
    new_choice = new WERD_CHOICE (new_string.string (),
                                  new_string_lengths.string(),
                                  word_res->best_choice->rating (),
                                  word_res->best_choice->certainty (),
                                  word_res->best_choice->permuter ());
    delete word_res->best_choice;
    word_res->best_choice = new_choice;
    word_res->reject_map = new_map;
  }
}


/*************************************************************************
 * check_blob_occ()
 *
 * Checks blob for position relative to position above baseline
 * Return 0 for reject, or (possibly case shifted) confirmed char
 *************************************************************************/

void check_blob_occ(char* proposed_char,
                    INT16 blob_ht_above_baseline,
                    float x_ht,
                    float caps_ht,
                    char* confirmed_char) {
  BOOL8 blob_definite_x_ht;
  BOOL8 blob_definite_caps_ht;
  float acceptable_variation;

  acceptable_variation = (caps_ht - x_ht) * x_ht_variation;
  /* ??? REJECT if expected descender and nothing significantly below BL */

  /* ??? REJECT if expected ascender and nothing significantly above x-ht */

  /*
    IF AMBIG_CAPS_X_CHS
      IF blob is definitely an ascender ( > xht + xht err )AND
        char is an x-ht char
      THEN
        flip case
      IF blob is defintiely an x-ht ( <= xht + xht err ) AND
        char is an ascender char
      THEN
        flip case
  */
  blob_definite_x_ht = blob_ht_above_baseline <= x_ht + acceptable_variation;
  blob_definite_caps_ht = blob_ht_above_baseline >=
    caps_ht - acceptable_variation;

  if (STRING (chs_ambig_caps_x).contains (*proposed_char)) {
    if ((!blob_definite_x_ht && !blob_definite_caps_ht) ||
        ((strcmp(proposed_char, "0") == 0) && !blob_definite_caps_ht) ||
        ((strcmp(proposed_char, "o") == 0) && !blob_definite_x_ht)) {
      strcpy(confirmed_char, "");
      return;
    }

    else if (blob_definite_caps_ht &&
    STRING (chs_x_ht).contains (*proposed_char)) {
      if (x_ht_case_flip) {
                                 //flip to upper case
        proposed_char[0] = (char) toupper (*proposed_char);
        return;
      } else {
        strcpy(confirmed_char, "");
        return;
      }
    }

    else if (blob_definite_x_ht &&
    !STRING (chs_x_ht).contains (*proposed_char)) {
      if (x_ht_case_flip) {
                                 //flip to lower case
        proposed_char[0] = (char) tolower (*proposed_char);
      } else {
        strcpy(confirmed_char, "");
        return;
      }
    }
  }
  else
  if ((STRING (chs_non_ambig_x_ht).contains (*proposed_char)
    && !blob_definite_x_ht)
    || (STRING (chs_non_ambig_caps_ht).contains (*proposed_char)
        && !blob_definite_caps_ht)) {
    strcpy(confirmed_char, "");
    return;
  }
  strcpy(confirmed_char, proposed_char);
  return;
}


float estimate_from_stats(STATS &stats) {
  if (stats.get_total () <= 0)
    return 0.0;
  else if (stats.get_total () >= 3)
    return stats.ile (0.5);      //median
  else
    return stats.mean ();
}


void improve_estimate(WERD_RES *word_res,
                      float &est_x_ht,
                      float &est_caps_ht,
                      STATS &x_ht,
                      STATS &caps_ht) {
  PBLOB_IT blob_it;
  INT16 blob_ht_above_baseline;

  const char *word_str;
  INT16 i;
  INT16 offset;
  BOX blob_box;                  //blob bounding box
  char confirmed_char[UNICHAR_LEN + 1];
  char temp_char[UNICHAR_LEN + 1];
  float new_val;

  /* IMPROVE estimates here - if good estimates, and case ambig chars,
    rescan blobs to fix case ambig blobs, re-estimate hts  ??? maybe always do
    it after deciding x-height
  */

  blob_it.set_to_list (word_res->outword->blob_list ());
  word_str = word_res->best_choice->string ().string ();
  for (blob_it.mark_cycle_pt (), i = 0, offset = 0;
       !blob_it.cycled_list (); blob_it.forward (),
           offset += word_res->best_choice->lengths()[i++]) {
    if ((STRING (chs_ambig_caps_x).contains (word_str[offset])) &&
    (!dodgy_blob (blob_it.data ()))) {
      blob_box = blob_it.data ()->bounding_box ();
      blob_ht_above_baseline = blob_box.top () - bln_baseline_offset;
      strncpy(temp_char, word_str + offset,
              word_res->best_choice->lengths()[i]);
      temp_char[word_res->best_choice->lengths()[i]] = '\0';
      check_blob_occ (temp_char,
                      blob_ht_above_baseline,
                      est_x_ht, est_caps_ht, confirmed_char);
      if (strcmp(confirmed_char, "") != 0)
        if (STRING (chs_x_ht).contains (*confirmed_char))
          x_ht.add (blob_ht_above_baseline, 1);
      else
        caps_ht.add (blob_ht_above_baseline, 1);
    }
  }
  new_val = estimate_from_stats (x_ht);
  if (new_val > 0)
    est_x_ht = new_val;
  new_val = estimate_from_stats (caps_ht);
  if (new_val > 0)
    est_caps_ht = new_val;
}


void reject_ambigs(  //rej any accepted xht ambig chars
                   WERD_RES *word) {
  const char *word_str;
  int i = 0;

  word_str = word->best_choice->string ().string ();
  while (*word_str != '\0') {
    if (STRING (chs_ambig_caps_x).contains (*word_str))
      word->reject_map[i].setrej_xht_fixup ();
    word_str += word->best_choice->lengths()[i++];
  }
}


void est_ambigs(                          //xht ambig ht stats
                WERD_RES *word_res,
                STATS &stats,
                float *ambig_lc_x_est,    //xht est
                float *ambig_uc_caps_est  //caps est
               ) {
  float x_ht_ok_variation;
  STATS short_ambigs (0, 300);
  STATS tall_ambigs (0, 300);
  PBLOB_IT blob_it;
  BOX blob_box;                  //blob bounding box
  INT16 blob_ht_above_baseline;

  const char *word_str;
  INT16 i;
  INT16 offset;
  float min;                     //min ambig ch ht
  float max;                     //max ambig ch ht
  float short_limit;             // for lower case
  float tall_limit;              // for upper case

  x_ht_ok_variation =
    (bln_x_height / x_ht_fraction_of_caps_ht - bln_x_height) * x_ht_variation;

  if (stats.get_total () == 0) {
    *ambig_lc_x_est = 0;
    *ambig_uc_caps_est = 0;
  }
  else {
    min = stats.ile (0.0);
    max = stats.ile (0.99999);
    if ((max - min) < x_ht_ok_variation) {
      *ambig_lc_x_est = *ambig_uc_caps_est = stats.mean ();
      //close enough
    }
    else {
    /* Try reclustering into lower and upper case chars */
      short_limit = min + (max - min) * x_ht_variation;
      tall_limit = max - (max - min) * x_ht_variation;
      word_str = word_res->best_choice->string ().string ();
      blob_it.set_to_list (word_res->outword->blob_list ());
      for (blob_it.mark_cycle_pt (), i = 0, offset = 0;
      !blob_it.cycled_list (); blob_it.forward (),
               offset += word_res->best_choice->lengths()[i++]) {
        if (word_res->reject_map[i].accepted () &&
          STRING (chs_ambig_caps_x).contains (word_str[offset]) &&
        (!dodgy_blob (blob_it.data ()))) {
          blob_box = blob_it.data ()->bounding_box ();
          blob_ht_above_baseline =
            blob_box.top () - bln_baseline_offset;
          if (blob_ht_above_baseline <= short_limit)
            short_ambigs.add (blob_ht_above_baseline, 1);
          else if (blob_ht_above_baseline >= tall_limit)
            tall_ambigs.add (blob_ht_above_baseline, 1);
        }
      }
      *ambig_lc_x_est = short_ambigs.mean ();
      *ambig_uc_caps_est = tall_ambigs.mean ();
      /* Cop out if we havent got sensible clusters. */
      if (*ambig_uc_caps_est - *ambig_lc_x_est <= x_ht_ok_variation)
        *ambig_lc_x_est = *ambig_uc_caps_est = stats.mean ();
      //close enough
    }
  }
}


/*************************************************************************
 * dodgy_blob()
 * Returns true if the blob has more than one outline, one above the other.
 * These are dodgy as the top blob could be noise, causing the bounding box xht
 * to be misleading
 *************************************************************************/

BOOL8 dodgy_blob(PBLOB *blob) {
  OUTLINE_IT outline_it = blob->out_list ();
  INT16 highest_bottom = -MAX_INT16;
  INT16 lowest_top = MAX_INT16;
  BOX outline_box;

  if (x_ht_include_dodgy_blobs)
    return FALSE;                //no blob is ever dodgy
  for (outline_it.mark_cycle_pt ();
  !outline_it.cycled_list (); outline_it.forward ()) {
    outline_box = outline_it.data ()->bounding_box ();
    if (lowest_top > outline_box.top ())
      lowest_top = outline_box.top ();
    if (highest_bottom < outline_box.bottom ())
      highest_bottom = outline_box.bottom ();
  }
  return highest_bottom >= lowest_top;
}
