0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

概要

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

Jupyter notebook スクリーンショット

Mac版

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

opencv01.png

opencv02.png

Windows VC++版

openvc_win.png

ファイル構成

xxxx=PYTHONPATHが通っているディレクトリ
xxxx/
├── gcc/
│   └── kernel.json
├── kernel/
│   └── gcckernel.py
└── magic/
    ├── gcc_magic.py  
    ├── magic_decorator.py
    └── image.py
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情報のインストール

> jupter kernelspec install gcc --user

kernel本体

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

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="パラメータが2個でない")
        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]

別のイメージ表示方法

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で画像のサイズと1列に何個表示させるかをプログラムから指定
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以外で画像を横に並べられる方法があるのかがわからなかった。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?