Directorio de artículos
1. Introducción del chip
-
Descripción general del entorno
Sistema de PC: Ubuntu 18.04 LTS, tipo de chip de 64 bits
y sistema: RV1126, Linux de 32 bitsLa situación básica del chip se muestra en la siguiente figura.
-
cadena de herramientas RK
Dirección de descarga de la cadena de herramientas: https://github.com/rockchip-linux/rknn-toolkit
Versión de RKNN-ToolKit: V1.7.3La siguiente figura es la introducción [Léame] del enlace de software anterior. Diferentes plataformas de chips son adecuadas para diferentes cadenas de herramientas. 【RK1808/RK1806/RK3399Pro/RV1109/RV1126】La cadena de herramientas de adaptación de chip de la serie es 【RKNN-Toolkit】:
2. Breve descripción del proceso de implementación.
El proceso básico es el siguiente: 1. Conversión de modelo, 2. Conversión de C++, 3. Operación RV1126
- Instale la versión Python de la cadena de herramientas ( rknn-toolkit ) en la PC y exporte (.onnx, .pt, .ckpt) a modelos (.rknn) en la PC. Este paso puede verificar si el modelo que entrenó se puede exportar con éxito, si el operador de convolución es compatible, si es cuantificable, si el efecto de predicción cumple con las expectativas y otras funciones.
- Configure la cadena de herramientas de la versión C ( rknpu ) en el lado de la PC y reescriba el proyecto de la versión [Python] en un proyecto de razonamiento [C++]. Utilice la cadena de herramientas [arm-32bit] para compilarlo en un archivo ejecutable ( no puede utilizar el entorno g++ de Ubuntu para compilar ).
- Coloque el modelo RKNN obtenido y los archivos ejecutables y algunas bibliotecas dependientes necesarias (.so) de RK en el chip, que se puede ejecutar directamente.
- Evaluación cuantitativa y evaluación del desempeño en el tablero.
3. Configuración del entorno de desarrollo (RKNN-Toolkit)
Combinado con la documentación oficial, analiza principalmente la función central RKNN de la versión Python del código en ejecución. El código está organizado de la siguiente manera:
3.1 Prueba de instalación del software
-
Instalación de software básico
cd ~/rknn-toolkit-master/ cd packages(软件安装,见上图的目录截图) pip install -r requirements-cpu.txt(cpu,gpu 版本都可以,如果下载慢,可以尝试其它方式,逐个安装软件)
-
Instalación del software RKNN
Descargue el paquete de software: rknn-toolkit-v1.7.3-packages.zip , descomprímalo en el directorio raíz y siga los pasos a continuación para instalarlo:cd rknn-toolkit-v1.7.3-packages/packages pip install rknn_toolkit-1.7.3-cp36-cp36m-linux_x86_64.whl
La prueba de instalación se completa y no se imprime nada, lo que indica que la instalación se realizó correctamente, como se muestra en la siguiente figura
-
Ejemplo de simulación ejecutándose en PC
Ejemplo de prueba:
examples/onnx/yolov5/test.py
Modelo a convertir: yolov5s.onnx
Modelo convertido: yolov5s.rknn
Resultado de visualización: Se puede ver que la operación fue exitosa en el simulador de PC y el resultado es normal.
3.2, análisis de código de muestra
-
Función API principal
para crear objetos RKNNrknn = RKNN()
Parámetros de API: imprima la información de registro durante el proceso de conversión del modelo, lo cual es útil para solucionar problemas
Configurar los parámetros de RKNN
# 配置预处理参数 rknn.config()
Antes de construir el modelo RKNN, es necesario configurar los parámetros del modelo, como el valor medio del canal, el orden de los canales y el tipo de cuantificación. Estas operaciones se pueden configurar a través de la interfaz [config], de la siguiente manera :
modelo de carga
ret = rknn.load(model='model_path')
Tome la carga del modelo ONNX como ejemplo, preste atención al parámetro: salidas, a veces es necesario obtener el resultado de salida del nodo especificado del modelo, este parámetro es muy útil. La descripción específica se muestra en la siguiente figura:
compilar modelo
ret = rknn.build()
Exportar modelo RKNN
ret = rknn.export_rknn(RKNN_MODEL)
Exporte archivos de modelo RKNN a través de esta interfaz para la implementación del modelo.
Cargar modelo RKNN
ret = rknn.load_rknn(path='./yolov5s.rknn')
Inicializar el entorno operativo
ret = rknn.init_runtime()
Inicialice el entorno operativo y especifique la plataforma operativa. Los parámetros relevantes son los siguientes:
razonamiento modelo
outputs = rknn.inference(inputs=[img])
Antes de la inferencia del modelo, se debe construir o cargar un modelo RKNN.
Evaluación del desempeño del modelo
rknn.eval_perf(inputs=[image], is_print=True)
Implementar en la junta directiva y evaluar el desempeño es un paso esencial, de la siguiente manera
uso de memoria
memory_detail = rknn.eval_memory()
-
Análisis de ejemplo
En el lado de la PC, utilice el entorno Python configurado para convertir el modelo ONNX al modelo de formato RKNN.import os import numpy as np import cv2 from rknn.api import RKNN ONNX_MODEL = 'yolov5s.onnx' RKNN_MODEL = 'yolov5s.rknn' IMG_PATH = './bus.jpg' DATASET = './dataset.txt' QUANTIZE_ON = True BOX_THRESH = 0.5 NMS_THRESH = 0.6 IMG_SIZE = (640, 640) # (width, height), such as (1280, 736) CLASSES = ("person", "bicycle", "car","motorbike ","aeroplane ","bus ","train","truck ","boat","traffic light", "fire hydrant","stop sign ","parking meter","bench","bird","cat","dog ","horse ","sheep","cow","elephant", "bear","zebra ","giraffe","backpack","umbrella","handbag","tie","suitcase","frisbee","skis","snowboard","sports ball","kite", "baseball bat","baseball glove","skateboard","surfboard","tennis racket","bottle","wine glass","cup","fork","knife ", "spoon","bowl","banana","apple","sandwich","orange","broccoli","carrot","hot dog","pizza ","donut","cake","chair","sofa", "pottedplant","bed","diningtable","toilet ","tvmonitor","laptop ","mouse ","remote ","keyboard ","cell phone","microwave ", "oven ","toaster","sink","refrigerator ","book","clock","vase","scissors ","teddy bear ","hair drier", "toothbrush ") def sigmoid(x): return 1 / (1 + np.exp(-x)) def xywh2xyxy(x): # Convert [x, y, w, h] to [x1, y1, x2, y2] y = np.copy(x) y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y return y def process(input, mask, anchors): anchors = [anchors[i] for i in mask] grid_h, grid_w = map(int, input.shape[0:2]) box_confidence = sigmoid(input[..., 4]) box_confidence = np.expand_dims(box_confidence, axis=-1) box_class_probs = sigmoid(input[..., 5:]) box_xy = sigmoid(input[..., :2])*2 - 0.5 col = np.tile(np.arange(0, grid_w), grid_h).reshape(-1, grid_w) row = np.tile(np.arange(0, grid_h).reshape(-1, 1), grid_w) col = col.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2) row = row.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2) grid = np.concatenate((col, row), axis=-1) box_xy += grid box_xy *= (int(IMG_SIZE[1]/grid_h), int(IMG_SIZE[0]/grid_w)) box_wh = pow(sigmoid(input[..., 2:4])*2, 2) box_wh = box_wh * anchors box = np.concatenate((box_xy, box_wh), axis=-1) return box, box_confidence, box_class_probs def filter_boxes(boxes, box_confidences, box_class_probs): """Filter boxes with box threshold. It's a bit different with origin yolov5 post process! # Arguments boxes: ndarray, boxes of objects. box_confidences: ndarray, confidences of objects. box_class_probs: ndarray, class_probs of objects. # Returns boxes: ndarray, filtered boxes. classes: ndarray, classes for boxes. scores: ndarray, scores for boxes. """ boxes = boxes.reshape(-1, 4) box_confidences = box_confidences.reshape(-1) box_class_probs = box_class_probs.reshape(-1, box_class_probs.shape[-1]) _box_pos = np.where(box_confidences >= BOX_THRESH) boxes = boxes[_box_pos] box_confidences = box_confidences[_box_pos] box_class_probs = box_class_probs[_box_pos] class_max_score = np.max(box_class_probs, axis=-1) classes = np.argmax(box_class_probs, axis=-1) _class_pos = np.where(class_max_score* box_confidences >= BOX_THRESH) boxes = boxes[_class_pos] classes = classes[_class_pos] scores = (class_max_score* box_confidences)[_class_pos] return boxes, classes, scores def nms_boxes(boxes, scores): """Suppress non-maximal boxes. # Arguments boxes: ndarray, boxes of objects. scores: ndarray, scores of objects. # Returns keep: ndarray, index of effective boxes. """ x = boxes[:, 0] y = boxes[:, 1] w = boxes[:, 2] - boxes[:, 0] h = boxes[:, 3] - boxes[:, 1] areas = w * h order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) xx1 = np.maximum(x[i], x[order[1:]]) yy1 = np.maximum(y[i], y[order[1:]]) xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]]) yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]]) w1 = np.maximum(0.0, xx2 - xx1 + 0.00001) h1 = np.maximum(0.0, yy2 - yy1 + 0.00001) inter = w1 * h1 ovr = inter / (areas[i] + areas[order[1:]] - inter) inds = np.where(ovr <= NMS_THRESH)[0] order = order[inds + 1] keep = np.array(keep) return keep def yolov5_post_process(input_data): masks = [[0, 1, 2], [3, 4, 5], [6, 7, 8]] anchors = [[10, 13], [16, 30], [33, 23], [30, 61], [62, 45], [59, 119], [116, 90], [156, 198], [373, 326]] boxes, classes, scores = [], [], [] for input,mask in zip(input_data, masks): b, c, s = process(input, mask, anchors) b, c, s = filter_boxes(b, c, s) boxes.append(b) classes.append(c) scores.append(s) boxes = np.concatenate(boxes) boxes = xywh2xyxy(boxes) classes = np.concatenate(classes) scores = np.concatenate(scores) nboxes, nclasses, nscores = [], [], [] for c in set(classes): inds = np.where(classes == c) b = boxes[inds] c = classes[inds] s = scores[inds] keep = nms_boxes(b, s) nboxes.append(b[keep]) nclasses.append(c[keep]) nscores.append(s[keep]) if not nclasses and not nscores: return None, None, None boxes = np.concatenate(nboxes) classes = np.concatenate(nclasses) scores = np.concatenate(nscores) return boxes, classes, scores def draw(image, boxes, scores, classes): """Draw the boxes on the image. # Argument: image: original image. boxes: ndarray, boxes of objects. classes: ndarray, classes of objects. scores: ndarray, scores of objects. all_classes: all classes name. """ for box, score, cl in zip(boxes, scores, classes): top, left, right, bottom = box print('class: {}, score: {}'.format(CLASSES[cl], score)) print('box coordinate left,top,right,down: [{}, {}, {}, {}]'.format(top, left, right, bottom)) top = int(top) left = int(left) right = int(right) bottom = int(bottom) cv2.rectangle(image, (top, left), (right, bottom), (255, 0, 0), 2) cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score), (top, left - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) def letterbox(im, new_shape=(640, 640), color=(0, 0, 0)): # Resize and pad image while meeting stride-multiple constraints shape = im.shape[:2] # current shape [height, width] if isinstance(new_shape, int): new_shape = (new_shape, new_shape) # Scale ratio (new / old) r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) # Compute padding ratio = r, r # width, height ratios new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding dw /= 2 # divide padding into 2 sides dh /= 2 if shape[::-1] != new_unpad: # resize im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border return im, ratio, (dw, dh) if __name__ == '__main__': # Create RKNN object # 创建RKNN对象 rknn = RKNN() if not os.path.exists(ONNX_MODEL): print('model not exist') exit(-1) # pre-process config # 配置模型运行的相关参数,具体如下 print('--> Config model') rknn.config(reorder_channel='0 1 2', # 调整通道输入顺序 mean_values=[[0, 0, 0]], # 均值 std_values=[[255, 255, 255]], # 方差 optimization_level=3, # 打开所有优化选项 target_platform = 'rv1126', # 指定RKNN的运行平台 output_optimize=1, # 官方文档未提供该参数的说明 quantize_input_node=QUANTIZE_ON) # 是否对输入节点进行量化 print('done') # Load ONNX model # 加载待转换模型(onnx) print('--> Loading model') ret = rknn.load_onnx(model=ONNX_MODEL) if ret != 0: print('Load yolov5 failed!') exit(ret) print('done') # Build model # 构建RKNN模型 # do_quantization:是否进行量化 # dataset:参与量化的输入数据 print('--> Building model') ret = rknn.build(do_quantization=QUANTIZE_ON, dataset=DATASET) if ret != 0: print('Build yolov5 failed!') exit(ret) print('done') # Export RKNN model # 导出模型为rknn print('--> Export RKNN model') ret = rknn.export_rknn(RKNN_MODEL) if ret != 0: print('Export yolov5rknn failed!') exit(ret) print('done') # ============================================================ # 上述模型转换成功后,后续代码对模型进行性能评估 # ============================================================ # init runtime environment # 初始化运行环境 print('--> Init runtime environment') ret = rknn.init_runtime() # ret = rknn.init_runtime('rk1808', device_id='1808') if ret != 0: print('Init runtime environment failed') exit(ret) print('done') # Set inputs img = cv2.imread(IMG_PATH) img, ratio, (dw, dh) = letterbox(img, new_shape=(IMG_SIZE[1], IMG_SIZE[0])) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Inference # 模型推理 print('--> Running model') outputs = rknn.inference(inputs=[img]) # post process input0_data = outputs[0] input1_data = outputs[1] input2_data = outputs[2] input0_data = input0_data.reshape([3,-1]+list(input0_data.shape[-2:])) input1_data = input1_data.reshape([3,-1]+list(input1_data.shape[-2:])) input2_data = input2_data.reshape([3,-1]+list(input2_data.shape[-2:])) input_data = list() input_data.append(np.transpose(input0_data, (2, 3, 0, 1))) input_data.append(np.transpose(input1_data, (2, 3, 0, 1))) input_data.append(np.transpose(input2_data, (2, 3, 0, 1))) boxes, classes, scores = yolov5_post_process(input_data) img_1 = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) if boxes is not None: draw(img_1, boxes, scores, classes) # cv2.imshow("post process result", img_1) # cv2.waitKeyEx(0) # 性能评估 rknn.eval_perf(inputs=[img],is_print=True) rknn.release()
4. Configuración del entorno de desarrollo (RKNN-NPU)
A través del tutorial anterior, convertimos el modelo (onnx) al modelo (rknn) en el lado de la PC y obtuvimos los resultados esperados, para que se pueda llevar a cabo el proceso de verificación en la placa. A continuación, debemos ejecutar el modelo en un chip específico (como RV1126) para completar el proceso de implementación completo desde la PC al chip.
4.1 Estructura del código fuente
Este proyecto proporciona principalmente controladores y ejemplos para Rockchip NPU. La estructura del código fuente del proyecto se muestra en la siguiente figura.
Chips aplicables: RK1808/RK1806, RV1109/RV1126
Plataforma de configuración: Ubuntu18.04
Estructura del código fuente:
4.2 Compile el código fuente
-
Descargar el compilador cruzado [arm]
Dirección de descarga: https://releases.linaro.org/components/toolchain/binaries/6.4-2017.08/arm-linux-gnueabihf/
Versión del compilador: compilador cruzado de 32 bits
: descomprima el compilador cruzado a una ruta fija, en Ubuntu Compile el proyecto y ejecute el programa en la placa RV1126.
arm-none-eabi-gcc: herramienta de compilación cruzada ARM lanzada por GNU, que se puede utilizar para compilar chips ARM MCU (32 bits), como programas de chips ARM7, ARM9 y Cortex-M/R.
aarch64-linux-gnu-gcc: la herramienta de compilación cruzada ARM basada en GCC de Linaro se puede utilizar para realizar una compilación cruzada de programas bare-metal, u-boot, kernel de Linux, sistema de archivos y aplicaciones de aplicaciones en objetivos ARMv8 (64 bits). -
CMakeLists.txt
Preste atención a la versión de la biblioteca : RV1126 es un sistema de 32 bits y algunas bibliotecas dependientes también deben compilarse en bibliotecas de 32 bits con la cadena de herramientas anterior.cmake_minimum_required(VERSION 3.4.1) project(rknn_yolov5_demo_linux) set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -s -O3") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -s -O3") if (CMAKE_C_COMPILER MATCHES "aarch64") set(LIB_ARCH lib64) else() set(LIB_ARCH lib) endif() # rga set(RGA_DIR ${CMAKE_SOURCE_DIR}/../3rdparty/rga) include_directories(${RGA_DIR}/include) # drm set(DRM_DIR ${CMAKE_SOURCE_DIR}/../3rdparty/drm) include_directories(${DRM_DIR}/include) include_directories(${DRM_DIR}/include/libdrm) include_directories(${CMAKE_SOURCE_DIR}/include) # rknn api set(RKNN_API_PATH ${CMAKE_SOURCE_DIR}/../../librknn_api) include_directories(${RKNN_API_PATH}/include) set(RKNN_API_LIB ${RKNN_API_PATH}/${LIB_ARCH}/librknn_api.so) #stb include_directories(${CMAKE_SOURCE_DIR}/../3rdparty/) set(CMAKE_INSTALL_RPATH "lib") add_executable(rknn_yolov5_demo src/drm_func.c src/rga_func.c src/postprocess.cc src/main.cc ) target_link_libraries(rknn_yolov5_demo ${RKNN_API_LIB} dl ) # install target and libraries set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/install/rknn_yolov5_demo) install(TARGETS rknn_yolov5_demo DESTINATION ./) install(DIRECTORY model DESTINATION ./) install(PROGRAMS ${RKNN_API_LIB} DESTINATION lib)
-
Configurar【build.sh】
#!/bin/bash set -e # for rk1808 aarch64 # GCC_COMPILER=${RK1808_TOOL_CHAIN}/bin/aarch64-linux-gnu # for rk1806 armhf # GCC_COMPILER=~/opts/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf # for rv1109/rv1126 armhf # 自己添加的编译器路径【RV1109_TOOL_CHAIN】 RV1109_TOOL_CHAIN='/home/ll/Mount/kxh_2023/RV1126/gcc-linaro-6.4.1-2017.08-i686_arm-linux-gnueabihf' GCC_COMPILER=${RV1109_TOOL_CHAIN}/bin/arm-linux-gnueabihf ROOT_PWD=$( cd "$( dirname $0 )" && cd -P "$( dirname "$SOURCE" )" && pwd ) # build rockx BUILD_DIR=${ROOT_PWD}/build if [[ ! -d "${BUILD_DIR}" ]]; then mkdir -p ${BUILD_DIR} fi cd ${BUILD_DIR} cmake .. \ -DCMAKE_C_COMPILER=${GCC_COMPILER}-gcc \ -DCMAKE_CXX_COMPILER=${GCC_COMPILER}-g++ make -j4 make install cd - # cp run_rk180x.sh install/rknn_yolov5_demo/ cp run_rv1109_rv1126.sh install/rknn_yolov5_demo/
Sobre la base del [build.sh] original, simplemente agregue la ruta del compilador cruzado [RV1109_TOOL_CHAIN] para compilar el programa.
cd ~/RV1126/rknpu-master/rknpu-master/rknn/rknn_api/examples/rknn_yolov5_demo ./build.sh
El archivo generado está en el directorio [instalación] y luego se copia en la placa [RV1126] para ejecutarlo.
4.3, análisis del código fuente
-
Modelo de inicialización de la función API principal
// 读取二进制的RKNN模型 unsigned char* model_data = load_model(model_name, &model_data_size); // 创建 rknn_context 对象 ret = rknn_init(&ctx, model_data, model_data_size, 0)
La función de inicialización rknn_init creará el objeto rknn_context, cargará el modelo RKNN y realizará comportamientos de inicialización específicos de acuerdo con [bandera]
rknn_query
// 查询命令接口 // 以查询SDK版本信息的命令为例 ret = rknn_query(ctx, RKNN_QUERY_SDK_VERSION, &version, sizeof(rknn_sdk_version)); // 查询输入输出个数的命令 rknn_input_output_num io_num; ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); if (ret < 0) { printf("rknn_init error ret=%d\n", ret); return -1; } printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output);
La función rknn_query puede consultar y obtener información como la entrada y salida del modelo, el tiempo de ejecución y la versión del SDK. Los
comandos de consulta admitidos son los siguientes:
Establecer la entrada del modelo.
// 设置输入的基本结构 rknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = width * height * channel; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].pass_through = 0; // 设置输入信息 rknn_inputs_set(ctx, io_num.n_input, inputs);
Los datos de entrada del modelo se pueden configurar mediante la función rknn_inputs_set. Esta función puede admitir múltiples entradas, cada una de las cuales es un objeto de estructura rknn_input, y el usuario debe configurar este objeto antes de pasarlo.
razonamiento modelo
ret = rknn_run(ctx, NULL);
La función rknn_run ejecutará una inferencia del modelo y los datos de entrada deben configurarse a través de la función rknn_inputs_set antes de llamar
Obtener resultados de inferencia del modelo
rknn_output outputs[io_num.n_output]; memset(outputs, 0, sizeof(outputs)); for (int i = 0; i < io_num.n_output; i++) { outputs[i].want_float = 0; // 标识是否需要将输出数据转为 float 类型输出。 } ret = rknn_outputs_get(ctx, io_num.n_output, outputs, NULL);
La función rknn_outputs_get puede obtener los datos de salida de la inferencia del modelo. Esta función es capaz de recuperar múltiples datos de salida a la vez. Cada salida es un objeto de estructura rknn_output, y cada objeto rknn_output debe crearse y configurarse por turno antes de la llamada a la función.
Liberar la salida
ret = rknn_outputs_release(ctx, io_num.n_output, outputs);
La función rknn_outputs_release liberará los recursos relacionados con la salida obtenida por la función rknn_outputs_get
-
Análisis de ejemplo
Para aclarar todo el flujo de procesamiento, aquí solo se analiza la parte de la función principal del código. Los pasos básicos son los siguientes:
a. Inicializar el modelo
b. Obtener el número de entradas y salidas
c. Leer la imagen
d. Configurar los datos de entrada ,
por ejemplo, posprocesamiento h. liberar recursos/*------------------------------------------- Main Functions -------------------------------------------*/ int main(int argc, char** argv) { int status = 0; char* model_name = NULL; rknn_context ctx; void* drm_buf = NULL; int drm_fd = -1; int buf_fd = -1; // converted from buffer handle unsigned int handle; size_t actual_size = 0; int img_width = 0; int img_height = 0; int img_channel = 0; rga_context rga_ctx; drm_context drm_ctx; const float nms_threshold = NMS_THRESH; const float box_conf_threshold = BOX_THRESH; struct timeval start_time, stop_time; int ret; memset(&rga_ctx, 0, sizeof(rga_context)); memset(&drm_ctx, 0, sizeof(drm_context)); if (argc != 3) { printf("Usage: %s <rknn model> <bmp> \n", argv[0]); return -1; } printf("post process config: box_conf_threshold = %.2f, nms_threshold = %.2f\n", box_conf_threshold, nms_threshold); model_name = (char*)argv[1]; char* image_name = argv[2]; if (strstr(image_name, ".jpg") != NULL || strstr(image_name, ".png") != NULL) { printf("Error: read %s failed! only support .bmp format image\n", image_name); return -1; } /* Create the neural network */ // 1. 加载二进制rknn模型 // 2. 初始化 rknn_context 对象 printf("Loading mode...\n"); int model_data_size = 0; unsigned char* model_data = load_model(model_name, &model_data_size); ret = rknn_init(&ctx, model_data, model_data_size, 0); if (ret < 0) { printf("rknn_init error ret=%d\n", ret); return -1; } // 3. 查询SDK的版本信息 rknn_sdk_version version; ret = rknn_query(ctx, RKNN_QUERY_SDK_VERSION, &version, sizeof(rknn_sdk_version)); if (ret < 0) { printf("rknn_init error ret=%d\n", ret); return -1; } printf("sdk version: %s driver version: %s\n", version.api_version, version.drv_version); // 4. 查询模型输入和输出的数量 rknn_input_output_num io_num; ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); if (ret < 0) { printf("rknn_init error ret=%d\n", ret); return -1; } printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output); // 5. 查询输入的属性信息:比如大小,shape,name rknn_tensor_attr input_attrs[io_num.n_input]; memset(input_attrs, 0, sizeof(input_attrs)); for (int i = 0; i < io_num.n_input; i++) { input_attrs[i].index = i; ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr)); if (ret < 0) { printf("rknn_init error ret=%d\n", ret); return -1; } dump_tensor_attr(&(input_attrs[i])); } // 6. 查询输出的属性信息:比如大小,shape,name,输出个数 rknn_tensor_attr output_attrs[io_num.n_output]; memset(output_attrs, 0, sizeof(output_attrs)); for (int i = 0; i < io_num.n_output; i++) { output_attrs[i].index = i; ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr)); dump_tensor_attr(&(output_attrs[i])); if (output_attrs[i].qnt_type != RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC || output_attrs[i].type != RKNN_TENSOR_UINT8) { fprintf(stderr, "The Demo required for a Affine asymmetric u8 quantized rknn model, but output quant type is %s, output " "data type is %s\n", get_qnt_type_string(output_attrs[i].qnt_type), get_type_string(output_attrs[i].type)); return -1; } } int channel = 3; int width = 0; int height = 0; if (input_attrs[0].fmt == RKNN_TENSOR_NCHW) { printf("model is NCHW input fmt\n"); width = input_attrs[0].dims[0]; height = input_attrs[0].dims[1]; } else { printf("model is NHWC input fmt\n"); width = input_attrs[0].dims[1]; height = input_attrs[0].dims[2]; } printf("model input height=%d, width=%d, channel=%d\n", height, width, channel); // Load image // 7. 读取图像数据,也可以用opencv CImg<unsigned char> img(image_name); unsigned char* input_data = NULL; input_data = load_image(image_name, &img_height, &img_width, &img_channel, &input_attrs[0]); if (!input_data) { return -1; } // 8. 设置输入的属性信息 rknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = width * height * channel; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].pass_through = 0; // DRM alloc buffer drm_fd = drm_init(&drm_ctx); drm_buf = drm_buf_alloc(&drm_ctx, drm_fd, img_width, img_height, channel * 8, &buf_fd, &handle, &actual_size); memcpy(drm_buf, input_data, img_width * img_height * channel); void* resize_buf = malloc(height * width * channel); // init rga context RGA_init(&rga_ctx); img_resize_slow(&rga_ctx, drm_buf, img_width, img_height, resize_buf, width, height); inputs[0].buf = resize_buf; gettimeofday(&start_time, NULL); // 8. 设置输入的属性信息 rknn_inputs_set(ctx, io_num.n_input, inputs); rknn_output outputs[io_num.n_output]; memset(outputs, 0, sizeof(outputs)); for (int i = 0; i < io_num.n_output; i++) { outputs[i].want_float = 0; } // 9. 执行推理一次 ret = rknn_run(ctx, NULL); // 10. 获取推理结果 ret = rknn_outputs_get(ctx, io_num.n_output, outputs, NULL); gettimeofday(&stop_time, NULL); printf("once run use %f ms\n", (__get_us(stop_time) - __get_us(start_time)) / 1000); // post process // 10. 对输出进行后处理 float scale_w = (float)width / img_width; float scale_h = (float)height / img_height; detect_result_group_t detect_result_group; // 10.1:获取量化常量值,用于反量化 std::vector<float> out_scales; std::vector<uint32_t> out_zps; for (int i = 0; i < io_num.n_output; ++i) { out_scales.push_back(output_attrs[i].scale); out_zps.push_back(output_attrs[i].zp); } // 10.2:开始后处理 post_process((uint8_t*)outputs[0].buf, (uint8_t*)outputs[1].buf, (uint8_t*)outputs[2].buf, height, width, box_conf_threshold, nms_threshold, scale_w, scale_h, out_zps, out_scales, &detect_result_group); // Draw Objects // 10.3: 绘制预测结果 char text[256]; const unsigned char blue[] = { 0, 0, 255}; const unsigned char white[] = { 255, 255, 255}; for (int i = 0; i < detect_result_group.count; i++) { detect_result_t* det_result = &(detect_result_group.results[i]); sprintf(text, "%s %.2f", det_result->name, det_result->prop); printf("%s @ (%d %d %d %d) %f\n", det_result->name, det_result->box.left, det_result->box.top, det_result->box.right, det_result->box.bottom, det_result->prop); int x1 = det_result->box.left; int y1 = det_result->box.top; int x2 = det_result->box.right; int y2 = det_result->box.bottom; // draw box img.draw_rectangle(x1, y1, x2, y2, blue, 1, ~0U); img.draw_text(x1, y1 - 12, text, white); } img.save("./out.bmp"); // 11:释放内存中的输出数据 ret = rknn_outputs_release(ctx, io_num.n_output, outputs); // release // 12: 释放rknn_context对象,以及其它资源 ret = rknn_destroy(ctx); drm_buf_destroy(&drm_ctx, drm_fd, buf_fd, handle, drm_buf, actual_size); drm_deinit(&drm_ctx, drm_fd); RGA_deinit(&rga_ctx); if (model_data) { free(model_data); } if (resize_buf) { free(resize_buf); } stbi_image_free(input_data); return 0; }
4.4, operación del lado del chip
- adb push instalar /datos de usuario/
- exportar LD_LIBRARY_PATH=./install/lib
- rknn_yolov5_demo <ruta_modelo> <ruta_imagen>
O puede crear un script [sh] para ejecutar, el script específico es el siguiente:
#!/bin/bash
set -e # 如果单个文件运行错误,则报错,并停止运行;如果没有该语句,则报错,但不会停止运行
# 输入数据文件夹
data_path="image_dir/"
# 保存数据文件夹
savepath="result/"
export LD_LIBRARY_PATH=/tmp/lib
# 遍历路径下的每一个图片,运行程序
for dir in $(ls $data_path)
do
echo dir
./model/yolov5_rknn_demo--model_path=./model/yolov5.rknn --data_path=$path/$dir --save_path=$savepath/
done
5. Análisis del algoritmo de cuantificación.
-
introducción básica
El propósito de la cuantificación: el modelo de cuantificación utiliza una precisión más baja (como int8 / uint8 / int16) para guardar la información de peso del modelo y puede usar menos espacio de almacenamiento durante la implementación para obtener una velocidad de inferencia más rápida. Sin embargo, cada marco de aprendizaje profundo generalmente utiliza datos de punto flotante al entrenar y guardar el modelo, por lo que la cuantificación del modelo es una parte muy importante del proceso de conversión del modelo.
RKNN Toolkit admite modelos de cuantificación principalmente en las dos formas siguientes:
-
Cuantización estática después del entrenamiento.
Al utilizar este método, RKNN Toolkit carga el modelo de punto flotante entrenado por el usuario y luego lo estima de acuerdo con el método de cuantificación especificado por la interfaz de configuración y el conjunto de datos de calibración proporcionado por el usuario (un pequeño subconjunto de datos de entrenamiento o verificación). datos, alrededor de 100 ~ 500). El rango (mínimo, máximo) de todos los datos flotantes en el modelo. Actualmente, RKNN Toolkit admite 3 métodos de cuantificación.
Método 1: ametric_quantized-u8 (método de cuantificación predeterminado)
Este es el algoritmo de cuantificación posterior al entrenamiento compatible con TensorFlow y recomendado por Google. Según la descripción del artículo "Cuantización de redes convolucionales profundas para una inferencia eficiente: un documento técnico", este método de cuantificación tiene la menor pérdida de precisión. La fórmula de cálculo es la siguiente:
quant: representa el número después de la cuantificación, float_num: representa el número de coma flotante antes de la cuantificación, escala: factor de escala (float32), zero-points representa el valor de cuantificación correspondiente (tipo int32) cuando el número real es 0, y finalmente satura el cuanto a [range_min, range_max]. Porque debe cuantificarse al tipo uint8, range_max=255, range_min=0.
Método 2: punto_fijo_dinámico-i8
Método 3: punto_fijo_dinámico-i16
La fórmula de cuantificación de Dynamic_fixed_point-i16 es la misma que la de Dynamic_fixed_point-i8, excepto que su ancho de bits bw es 16, que es un rango saturado. La NPU de RK3399Pro o RK1808 viene con una unidad informática 300Gops int16. Para algunos modelos que pierden mucha precisión después de ser cuantificados a 8 bits, se puede considerar este método de cuantificación.
-
Entrenamiento consciente de la cuantificación
Se puede obtener un modelo con pesos cuantificados mediante un entrenamiento consciente de la cuantificación. RKNN Toolkit actualmente admite TensorFlow y PyTorch, dos marcos que cuantifican los modelos obtenidos mediante entrenamiento perceptivo. Consulte el siguiente enlace para obtener detalles técnicos del entrenamiento con reconocimiento de cuantificación:
TensorFlow: https://www.tensorflow.org/model_optimization/guide/quantization/training
Pytorch: https://pytorch.org/blog/introduction-to -cuantización-en-pytorch /