とある科学の備忘録

とある科学の備忘録

CやPythonのプログラミング、Arduino等を使った電子工作をメインに書いています。また、木製CNCやドローンの自作製作記も更新中です。たまに機械学習とかもやってます。

【C++】MinGW (g++)とCMakeでOpenCVとopencv-contributeをビルドする(Windows)

  

バージョン

$ cmake --version
cmake version 3.17.2

$ make -v
GNU Make 3.81

$ mingw32-make -v
GNU Make 4.2.1

$ g++ -v 
gcc version 8.1.0 

はじめに

タイトルの通りです。開発環境がVisual Studioを使っているならば、ココの「Windows」ボタンからビルド済みのものをインストールできますが、g++, gccでビルドされたものは見つかりませんでした。
[追記]...と思っていたら、ありました!!
https://github.com/huihut/OpenCV-MinGW-Build
まぁでも最新バージョンではないっぽいし、自分でビルドすることにしました。


ということで、OpenCVの適当なバージョンをダウンロードして、cmakeで普通にやろうとしたのですが、

$ cmake [directry]

ってしても、抑もConfigureが通りません。そして、そのままmakeをしても、

gcc: error: long: No such file or directory
C:\PROGRA~1\MINGW-~1\X86_64~1.0-P\mingw64\bin\windres.exe: preprocessing failed.
make[2]: *** [modules/core/CMakeFiles/opencv_core.dir/vs_version.rc.obj] エラー 1
make[1]: *** [modules/core/CMakeFiles/opencv_core.dir/all] エラー 2
make: *** [all] エラー 2

 
もちろんですがエラーが出てしまいました。

ということで、CMakeのGUIを使って設定する必要があります。


ビルド方法

1. opencv-3.4 とcontributeをダウンロードする
ここ↓から、最新バージョンのOpenCVをgit cloneしてきます。
github.com
そして、ここ↓から最新バージョンのOpencv-contributeをgit cloneしてきます。
github.com

Contributeとは:OpenCvの追加物みたいなもので、顔認識やセグメンテーションのような機能を追加できます(機能一覧についてはここを見てください)。十分にテストされていないAPIなので、OpenCV本体と一緒にするべきではない、という考えのようです。

 

2. opencv-3.4の中にbuildフォルダを作る(名前は何でもOK)

opencv-3.4
↳build_mingw (←作成)
opencv_contrib-3.4

みたいな感じです。


3. Cmake GUIを開く
CMakeGUIを開いて、一番上にある「Where is the source code」と「Where to build the binaries」を設定します。
ソースコードの位置は hoge/opencv-3.4、ビルド先にはhoge/opencv-3.4/build_mingw みたいな感じで先ほど作ったフォルダを指定します。
そして、Configureボタンを押します。

4. コンパイラMinGWに指定する
Configureボタンを押すと、以下の様なコンパイラーを選択する画面が表示されます。
f:id:pythonjacascript:20201113010159j:plain
この写真のように設定してFinishを押します  

7. 以下の様な設定にします。
Configureが終わると、未解決の項目が赤くなって大量に表示されているはずです。ここからビルド設定を行います。ここは個人の環境によって変わるかもしれませんが、僕の環境では以下の設定でビルドが通りました。

  • BUILD_JAVAのチェックを外す
  • BUIKD_PYTHONのチェックを外す
  • BUILD_OPENEXRのチェックを外す
  • WITH_IPPのチェックを外す
  • ENABLE_PRECOMPILED_HEADERS のチェックを外す
  • WITH_OPENGL のチェックを付ける
  • OPENCV_ENABLE_NONFREEのチェックを付ける
  • OPENCV_EXTRA_MODULES_PATH のパスを「hoge/opencv_contrib-3.4/modules」に設定する
  • BUILD_opencv_worldのチェックを付ける(これをチェックすると、libファイルやdllファイルが「opencv_world_[version].dll」の様な名前で一つにまとめてくれる)


7. DOPENCV_ALLOCATOR_STATS_COUNTER_TYPEを追加
CMake GUIの右上に「Add Entry」というボタンがあるので、そこを押します。
そして、
f:id:pythonjacascript:20201113010950j:plain

  • Name = DOPENCV_ALLOCATOR_STATS_COUNTER_TYPE
  • Type = STRING
  • Value = int64_t(上写真ではDescriptionの所に書き込んでいますがミスですm(__)m。Valueが正解)

となるように設定します。

f:id:pythonjacascript:20201113011135j:plain
このように表示されればOKです。


10. 「Configure」→「Generate」
これで、一通りの設定が終わったので、もう一度Configure をしてみましょう!
エラーが出ずに最後までConfigureできたら成功です。Generateもします。


11. コマンドプロンプトで「make」!
ここまで順調に来ていれば、mingw_buildフォルダ(2.で作成したフォルダ)にMakeFileがあるはずなので、

$ cd mingw_make  #(2.で作成したフォルダ)
$ make

でmakeします。

上手くいけば、以下のファイルが生成されているはずです

  • mingw_make/bin/libopencv_world3412.dll
  • mingw_make/lib/libopencv_world3412.dll.a

上の二つがあればビルドはできています!!(*'ω'*)


HelloWorld

適当にプログラムを作ります。HelloWorldという文字を出力するプログラムです。

//main.cpp
#include <opencv2/opencv.hpp>
int main() {
  cv::Mat image(200, 800, CV_8UC3);
  
  cv::String text = "Hello, world";
  cv::putText(image, text, cv::Point(100,100), cv::FONT_HERSHEY_SIMPLEX, 3, cv::Scalar(255,255,0), 5, cv::LINE_AA);// テキストを描画

  // 画像を表示
  cv::imshow("hello_world", image);
  cv::waitKey(0);
  return 0;
}



CmakeListsは以下のようになります。

# CmakeLists.txt
cmake_minimum_required(VERSION 2.8)

set(PROJECT_NAME Test)
project(${PROJECT_NAME} CXX)

set(OpenCV_DIR "hogw/mingw_build") #2.で作成したフォルダ
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
    include_directories(${OpenCV_INCLUDE_DIRS})
    link_libraries(${OpenCV_LIBS})
endif()

add_executable(${PROJECT_NAME} main.cpp main.hpp)
target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBRARIES})


コマンドプロンプトで、

$ cd (上で書いた.cppとCMakeFilesがあるディレクトリ)
$ mkdir build 
$ cmake .. -G"MinGW Makefiles"  # コンパイラをMinGWに指定する
$ make

これでTest.exeが生成されればOKです!!

動的ライブラリファイル「(2.で作ったフォルダ)/bin/libopencv_world3412.dll」をTest.extと同じ階層にコピーしてきます。
これで、OpenCVのプログラムを実行できるはずです、、、!

HelloWorld実行

実行できたぁ~~
f:id:pythonjacascript:20201113012407j:plain

【C++/MinGW】FreeTypeを使ってTTFファイルから文字レンダリング

FreeTypeとは?

FreeTypeはフォントエンジンを実装したライブラリです。つまり、std::stringとかcharとかの文字列を画像に変換することができます。この時、フォントファイル(ttf等)を指定したり、太字/斜体でレンダリングすることできます。TrueType、Type1フォント、OpenTypeなどのフォント形式をサポートしているようです。


FreeTypeのインストール

まず、下のURKから、最新版のFreeTyoeをダウンロードしてきます。
FreeType Downloads

僕がダウンロードしたのはバージョン2.10.4です。

解凍して中身を見ると、CMakeListsがあったので、CMakeで入れることにします。

$ cd [ダウンロードしたFreeTypeライブラリのフォルダ]
$ mkdir build
$ cd build
$ cmake .. -G "MinGW Makefiles" 
$ make

下から二行目の"MinGW Makefiles" はコンパイラを指定しているだけで、通常は付ける必要はありません。筆者の環境ではVisualStudioのコンパイラとg++が混在しているので一応指定しておいただけです。

makeコマンドがうまくいくと、作成したbuildフォルダの中に「libfreetype.a」ができるので、これをリンクすればOKです。


サンプルコード

#include <opencv2/opencv.hpp>

#include <stdio.h>
#include <math.h>
#include <codecvt>
#include <string>
#include <iostream>
#include <locale>

#include <ft2build.h>
#include FT_FREETYPE_H

#define WIDTH   500
#define HEIGHT  300


class TextRender{
private:
  FT_Library library;   /* handle to library     */
  FT_Face face;      /* handle to face object */
  unsigned char *image;
  void draw_bitmap(int x, int y);
  FT_GlyphSlot slot;  // グリフへのショートカット
  std::u32string  u32str;

public:
  TextRender();
  ~TextRender();
  bool init();
  bool setFont(const char *font_file_name, int char_width, int char_height);
  void drawString(const char text[]);
  unsigned char *getImage();
};

TextRender::TextRender():image(nullptr){ init();}


// 1. libraryを初期化
bool TextRender::init(){
  if(image != nullptr) free(image);
  image = nullptr;
  auto error = FT_Init_FreeType( &library ) ;
  if (error) { std::cerr << "[ERROR] Failed to init FreeType library!" << std::endl; }
  return !error;
}

//フォントファイルを読み込む
bool TextRender::setFont(const char *font_file_name, int char_width, int char_height){
  // 2. faceを作成
  auto error = FT_New_Face( library, font_file_name, 0, &face);
  if ( error == FT_Err_Unknown_File_Format ){
    std::cerr << "[ERROR] Font file format is not supported!! " << std::endl; return false;
  }else if ( error ){
    std::cerr << "[ERROR] Font file not found or it is broken! " << std::endl; return false;
  }
  
  // 3. 文字サイズを設定
  error = FT_Set_Char_Size(face, 0,
                  char_width*char_height, // 幅と高さ
                  300, 300);  // 水平、垂直解像度*/
  
  slot = face->glyph;  // グリフへのショートカット
  return !error;
}


void TextRender::drawString(const char text[]){
  image = new unsigned char[WIDTH*HEIGHT];
  
  std::u32string  u32str  = std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t>().from_bytes(text);

  int curPosX = 0;
  int curPosY = 60; //現在のカーソル位置
  int last_height = 0; //最後に文字を書いたときの文字の大きさ
  
  for (int n = 0; n < u32str.size(); n++ ){
    if(u32str[n] == '\n'){
      curPosX = 0;
      curPosY += last_height + 20;
    }else{
      if (FT_Load_Char( face, u32str[n], FT_LOAD_RENDER )) continue; //一文字レンダリング
      // int yMax = face->bbox.yMax;
      // int yMin = face->bbox.yMin;
      // int baseline = bitmap->rows * yMax / (yMax - yMin);
      draw_bitmap(curPosX, curPosY - slot->bitmap_top); //imageにslot->bitmapの中身をコピーする
    }
    last_height = (slot->bitmap).rows;

    curPosX += slot->advance.x >> 6;
    curPosY += slot->advance.y >> 6;
   }
};

//生成された位置も自分の画像データをimageにコピーする
void TextRender::draw_bitmap(int x, int y){
  int  i, j, p, q;
  const int  x_max = x + (slot->bitmap).width;
  const int  y_max = y + (slot->bitmap).rows;

  for ( i = x, p = 0; i < x_max; i++, p++ ){
    for ( j = y, q = 0; j < y_max; j++, q++ ){
      if ( i < 0      || j < 0       || i >= WIDTH || j >= HEIGHT) continue;
      image[j*WIDTH + i] |= (slot->bitmap).buffer[q *(slot->bitmap).width + p];
    }
  }
}


unsigned char *TextRender::getImage(){ return image; }

TextRender::~TextRender(){
  if(image != nullptr) free(image);
  FT_Done_Face    ( face );
  FT_Done_FreeType( library );
}


int main(){
  TextRender TR;
  TR.setFont("Meiryo.ttf", 16, 34);


  //レンダリング
  unsigned char *image;
  const char text[] = "こんにちは、世界!\nHello, World!!";
  TR.drawString(text);
  image =  TR.getImage();


  //画像を表示
  cv::Mat image_cv(HEIGHT, WIDTH, CV_8UC1,image);   
  cv::imshow("output", image_cv);
  cv::waitKey(0);

  return 0;
}


上のコードをコンパイルする為のCmakeListtsも載せておきます。
CmakeListsでしていることは

  • OpenCVを見つけていれる
  • [ダウンロードしたFreeTypeライブラリ]/includeフォルダを#includeでの読み込み対象にする
  • libfreetype.aをリンクする
  • main.cpp(上のプログラムのこと)から実行ファイルを生成

という感じです

cmake_minimum_required( VERSION 3.5 )
project(test CXX)

set(OpenCV_DIR "C:/Users/Owner/Library/Opencv/opencv-3.4/build_mingw")
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
    include_directories(${OpenCV_INCLUDE_DIRS})
    link_libraries(${OpenCV_LIBS})
endif()

set(FREETYPE_DIR "C:/Users/Owner/Library/freetype-2.10.4")
include_directories(${FREETYPE_DIR}/include)

add_executable( test main.cpp)
target_link_libraries( test ${FREETYPE_DIR}/build/libfreetype.a ${OpenCV_LIBRARIES})

実行結果

下の画像の様な結果になります
Meiryo.ttfが読み込まれて日本語もきちんと表示できているのがわかると思います
f:id:pythonjacascript:20201030181324j:plain
  

FreeTypeの使い方

初期化

まず、こんな感じでincludeします

#include <ft2build.h>
#include FT_FREETYPE_H

次に、色々初期化します。

FT_Library library;
FT_Face face; 
  
// 1. libraryを初期化
auto error = FT_Init_FreeType( &library ) ;
if (error) {... }

// 2. faceを作成
error = FT_New_Face( library, "hoge.ttf", 0, &face);
if (error) {... }

  
  // 3. 文字サイズを設定
  error = FT_Set_Char_Size(face, 0,
                  16*54, // 幅と高さ
                  300, 300);  // 水平、垂直解像度*/

ここでいじるのはフォントファイル名と文字サイズくらいだと思います

太字or斜体に設定する

太字や車体などの設定は、以下の関数を使ってグリフに適応させます

FT_GlyphSlot_Oblique(face->glyph );  //斜体にする
FT_GlyphSlot_Embolden(face->glyph );//太字にする

これらを使うにはグリフを読み込む必要があり、TextRender::drawString関数のfor文の中身を以下の様に変更する必要があります。

  for (int n = 0; n < u32str.size(); n++ ){
    if(u32str[n] == '\n'){
      curPosX = 0;
      curPosY += last_height + 20;
    }else{
     // 追加!今からレンダリングする式のグリフを取得
      FT_UInt glyph_index = FT_Get_Char_Index( face, u32str[n] );

     // 追加!そのグリフの設定を読み取る(ここで1bitビットマップに設定したりカラー画像にしたり色々変えられる)
      auto error = FT_Load_Glyph( face, glyph_index, FT_LOAD_DEFAULT );
      if ( error )continue;  /* ignore errors */
    
     // 追加!
      FT_GlyphSlot_Oblique(face->glyph );  //斜体にする
      FT_GlyphSlot_Embolden(face->glyph );//太字にする

      //追加!1文字レンダリング!
      error = FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL );
      if ( error ) continue;

      draw_bitmap(curPosX, curPosY - slot->bitmap_top); //imageにslot->bitmapの中身をコピーする
    }
    last_height = (slot->bitmap).rows;

    curPosX += slot->advance.x >> 6;
    curPosY += slot->advance.y >> 6;
   }

 
上のプログラムの様に、各文字のグリフを取得して、それ経由でレンダリングすることもできます。その時に、太字は斜体などの設定をグリフに適応させる、という事でしょう。
ちなみに、最初のプログラムで使っていた FT_Load_Char関数とごちゃ混ぜにするとフリーズするので気を付けてください。

また、FT_GlyphSlot_Oblique関数とFT_GlyphSlot_Embolden関数は freetype/ftsynth.h で宣言されているので、冒頭のinclude文を

#include <ft2build.h>
#include <freetype/ftsynth.h>
#include FT_FREETYPE_H

に書き直してください

参考サイト→FreeType2での太字、斜体 - HYPERSPACE UNIVERSE NEWS

実行するとこうなります:
f:id:pythonjacascript:20201030190940j:plain
 

レンダリング(方法1)

FT_Load_Char( face, ’a’, FT_LOAD_RENDER)

このように書くことで、'a'という一文字がレンダリングされて、face->glyph.bitmapにビットマップとして格納されます。(上のプログラムでは、slot = face->glyph;というショートカットを作成しているので、slot->bitmapと書いています)

このビットマップのサイズ等は以下の様に取得できます

計算
横幅 slot->bitmap.width
高さ slot->bitmap.rows
yMax face->bbox.yMax;
yMin face->bbox.yMin
文字のベースライン bitmap->rows * yMax / (yMax - yMin)

yMinとyMaxが何を意味するのか分かりませんが、文字のベースラインを計算するにはこの値を使うみたいです。

レンダリング(方法2)

「太字or斜体にする」の節で使用した、グリフに基づくレンダリング方法です

// 文字'a'のグリフを取得
FT_UInt glyph_index = FT_Get_Char_Index( face, 'a');
auto error = FT_Load_Glyph( face, glyph_index, FT_LOAD_DEFAULT );// そのグリフの設定を読み取る
FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL );//1文字レンダリング!

行数が上の方法1の三倍になっていますが、やっていることは同じで「a」をface->glyph.bitmapにビットマップとして格納します

【Python】無料でジオコーディング(住所→緯度経度etc.)

住所⇔緯度&経度の変換をしたい、目的地までのルート検索や所要時間を調べたい、と思い良いものがないかなぁと思っていました。
すると、↓の記事でGoogle Maps APIというものがあることを知り、
qiita.com

使ってみよ~~、と思ったのですが、なんと有料でした!


一学生として有料は厳しい、、、、。で調べていくと、OSM(Open Street Map)が提供しているデータを使えばジオコーディングが無料でできるらしい。おお~~(≧▽≦)


geopyというものを使うと、住所→緯度&軽度の取得ができます

  • Python 3.7.3 on win32
  • geographiclib-1.50 geopy-2.0.0
$ pip install geopy

でgeopyを入れます。

プログラム

# -*- coding: utf-8 -*-
from geopy.geocoders import Nominatim

def main():
  geolocator = Nominatim(user_agent="test-dayo")
  location = geolocator.geocode("日本 北九州")
  print("Lat, long = ",location.latitude, location.longitude)
  print("full address = ", location.address)

  # 辞書として読み取る
  loc_dict = dict(location.raw)
  print("Lat, long = ", loc_dict["lat"], loc_dict["lon"])
  print("full address = ", loc_dict["display_name"])
  print("class and type = ", loc_dict["class"], loc_dict["type"])

  url = f"https://www.google.com/maps/search/?api=1&query={location.latitude, location.longitude}"
  
  import webbrowser
  webbrowser.open(url)

main()

上のプログラムを実行すると、

Lat, long =  35.675886399999996 139.74505141191034
full address =  国会議事堂, 1, 国道246号, 永田町1, 永田町, 千代田区, 100-0014, 日本 (Japan)
Lat, long =  35.675886399999996 139.74505141191034
full address =  国会議事堂, 1, 国道246号, 永田町1, 永田町, 千代田区, 100-0014, 日本 (Japan)
class and type =  office government

 
と表示され、ブラウザで国会議事堂のGoogleMapが開くと思います。
f:id:pythonjacascript:20201024111219j:plain


ほかのライブラリ

他にも数種類GeoCodingをするPythonライブラリがあるみたいです。

GeoCoder

$ pip install geocoder

でgeocoderを入れます。

import geocoder

# 下のコードはGoogleAPIの登録が必須になってから動かなくなっている
# g = geocoder.google('Mountain View, CA')
# print(g.latlng)

location = '日本 国会議事堂'
ret = geocoder.osm(location, timeout=5.0)
print(ret.latlng)

# 出力結果:[35.675886399999996, 139.74505141191034]

medium.com