概要
自分が上記の本でC++版OpenCVの学習を行うために、下記の投稿記事をOpenCVの画像をnotebookに表示できるようにしたもので、内容はソースコードとメモ程度です。
各学習のソースと画像が同一画面で見られるのは学習において便利です。Python版OpenCVなら可能なので、C++版が出来ないか考え作ってみました。
Pythonなどのインタプリタと違って1セルごとにコンパイルするので、やはり反応の遅さ(特にVC++のコンパイルが遅い)を感じますが、実際VScodeやVisual Studioを使ってもやはりコンパイルは伴うので同じかも。
長いコードはVScodeで作って、Jupyter notebookにコピペして実行するのが一番良いかもしれません。
Jupyter notebook スクリーンショット
Mac版
-
imshow
を使うとブラウザ外に表示される -
display
はnotebook上に表示、これは後述のmyheader.h
にある自作関数
Windows VC++版
ファイル構成
xxxx/
├── gcc/
│ └── kernel.json
├── kernel/
│ └── gcckernel.py
└── magic/
├── gcc_magic.py
├── magic_decorator.py
└── image.py
yyyy/
├── img/
├── config_gcc.py
├── Makefile
└── myheader.h
kernel情報
{
"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の予約語なので関数名として使えないので、in
はinside
に変換して処理する。
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処理コード
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を実現するためのデコレータ
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に表示させるための処理
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列に何個表示させるかをプログラムから指定
#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可能
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
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で受け取ってからソースファイルに書き込む
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以外で画像を横に並べられる方法があるのかがわからなかった。