opencv_contrib icon indicating copy to clipboard operation
opencv_contrib copied to clipboard

module freetype: adding advance parameter to getTextSize for positioning bounding box

Open abar52 opened this issue 7 years ago • 1 comments

freetype: adding advance parameter to getTextSize for positioning bounding box

System information (version)

  • OpenCV => recent 4.0 branch
  • Operating System / Platform => Ubuntu 18.10 64bit
  • Compiler => GCC 8.2 + CUDA 10

running example code from freetype documentation

the font come from {here}

the complete sentence in the example means:

// ▄▄▄ ▄ ▄▄▄▄▄ ▄ ▄ ▄▄▄ ▄ ▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄ ▄ ▄ ▄ ▄▄▄ ▄ ▄ ▄ ▄
// █▄█▄█▄▄▄█ ▄ █ █ █ ▄ █▄▄▄▄▄█ ▄ █ █ ▄▄█ █ █ █ █▄█▄█▄█▄█▄█
// █▄▄ ▄ ▄ ▄▄█▄█ █ █▄▄▄█ █▄█ ▄▄█▄█ █ █▄█▄█▄█ █ █▄▄▄▄▄▄▄▄ ▄
//   In the name of Allah, Most Gracious, Most Merciful
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/freetype.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

int main()
{
    //cv::String text = "\ufc45\ufb50\ufc44\ufb50\ufc43\ufb50\ufc42\ufb50\ufc41";
    //cv::String text = "\ufc44\ufb50\ufc43\ufb50\ufc42\ufb50\ufc41";
    cv::String text = "\ufc43";
    int fontHeight = 150;
    int thickness = 1;
    int linestyle = 8;
    cv::Mat img( 400, 1200, CV_8UC3, cv::Scalar::all( 0 ) );
    int baseline = 0;

    cv::Ptr<cv::freetype::FreeType2> ft2;
    ft2 = cv::freetype::createFreeType2();

    ft2->setSplitNumber( 8 );

    ft2->loadFontData( "QCF2001.ttf", 0 );
    cv::Size textSize = ft2->getTextSize( text,
        fontHeight,
        thickness,
        &baseline );
    if ( thickness > 0 ) {
        baseline += thickness;
    }

    // show baseline and advance
    std::cout << "baseline " << baseline << std::endl;

    // center the text
    cv::Point textOrg( ( img.cols - textSize.width ) / 2,
        ( img.rows + textSize.height ) / 2 );

    // draw the box
    std::cout << "rectanle " << cv::Point( 0, baseline )
              << ", " << cv::Point( textSize.width, -textSize.height ) << std::endl;
    cv::rectangle( img, textOrg + cv::Point( 0, baseline ),
        textOrg + cv::Point( textSize.width, -textSize.height ),
        cv::Scalar( 0, 255, 0 ), 1, 8 );

    // ... and the baseline first
    std::cout << "baseline " << cv::Point( 0, thickness )
              << ", " << cv::Point( textSize.width, thickness ) << std::endl;
    cv::line( img, textOrg + cv::Point( 0, thickness ),
        textOrg + cv::Point( textSize.width, thickness ),
        cv::Scalar( 0, 0, 255 ), 1, 8 );

    // then put the text itself
    ft2->putText( img, text, textOrg, fontHeight,
        cv::Scalar::all( 255 ), thickness, linestyle, true );

    cv::imshow( "img_path", img );

    cv::waitKey( 0 );
}

without surprise, the result is

qcf-fc43-no-advance

and now, adding advance to getTextSize

#include "freetype.hpp"
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

int main()
{
    //cv::String text = "\ufc45\ufb50\ufc44\ufb50\ufc43\ufb50\ufc42\ufb50\ufc41";
    //cv::String text = "\ufc44\ufb50\ufc43\ufb50\ufc42\ufb50\ufc41";
    cv::String text = "\ufc43";
    int fontHeight = 150;
    int thickness = 1;
    int linestyle = 8;
    cv::Mat img( 400, 1200, CV_8UC3, cv::Scalar::all( 0 ) );
    int baseline = 0;
    int advance = 0;

    cv::Ptr<cv::freetype::FreeType2> ft2;
    ft2 = cv::freetype::createFreeType2();

    ft2->setSplitNumber( 8 );

    ft2->loadFontData( "QCF2001.ttf", 0 );
    cv::Size textSize = ft2->getTextSize( text,
        fontHeight,
        thickness,
        &baseline,
        &advance );
    if ( thickness > 0 ) {
        baseline += thickness;
    }

    // show baseline and advance
    std::cout << "baseline " << baseline << ", advance "
              << advance << ", Size " << textSize << std::endl;

    // center the text
    cv::Point textOrg( ( img.cols - advance ) / 2, // ( img.cols - textSize.width ) / 2,
        ( img.rows + textSize.height ) / 2 );

    // draw the box
    std::cout << "rectanle " << cv::Point( advance - textSize.width, baseline )
              << ", " << cv::Point( advance, -textSize.height ) << std::endl;
    cv::rectangle( img, textOrg + cv::Point( advance - textSize.width, baseline ),
        textOrg + cv::Point( advance, -textSize.height ),
        cv::Scalar( 0, 255, 0 ), 1, 8 );

    // ... and the vertical origin
    std::cout << "origin " << cv::Point( 0, baseline )
              << ", " << cv::Point( 0, thickness ) << std::endl;
    cv::line( img, textOrg + cv::Point( 0, baseline ),
        textOrg + cv::Point( 0, -textSize.height ),
        cv::Scalar( 0, 0, 255 ), 1, 8 );

    // ... and the baseline first
    std::cout << "baseline " << cv::Point( advance - textSize.width, thickness )
              << ", " << cv::Point( advance, thickness ) << std::endl;
    cv::line( img, textOrg + cv::Point( advance - textSize.width, thickness ),
        textOrg + cv::Point( advance, thickness ),
        cv::Scalar( 0, 0, 255 ), 1, 8 );

    // then put the text itself
    ft2->putText( img, text, textOrg, fontHeight,
        cv::Scalar::all( 255 ), thickness, linestyle, true );

    cv::imshow( "img_path", img );

    cv::waitKey( 0 );
}

and the result is a matching bounding box

qcf-fc43

the submited proposition is

add this function to freetype.hpp

CV_WRAP virtual Size getTextSize(const String& text,
                    int fontHeight, int thickness,
                    CV_OUT int* baseLine,
                    CV_OUT int* advanceX) = 0;

add this implementation to freetype.cpp

...
Size getTextSize(
    const String& text, int fontHeight, int thickness,
    CV_OUT int* baseLine, CV_OUT int* advanceX ) CV_OVERRIDE;
...
...
Size FreeType2Impl::getTextSize(
    const String& _text,
    int _fontHeight,
    int _thickness,
    CV_OUT int* _baseLine,
    CV_OUT int* _advanceX )
{
    if ( _text.empty() ) {
        return Size( 0, 0 );
    }

    CV_Assert( _fontHeight >= 0 );
    if ( _fontHeight == 0 ) {
        return Size( 0, 0 );
    }

    CV_Assert( !FT_Set_Pixel_Sizes( mFace, _fontHeight, _fontHeight ) );

    hb_buffer_t* hb_buffer = hb_buffer_create();
    CV_Assert( hb_buffer != NULL );
    Point _org( 0, 0 );

    if (advanceX)
      *_advanceX = 0;

    unsigned int textLen;
    hb_buffer_guess_segment_properties( hb_buffer );
    hb_buffer_add_utf8( hb_buffer, _text.c_str(), -1, 0, -1 );
    hb_glyph_info_t* info = hb_buffer_get_glyph_infos( hb_buffer, &textLen );
    CV_Assert( info != NULL );
    hb_shape( mHb_font, hb_buffer, NULL, 0 );

    _org.y -= _fontHeight;
    int xMin = INT_MAX, xMax = INT_MIN;
    int yMin = INT_MAX, yMax = INT_MIN;

    for ( unsigned int i = 0; i < textLen; i++ ) {
        CV_Assert( !FT_Load_Glyph( mFace, info[i].codepoint, 0 ) );

        FT_GlyphSlot slot = mFace->glyph;
        FT_Outline outline = slot->outline;
        FT_BBox bbox;

        if (advanceX)
          *_advanceX += slot->advance.x >> 6;

        // Flip
        FT_Matrix mtx = { 1 << 16, 0, 0, -( 1 << 16 ) };
        FT_Outline_Transform( &outline, &mtx );

        // Move
        FT_Outline_Translate( &outline,
            cOutlineOffset,
            cOutlineOffset );

        // Move
        FT_Outline_Translate( &outline,
            ( FT_Pos )( _org.x << 6 ),
            ( FT_Pos )( ( _org.y + _fontHeight ) << 6 ) );

        CV_Assert( !FT_Outline_Get_BBox( &outline, &bbox ) );

        // If codepoint is space(0x20), it has no glyph.
        // A dummy boundary box is needed when last code is space.
        if (
            ( bbox.xMin == 0 ) && ( bbox.xMax == 0 ) && ( bbox.yMin == 0 ) && ( bbox.yMax == 0 ) ) {
            bbox.xMin = ( _org.x << 6 );
            bbox.xMax = ( _org.x << 6 ) + ( mFace->glyph->advance.x );
            bbox.yMin = yMin;
            bbox.yMax = yMax;

            bbox.xMin += cOutlineOffset;
            bbox.xMax += cOutlineOffset;
            bbox.yMin += cOutlineOffset;
            bbox.yMax += cOutlineOffset;
        }

        xMin = cv::min( xMin, ftd( bbox.xMin ) );
        xMax = cv::max( xMax, ftd( bbox.xMax ) );
        yMin = cv::min( yMin, ftd( bbox.yMin ) );
        yMax = cv::max( yMax, ftd( bbox.yMax ) );

        _org.x += ( mFace->glyph->advance.x ) >> 6;
        _org.y += ( mFace->glyph->advance.y ) >> 6;
    }

    hb_buffer_destroy( hb_buffer );

    int width = xMax - xMin;
    int height = -yMin;

    if ( _thickness > 0 ) {
        width = cvRound( width + _thickness * 2 );
        height = cvRound( height + _thickness * 1 );
    } else {
        width = cvRound( width + 1 );
        height = cvRound( height + 1 );
    }

    if ( _baseLine ) {
        *_baseLine = yMax;
    }

    return Size( width, height );
}
...

and the whole story become

the-whole-picture

abar52 avatar Oct 29 '18 09:10 abar52

Hi, nice one! I know this is old, but I am struggling with the fact that I want a real bounding box for text too.

According to this, are you computing what is called the "descender" for _advanceX? image I don't really know how to write in Arabic script (apart from the fact that the text goes from right to left), but i think there are accents too, as in Latin fonts?

Look at this one, the font is Open dyslexic v3, I used bottomLeftOrigin=false in putText() image The second horizontal white line is the baseline. The first line is the origin, so we can suppose the "gap" between the first line and the letter A is the "internal leading", the place where the accents are supposed to be added. In my example, the accent for the letter E goes OVER the first line. This is a second problem.

Does your bounding box take this into account ? The height of the bounding box should be "ascender" + "descender". But with the example above, we can see that there are exceptions !

What is called "fontHeight" in OpenCV is only the "ascender", and that's an error if we consider the definitions from the first image for "font height" and "point size".

Maybe I should open a bug.

AbsurdePhoton avatar Jan 12 '24 01:01 AbsurdePhoton