개발 관련/SW, App 관련

Darknet 사물인식 진행 상황

by 소서리스25 2024. 9. 26.
반응형

Darknet 사물인식 진행 상황

 

지난번 커스텀데이터로 Darknet 훈련을 통해 가중치(.weights) 파일을 얻고 이 가중치를 통한 테스트는 정상적으로 잘되었었다.

 

문제는 이 가중치 파일을 다이렉트로 onnx 컨버팅이 되지 않아 다시 keras의 .h로 변환한 뒤 onnx로 변환했다. 그러나 변환한 keras로도 훈련이 되지 않았고 이를 변환한 onnx 파일을 Unity에서 적용하여 Android 앱으로 빌드하여 사물인식을 시도하였으나 그것도 제대로 된 Bounding Box 출력이 되지 않았다. 

 

따라서 여기에서의 문제는 바로 keras의 .h로 변환하는데서 문제가 발생하는 것이라 확신했다.

참고한 yad2k 의 이슈에서도 이 문제가 제기되었고 다른 방법으로 해결했다고 한 사항이 있으나 내게는 적용되지 않았다. 아마도 오래된 탓에 여러 가지 문제가 있었으리라 생각된다.

 

그래서 최근 다른 방법을 시도했다. 

그 방법은 keras 방식이 아닌 Coreml Model로 변환한 뒤 다시 onnx로 변환하는 것이다. 사실 이것도 된다는 보장은 없었지만 시도해 볼만은 했다. 여기에서 얻은 결과를 Unity에 적용한 사례는 구글 검색으로도 아직 보질 못했기에 일단 한번 해보자는 심산이었다.

 

진행과정은 역시나 뭐하나 쉬운 게 없다.

컨버팅 관련 검색을 하면 아래와 같은 사이트가 나타난다. 이를 적용해 봤으나 아무리 해도 되질 않았다.

 

이 사물인식을 진행하면서 가장 많은 오류를 다양한 참고자료의 코드들이 한 번에 제대로 동작하는 경우가 거의 없었다. 우선은 자신의 환경에 맞는 버전을 찾아야 했고, 찾은 뒤에도 각 패키지간에 버전 차이로 또다시 업그레이드나 다운그레이드를 해야만 했다.

 

결과적으로는 코드 수정 및 버전 맞추기 등등.... 그러한 모든 과정을 다 거치고 인식하는 앱을 뽑아내게 되었다.

다음 영상이 Darknet의 weights > coreml model > onnx > unity android 앱으로 빌드한 사물인식의 결과영상이다.

 

 

unity android앱의 용량이 꽤나 큰데 darknet 훈련시 cfg의 max_batchs 값이 4,000일때 약 166메가, 10,000일 때는 약 322메가로 두 배정도 크기가 커진다. weights의 파일은 둘 다 동일한 43메가 정도로 yolov2-tiny로 훈련했기에 동일하지만 unity barracuda에서 처리되면서 용량이 훈련양에 비례해 커지는 것 같다.(찾아보니 클래스 x 2000 값으로 한다고 한다.)

 

그렇다면 결과물의 차이가 있냐는 것인데 결론부터 말하면 차이가 없다. 훈련의 수보다는 표본의 수가 중요한 것 같다. 표본의 수가 많고 훈련을 많이 하면 인식률이 그만큼 높다는 것이겠다. 표본수가 200장도 안된다..;;;;

 

그러면 진행에 성공한 환경 정보는 구체적으로 다음과 같다. 우선 가상환경을 만들어서 각각 설치해 준다.

*python = 3.7
*keras = 2.3.0
numpy = 1.18.5
*onnx = 1.14.1
*keras2onnx = 1.7.0
*h5py = 2.10.0 >> uninstall 후 재설치
*protobuf > 3.20.2 >> uninstall 후 재설치

*weights > coreml model 컨버팅시
coremltools = 4.1
onnxmltools = 1.3.2

 

특히 coreml model 변환시에는 버전을 딱 맞춰야 한다. 좀 더 상위로 올렸더니 오류가 발생한다. 너무 낮춰도 오류가 발생한다. -_-;;

여기서 onnxmltools의 버전이 중요한데 저 버전인가 까지가 oppset = 9이다. 즉, Unity에서 인식 가능한 버전이 9까지이다. 이는 상위버전에서도 동일하게 적용된다. tensorflow 버전도 2.x보다 1.x를 쓰라고 되어 있는 것을 봤던거 같다. 상세한 사항은 unity의 barracuda 설명/소개 페이지를 참고하면 된다.

 

그리고 이제는 python 파일의 코드를 수정해 줘야 한다. python를 잘 몰라서 해외 여러 깃과 블로그 등을 찾아봤지만 답을 찾을 수 없어서 여러 파일들을 비교해 가면서 수정하였다. 비교적 간단하게 수정하면 되는 것이었다.;;; 

 

먼저 weights 파일을 mlmodel로 컨버팅하는 것이다. 다음 깃에서 python 파일을 찾을 수 있었으나 수정을 해줘야 한다.

 

Convert darknet pre-trained model to CoreML model · GitHub

 

Convert darknet pre-trained model to CoreML model

Convert darknet pre-trained model to CoreML model. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

 

그대로 하지 말고 아래의 코드를 convert2CoreML.py로 저장한다.

#! /usr/bin/env python
"""
Reads Darknet config and weights and creates Keras model with TF backend.

"""
import argparse
import configparser
import io
import os
from collections import defaultdict

from PIL import Image
from yolo3.utils import letterbox_image

import numpy as np
from keras import backend as K
from keras.engine.base_layer import Layer
from keras.layers import (Conv2D, Input, ZeroPadding2D, Add,
                          UpSampling2D, MaxPooling2D, Concatenate, Lambda)
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.normalization import BatchNormalization
from keras.models import Model, load_model
from keras.regularizers import l2
from keras.utils.vis_utils import plot_model as plot

import coremltools
from coremltools.proto import NeuralNetwork_pb2

parser = argparse.ArgumentParser(description='Darknet To Keras Converter.')
parser.add_argument('config_path', help='Path to Darknet cfg file.')
parser.add_argument('weights_path', help='Path to Darknet weights file.')
parser.add_argument('output_path', help='Path to output Keras model file.')
parser.add_argument(
    '-p',
    '--plot_model',
    help='Plot generated Keras model and save as image.',
    action='store_true')
parser.add_argument(
    '-w',
    '--weights_only',
    help='Save as Keras weights file instead of model file.',
    action='store_true')

class Mish(Layer):
    '''
    Mish Activation Function.
    .. math::
        mish(x) = x * tanh(softplus(x)) = x * tanh(ln(1 + e^{x}))
    Shape:
        - Input: Arbitrary. Use the keyword argument `input_shape`
        (tuple of integers, does not include the samples axis)
        when using this layer as the first layer in a model.
        - Output: Same shape as the input.
    Examples:
        >>> X_input = Input(input_shape)
        >>> X = Mish()(X_input)
    '''

    def __init__(self, **kwargs):
        super(Mish, self).__init__(**kwargs)
        self.supports_masking = True

    def call(self, inputs):
        return inputs * K.tanh(K.softplus(inputs))

    def get_config(self):
        config = super(Mish, self).get_config()
        return config

    def compute_output_shape(self, input_shape):
        return input_shape

def convert_mish(layer):
    params = NeuralNetwork_pb2.CustomLayerParams()
    params.className = "Mish"
    params.description = "Mish Activation Layer"
    return params

def unique_config_sections(config_file):
    """Convert all config sections to have unique names.

    Adds unique suffixes to config sections for compability with configparser.
    """
    section_counters = defaultdict(int)
    output_stream = io.StringIO()
    with open(config_file) as fin:
        for line in fin:
            if line.startswith('['):
                section = line.strip().strip('[]')
                _section = section + '_' + str(section_counters[section])
                section_counters[section] += 1
                line = line.replace(section, _section)
            output_stream.write(line)
    output_stream.seek(0)
    return output_stream

# %%
def _main(args):
    config_path = os.path.expanduser(args.config_path)
    weights_path = os.path.expanduser(args.weights_path)
    assert config_path.endswith('.cfg'), '{} is not a .cfg file'.format(
        config_path)
    assert weights_path.endswith(
        '.weights'), '{} is not a .weights file'.format(weights_path)

    output_path = os.path.expanduser(args.output_path)
    assert output_path.endswith(
        '.mlmodel'), 'output path {} is not a .mlmodel file'.format(output_path)
    output_root = os.path.splitext(output_path)[0]

    # Load weights and config.
    print('Loading weights.')
    weights_file = open(weights_path, 'rb')
    major, minor, revision = np.ndarray(
        shape=(3, ), dtype='int32', buffer=weights_file.read(12))
    if (major*10+minor)>=2 and major<1000 and minor<1000:
        seen = np.ndarray(shape=(1,), dtype='int64', buffer=weights_file.read(8))
    else:
        seen = np.ndarray(shape=(1,), dtype='int32', buffer=weights_file.read(4))
    print('Weights Header: ', major, minor, revision, seen)

    print('Parsing Darknet config.')
    unique_config_file = unique_config_sections(config_path)
    cfg_parser = configparser.ConfigParser()
    cfg_parser.read_file(unique_config_file)

    print('Creating Keras model.')
    input_layer = Input(shape=(416, 416, 3))
    prev_layer = input_layer
    all_layers = []

    weight_decay = float(cfg_parser['net_0']['decay']
                         ) if 'net_0' in cfg_parser.sections() else 5e-4
    count = 0
    out_index = []
    for section in cfg_parser.sections():
        print('Parsing section {}'.format(section))
        if section.startswith('convolutional'):
            filters = int(cfg_parser[section]['filters'])
            size = int(cfg_parser[section]['size'])
            stride = int(cfg_parser[section]['stride'])
            pad = int(cfg_parser[section]['pad'])
            activation = cfg_parser[section]['activation']
            batch_normalize = 'batch_normalize' in cfg_parser[section]

            padding = 'same' if pad == 1 and stride == 1 else 'valid'

            # Setting weights.
            # Darknet serializes convolutional weights as:
            # [bias/beta, [gamma, mean, variance], conv_weights]
            prev_layer_shape = K.int_shape(prev_layer)

            weights_shape = (size, size, prev_layer_shape[-1], filters)
            darknet_w_shape = (filters, weights_shape[2], size, size)
            weights_size = np.product(weights_shape)

            print('conv2d', 'bn'
                  if batch_normalize else '  ', activation, weights_shape)

            conv_bias = np.ndarray(
                shape=(filters, ),
                dtype='float32',
                buffer=weights_file.read(filters * 4))
            count += filters

            if batch_normalize:
                bn_weights = np.ndarray(
                    shape=(3, filters),
                    dtype='float32',
                    buffer=weights_file.read(filters * 12))
                count += 3 * filters

                bn_weight_list = [
                    bn_weights[0],  # scale gamma
                    conv_bias,  # shift beta
                    bn_weights[1],  # running mean
                    bn_weights[2]  # running var
                ]

            conv_weights = np.ndarray(
                shape=darknet_w_shape,
                dtype='float32',
                buffer=weights_file.read(weights_size * 4))
            count += weights_size

            # DarkNet conv_weights are serialized Caffe-style:
            # (out_dim, in_dim, height, width)
            # We would like to set these to Tensorflow order:
            # (height, width, in_dim, out_dim)
            conv_weights = np.transpose(conv_weights, [2, 3, 1, 0])
            conv_weights = [conv_weights] if batch_normalize else [
                conv_weights, conv_bias
            ]

            # Handle activation.
            act_fn = None
            if activation == 'leaky':
                pass  # Add advanced activation later.
            elif activation == 'mish':
                pass
            elif activation != 'linear':
                raise ValueError(
                    'Unknown activation function `{}` in section {}'.format(
                        activation, section))

            # Create Conv2D layer
            if stride>1:
                # Darknet uses left and top padding instead of 'same' mode
                prev_layer = ZeroPadding2D(((1,0),(1,0)))(prev_layer)
            conv_layer = (Conv2D(
                filters, (size, size),
                strides=(stride, stride),
                kernel_regularizer=l2(weight_decay),
                use_bias=not batch_normalize,
                weights=conv_weights,
                activation=act_fn,
                padding=padding))(prev_layer)

            if batch_normalize:
                conv_layer = (BatchNormalization(
                    weights=bn_weight_list))(conv_layer)
            prev_layer = conv_layer

            if activation == 'linear':
                all_layers.append(prev_layer)
            elif activation == 'mish':
                act_layer = Mish()(prev_layer)
                prev_layer = act_layer
                all_layers.append(act_layer)
            elif activation == 'leaky':
                act_layer = LeakyReLU(alpha=0.1)(prev_layer)
                prev_layer = act_layer
                all_layers.append(act_layer)

        elif section.startswith('route'):
            ids = [int(i) for i in cfg_parser[section]['layers'].split(',')]
            layers = [all_layers[i] for i in ids]
            if len(layers) > 1:
                print('Concatenating route layers:', layers)
                concatenate_layer = Concatenate()(layers)
                all_layers.append(concatenate_layer)
                prev_layer = concatenate_layer
            else:
                skip_layer = layers[0]  # only one layer to route
                all_layers.append(skip_layer)
                prev_layer = skip_layer

        elif section.startswith('maxpool'):
            size = int(cfg_parser[section]['size'])
            stride = int(cfg_parser[section]['stride'])
            all_layers.append(
                MaxPooling2D(
                    pool_size=(size, size),
                    strides=(stride, stride),
                    padding='same')(prev_layer))
            prev_layer = all_layers[-1]

        elif section.startswith('shortcut'):
            index = int(cfg_parser[section]['from'])
            activation = cfg_parser[section]['activation']
            assert activation == 'linear', 'Only linear activation supported.'
            all_layers.append(Add()([all_layers[index], prev_layer]))
            prev_layer = all_layers[-1]

        elif section.startswith('upsample'):
            stride = int(cfg_parser[section]['stride'])
            assert stride == 2, 'Only stride=2 supported.'
            all_layers.append(UpSampling2D(stride)(prev_layer))
            prev_layer = all_layers[-1]

        elif section.startswith('yolo'):
            out_index.append(len(all_layers)-1)
            all_layers.append(None)
            prev_layer = all_layers[-1]

## me add
        elif section.startswith('region'):
            with open('{}_anchors.txt'.format(output_root), 'w') as f:
                print(cfg_parser[section]['anchors'], file=f)
##			
			
        elif section.startswith('net'):
            pass

        else:
            raise ValueError(
                'Unsupported section header type: {}'.format(section))

    # Create and save model.
    if len(out_index)==0: out_index.append(len(all_layers)-1)

    model = Model(inputs=input_layer, outputs=[all_layers[i] for i in out_index])
    model.summary()

    coreml_model = coremltools.converters.keras.convert(
        model,
        input_names='input1', image_input_names='input1', output_names='output1', image_scale=1/255.,
#me        input_names='input1', image_input_names='input1', output_names=['output3', 'output2', 'output1'], image_scale=1/255.,		
        add_custom_layers=True,custom_conversion_functions={ "Mish": convert_mish })
    coreml_model.input_description['input1'] = 'Input image'
    coreml_model.output_description['output1'] = 'The 13x13 grid (Scale1)'
#me    coreml_model.output_description['output2'] = 'The 26x26 grid (Scale2)'
#me    coreml_model.output_description['output3'] = 'The 52x52 grid (Scale3)'
    coreml_model.save(output_path)

if __name__ == '__main__':
    _main(parser.parse_args())

 

원본과 비교하면 yolov2-tiny의 cfg를 사용하면 오류가 발생하는데 첫 번째가 중간에 #me add 부분인 region이 없어서 오류가 난다.

그 부분을 추가해 주면 된다. 중요한 것은 아니지만 anchor 정보를 파일로 저장하는 것이다. cfg에서 삭제해도 될지 모르지만 다른 것에 영향이 있을지 모르니 놔두고 그냥 코드만 추가해 주면 된다. 

 

다음으로 위 코드의 298번째줄인데 keras로부터 읽는 부분이 3개로 복수는 안된다고 한다. 따라서 ouput이 3개로 출력되니 안 되는 것이다. 어차피 커스텀데이터셋은 1개의 입력과 출력밖에는 안 쓰니까 나머지 ouput의 2개는 지워주면 된다. 추가 출력이 없으니까 적용이 안 되는 것으로 생각된다. 

 

여기까지 하면 정상적으로 mlmodel이 출력되어 저장된다.

 

그다음으로 이제 onnx로 컨버팅 하는 것이다.

 

다음의 코드를 convert_ml2onnx.py로 저장하자

import onnxmltools
import coremltools

# Load a Core ML model
coreml_model = coremltools.utils.load_spec('input.mlmodel')

# Convert the Core ML model into ONNX
onnx_model = onnxmltools.convert_coreml(coreml_model, 'sample model')

# Save as protobuf
onnxmltools.utils.save_model(onnx_model, 'output.onnx')

 

따옴표 된 부분을 자신의 것으로 적절하게 수정하고 python에서 실행하면 정상적으로 onnx로 컨버팅 된다.

 

이제 컨버팅 된 onnx 파일을 unity에서 불러와 적용하고 android 앱으로 빌드해서 테스트해 보면 된다.

 

keras로 안되었던 것이 Coreml model로 되는 것으로 봐서 keras 컨버팅이 문제가 있는 것이 확실하다.

아직도 keras로 정상적인 방법을 모르겠다.

반응형

댓글