putText()関数でtruetype fontを使ったテキスト描画ができるようになりました！

この記事は[OpenCV Advent Calendar 2020](https://qiita.com/advent-calendar/2020/opencv)の19日目の記事です．

続編→ [putText()関数でtruetype fontを使った *"日本語"* 描画ができるようになります！](https://qiita.com/hon_no_mushi/items/9b98d35ed0b2263c437b)


# TL;DR

- OpenCV imgprocのputText()関数で、STB Libraryによるtruetype fontを使ったテキスト描画ができるようになりました！
    - UTF-8入力はうれしい。
    - 追加ライブラリもいらない！
    - フォントデータもあらかじめ組み込まれている、
    - harfbuzzライブラリがあれば、リガチャできる！（これは大きい）
    - 内蔵フォントでほぼ何も考えなくてもきれいに描画できる！
    - 外部フォント指定は~~…できないかも？~~ 複数個指定できます！！（2020/12/20 6:15更新)
    - G-API対応もできている（っぽい）
    - これから更なる発展が期待できる！！

2020/12/19 7:19 追記。
「なりました」と書いているけど、厳密には「なります（未来系）」が正しい気が若干しますので、一応メモ。

2020/12/20 6:18 追記。
「外部フォント指定もできますよ！」とのことでしたので、こちらも追記しました。最強ですね……

ありがとうございます！

## Motivation (使いまわし）

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">あっ，freetype2モジュールネタで書いてくれそうな人だ！あと，OpenCVのテキスト描画周りはちょっと進展あったみたいです．<a href="https://t.co/c89KwQge7M">https://t.co/c89KwQge7M</a></p>&mdash; dandelion (@dandelion1124) <a href="https://twitter.com/dandelion1124/status/1323588271604035584?ref_src=twsrc%5Etfw">November 3, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

https://github.com/opencv/opencv/wiki/2020#2020-10-28

> Another round with truetype font suppport. Made use of STB_truetype (tiny, 4Kloc single-file ttf parser and renderer), patched it to support variable fonts, almost refactored previous patch to use STB_truetype instead of freetype. Patch size went down from several megabytes to ~300Kb.

なるほど！！STB! なんだこれ！から調査をしてみる。

# STB_truetypeとは？

- https://github.com/nothings/stb
- single-file public domain (or MIT licensed) libraries for C/C++
- 1つのファイルだけで、色々な機能を実現できる(しかもMIT License... ありがたや、ありがたや...)
    - stb_image.h (7762 byte) : image loading/decoding from file/memory: JPG, PNG, TGA, BMP, PSD, GIF, HDR, PIC
    - stb_truetype.h (5011 byte) : parse, decode, and rasterize characters from truetype fonts
    - stb_sprintf.h ( 1879 byte) : fast sprintf, snprintf for C/C++
    - etc...


# 実際に環境構築してみよう！

## 注意事項(2020/12/19 7:20追記)

OpenCV 4.5.0 と、OpenCV Next(実質5.0)は共存うまくできない・・・かも？このあたりは要調査ですが、nextをテストするときは4.5.0を sudo make uninstall しておいたほうが無難ですね。

## OpenCV nextのダウンロード

### OpenCVのbranch構成

まず、OpenCVのbranch構成はこうなっている。

- master ( default )
- 2.4
- 3.4
- next ★

2020/12/2 現在、この修正はnext branchにしか入っていないので、こちらをcloneする。

### next branchのcloneとconfiguration

このあたりはいつもの手順。

```bash
$ git clone -b next https://github.com/opencv/opencv.git
$ mv opencv opencv_next
$ cd opencv_next
$ mkdir build ; cd build 
$ cmake .. ; cmake-gui ..
```

### ソースコード

……これ、1行もHERSHEYのコードから直してないんですけど…

```c
#include <opencv2/imgproc.hpp> // cv::FONT*, cv::LINE*, cv::FILLED
#include <opencv2/highgui.hpp> // imwrite
#include <iostream>

using namespace cv;

int main(int argc, char *argv[])
{
    if (argc < 2) {
        std::cerr << "Filename required" << std::endl;
        return 1;
    }

    String text = "Funny text inside the box";
    int fontFace = FONT_HERSHEY_SCRIPT_SIMPLEX;
    double fontScale = 2;
    int thickness = 3;

    Mat img(600, 800, CV_8UC3, Scalar::all(0));

    int baseline=0;
    Size textSize = getTextSize(text, fontFace,
                                fontScale, thickness, &baseline);
    baseline += thickness;

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

    // draw the box
    rectangle(img, textOrg + Point(0, baseline),
              textOrg + Point(textSize.width, -textSize.height),
              Scalar(0,0,255));
    // ... and the baseline first
    line(img, textOrg + Point(0, thickness),
         textOrg + Point(textSize.width, thickness),
         Scalar(0, 0, 255));

    // then put the text itself
    putText(img, text, textOrg, fontFace, fontScale,
            Scalar::all(255), thickness, 8);

    cv::imwrite(argv[1], img);
    return 0;
}
```

### 実行結果

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/a2be9a69-39ec-44c2-8b12-114dec5240d3.png)

お、おおお！！！　骨骨じゃない！！truetype fontだああああ！！！

でもでも、色々疑問が残りますよね！！

- フォントファイル指定してなかったよなあ・・・
- フォントファイルはどこからでてきたんだ？

<参考>
OpenCV 4.5.0 で、line_width = 1 にするとこういう画像になる。

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/587bd2b3-c620-8ba7-b63e-3ea2101e8280.png)


### もうちょっと出力例

http://opencv.jp/cookbook/opencv_drawing.html#id8

これをベースに、OpenCV 4.5.0 と nextの出力例を比較してみたかったけど、



OpenCV 4.5.0, linewidth = 1

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/693f7a92-987a-a2dd-d016-7a618bc7c972.png)


OpenCV 4.5.0, linewidth = 2

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/88cd722c-eea5-5e1c-fd75-07ecc4cd9a30.png)

next, line width = 1

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/33f99f87-fb6b-7222-36be-9f9fd74c62f0.png)

next, line width = 2

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/890a363b-1f1c-5f2c-ed97-f02a41833390.png)

### ソースコード

http://opencv.jp/cookbook/opencv_drawing.html#id8 を一部修正。

```c:a.cpp
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

int
main(int argc, char *argv[])
{
  cv::Mat img = cv::Mat::zeros(500, 500, CV_8UC3);

  int face[] = {cv::FONT_HERSHEY_SIMPLEX, cv::FONT_HERSHEY_PLAIN, cv::FONT_HERSHEY_DUPLEX, cv::FONT_HERSHEY_COMPLEX,
                cv::FONT_HERSHEY_TRIPLEX, cv::FONT_HERSHEY_COMPLEX_SMALL, cv::FONT_HERSHEY_SCRIPT_SIMPLEX,
                cv::FONT_HERSHEY_SCRIPT_COMPLEX, cv::FONT_ITALIC};

#define CV_AA 16

  // 画像，テキスト，位置（左下），フォント，スケール，色，線太さ，種類
  cv::putText(img, "OpenCV", cv::Point(50,50), face[0], 1.2, cv::Scalar(0,0,200), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(50,100), face[1], 1.2, cv::Scalar(0,200,0), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(50,150), face[2], 1.2, cv::Scalar(200,0,0), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(50,200), face[3], 1.2, cv::Scalar(0,100,100), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(50,250), face[4], 1.2, cv::Scalar(100,100,0), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(50,300), face[5], 1.2, cv::Scalar(100,0,100), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(50,350), face[6], 1.2, cv::Scalar(100,100,100), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(50,400), face[7], 1.2, cv::Scalar(100,100,200), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,50), face[0]|face[8], 1.2, cv::Scalar(100,200,100), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,100), face[1]|face[8], 1.2, cv::Scalar(200,100,100), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,150), face[2]|face[8], 1.2, cv::Scalar(200,200,100), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,200), face[3]|face[8], 1.2, cv::Scalar(200,100,200), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,250), face[4]|face[8], 1.2, cv::Scalar(100,200,200), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,300), face[5]|face[8], 1.2, cv::Scalar(100,200,255), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,350), face[6]|face[8], 1.2, cv::Scalar(100,255,200), 1, CV_AA);
  cv::putText(img, "OpenCV", cv::Point(300,400), face[7]|face[8], 1.2, cv::Scalar(255,200,100), 1, CV_AA);

  cv::imwrite ("a.png", img);
}
```

## 外部フォントも描画できる (2020/12/20 6:22追記)

> 記事内の1つ目のソースコードをちょっと変更して外部フォントが使えることを確認しました。
> getTextSizeの方もオーバーロードが増えていたのでソースも読まずに見た目だけで調整してみました。が、これで合っているか自信がないです・・・

とコメント頂きましたので、

```
    int fontFace = FONT_HERSHEY_SCRIPT_SIMPLEX;
　　↓
    FontFace fontFace("フォントファイル");
```

に置き換える事で、日本語の描画もできる。素敵！！ご指摘、ありがとうございました！

# ちょっとライブラリをもう少し深堀してみる。

## opencv/modules/imgproc/fonts

gz圧縮されたフォントデータが格納されている。

- https://github.com/opencv/opencv/tree/next/modules/imgproc/fonts
- Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik),
- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

## opencv/modules/imgproc/src/drawing.cpp

従来のdrawing.cppでは、引数省略したときに全部引数を指定した場合の関数を呼び出すコードが含まれます。

おや？CvFont

```cpp
CV_IMPL void
cvPutText( CvArr* _img, const char *text, CvPoint org, const CvFont *_font, CvScalar color )
{
    cv::Mat img = cv::cvarrToMat(_img);
    CV_Assert( text != 0 && _font != 0);
    cv::putText( img, text, org, _font->font_face, (_font->hscale+_font->vscale)*0.5,
                color, _font->thickness, _font->line_type,
                CV_IS_IMAGE(_img) && ((IplImage*)_img)->origin != 0 );
}
```

## opencv/modules/imgproc/src/drawing_text.cpp

### 旧putText()

新規drawing_text.cpp で、いつも通りのputText()を呼び出すと、hersheyToTruetype()して、新しいputText()を呼び出すようになっている。これですね！

```c++
void putText( InputOutputArray _img, const String& text, Point org,
              int fontFace, double fontScale, Scalar color,
              int thickness, int, bool bottomLeftOrigin )

{
    String ttname;
    int ttsize = 0;
    int ttweight = 0;
    hersheyToTruetype(fontFace, fontScale, thickness, ttname, ttsize, ttweight);
    FontFace fface(ttname);
    PutTextFlags flags = bottomLeftOrigin ? PUT_TEXT_ORIGIN_BL : PUT_TEXT_ORIGIN_TL;
    putText(_img, text, org, color, fface, ttsize, ttweight, flags);
}

```

ここで、hersheyToTruetype()関数の中身を見ると、従来のfont指定を書き換えてくれる。だから、どのフォントを使えばいいのか指定をしなくても済んだのですね！

```c++
static void hersheyToTruetype(int fontFace, double fontScale, int thickness,
                              String& ttname, int& ttsize, int& ttweight)
{
    double sf = 0;
    switch(fontFace & ~FONT_ITALIC)
    {
    case FONT_HERSHEY_PLAIN:
        ttname = "sans";
        sf = 6.6;
        ttweight = thickness <= 1 ? 400 : 800;
        break;
    case FONT_HERSHEY_SIMPLEX:
        ttname = "sans";
        sf = 3.7;
        ttweight = thickness <= 1 ? 400 : 600;
        break;
    case FONT_HERSHEY_DUPLEX:
        ttname = "sans";
        sf = 3.7;
        ttweight = thickness <= 1 ? 600 : 800;
        break;
:
:
```


## 新putText()

新しいputText()では、fontRenderEngine instanceのputTextを呼び出すようになっています。


```
Point putText(InputOutputArray img_, const String& str,
             Point org, Scalar color_,
             FontFace& fontface, int size,
             int weight, PutTextFlags flags, Range wrap)
{
    uchar color[] =
    {
        saturate_cast<uchar>(color_[0]), saturate_cast<uchar>(color_[1]),
        saturate_cast<uchar>(color_[2]), saturate_cast<uchar>(color_[3])
    };
    Mat img = img_.getMat();
    int nch = img.channels();
    CV_Assert(img.depth() == CV_8U);
    CV_Assert(nch == 1 || nch == 3 || nch == 4);

    return fontRenderEngine.putText_(img, img.size(), str, org, color, fontface, size,
                                     weight, flags, wrap, true, 0);
}
```

# STB_truetypeでできること、できないこと


|   | (1) putText |  (2)freetype2 wrapper | (3)G-API FTEXT primitive | (4) putText（STB_truetype) |
|:-:|:-:|:-:|:-:|:-:|
| 追加ライブラリ | Not needed | Needed | Not needed | Not needed |
| 入力データ | ASCII | UTF-8 | UTF-16 | UTF-8 |
| リガチャ対応 | Not supported | OK | Not supported | OK |
| 描画色指定  | OK | OK | OK | OK |
| 描画線幅指定  | OK  | OK | Not supported | 若干Supported | 
| 内部フォント指定  | OK | Not supported | Not supported | OK |
| 外部フォント指定  | Not supported | OK | OK | ~~NG~~ OK!! |
| 複数外部フォント | Not supported | OK | Not supported | ~~NG~~ OK!! |
| 二値ビットマップ描画 | Not supported | OK | Not supported | Not supported |
| 多値ビットマップ描画 | Not supported | OK | OK | OK | 
| パス描画(二値/多値) | OK | OK | Not supported | Not supported |
| G-API対応 | OK | NG | OK | OK!!!! |
| Glyph Cache | (不要) | Not Supported | Supported | Supported |
| 8UC1 | OK | Not supported | OK(Maybe) | OK |
| 8UC3 | OK | OK | OK(Maybe) | OK |
| 8UC4 | OK | Not supported | OK(Maybe) | OK |

### FontFaceがちょっと気になる…
~~これを使えば外部フォントも利用できるかも？と考えたのですが、どうにも"sans"とかの指定だけで、ファイル名そのものが指定できないようにも見える。このあたりは正規版がでてから検証が必要ですね。~~

ここは、もうできる！！で終了です (2020/12/20 6:22)

# まとめ
- OpenCV imgprocのputText()関数で、STB Libraryによるtruetype fontを使ったテキスト描画ができるようになりました！
    - UTF-8入力はうれしい。
    - 追加ライブラリもいらない！
    - フォントデータもあらかじめ組み込まれている、
    - harfbuzzライブラリがあれば、リガチャできる！（これは大きい）
    - 内蔵フォントでほぼ何も考えなくてもきれいに描画できる！
    - 外部フォント指定は~~…できないかも？~~ 複数個指定できます！！（2020/12/20 6:15更新)
    - G-API対応もできている（っぽい）
    - これから更なる発展が期待できる！！


ご精読ありがとうございました！