태그 보관물: openvino

OpenVINO Object Detection Model의 전처리와 후처리를 간단하게

AI model들은 입출력 layer의 구조가 다르므로 서로 다른 pre/post processing을 필요로 한다. Vision model을 예로 들면 어떤 모델은 입력을 416×416으로 받게 되어 있어서 입력 전에 원본 크기로 부터 resizing을 해주어야 하고, 어떤 모델은 resizing layer을 포함하고 있어서 그냥 입력해도 잘 동작하기도 한다. 또한 어떤 모델은 입력값을 정규화 해서 입력해 주어야 하고, 어떤 모델은 정규화가 필요 없기도 하다.

입력층 뿐 아니라 출력층도 제각각이다. Object detection을 수행하는 모델들을 보면 YOLO같은 모델은 score값을 bounding box와 함께 전달하고, D-Fine같은 모델은 bounding box, score, label을 각각 따로 출력하기도 한다.

어떤 모델의 입출력 형태의 차이를 보고 싶으면 읽어들인 모델의 inputs와 outputs 변수의 내용을 살펴보면 된다.

def print_model_io_layer(ovcore: Core, model_path: str, model_name: str):
    read_model = ovcore.read_model(model_path)
    print(f"[{model_name}]")

    # Display input layer
    for idx, input_layer in enumerate(read_model.inputs):
        print(f"Input({idx+1}):")
        print("  any_name     :", input_layer.get_any_name())
        print("  names        :", input_layer.get_names())
        print("  shape        :", input_layer.get_partial_shape())
        print("  element type :", input_layer.get_element_type())

    # Display output layer
    for idx, output_layer in enumerate(read_model.outputs):
        print(f"Output({idx+1}):")
        print("  any_name     :", output_layer.get_any_name())
        print("  names        :", output_layer.get_names())
        print("  shape        :", output_layer.get_partial_shape())
        print("  element type :", output_layer.get_element_type())

이렇게 다양한 모델들의 입출력을 직접 처리하는 것은 여간 번거로운 일이 아니다. OpenVINO에서는 모델에 전/후처리를 사용자가 지정할 수 있는 PrePostProcessor를 지원하기는 하지만 이 마저도 모델별로 상이한 부분을 직접 수정해 주어야 하기 때문에 번거로운 것은 마찬가지다.

OpenVINO Model API 사용법

예를 들어 ATSS, YOLOX-S, YOLOX-Tiny, D-Fine 네개의 OpenVINO model과 각각에 대한 ONNX model들까지 8개의 서로 다른 세개의 서로 다른 모델들이 있다고 할 때, 8개의 서로 다른 입출력 결과를 처리하는 것은 아주 많은 노력을 필요로 할 것이다.

openvino-model-api를 사용하면 이러한 부분을 비교적 간단하게 처리 할 수 있다. 사용하려면 다음과 같이 필요한 패키지들을 설치해 준다.

pip install openvino openvino-model-api onnx

설치가 끝나면 먼저 서로 다른 입출력 계층을 맞춰 주는 모델을 생성한다. 추론에 사용하고자 하는 모델 파일로 OpenvinoAdapter를 만들어서 create_model()에 넘겨 준다. 이렇게 생성된 모델은 입력을 위한 전처리 과정을 자동으로 수행한다.

from openvino import Core
from model_api.adapters import OpenvinoAdapter
from model_api.models import Model


ovcore = Core()

# Adapter를 생성하고 create_model()에 넘겨준다.
ovadapter = OpenvinoAdapter(ovcore, model_path)
model = Model.create_model(ovadapter, preload=True)

그리고 나서 추론을 실행하면 그 결과는 DetectionResult 타입으로 정리되어 반환된다. 따라서 모델별로 서로다른 후처리(post processing) 과정 필요 없이 DetectionResult으로 반환되는 결과를 처리해주면 된다.

# 추론 실행
detection_result = model(test_image)

결과물 처리를 위해서 detection_result를 원본 이미지에 overlay하는 함수로 만들었다(overlay_detection_result). 이 함수는 Threshold값을 넘는 결과물을 원본 이미지 위에 bounding box와 label로 표시하고 detect된 object의 갯수와 함께 반환해 준다.

DetectionResult에서 참조되는 주요한 멤버변수는 Socre값을 나타내는 .score, bounding box를 나타내는 .xmin, .ymin, .xmax, .ymax 그리고 label을 가지고 있는 .str_label이다. Bounding box 좌표도 입력 이미지의 크기에 맞게 이미 scaling되어 있으므로 좌표를 그대로 가져다 쓰면 된다.

from model_api.models.utils import DetectionResult


def overlay_detection_result(
    original_image: np.ndarray, detection_result: DetectionResult, threshold: float
) -> Tuple[np.ndarray, int]:
    num_detected = 0
    processed_image = original_image.copy()

    for det in detection_result[0]:
        score = det.score
        if score >= threshold:
            num_detected += 1

            # Bounding box
            x1 = int(det.xmin)
            y1 = int(det.ymin)
            x2 = int(det.xmax)
            y2 = int(det.ymax)

            # Clamping
            x1 = max(0, min(x1, original_image.shape[1]))
            y1 = max(0, min(y1, original_image.shape[0]))
            x2 = max(0, min(x2, original_image.shape[1]))
            y2 = max(0, min(y2, original_image.shape[0]))

            # Draw BBox
            cv2.rectangle(processed_image, (x1, y1), (x2, y2), BBOX_COLOR, 2)

            # Draw label.
            display_text = (
                f"{det.str_label} {score:.2f}" if {det.str_label} else f"{score:.2f}"
            )
            cv2.putText(
                processed_image,
                display_text,
                (x1, y1 - 10),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.5,
                LABEL_COLOR,
                2,
            )
    return processed_image, num_detected

8개의 모델에 시험해 보자

사용하려는 8개의 모델들은 다음과 같다.

atss_model_file = os.path.join(MODEL_PATH, "atss_fp32/model.xml")
atss_onnx_model_file = os.path.join(MODEL_PATH, "atss_onnx_fp32/model.onnx")
yolos_model_file = os.path.join(MODEL_PATH, "yolos_fp32/model.xml")
yolos_onnx_model_file = os.path.join(MODEL_PATH, "yolos_onnx_fp32/model.onnx")
yolotiny_model_file = os.path.join(MODEL_PATH, "yolotiny_fp32/model.xml")
yolotiny_onnx_model_file = os.path.join(MODEL_PATH, "yolotiny_onnx_fp32/model.onnx")
dfinex_model_file = os.path.join(MODEL_PATH, "dfinex_fp32/model.xml")
dfinex_onnx_model_file = os.path.join(MODEL_PATH, "dfinex_onnx_fp32/model.onnx")

이것들을 all_models라는 list에 넣고 한번에 돌린다. 즉, 각 모델들의 서로 다른 입력과 출력 계층에 대한 처리를 단일한 코드로 수행하는 것이다.

ovcore = Core()


# Test image
test_image = cv2.imread("./data/test_image.jpg")

threshold = 0.8
for m in all_models:
    model_path = m[0]
    model_name = m[1]
    ovadapter = OpenvinoAdapter(ovcore, model_path)
    model = Model.create_model(ovadapter, preload=True)
    detection_result = model(test_image)
    proc_image, num_det = overlay_detection_result(
        test_image, detection_result, threshold
    )

    print(f"{model_name} :: {num_det} objects detected (threshold: {threshold}).")
    cv2.imwrite(f"output_{model_name}.jpg", proc_image)

전체 소스코드

OpenVINO로 Intel NPU에서 추론 실행하기

Intel 14세대 Meteor Lake 부터 NPU가 함께 embedding되어 제공된다. Linux system에서 사용하려면 kernel version 6.2이상이 설치되어 있어야 한다.

NPU 디바이스 확인

CPU: Intel(R) Core(TM) Ultra 5
OS: Ubuntu 24.04
Linux kernel: 6.14.0-33-generic

lspci 명령어로 NPU 디바이스가 인식되어 있는지를 확인할 수 있다.

$ lspci -v|grep -A 9 NPU
00:0b.0 Processing accelerators: Intel Corporation Arrow Lake NPU (rev 01)
        DeviceName: Onboard - Other
        Subsystem: Gigabyte Technology Co., Ltd Device 7270
        Flags: bus master, fast devsel, latency 0, IRQ 154, IOMMU group 6
        Memory at 6408000000 (64-bit, non-prefetchable) [size=128M]
        Memory at 6410065000 (64-bit, non-prefetchable) [size=4K]
        Capabilities: <access denied>
        Kernel driver in use: intel_vpu
        Kernel modules: intel_vpu

커널에서 잘 인식 되었다면 아래의 경로에 디바이스 노드가 보이게 된다.

$ ls -l /dev/accel/accel0 
crw-rw---- 1 root render 261, 0 Oct 10 19:27 /dev/accel/accel0

Linux용 NPU driver 설치

이제 NPU를 Linux에서 사용하기 위한 드라이버를 설치해야 한다. 관련한 내용은 Intel GitHub의 Linux NPU driver release 문서에 자세히 설명되어 있다.

GitHub의 release page를 찾아가서 압축파일을 다운로드 받아 dpkg로 설치하면 모든 준비가 완료된다.

wget https://github.com/intel/linux-npu-driver/releases/download/v1.24.0/linux-npu-driver-v1.24.0.20251003-18218973328-ubuntu2404.tar.gz
tar -xf linux-npu-driver-v1.24.0.20251003-18218973328-ubuntu2404.tar.gz

sudo apt update
sudo apt install libtbb12

sudo dpkg -i *.deb

# Level zero가 설치되지 않은경우에만
wget https://github.com/oneapi-src/level-zero/releases/download/v1.24.2/level-zero_1.24.2+u24.04_amd64.deb
sudo dpkg -i level-zero*.deb

OpenVINO device 목록 확인

OpenVINO의 Core() class안에 있는 available_devices 멤버 변수를 출력하면 사용가능 한 추론 장치의 목록이 표시되는데 아래와 같이 CPU, GPU와 함께 NPU가 표시되는 것을 볼 수 있다.

python3 -m venv .venv
source .venv/bin/activate
(.venv) pip install setuptools
(.venv) pip install openvino openvino-dev
(.venv)$ python3 -c "from openvino import Core; print(Core().available_devices)"
['CPU', 'GPU', 'NPU']

성능측정 결과

Ultralytics의 문서를 따라 YOLOV11n을 OpenVINO용으로 변환해서 OpenVINO benchmark_app으로 측정하였다.

NPU 벤치마크 결과는 다음과 같다. CPU보다 다소 좋은 성능을 보이고, 실행하는 동안 CPU utilization이 증가되지 않는 것이 확인된다.

$ benchmark_app -m ./yolo11n_openvino_model/yolo11n.xml -d NPU
...
[ INFO ] Execution Devices:['NPU']
[ INFO ] Count:            9704 iterations
[ INFO ] Duration:         60033.42 ms
[ INFO ] Latency:
[ INFO ]    Median:        24.50 ms
[ INFO ]    Average:       24.52 ms
[ INFO ]    Min:           23.54 ms
[ INFO ]    Max:           69.62 ms
[ INFO ] Throughput:   161.64 FPS

CPU로 수행한 결과는 다음과 같다.

$ benchmark_app -m ./yolo11n_openvino_model/yolo11n.xml -d CPU
...
[ INFO ] Execution Devices:['CPU']
[ INFO ] Count:            6351 iterations
[ INFO ] Duration:         60047.25 ms
[ INFO ] Latency:
[ INFO ]    Median:        24.51 ms
[ INFO ]    Average:       28.31 ms
[ INFO ]    Min:           23.05 ms
[ INFO ]    Max:           45.46 ms
[ INFO ] Throughput:   105.77 FPS

iGPU로 실행했을 때 throuput은 가장 좋으나 Max latency가 높은 것이 관찰되는데 아마도 로딩을 위한 메모리 복사 소요 시간으로 추정된다.

$ benchmark_app -m ./yolo11n_openvino_model/yolo11n.xml -d GPU
...
[ INFO ] Execution Devices:['GPU.0']
[ INFO ] Count:            87056 iterations
[ INFO ] Duration:         60013.22 ms
[ INFO ] Latency:
[ INFO ]    Median:        10.77 ms
[ INFO ]    Average:       10.83 ms
[ INFO ]    Min:           4.51 ms
[ INFO ]    Max:           1010.93 ms
[ INFO ] Throughput:   1450.61 FPS

OpenVINO python을 이용한 inference 예제

OpenVINO를 이용해서 TensorFlow(Keras)로 training한 모델로 추론(inference)을 수행하는 간단한 예제를 작성해 보았다.

TensorFlow model을 freeze하기

Training된 모델을 model optimizer에 넣기 전에 freeze시켜야 하는데, output_node_names를 입력하라는 오류 메세지가 계속 뜬다면 제대로 freezing을 수행했는지 확인해 보는게 좋다. 알아보기 쉽게 하기 위해 입출력 layer에 ‘name=’ parameter로 다음과 같이 이름을 지정해 주었다.

model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28), name='input'),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(10, activation='softmax', name='output')
])

그러면 layer의 이름들을 출력할 때 다음과 같이 나온다. 전체 MINST model training과 freezing 과정은 이 CoLab에 적어 두었으니 참조.

------------------------------------------------------------
Frozen model layers:
x
sequential/input/Const
sequential/input/Reshape
sequential/dense/MatMul/ReadVariableOp/resource
sequential/dense/MatMul/ReadVariableOp
sequential/dense/MatMul
sequential/dense/BiasAdd/ReadVariableOp/resource
sequential/dense/BiasAdd/ReadVariableOp
sequential/dense/BiasAdd
sequential/dense/Relu
sequential/dropout/Identity
sequential/output/MatMul/ReadVariableOp/resource
sequential/output/MatMul/ReadVariableOp
sequential/output/MatMul
sequential/output/BiasAdd/ReadVariableOp/resource
sequential/output/BiasAdd/ReadVariableOp
sequential/output/BiasAdd
sequential/output/Softmax
Identity
------------------------------------------------------------

모델 옵티마이저(mo-tf.py)

Freeze된 모델을 다운로드 받은 후에 TensorFlow용 model optimizer인 mo-tf.py를 실행 시키면 model을 나타내는 xml file과 weight값을 저장하는 bin file이 생성된다. 이 때 training된 모델은 입력 shape을 [-1, 28, 28]로 알고 있기 때문에 음수가 아닌 값을 넣어 달라는 에러가 생긴다. –input_shape parameter를 다음과 같이 적어준다.

/opt/intel/openvino_2021/deployment_tools/model_optimizer/mo_tf.py --input_model ./model/mnist_model/frozen_graph.pb  --input_shape [28,28]

OpenVINO를 이용한 inference

Model optimizer가 수행되었다면 이제 xml file을 이용해 model을 load하고 inference를 수행하면 된다. 다음은 Training 후 freezing과 model optimization이 수행된 XML file을 이용해서 inference를 수행하는 간단한 코드이다.

출력결과

$ python3 ./infer_mnist.py ./model/mnist_model/frozen/frozen_graph.xml

        Model path= ./model/mnist_model/frozen/frozen_graph.xml 
        Device= CPU
Accuracy: 0.9789 (hit: 9789/ miss: 211)