輪郭追跡処理をやってみよう! その3

輪郭追跡処理をやってみよう!輪郭追跡処理をやってみよう!その2の続きです。これまで静止画像を使って目標画像の抽出を試してきましたが、今回はUSBのカメラを使って動画に対する目標画像の抽出を行います。

USBカメラで撮像する画像
GIMPを使ってアニメーションGIFデータを作成しました。このように野外を歩くシカの赤外線画像的な動画です。

これをブラウザで開いて、その様子をUSBカメラで撮像します。

プログラムの構造
次のような作りです。カメラ画像の取り扱いはOpenCVを用いました。

関数定義 ー これまでに作った自前の輪郭追跡処理の関数群です。main関数から呼ばれます。
カメラの初期化設定 ー 画像サイズ(大きいと画像処理に時間をたくさん要するので小さめにします。(640x480としました。)
while true:
キャプチャー - OpenCVの関数を用います
 画像表示 - 目標画像の抽出中なら輪郭ありの画像を表示し、そうでないなら輪郭無しの画像を表示します。
 キー入力待ち(15ms 待ち)- q:終了、x:目標画像の抽出開始、e:目標画像の抽出終了
main関数 ー 目標画像の抽出を指示するキーが入力されたらmain関数を実行します。2値化を行い、輪郭追跡を行って目標画像の抽出します。

2値化については、OpenCVの関数を用いました。cv2.threshold(im_gray, 0, 1, cv2.THRESH_OTSU)
第4引数にcv2.THRESH_OTSUを指定することで2値化のスレッシュレベルは自動で算出され、これを用いて2値化されます。スレッシュレベルより低い輝度は0に、スレッシュレベルより多い輝度は1に2値化しています。
目標画像の抽出を行うのに、自前の関数を使う必要はありません。全てOpenCVが持っている関数で実行可能です。背景差分法を使えば移動物体の抽出ができますが、ここではこれまでに紹介した自前の関数を使って、動くシカの抽出を試しました。


ソースコード

import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import numpy as np
import copy
import csv
import time
# from numba import jit
import my_module as my
import keyboard

# デバッグ用キー、注意:main関数実行毎フレームごとに実行されるので膨大なデータが出力される。
debug = False
# csv_out 2値化画像をCSV出力する
csv_out = False
# time_calct main関数の実行時間をテキストファイルに出力する
time_calc = False
# edge_save 輪郭付き画像を保存する
edge_save = False
# position_out 中心座標との目標画像中心とのずれ量をテキストファイルに出力する
position_out = True

# camera image size
x_size = 640
y_size = 480
# center position
x_center = int(x_size / 2 - 1)
y_center = int(y_size / 2 - 1)

image_size_max_x = 3000
image_size_max_y = 3000
chain_code_data_max = 50000
margin_top = 3
margin_bottom = 3
margin_right = 3
margin_left = 3

# 輪郭追跡処理のためのデータ
search_order = [
    [7, 0, 1, 2, 3, 4, 5, 6],
    [7, 0, 1, 2, 3, 4, 5, 6],
    [1, 2, 3, 4, 5, 6, 7, 0],
    [1, 2, 3, 4, 5, 6, 7, 0],
    [3, 4, 5, 6, 7, 0, 1, 2],
    [3, 4, 5, 6, 7, 0, 1, 2],
    [5, 6, 7, 0, 1, 2, 3, 4],
    [5, 6, 7, 0, 1, 2, 3, 4]
]
move_x = [1, 1, 0, -1, -1, -1, 0, 1]
move_y = [0, -1, -1, -1, 0, 1, 1, 1]

initial_direction_table = [0, 3, 2, 3, 1, 3, 2, 3, 4, 4, 4, 4, 4, 4, 4, 4]


class Chaincode:
    def __init__(self):
        self.chain_position_x = [0] * image_size_max_x
        self.chain_position_y = [0] * image_size_max_y
        self.chain_position_yx = []
        self.chaincode = [0] * chain_code_data_max
        self.chaincode_length = 0
        self.x_min = 0
        self.y_min = 0
        self.x_max = 0
        self.y_max = 0


class Parameters:
    pixel_count = 0


d_table = [
    [0, 1, 1, 1, 1, 0, 0, 0],
    [0, 1, 1, 1, 1, 0, 0, 0],
    [0, 1, 1, 1, 1, 0, 0, 0],
    [0, 1, 1, 1, 1, 0, 0, 0],
    [-1, 0, 0, 0, 0, -1, -1, -1],
    [-1, 0, 0, 0, 0, -1, -1, -1],
    [-1, 0, 0, 0, 0, -1, -1, -1],
    [-1, 0, 0, 0, 0, -1, -1, -1]
]

c_table = [
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 0, 0, 1, 1, 1],
    [1, 0, 0, 0, 0, 1, 1, 1],
    [1, 1, 0, 0, 0, 1, 1, 1],
    [1, 1, 1, 0, 0, 1, 1, 1],
    [1, 1, 1, 1, 0, 1, 1, 1]
]


def image_to_csv(img):
    row_size = img.shape[0]
    column_size = img.shape[1]

    # 輝度値をCSVファイルで保存
    with open('output/' + image_filename + '.csv', 'w', newline='') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerows(img)


def get_threshold_level(gray_img, ratio, bias):
    row_size = gray_img.shape[0]
    column_size = gray_img.shape[1]
    # 輝度の濃淡レベルは8bit(0-255:256段階)とする
    histogram = [0] * 256
    _threshold_level = 0

    # 輝度ヒストグラムの作成
    for row in range(row_size):
        for column in range(column_size):
            histogram[gray_img[row][column]] += 1

    # 最頻度輝度値の算出
    mode = histogram.index(max(histogram))
    # _threshold_level = int(mode * 0.01 * ratio + 0.5)
    _threshold_level = mode + bias

    return _threshold_level


# @jit(nopython=True, cache=True)
def binarizer(gray_img, threshold):
    row_size = gray_img.shape[0]
    column_size = gray_img.shape[1]
    # 空の二次元配列を作成
    bin_img = np.empty((row_size, column_size), dtype=np.uint8)
    # 輝度値が閾値より暗ければ(小さければ)0に変換、そうでなければ1に変換し2値化する
    # for row in range(row_size):
    #     for column in range(column_size):
    #         if gray_img[row][column] > threshold:
    #             bin_img[row][column] = 1
    #         else:
    #             bin_img[row][column] = 0
    bin_img = copy.copy(gray_img)
    bin_img[gray_img > threshold] = 1
    bin_img[gray_img <= threshold] = 0
    return bin_img


def raster_scan(bin_img):
    _result = False
    _start_point_row = 0
    _start_point_column = 0
    row_size = bin_img.shape[0]
    column_size = bin_img.shape[1]
    # 最初に1が登場する画素の位置を探して関数から抜ける。位置情報を返す。
    for row in range(row_size):
        for column in range(column_size):
            if bin_img[row][column] == 1:
                _start_point_row = row
                _start_point_column = column
                _result = True
                return _result, _start_point_row, _start_point_column


def summit_scan(bin_img):
    # ndarray の添え字アクセスは遅いので画像データはListに変換した形で受け取る
    _judge = False
    row_size = bin_img.shape[0]
    column_size = bin_img.shape[1]
    summit_memory = []

    # 頂点であると判断する15パターンのどれかに該当するか確認し、頂点であればその座標を記録する。
    # 頂点は後に実行されるエッジトレースのスタート座標を示す。
    # b3 b2 b1
    # b4 b8 b0
    # b5 b6 b7
    for row in range(1, row_size - 1):
        for column in range(1, column_size - 1):
            b8 = bin_img[row][column]
            b7 = bin_img[row + 1][column + 1]
            b6 = bin_img[row + 1][column]
            b5 = bin_img[row + 1][column - 1]
            b4 = bin_img[row][column - 1]
            b3 = bin_img[row - 1][column - 1]
            b2 = bin_img[row - 1][column]
            b1 = bin_img[row - 1][column + 1]
            b0 = bin_img[row][column + 1]
            _judge = b8 and (not (b1 or b2 or b3 or b4)) and (b0 or b5 or b6 or b7)
            if _judge:
                summit_memory.append([column, row])
    return summit_memory


# 輪郭追跡された画像の位置情報をもとに画像の重なりをチェックし、他の画像の内側にある画像の頂点やチェーンコード情報を削除する。
def overlap_remover(_summit_memory, _chain_code_list):
    copy_summit_memory = copy.copy(_summit_memory)
    copy_chain_code_list = copy.copy(_chain_code_list)
    n = 1
    for ref in copy_chain_code_list:
        for value in copy_chain_code_list[n:]:
            if (
                    (ref.x_min <= value.x_min) and
                    (ref.x_max >= value.x_max) and
                    (ref.y_min <= value.y_min) and
                    (ref.y_max >= value.y_max)
            ):
                del_index = copy_chain_code_list.index(value)
                copy_chain_code_list.remove(value)
                del copy_summit_memory[del_index]
        n += 1
    return copy_summit_memory, copy_chain_code_list


# 輪郭処理のスタート地点の初期方向コードを作成する
def get_initial_direction(start_y, start_x, bin_img):
    initial_direction = 0
    _current_x = start_x
    _current_y = start_y
    # 初期方向コードを得るための輝度データを取得
    b4 = int(bin_img[_current_y][_current_x])
    b3 = int(bin_img[_current_y][_current_x + 1])
    b2 = int(bin_img[_current_y + 1][_current_x - 1])
    b1 = int(bin_img[_current_y + 1][_current_x])
    b0 = int(bin_img[_current_y + 1][_current_x + 1])
    if b4 == 0:
        initial_direction = 0
        return initial_direction
    code = (b3 << 3) + (b2 << 2) + (b1 << 1) + b0
    initial_direction = initial_direction_table[code]
    return initial_direction


# 一つの抽出画像のチェーンコードを生成する
def making_chain_code(start_y, start_x, initial_direction, bin_img):
    result = False
    # new_chaincode_reg = copy.copy(chaincode_reg)
    chaincode_reg = Chaincode()
    _current_x = start_x
    _current_y = start_y
    _first_direction = initial_direction
    _around_binary_data = [0] * 8
    chaincode_reg.chain_position_x[0] = _current_x
    chaincode_reg.chain_position_y[0] = _current_y
    chaincode_reg.chain_position_yx.append([chaincode_reg.chain_position_y[0], chaincode_reg.chain_position_x[0]])
    chaincode_reg.chaincode[0] = initial_direction

    while True:
        # 注目画素の8近傍画素輝度データの一部が画像エリア外になるなら処理終了し、チェーンコードは破棄する。
        if (_current_x + 1 >= bin_img.shape[1]) or (_current_y + 1 >= bin_img.shape[0]) or \
                (_current_x - 1 <= 0):
            result = False
            break
        # 注目画素の8近傍画素輝度データを取得
        _around_binary_data[0] = bin_img[_current_y][_current_x + 1]
        _around_binary_data[1] = bin_img[_current_y - 1][_current_x + 1]
        _around_binary_data[2] = bin_img[_current_y - 1][_current_x]
        _around_binary_data[3] = bin_img[_current_y - 1][_current_x - 1]
        _around_binary_data[4] = bin_img[_current_y][_current_x - 1]
        _around_binary_data[5] = bin_img[_current_y + 1][_current_x - 1]
        _around_binary_data[6] = bin_img[_current_y + 1][_current_x]
        _around_binary_data[7] = bin_img[_current_y + 1][_current_x + 1]
        # 次に進むべき画素の位置を探す
        for i in range(8):
            direction = search_order[_first_direction][i]
            if _around_binary_data[direction] == 1:
                _current_x = _current_x + move_x[direction]
                _current_y = _current_y + move_y[direction]
                chaincode_reg.chaincode[chaincode_reg.chaincode_length] = direction
                _first_direction = direction
                chaincode_reg.chaincode_length += 1
                if chaincode_reg.chaincode_length > chain_code_data_max:
                    result = False
                    break
                chaincode_reg.chain_position_x[chaincode_reg.chaincode_length] = _current_x
                chaincode_reg.chain_position_y[chaincode_reg.chaincode_length] = _current_y
                chaincode_reg.chain_position_yx.append(
                    [chaincode_reg.chain_position_y[chaincode_reg.chaincode_length],
                     chaincode_reg.chain_position_x[chaincode_reg.chaincode_length]])
                break
        chaincode_reg.x_min = min(chaincode_reg.chain_position_x[:chaincode_reg.chaincode_length])
        chaincode_reg.y_min = min(chaincode_reg.chain_position_y[:chaincode_reg.chaincode_length])
        chaincode_reg.x_max = max(chaincode_reg.chain_position_x[:chaincode_reg.chaincode_length])
        chaincode_reg.y_max = max(chaincode_reg.chain_position_y[:chaincode_reg.chaincode_length])

        # スタート地点に戻ったら正常終了
        if (_current_x == start_x) and (_current_y == start_y):
            result = True
            break
    return result, chaincode_reg


# 輪郭を赤色で着色する
def edge_paint(chain_yx, img_rgb):
    _new_img = copy.copy(img_rgb)
    for yx in chain_yx:
        _new_img[yx[0]][yx[1]] = (0, 0, 255)
    return _new_img


# 画像パラメータの計算(ここでは画素数計算のみ)
def calculate_image_parameter(_chaincode_reg, _parameters):
    _pixel = 0
    x = d = c = 0
    # chaincode_reg.chain_position_x[chaincode_reg.chaincode_length] = chaincode_reg.chain_position_x[0]
    _chaincode_reg.chaincode[_chaincode_reg.chaincode_length] = _chaincode_reg.chaincode[0]
    for i in range(1, _chaincode_reg.chaincode_length + 1):
        x = _chaincode_reg.chain_position_x[i]
        d = d_table[_chaincode_reg.chaincode[i - 1]][_chaincode_reg.chaincode[i]]
        c = c_table[_chaincode_reg.chaincode[i - 1]][_chaincode_reg.chaincode[i]]
        _pixel += (x * d + c)
    _parameters.pixel_count = _pixel


# 一つの抽出画像の切り出しのための座標4点データを取得する
def get_position(_chaincode_reg, img):
    _position_list = []
    _x_max = img.shape[1] - 1
    _y_max = img.shape[0] - 1
    _x_start_position = _chaincode_reg.x_min - margin_right
    _y_start_position = _chaincode_reg.y_min - margin_top
    _x_end_position = _chaincode_reg.x_max + margin_left
    _y_end_position = _chaincode_reg.y_max + margin_bottom
    if _x_start_position < 0:
        _x_start_position = 0
    if _y_start_position < 0:
        _y_start_position = 0
    if _x_end_position > _x_max:
        _x_end_position = _x_max
    if _y_end_position > _y_max:
        _y_end_position = _y_max
    _position_list = [_y_start_position, _x_start_position, _y_end_position, _x_end_position]
    return _position_list


# 読み込んだ画像内のすべての抽出画像それぞれの切り出しのための座標データを取得する。
def get_position_list(_chain_code_list, img):
    _position_lists = []
    for _chain_code in _chain_code_list:
        _position_list_temp = get_position(_chain_code, img)
        _position_lists.append(_position_list_temp)
    return _position_lists


# 指定された一つの切り出し画像を生成する。
def get_crop_image_single(_position_list, num, img):
    # Numpy array のスライスを使って画像切り出し
    _img_clop = img[_position_list[0]:_position_list[2] + 1, _position_list[1]:_position_list[3] + 1]
    cv2.imwrite('output/' + 'clop_img_' + image_filename + '_' + str(num) + '.bmp', _img_clop)


# 読み込んだ画像の中のすべての切り出し画像をそれぞれ生成する。
def get_crop_image_multiples(_position_list_chain, img):
    for index, p_list in enumerate(_position_list_chain):
        # Numpy array のスライスを使って画像切り出し
        _img_clop = img[p_list[0]:p_list[2] + 1, p_list[1]:p_list[3] + 1]
        cv2.imwrite('output/' + 'clop_img_' + image_filename + '_' + str(index + 1) + '.bmp', _img_clop)


def main():
    global global_count
    global_count = global_count + 1
    if global_count > global_count_limit:
        global_count = 0

    im_gray = cv2.cvtColor(im_rgb, cv2.COLOR_BGR2GRAY)

    # 2値化ための閾値を得る ここでは使わない。(2値化処理の第4引数に大津のアルゴリズムを用いている)
    # threshold_level = get_threshold_level(im_gray, 90, 30)

    # グレースケールのRAW画像を2値化する。
    # Python code + Nmpy を使う場合
    # bin_im_gray = binarizer(im_gray, threshold_level)
    # OpenCVを使う場合
    # response, bin_im_gray = cv2.threshold(im_gray, threshold_level, 1, cv2.THRESH_BINARY_INV)
    # response, bin_im_gray = cv2.threshold(im_gray, threshold_level, 1, cv2.THRESH_BINARY)
    response, bin_im_gray = cv2.threshold(im_gray, 0, 1, cv2.THRESH_OTSU)
    # Cを使う場合
    # bin_im_gray = my.binarizer(im_gray, threshold_level)
    # 2値化した画像をcsvファイルで出力
    if csv_out:
        image_to_csv(bin_im_gray)

    # 輪郭抽出処理のスタート座標を得る。(画像上にはスタート座標が複数ある想定)
    # summit_memory = summit_scan(bin_im_gray)      # Made by Python code
    # my.hello_world()
    summit_memory = my.summit_scanner(bin_im_gray)  # Made by C code

    # チェーンコードを作成
    chain_code_list = []
    for summit in summit_memory:
        # 輪郭追跡のための初期方向コードを得る。
        initial_direction_code = get_initial_direction(summit[1], summit[0], bin_im_gray)
        # chaincode_registers = Chaincode()
        # チェーンコードを得る
        # チェーンコード = ここでは輪郭追跡を行って得られて画像情報の集まり(輪郭の位置情報を含む)
        chaincode_result, chaincode = making_chain_code(summit[1], summit[0], initial_direction_code, bin_im_gray)
        # チェーンコードのリストを作る
        if chaincode_result:
            chain_code_list.append(chaincode)

    # チェーンコードの粒子座標を用いて重なりをチェックし重なっている画像のチェーンコードデータと頂点メモリデータを削除する
    new_summit_memory, new_chain_code_list = overlap_remover(summit_memory, chain_code_list)

    # 原画像中の抽出画像に輪郭を表示する。
    global global_edge_img
    global_edge_img = copy.copy(im_rgb)
    for edge_data in new_chain_code_list:
        global_edge_img = edge_paint(edge_data.chain_position_yx, global_edge_img)
    if edge_save:
        rtn = cv2.imwrite('output/' + str(global_count) + '_with_edge' + '.bmp', global_edge_img)

    # 原画像中の最初の抽出画像を切り出す
    # 何番目の画像か指定する
    n = 1
    position_list = get_position(new_chain_code_list[n - 1], im_rgb)

    # [_y_start_position, _x_start_position, _y_end_position, _x_end_position]
    x_tc = int((position_list[1] + position_list[3]) / 2 + 0.5)
    y_tc = int((position_list[0] + position_list[2]) / 2 + 0.5)
    x_e = x_tc - x_center
    y_e = y_tc - y_center

    if position_out:
        global global_p_handle_f
        global_p_handle_f.write("Center position [y, x] %d,%d : Error position [y, x] %d,%d\n" % (y_tc, x_tc, y_e, x_e))


"-------------------------------------------------------------------------------------"

# key input status
Extraction = False
pressed_x = False
pressed_e = False
pressed_c = False

global_edge_img = False
global_p_handle_f = None
global_t_handle_f = None
global_count = 0
global_count_limit = 9999
main_exe = False
print("q:終了, x:目標画像抽出開始, e:抽出終了")
print("キャプチャーを開始します")
# VideoCaptureオブジェクト取得(カメラデバイスが複数ある場合は、事前にカメラデバイスの番号を確認しておく)
capture = cv2.VideoCapture(1)
# 画像の幅を設定する
capture.set(cv2.CAP_PROP_FRAME_WIDTH, x_size)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, y_size)

if position_out:
    global_p_handle_f = open('output/' 'position.txt', 'w', encoding='UTF-8')
if time_calc:
    global_t_handle_f = open('output/' 'time.txt', 'w', encoding='UTF-8')

while True:
    # 画像ファイルを読み込む
    # フレームを取得する
    ret, frame = capture.read()
    im_rgb = frame.copy()

    # 画像を表示する
    if not main_exe:
        cv2.imshow("Frame", frame)
    else:
        cv2.imshow("Frame", global_edge_img)
    cv2.waitKey(15)

    if not ret:
        print("not capture")
        break
    if debug:
        print(im_rgb.shape)
        print(im_rgb.dtype)

    if Extraction:
        if time_calc:
            start = time.perf_counter()
        main()
        main_exe = True
        if time_calc:
            end = time.perf_counter() - start
            global_t_handle_f.write(f'{end}秒!\n')

    # キーボード入力処理
    if keyboard.is_pressed('q'):
        Extraction = False
        main_exe = False
        if position_out:
            global_p_handle_f.close()
        print("プログラムを終了しました")
        break

    elif keyboard.is_pressed('x') and not pressed_x:
        Extraction = True
        pressed_x = True
        print("ターゲティングを開始しました")
    elif not keyboard.is_pressed('x') and pressed_x:
        pressed_x = False
        main_exe = False
    elif keyboard.is_pressed('e') and not pressed_e:
        Extraction = False
        main_exe = False
        pressed_e = True
        print("ターゲティングを中止しました")
    elif not keyboard.is_pressed('e') and pressed_e:
        pressed_e = False

# カメラデバイスクローズ
capture.release()
# ウィンドウクローズ
cv2.destroyAllWindows()


実行結果
目標画像の抽出中は下記のようになります。輪郭は赤で示しました。ツノや背中の抽出はできていないですね。



出力データ
画像のど真ん中を(y:0、x:0)として、シカの画像の中心がどこにあるかを示す座標データを出力するようにしました。
次回は、この座標データを使って何か面白いことができないか探りたいと思います。




輪郭追跡処理をやってみよう! その3” に対して1件のコメントがあります。

コメントは受け付けていません。