/********************************************************************** * File: blobbox.h (Formerly blobnbox.h) * Description: Code for the textord blob class. * Author: Ray Smith * Created: Thu Jul 30 09:08:51 BST 1992 * * (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. * **********************************************************************/ #ifndef BLOBBOX_H #define BLOBBOX_H #include "clst.h" #include "elst2.h" #include "werd.h" #include "ocrblock.h" #include "statistc.h" enum PITCH_TYPE { PITCH_DUNNO, // insufficient data PITCH_DEF_FIXED, // definitely fixed PITCH_MAYBE_FIXED, // could be PITCH_DEF_PROP, PITCH_MAYBE_PROP, PITCH_CORR_FIXED, PITCH_CORR_PROP }; // The possible tab-stop types of each side of a BLOBNBOX. // The ordering is important, as it is used for deleting dead-ends in the // search. ALIGNED, CONFIRMED and VLINE should remain greater than the // non-aligned, unset, or deleted members. enum TabType { TT_NONE, // Not a tab. TT_DELETED, // Not a tab after detailed analysis. TT_MAYBE_RAGGED, // Initial designation of a tab-stop candidate. TT_MAYBE_ALIGNED, // Initial designation of a tab-stop candidate. TT_CONFIRMED, // Aligned with neighbours. TT_VLINE // Detected as a vertical line. }; // The possible region types of a BLOBNBOX. // Note: keep all the text types > BRT_UNKNOWN and all the image types less. // Keep in sync with kBlobTypes in colpartition.cpp and BoxColor, and the // *Type static functions below. enum BlobRegionType { BRT_NOISE, // Neither text nor image. BRT_HLINE, // Horizontal separator line. BRT_VLINE, // Vertical separator line. BRT_RECTIMAGE, // Rectangular image. BRT_POLYIMAGE, // Non-rectangular image. BRT_UNKNOWN, // Not determined yet. BRT_VERT_TEXT, // Vertical alignment, not necessarily vertically oriented. BRT_TEXT, // Convincing text. BRT_COUNT // Number of possibilities. }; // enum for elements of arrays that refer to neighbours. // NOTE: keep in this order, so ^2 can be used to flip direction. enum BlobNeighbourDir { BND_LEFT, BND_BELOW, BND_RIGHT, BND_ABOVE, BND_COUNT }; // enum for special type of text characters, such as math symbol or italic. enum BlobSpecialTextType { BSTT_NONE, // No special. BSTT_ITALIC, // Italic style. BSTT_DIGIT, // Digit symbols. BSTT_MATH, // Mathmatical symobls (not including digit). BSTT_UNCLEAR, // Characters with low recognition rate. BSTT_SKIP, // Characters that we skip labeling (usually too small). BSTT_COUNT }; inline BlobNeighbourDir DirOtherWay(BlobNeighbourDir dir) { return static_cast(dir ^ 2); } // BlobTextFlowType indicates the quality of neighbouring information // related to a chain of connected components, either horizontally or // vertically. Also used by ColPartition for the collection of blobs // within, which should all have the same value in most cases. enum BlobTextFlowType { BTFT_NONE, // No text flow set yet. BTFT_NONTEXT, // Flow too poor to be likely text. BTFT_NEIGHBOURS, // Neighbours support flow in this direction. BTFT_CHAIN, // There is a weak chain of text in this direction. BTFT_STRONG_CHAIN, // There is a strong chain of text in this direction. BTFT_TEXT_ON_IMAGE, // There is a strong chain of text on an image. BTFT_LEADER, // Leader dots/dashes etc. BTFT_COUNT }; // Returns true if type1 dominates type2 in a merge. Mostly determined by the // ordering of the enum, LEADER is weak and dominates nothing. // The function is anti-symmetric (t1 > t2) === !(t2 > t1), except that // this cannot be true if t1 == t2, so the result is undefined. inline bool DominatesInMerge(BlobTextFlowType type1, BlobTextFlowType type2) { // LEADER always loses. if (type1 == BTFT_LEADER) return false; if (type2 == BTFT_LEADER) return true; // With those out of the way, the ordering of the enum determines the result. return type1 >= type2; } namespace tesseract { class ColPartition; } class BLOBNBOX; ELISTIZEH(BLOBNBOX) class BLOBNBOX :public ELIST_LINK { public: BLOBNBOX() { ConstructionInit(); } explicit BLOBNBOX(C_BLOB *srcblob) { box = srcblob->bounding_box(); ConstructionInit(); cblob_ptr = srcblob; area = static_cast(srcblob->area()); } ~BLOBNBOX() { if (owns_cblob_) delete cblob_ptr; } static BLOBNBOX* RealBlob(C_OUTLINE* outline) { C_BLOB* blob = new C_BLOB(outline); return new BLOBNBOX(blob); } // Rotates the box and the underlying blob. void rotate(FCOORD rotation); // Methods that act on the box without touching the underlying blob. // Reflect the box in the y-axis, leaving the underlying blob untouched. void reflect_box_in_y_axis(); // Rotates the box by the angle given by rotation. // If the blob is a diacritic, then only small rotations for skew // correction can be applied. void rotate_box(FCOORD rotation); // Moves just the box by the given vector. void translate_box(ICOORD v) { if (IsDiacritic()) { box.move(v); base_char_top_ += v.y(); base_char_bottom_ += v.y(); } else { box.move(v); set_diacritic_box(box); } } void merge(BLOBNBOX *nextblob); void really_merge(BLOBNBOX* other); void chop( // fake chop blob BLOBNBOX_IT *start_it, // location of this BLOBNBOX_IT *blob_it, // iterator FCOORD rotation, // for landscape float xheight); // line height void NeighbourGaps(int gaps[BND_COUNT]) const; void MinMaxGapsClipped(int* h_min, int* h_max, int* v_min, int* v_max) const; void CleanNeighbours(); // Returns positive if there is at least one side neighbour that has a // similar stroke width and is not on the other side of a rule line. int GoodTextBlob() const; // Returns the number of side neighbours that are of type BRT_NOISE. int NoisyNeighbours() const; // Returns true if the blob is noise and has no owner. bool DeletableNoise() const { return owner() == NULL && region_type() == BRT_NOISE; } // Returns true, and sets vert_possible/horz_possible if the blob has some // feature that makes it individually appear to flow one way. // eg if it has a high aspect ratio, yet has a complex shape, such as a // joined word in Latin, Arabic, or Hindi, rather than being a -, I, l, 1. bool DefiniteIndividualFlow(); // Returns true if there is no tabstop violation in merging this and other. bool ConfirmNoTabViolation(const BLOBNBOX& other) const; // Returns true if other has a similar stroke width to this. bool MatchingStrokeWidth(const BLOBNBOX& other, double fractional_tolerance, double constant_tolerance) const; // Returns a bounding box of the outline contained within the // given horizontal range. TBOX BoundsWithinLimits(int left, int right); // Estimates and stores the baseline position based on the shape of the // outline. void EstimateBaselinePosition(); // Simple accessors. const TBOX& bounding_box() const { return box; } // Set the bounding box. Use with caution. // Normally use compute_bounding_box instead. void set_bounding_box(const TBOX& new_box) { box = new_box; base_char_top_ = box.top(); base_char_bottom_ = box.bottom(); } void compute_bounding_box() { box = cblob_ptr->bounding_box(); base_char_top_ = box.top(); base_char_bottom_ = box.bottom(); baseline_y_ = box.bottom(); } const TBOX& reduced_box() const { return red_box; } void set_reduced_box(TBOX new_box) { red_box = new_box; reduced = TRUE; } inT32 enclosed_area() const { return area; } bool joined_to_prev() const { return joined != 0; } bool red_box_set() const { return reduced != 0; } int repeated_set() const { return repeated_set_; } void set_repeated_set(int set_id) { repeated_set_ = set_id; } C_BLOB *cblob() const { return cblob_ptr; } TabType left_tab_type() const { return left_tab_type_; } void set_left_tab_type(TabType new_type) { left_tab_type_ = new_type; } TabType right_tab_type() const { return right_tab_type_; } void set_right_tab_type(TabType new_type) { right_tab_type_ = new_type; } BlobRegionType region_type() const { return region_type_; } void set_region_type(BlobRegionType new_type) { region_type_ = new_type; } BlobSpecialTextType special_text_type() const { return spt_type_; } void set_special_text_type(BlobSpecialTextType new_type) { spt_type_ = new_type; } BlobTextFlowType flow() const { return flow_; } void set_flow(BlobTextFlowType value) { flow_ = value; } bool vert_possible() const { return vert_possible_; } void set_vert_possible(bool value) { vert_possible_ = value; } bool horz_possible() const { return horz_possible_; } void set_horz_possible(bool value) { horz_possible_ = value; } int left_rule() const { return left_rule_; } void set_left_rule(int new_left) { left_rule_ = new_left; } int right_rule() const { return right_rule_; } void set_right_rule(int new_right) { right_rule_ = new_right; } int left_crossing_rule() const { return left_crossing_rule_; } void set_left_crossing_rule(int new_left) { left_crossing_rule_ = new_left; } int right_crossing_rule() const { return right_crossing_rule_; } void set_right_crossing_rule(int new_right) { right_crossing_rule_ = new_right; } float horz_stroke_width() const { return horz_stroke_width_; } void set_horz_stroke_width(float width) { horz_stroke_width_ = width; } float vert_stroke_width() const { return vert_stroke_width_; } void set_vert_stroke_width(float width) { vert_stroke_width_ = width; } float area_stroke_width() const { return area_stroke_width_; } tesseract::ColPartition* owner() const { return owner_; } void set_owner(tesseract::ColPartition* new_owner) { owner_ = new_owner; } bool leader_on_left() const { return leader_on_left_; } void set_leader_on_left(bool flag) { leader_on_left_ = flag; } bool leader_on_right() const { return leader_on_right_; } void set_leader_on_right(bool flag) { leader_on_right_ = flag; } BLOBNBOX* neighbour(BlobNeighbourDir n) const { return neighbours_[n]; } bool good_stroke_neighbour(BlobNeighbourDir n) const { return good_stroke_neighbours_[n]; } void set_neighbour(BlobNeighbourDir n, BLOBNBOX* neighbour, bool good) { neighbours_[n] = neighbour; good_stroke_neighbours_[n] = good; } bool IsDiacritic() const { return base_char_top_ != box.top() || base_char_bottom_ != box.bottom(); } int base_char_top() const { return base_char_top_; } int base_char_bottom() const { return base_char_bottom_; } int baseline_position() const { return baseline_y_; } int line_crossings() const { return line_crossings_; } void set_line_crossings(int value) { line_crossings_ = value; } void set_diacritic_box(const TBOX& diacritic_box) { base_char_top_ = diacritic_box.top(); base_char_bottom_ = diacritic_box.bottom(); } BLOBNBOX* base_char_blob() const { return base_char_blob_; } void set_base_char_blob(BLOBNBOX* blob) { base_char_blob_ = blob; } void set_owns_cblob(bool value) { owns_cblob_ = value; } bool UniquelyVertical() const { return vert_possible_ && !horz_possible_; } bool UniquelyHorizontal() const { return horz_possible_ && !vert_possible_; } // Returns true if the region type is text. static bool IsTextType(BlobRegionType type) { return type == BRT_TEXT || type == BRT_VERT_TEXT; } // Returns true if the region type is image. static bool IsImageType(BlobRegionType type) { return type == BRT_RECTIMAGE || type == BRT_POLYIMAGE; } // Returns true if the region type is line. static bool IsLineType(BlobRegionType type) { return type == BRT_HLINE || type == BRT_VLINE; } // Returns true if the region type cannot be merged. static bool UnMergeableType(BlobRegionType type) { return IsLineType(type) || IsImageType(type); } // Helper to call CleanNeighbours on all blobs on the list. static void CleanNeighbours(BLOBNBOX_LIST* blobs); // Helper to delete all the deletable blobs on the list. static void DeleteNoiseBlobs(BLOBNBOX_LIST* blobs); // Helper to compute edge offsets for all the blobs on the list. // See coutln.h for an explanation of edge offsets. static void ComputeEdgeOffsets(Pix* thresholds, Pix* grey, BLOBNBOX_LIST* blobs); #ifndef GRAPHICS_DISABLED // Helper to draw all the blobs on the list in the given body_colour, // with child outlines in the child_colour. static void PlotBlobs(BLOBNBOX_LIST* list, ScrollView::Color body_colour, ScrollView::Color child_colour, ScrollView* win); // Helper to draw only DeletableNoise blobs (unowned, BRT_NOISE) on the // given list in the given body_colour, with child outlines in the // child_colour. static void PlotNoiseBlobs(BLOBNBOX_LIST* list, ScrollView::Color body_colour, ScrollView::Color child_colour, ScrollView* win); static ScrollView::Color TextlineColor(BlobRegionType region_type, BlobTextFlowType flow_type); // Keep in sync with BlobRegionType. ScrollView::Color BoxColor() const; void plot(ScrollView* window, // window to draw in ScrollView::Color blob_colour, // for outer bits ScrollView::Color child_colour); // for holes #endif // Initializes the bulk of the members to default values for use at // construction time. void ConstructionInit() { cblob_ptr = NULL; owns_cblob_ = false; area = 0; area_stroke_width_ = 0.0f; horz_stroke_width_ = 0.0f; vert_stroke_width_ = 0.0f; ReInit(); } // Initializes members set by StrokeWidth and beyond, without discarding // stored area and strokewidth values, which are expensive to calculate. void ReInit() { joined = false; reduced = false; repeated_set_ = 0; left_tab_type_ = TT_NONE; right_tab_type_ = TT_NONE; region_type_ = BRT_UNKNOWN; flow_ = BTFT_NONE; spt_type_ = BSTT_SKIP; left_rule_ = 0; right_rule_ = 0; left_crossing_rule_ = 0; right_crossing_rule_ = 0; if (area_stroke_width_ == 0.0f && area > 0 && cblob() != NULL) area_stroke_width_ = 2.0f * area / cblob()->perimeter(); owner_ = NULL; base_char_top_ = box.top(); base_char_bottom_ = box.bottom(); baseline_y_ = box.bottom(); line_crossings_ = 0; base_char_blob_ = NULL; horz_possible_ = false; vert_possible_ = false; leader_on_left_ = false; leader_on_right_ = false; ClearNeighbours(); } void ClearNeighbours() { for (int n = 0; n < BND_COUNT; ++n) { neighbours_[n] = NULL; good_stroke_neighbours_[n] = false; } } private: C_BLOB *cblob_ptr; // edgestep blob TBOX box; // bounding box TBOX red_box; // bounding box int area : 30; // enclosed area int joined : 1; // joined to prev int reduced : 1; // reduced box set int repeated_set_; // id of the set of repeated blobs TabType left_tab_type_; // Indicates tab-stop assessment TabType right_tab_type_; // Indicates tab-stop assessment BlobRegionType region_type_; // Type of region this blob belongs to BlobTextFlowType flow_; // Quality of text flow. inT16 left_rule_; // x-coord of nearest but not crossing rule line inT16 right_rule_; // x-coord of nearest but not crossing rule line inT16 left_crossing_rule_; // x-coord of nearest or crossing rule line inT16 right_crossing_rule_; // x-coord of nearest or crossing rule line inT16 base_char_top_; // y-coord of top/bottom of diacritic base, inT16 base_char_bottom_; // if it exists else top/bottom of this blob. inT16 baseline_y_; // Estimate of baseline position. int line_crossings_; // Number of line intersections touched. BLOBNBOX* base_char_blob_; // The blob that was the base char. float horz_stroke_width_; // Median horizontal stroke width float vert_stroke_width_; // Median vertical stroke width float area_stroke_width_; // Stroke width from area/perimeter ratio. tesseract::ColPartition* owner_; // Who will delete me when I am not needed BlobSpecialTextType spt_type_; // Special text type. BLOBNBOX* neighbours_[BND_COUNT]; bool good_stroke_neighbours_[BND_COUNT]; bool horz_possible_; // Could be part of horizontal flow. bool vert_possible_; // Could be part of vertical flow. bool leader_on_left_; // There is a leader to the left. bool leader_on_right_; // There is a leader to the right. // Iff true, then the destructor should delete the cblob_ptr. // TODO(rays) migrate all uses to correctly setting this flag instead of // deleting the C_BLOB before deleting the BLOBNBOX. bool owns_cblob_; }; class TO_ROW : public ELIST2_LINK { public: static const int kErrorWeight = 3; TO_ROW() { clear(); } //empty TO_ROW( //constructor BLOBNBOX *blob, //from first blob float top, //of row //target height float bottom, float row_size); void print() const; float max_y() const { //access function return y_max; } float min_y() const { return y_min; } float mean_y() const { return (y_min + y_max) / 2.0f; } float initial_min_y() const { return initial_y_min; } float line_m() const { //access to line fit return m; } float line_c() const { return c; } float line_error() const { return error; } float parallel_c() const { return para_c; } float parallel_error() const { return para_error; } float believability() const { //baseline goodness return credibility; } float intercept() const { //real parallel_c return y_origin; } void add_blob( //put in row BLOBNBOX *blob, //blob to add float top, //of row //target height float bottom, float row_size); void insert_blob( //put in row in order BLOBNBOX *blob); BLOBNBOX_LIST *blob_list() { //get list return &blobs; } void set_line( //set line spec float new_m, //line to set float new_c, float new_error) { m = new_m; c = new_c; error = new_error; } void set_parallel_line( //set fixed gradient line float gradient, //page gradient float new_c, float new_error) { para_c = new_c; para_error = new_error; credibility = (float)(blobs.length() - kErrorWeight * new_error); y_origin = (float)(new_c / sqrt(1 + gradient * gradient)); //real intercept } void set_limits( //set min,max float new_min, //bottom and float new_max) { //top of row y_min = new_min; y_max = new_max; } void compute_vertical_projection(); //get projection bool rep_chars_marked() const { return num_repeated_sets_ != -1; } void clear_rep_chars_marked() { num_repeated_sets_ = -1; } int num_repeated_sets() const { return num_repeated_sets_; } void set_num_repeated_sets(int num_sets) { num_repeated_sets_ = num_sets; } // true when dead BOOL8 merged; BOOL8 all_caps; // had no ascenders BOOL8 used_dm_model; // in guessing pitch inT16 projection_left; // start of projection inT16 projection_right; // start of projection PITCH_TYPE pitch_decision; // how strong is decision float fixed_pitch; // pitch or 0 float fp_space; // sp if fixed pitch float fp_nonsp; // nonsp if fixed pitch float pr_space; // sp if prop float pr_nonsp; // non sp if prop float spacing; // to "next" row float xheight; // of line int xheight_evidence; // number of blobs of height xheight float ascrise; // ascenders float descdrop; // descenders float body_size; // of CJK characters. Assumed to be // xheight+ascrise for non-CJK text. inT32 min_space; // min size for real space inT32 max_nonspace; // max size of non-space inT32 space_threshold; // space vs nonspace float kern_size; // average non-space float space_size; // average space WERD_LIST rep_words; // repeated chars ICOORDELT_LIST char_cells; // fixed pitch cells QSPLINE baseline; // curved baseline STATS projection; // vertical projection private: void clear(); // clear all values to reasonable defaults BLOBNBOX_LIST blobs; //blobs in row float y_min; //coords float y_max; float initial_y_min; float m, c; //line spec float error; //line error float para_c; //constrained fit float para_error; float y_origin; //rotated para_c; float credibility; //baseline believability int num_repeated_sets_; // number of sets of repeated blobs // set to -1 if we have not searched // for repeated blobs in this row yet }; ELIST2IZEH(TO_ROW) class TO_BLOCK :public ELIST_LINK { public: TO_BLOCK() : pitch_decision(PITCH_DUNNO) { clear(); } //empty TO_BLOCK( //constructor BLOCK *src_block); //real block ~TO_BLOCK(); void clear(); // clear all scalar members. TO_ROW_LIST *get_rows() { //access function return &row_list; } // Rotate all the blobnbox lists and the underlying block. Then update the // median size statistic from the blobs list. void rotate(const FCOORD& rotation) { BLOBNBOX_LIST* blobnbox_list[] = { &blobs, &underlines, &noise_blobs, &small_blobs, &large_blobs, NULL }; for (BLOBNBOX_LIST** list = blobnbox_list; *list != NULL; ++list) { BLOBNBOX_IT it(*list); for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { it.data()->rotate(rotation); } } // Rotate the block ASSERT_HOST(block->poly_block() != NULL); block->rotate(rotation); // Update the median size statistic from the blobs list. STATS widths(0, block->bounding_box().width()); STATS heights(0, block->bounding_box().height()); BLOBNBOX_IT blob_it(&blobs); for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { widths.add(blob_it.data()->bounding_box().width(), 1); heights.add(blob_it.data()->bounding_box().height(), 1); } block->set_median_size(static_cast(widths.median() + 0.5), static_cast(heights.median() + 0.5)); } void print_rows() { //debug info TO_ROW_IT row_it = &row_list; TO_ROW *row; for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { row = row_it.data(); tprintf("Row range (%g,%g), para_c=%g, blobcount=" INT32FORMAT "\n", row->min_y(), row->max_y(), row->parallel_c(), row->blob_list()->length()); } } // Reorganizes the blob lists with a different definition of small, medium // and large, compared to the original definition. // Height is still the primary filter key, but medium width blobs of small // height become medium, and very wide blobs of small height stay small. void ReSetAndReFilterBlobs(); // Deletes noise blobs from all lists where not owned by a ColPartition. void DeleteUnownedNoise(); // Computes and stores the edge offsets on each blob for use in feature // extraction, using greyscale if the supplied grey and thresholds pixes // are 8-bit or otherwise (if NULL or not 8 bit) the original binary // edge step outlines. // Thresholds must either be the same size as grey or an integer down-scale // of grey. // See coutln.h for an explanation of edge offsets. void ComputeEdgeOffsets(Pix* thresholds, Pix* grey); #ifndef GRAPHICS_DISABLED // Draw the noise blobs from all lists in red. void plot_noise_blobs(ScrollView* to_win); // Draw the blobs on on the various lists in the block in different colors. void plot_graded_blobs(ScrollView* to_win); #endif BLOBNBOX_LIST blobs; //medium size BLOBNBOX_LIST underlines; //underline blobs BLOBNBOX_LIST noise_blobs; //very small BLOBNBOX_LIST small_blobs; //fairly small BLOBNBOX_LIST large_blobs; //big blobs BLOCK *block; //real block PITCH_TYPE pitch_decision; //how strong is decision float line_spacing; //estimate // line_size is a lower-bound estimate of the font size in pixels of // the text in the block (with ascenders and descenders), being a small // (1.25) multiple of the median height of filtered blobs. // In most cases the font size will be bigger, but it will be closer // if the text is allcaps, or in a no-x-height script. float line_size; //estimate float max_blob_size; //line assignment limit float baseline_offset; //phase shift float xheight; //median blob size float fixed_pitch; //pitch or 0 float kern_size; //average non-space float space_size; //average space inT32 min_space; //min definite space inT32 max_nonspace; //max definite float fp_space; //sp if fixed pitch float fp_nonsp; //nonsp if fixed pitch float pr_space; //sp if prop float pr_nonsp; //non sp if prop TO_ROW *key_row; //starting row private: TO_ROW_LIST row_list; //temporary rows }; ELISTIZEH(TO_BLOCK) extern double_VAR_H(textord_error_weight, 3, "Weighting for error in believability"); void find_cblob_limits( //get y limits C_BLOB *blob, //blob to search float leftx, //x limits float rightx, FCOORD rotation, //for landscape float &ymin, //output y limits float &ymax); void find_cblob_vlimits( //get y limits C_BLOB *blob, //blob to search float leftx, //x limits float rightx, float &ymin, //output y limits float &ymax); void find_cblob_hlimits( //get x limits C_BLOB *blob, //blob to search float bottomy, //y limits float topy, float &xmin, //output x limits float &xymax); C_BLOB *crotate_cblob( //rotate it C_BLOB *blob, //blob to search FCOORD rotation //for landscape ); TBOX box_next( //get bounding box BLOBNBOX_IT *it //iterator to blobds ); TBOX box_next_pre_chopped( //get bounding box BLOBNBOX_IT *it //iterator to blobds ); void vertical_cblob_projection( //project outlines C_BLOB *blob, //blob to project STATS *stats //output ); void vertical_coutline_projection( //project outlines C_OUTLINE *outline, //outline to project STATS *stats //output ); #ifndef GRAPHICS_DISABLED void plot_blob_list(ScrollView* win, // window to draw in BLOBNBOX_LIST *list, // blob list ScrollView::Color body_colour, // colour to draw ScrollView::Color child_colour); // colour of child #endif // GRAPHICS_DISABLED #endif