C++版OpenCVの学習にJupyter notebookを使うためのメモ(g++, VC++)

# 概要

https://amzn.asia/d/9rAVQJL

自分が上記の本でC++版OpenCVの学習を行うために、下記の投稿記事をOpenCVの画像をnotebookに表示できるようにしたもので、内容はソースコードとメモ程度です。
各学習のソースと画像が同一画面で見られるのは学習において便利です。Python版OpenCVなら可能なので、C++版が出来ないか考え作ってみました。
Pythonなどのインタプリタと違って１セルごとにコンパイルするので、やはり反応の遅さ(特にVC++のコンパイルが遅い)を感じますが、実際VScodeやVisual Studioを使ってもやはりコンパイルは伴うので同じかも。
長いコードはVScodeで作って、Jupyter notebookにコピペして実行するのが一番良いかもしれません。


https://qiita.com/h6591/items/6fde2f0fc9cdeacb64d1

# Jupyter notebook スクリーンショット

## Mac版

- `imshow`を使うとブラウザ外に表示される
- `display`はnotebook上に表示、これは後述の`myheader.h`にある自作関数


![opencv01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/c6c46038-1406-46cb-bf38-1ac6839b1dd1.png)

![opencv02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/a7b0ef3f-c052-4095-9bd5-2bdde2895d57.png)



## Windows VC++版

![openvc_win.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/5f66780b-a580-4e9a-8194-ee07cdb67c15.png)


# ファイル構成

```terminal:xxxx=PYTHONPATHが通っているディレクトリ
xxxx/
├── gcc/
│   └── kernel.json
├── kernel/
│   └── gcckernel.py
└── magic/
    ├── gcc_magic.py  
    ├── magic_decorator.py
    └── image.py
```
```terminal:yyyy=Jupyter nootbookの起動ディレクトリ
yyyy/
├── img/
├── config_gcc.py
├── Makefile
└── myheader.h
```


### kernel情報

```gcc/kernel.json
{
    "argv": ["python", "-m",
             "kernel.gcckernel", "-f",
             "{connection_file}"],
    "display_name": "C++",
    "language": "c++",
    "env": {
        "PYTHONPATH": "/Users/user/xxxx"
    }
}
```

### kernel情報のインストール

```terminal
> jupter kernelspec install gcc --user
```

### kernel本体


Magic名`in`はPythonの予約語なので関数名として使えないので、`in`は`inside`に変換して処理する。

```kernel/gcckernel.py
import re
import importlib
from ipykernel.kernelbase import Kernel
from magic.magic_decorator import MagicReturn

GCC_MAGIC = "magic.gcc_magic"
 
# GCC用Custom Kernel
class GccKernel(Kernel):
    # cellデータの解析結果
    class MagicData:
        def __init__(self, magic="gcc", cell="", line="", iscell=True):
            self.magic = magic
            self.cell = cell
            self.line = line
            self.iscell = iscell
            
    implementation = 'gcc'
    implementation_version = '1.0'
    language_info = {
        'name': 'c++',
        'mimetype': 'text/x-c++src',
        'file_extension': '.cpp',
        'version': '17.0.0',
    }
    banner = "Compile by gcc and execute binary"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 後でmagicのmoduleを切り替えが可能にする目的で動的にしている(未実装)
        self.gccmagic_mod = importlib.import_module(GCC_MAGIC)
        magic_obj = self.gccmagic_mod.GCCMagics()
        self.line_magics = magic_obj.magics["line"]
        self.cell_magics = magic_obj.magics["cell"]     
        self.magic_obj = magic_obj # 念のため残す(たぶんgcされないと思う)

        
    def parse(self, code):
        mline = re.match("^.*$", code, re.MULTILINE)
        pos = mline.span()[1] + 1  # +1は改行コードを除くため
        cell = code[pos:]
        line = mline.group()
        magic_re = re.match(r" *(/{1,2})(\w+)", line)
        iscell = True            
        if magic_re == None:
            # 先頭行がmagicでないとき            
            default = self.magic_obj.default_magic()
            magic = default["magic"]
            cell = code
            line = default["line"]
        else:    
            line_pos = magic_re.span()[1] + 1
            line = line[line_pos:]
            magic_names = magic_re.groups()
            magic = magic_names[1]
            if magic_names[0] == "/":
                iscell = False
        return self.MagicData(magic, cell, line, iscell)
                
    def exec_magic(self, magic_data):
        if magic_data.magic == "in":
           magic_data.magic = "inside" 
        if magic_data.iscell:
            if magic_data.magic in self.cell_magics:
                func = self.cell_magics[magic_data.magic]
                return func(magic_data.line, magic_data.cell)
            else:
                return MagicReturn(stderr="該当するcell magicはありません")
        else:
            if magic_data.magic in self.line_magics:
                func = self.line_magics[magic_data.magic]
                return func(magic_data.line)
            else:
                return MagicReturn(stderr="該当するline magicはありません")
 
    def do_execute(self, code, silent, store_history=True, user_expressions=None,
                   allow_stdin=False):
        if not silent:
            src = code if code[-1] == "\n" else code + "\n"
            magic_data = self.parse(src)
            rc = self.exec_magic(magic_data)
            for stdout in rc.stdout:
                if stdout != "": 
                    stream_content = {'name': 'stdout', 'text': stdout}
                    self.send_response(self.iopub_socket, 'stream', stream_content)
            for stderr in rc.stderr:
                if stderr != "":
                    stream_content = {'name': 'stderr', 'text': stderr}
                    self.send_response(self.iopub_socket, 'stream', stream_content)
            for display in rc.display:
                self.send_response(self.iopub_socket, 'display_data', display)
                
        return {'status': 'ok',
                'execution_count': self.execution_count,
                'payload': [],
                'user_expressions': {},
               }

if __name__ == '__main__':
    from ipykernel.kernelapp import IPKernelApp
    IPKernelApp.launch_instance(kernel_class=GccKernel)
```

### Cell Magicの仕組みを使ったMagic処理コード

```magic/gcc_magic.py
import os
import importlib
import re
import shlex
import subprocess
from .image import make_image_data
from .magic_decorator import (Magics, magics_class,
                        cell_magic, line_magic, MagicReturn)

DELETE_FILES = []  # ["./a.out"]

class Config:
    module_name = "config_gcc"
    def __init__(self):
        self.mod = importlib.import_module(self.module_name)
        self.col = 6
        self.image_width = 200
        self.set_param()

    def reload(self):
        importlib.reload(self.mod)
        self.set_param()
        
    def set_param(self):
        params = self.mod.PARAMS
        self.main_source = params["main_source"]
        self.compile = params["compile"]
        self.delete_files = params["delete_files"]
        self.default_magic = params["default_magic"]
        self.image = params["image"]
        self.image_files = params["image_files"]
    
@magics_class
class GCCMagics(Magics):
    main_source = {}  # main関数内のソース
    other_source = {} # main関数外のソース
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.config = Config()
   
    @cell_magic
    def cc(self, line, cell):
        return self.compile_and_exec(cell)
    
    @cell_magic
    def inside(self, line, cell):
        src_key = line.strip()
        if src_key != "":
            self.main_source[src_key] = cell                
            rc = self.compile_and_exec(self.make_source())
        else:
            src = self.config.main_source
            rc = self.compile_and_exec(src.format("", cell))           
        return rc
    
    @cell_magic
    def inc(self, line, cell):
        self.clear("")
        return self.inside(line, cell)
    
    @cell_magic
    def out(self, line, cell, exec=True):
        src_key = line.strip()
        if src_key == "":
            self.other_source["50"] = cell
        else:
            self.other_source[src_key] = cell
        if exec:
            return self.compile_and_exec(self.make_source(), exec=False)
    
    @cell_magic
    def outc(self, line, cell):
        self.clear("")
        return self.out(line, cell)
    
    @cell_magic
    def io(self, line, cell):
        cellre = re.search("^///main//.*\n", cell, re.MULTILINE)
        span = cellre.span()
        out_src = cell[:span[0]]
        in_src = cell[span[1]:]
        self.out(line, out_src, False)
        return self.inside(line, in_src)

    @cell_magic
    def ioc(self, line, cell):
        self.clear("")
        if line.strip() == "":
            line = "55"
        return self.io(line, cell)

    @line_magic
    def list(self, line):
        return MagicReturn(stdout=self.make_source())

    @line_magic
    def run(self, line):
        return self.compile_and_exec(self.make_source())

    @line_magic
    def reconf(self, line):
        self.config.reload()
        msg = "reloaded {}".format(self.config.module_name)
        return MagicReturn(stdout=msg)  

    @line_magic
    def image(self, line):
        args = shlex.split(line)
        if len(args) != 2:
            return MagicReturn(stderr="パラメータが２個でない")
        if args[0].isnumeric:
            col = int(args[0])
            if col == 0:
                return MagicReturn(stderr="第1パラメータが正しくありません")
        else:
            return MagicReturn(stderr="第1パラメータが正しくありません")
        
        if args[1].isnumeric:
            image_width = int(args[1])
            if image_width < 50 and image_width > 0:
                return MagicReturn(stderr="第2パラメータが正しくありません")
        else:
            return MagicReturn(stderr="第2パラメータが正しくありません")
        self.col = col
        self.image_width = image_width

        msg = "イメージパラメータ設定完了 カラム数: {}, width: {}"
        msg = msg.format(self.col, self.image_width)
        return MagicReturn(stdout=msg)    

    @line_magic
    def clear(self, line):
        args = shlex.split(line)
        if len(args) == 0:
            self.main_source = {}
            self.other_source = {}
        for arg in args:
            if arg == "in":
                self.main_source = {}
            elif arg == "out":
                self.other_source = {}                
        return MagicReturn(stdout="クリアしました") 

    def make_source(self, src_key=None):
        other_src = ""
        main_src = ""
        for key in sorted(self.other_source):
            other_src += "//out {} \n".format(key)
            other_src += self.other_source[key]
                
        for key in sorted(self.main_source):
            main_src += "//in {} \n".format(key)
            main_src += self.main_source[key]
        
        src = self.config.main_source
        return src.format(other_src, main_src)
               
    def compile_and_exec(self, src, exec=True):
        comp = self.config.compile
        if exec:
            rc = subprocess.run(comp["all"], text=True, 
                                capture_output=True, shell=True, input=src)
        else:
            rc = subprocess.run(comp["compile"], text=True, 
                                capture_output=True, shell=True, input=src)   
        for file in self.config.delete_files:         
            if os.path.isfile(file):
                os.remove(file)
        if os.name == "nt":
            # Windowsのcompileで出力されたソース名を削除
            re_str = "^{}\n".format(self.config.delete_files[0])
            res = re.match(re_str, rc.stdout)
            if res != None:
                span = res.span()
                rc.stdout = rc.stdout[span[1]:]
        mr = MagicReturn(cp=rc) 
        mr.display = make_image_data(self.config.image_files,
                                     self.config.image) 
        return mr
    
    def default_magic(self):
        return self.config.default_magic
```

### Cell Magicを実現するためのデコレータ

```magic/magic_decorator.py
g_line_magics = []
g_cell_magics = []

def cell_magic(func):
    g_cell_magics.append(func.__name__)
    def _wrapper(self,*args, **kwargs):
        return func(self,*args,**kwargs)
    return _wrapper
def line_magic(func):
    g_line_magics.append(func.__name__)
    def _wrapper(self,*args, **kwargs):
        return func(self,*args,**kwargs)
    return _wrapper

class Magics:
    pass

def magics_class(cls):
    class NewClass(cls):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            cell_magics = {}
            line_magics = {}
            for fname in g_cell_magics:
                func = getattr(self, fname, False)
                if callable(func):
                    cell_magics[fname] = func 
                
            for fname in g_line_magics:
                func = getattr(self, fname, False)
                if callable(func):
                    line_magics[fname] = func

            self.magics = {'cell': cell_magics, 'line': line_magics}
    return NewClass


class MagicReturn:
    def __init__(self, **kwargs):
        self.stdout = []
        self.stderr = []
        self.display = []
        if "stdout" in kwargs:
            self.stdout = [kwargs["stdout"]]            
        if "stderr" in kwargs:
            self.stderr = [kwargs["stderr"]]            
        if "cp" in kwargs:
            cp = kwargs["cp"]
            self.stdout = [cp.stdout]
            self.stderr = [cp.stderr]           
        if "display" in  kwargs:
            self.display = [kwargs["display"]]
```

### 画像をnotebookに表示させるための処理

```image.py
import os
import base64
import glob
import urllib

HTML =r"""
<html>
<table>
{}
</table>
</html>
"""

def make_image_data(image_files, image):
    param_file = image_files["param"]
    col = image["col"]
    width = image["width"]
    if os.path.isfile(param_file):
        with open(param_file, mode="r") as f:
            param = eval(f.read())
        os.remove(param_file)
        col = param['col']
        width = param['width']
    imgs = glob.glob(image_files["image"])
    display = []
    base64_img = []
    if len(imgs) == 0:
        return display
    for img in sorted(imgs):
        if not os.path.isfile(img):
            continue
        with open(img, mode="rb") as f:
            imagebase64 = urllib.parse.quote(base64.b64encode(f.read()))
        os.remove(img)
        img_tag = r'<td><center><img src="data:image/png;base64,{}" {} /><br>{}</center></td>'
        if width == 0:
            width_param = ""
        else:
            width_param = r'width="{}"'.format(width)
        fname = img.split(".")
        textfile = "{}.txt".format(fname[0])
        title = "_"
        if os.path.isfile(textfile):
            with open(textfile, mode="r") as f:
                title = f.read()
            os.remove(textfile)
        base64_img.append(img_tag.format(imagebase64, width_param, title))
        
    img_len = len(base64_img)
    if img_len == 0:
        return display
    reminder = img_len % col
    if reminder != 0:
        base64_img += ["<td></td>"] * (col - reminder)
        img_len += (col - reminder)
    quotient = img_len // col
    table_tr = ""
    for i in range(quotient):
        j = col * i
        tr_images = "<tr>"
        for k in range(col):
            tr_images +=  base64_img[j + k]
        table_tr += (tr_images + "</tr>")
    html = [HTML.format(table_tr)]
    display_content = {"source": "kernel",
                    "data": {"text/html": html}, "metadata":{}}
    return [display_content]
```

別のイメージ表示方法

```py
image_content = {'source': 'kernel', 
                 'data': {'image/png': imagebase64},
                 'metadata': {"image/png":{"width":100}}
                }
self.send_response(self.iopub_socket, 'display_data', image_content)
```

## 自分でカスタマイズ可能なもの

- `set_img_param`で画像のサイズと１列に何個表示させるかをプログラムから指定

```c++:myheader.h
#pragma once

#include <iostream>
#include <fstream>
#include <opencv2/opencv.hpp>


using namespace cv;
using namespace std;

const string img = "lenna.png";
const string dir = "img/";
int image_count = 0;

void write_param(const string& file, const string& msg){
    ofstream outf(dir + file);
    outf << msg << "\n";
    outf.close();    
}

void set_img_param(int col_num, int width) {
    char data[100];
    snprintf(data, sizeof(data)-1, "{\"col\": %d, \"width\": %d}", col_num, width);
    write_param("image_param.txt", data);
}

void display(InputArray img, const string& comment="") {
    char fname[100];
    image_count++;
     
    snprintf(fname, sizeof(fname)-1, "%s%04d_image.png",dir.c_str(), image_count);
    imwrite(fname, img);
    string str;
    if (comment == "") 
        str = "-";
    else
        str = comment;
    snprintf(fname, sizeof(fname)-1, "%04d_image.txt", image_count);
    write_param(fname, str);
}
```

実行パラメータ
`/reconf` でreload可能

```config_gcc.py
import os

MAIN_SOURCE = """
#include "myheader.h"

{0}

int main(int argc, char *argv[]) {{

{1}

    return 0;
}}    
"""

# col:1行に何画像表示するか, width:表示画像の横ピクセル数　0=元の大きさ
IMAGE1 = {"col": 6, "width": 100}  
IMAGE2 = {"col": 1, "width": 0}  
IMAGE_FILES = {"image": "img/*.png", "param": "img/image_param.txt"}

if os.name == "nt":
    # VC++のとき
    COMPILE = {"all": "nmake /c", "compile": "nmake /c comp"}
    DELETE_FILES = ["temp.cpp", "temp.obj", "temp.exe"]
else:
    COMPILE = {"all": "make -s", "compile": "make -s comp"}
    DELETE_FILES = ["./a.out"]

DEFAULT_MAGIC = {'magic': 'inside', 'line': '50'}
#------------------------------------------------------

PARAMS={
    "main_source" : MAIN_SOURCE,
    "image": IMAGE1, 
    "image_files": IMAGE_FILES,
    "compile": COMPILE,
    "delete_files": DELETE_FILES,
    "default_magic": DEFAULT_MAGIC,
}
```

### Mac Makefile

```:Makefile
all: comp exec
comp:
	g++ -xc++ - -std=c++17  `pkg-config --cflags opencv4 --libs opencv4`
exec:
	./a.out
```


## Windows nmake用Makefile

```:
.SILENT :
all: edit
    cl temp.cpp /EHsc /nologo /source-charset:utf-8 /I "D:\opencv\build\include" /link /DYNAMICBASE "opencv_world4110.lib" /LIBPATH:"D:\opencv\build\x64\vc16\lib"
    temp.exe

comp: edit
    cl temp.cpp /EHsc /nologo /source-charset:utf-8 /I "D:\opencv\build\include" /c

edit:
    python source_edit.py
```

コンパイラのclが標準入力からのコンパイルが出来なさそうなので、Pythonで受け取ってからソースファイルに書き込む

```source_edit.py
import sys

input_text = sys.stdin.buffer.read()

# opencvのheaderでutf-8でコンパイルしないと警告が出るものがある
with open("temp.cpp", "w", encoding="utf-8", newline="\n") as f:
    f.write(input_text.decode("shift_jis"))
```

# 終わりに

画像をbase64にするかファイルを指定するか迷ってbase64にして画像ファイルを残さないことにしましたが、その分notebookの容量が大きくなった。またHTML以外で画像を横に並べられる方法があるのかがわからなかった。