基于i.MX6ULL的NCNN轻量级AI识别拍摄图片



前言

这个项目也是基于恩智浦芯片i.MX6ULL控制的嵌入式Linux系统,正点原子配套的开发板和OV5640摄像头、可以使用摄像头拍摄图片并且存储在本地,采用QT界面对本地的图片进行查看、删除,最好玩的是我加上了轻量化模型,可以对图片进行简单的物体识别,为了能在嵌入式板子上面运行模型,采用了Opencv对图片的处理+NCNN的推理框架。源代码会进行开源分享,将会放在本文最下方!!!


一、OV5640摄像头使用

1.编写设备树和驱动

(1)设备树
OV5640 的控制是通过 I2C 总线进行的。在我的开发板上,OV5640 挂载在 &i2c2 下,其 I2C 器件地址为 0x3c。

我们需要在 &i2c2 节点下添加 ov5640 子节点:

&i2c2 {
	clock_frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c2>;
	status = "okay";

	ov5640: ov5640@3c {
		compatible = "ovti,ov5640";
		reg = <0x3c>;
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_csi1 &csi_pwn_rst>;
		clocks = <&clks IMX6UL_CLK_CSI>;
		clock-names = "csi_mclk";
		pwn-gpios = <&gpio1 4 1>;
		rst-gpios = <&gpio1 2 0>;
		csi_id = <0>;
		mclk = <24000000>;
		mclk_source = <0>;
		status = "okay";
		port {
			ov5640_ep: endpoint {
				remote-endpoint = <&csi1_ep>;
			};
		};
	};
};

引脚复用配置中需要配置摄像头所需的硬件引脚。这包括 CSI 数据/时钟总线引脚,以及用于控制摄像头的电源(PWDN)和复位(RST)引脚。

在 &iomuxc 节点下的 imx6ul-evk 中,添加/确认以下配置:

pinctrl_csi1: csi1grp {
			fsl,pins = <
				MX6UL_PAD_CSI_MCLK__CSI_MCLK		0x1b088
				MX6UL_PAD_CSI_PIXCLK__CSI_PIXCLK	0x1b088
				MX6UL_PAD_CSI_VSYNC__CSI_VSYNC		0x1b088
				MX6UL_PAD_CSI_HSYNC__CSI_HSYNC		0x1b088
				MX6UL_PAD_CSI_DATA00__CSI_DATA02	0x1b088
				MX6UL_PAD_CSI_DATA01__CSI_DATA03	0x1b088
				MX6UL_PAD_CSI_DATA02__CSI_DATA04	0x1b088
				MX6UL_PAD_CSI_DATA03__CSI_DATA05	0x1b088
				MX6UL_PAD_CSI_DATA04__CSI_DATA06	0x1b088
				MX6UL_PAD_CSI_DATA05__CSI_DATA07	0x1b088
				MX6UL_PAD_CSI_DATA06__CSI_DATA08	0x1b088
				MX6UL_PAD_CSI_DATA07__CSI_DATA09	0x1b088
			>;
		};

		
 		csi_pwn_rst: csi_pwn_rstgrp {
        fsl,pins = <
            MX6UL_PAD_GPIO1_IO02__GPIO1_IO02    0x10b0     /*reset引脚*/
            MX6UL_PAD_GPIO1_IO04__GPIO1_IO04    0x10b0    /*电源引脚*/
        >;
  	};

CSI 主控节点端点连接,在 Linux V4L2 框架中,使用 OF-Graph机制来描述硬件的数据流向。刚刚我们在 OV5640 节点里定义了 ov5640_ep 连向 csi1_ep,现在我们需要在 i.MX6ULL 的 CSI 控制器节点中把另一端接上:

&csi {
	status = "okay";

	port {
		csi1_ep: endpoint {
			remote-endpoint = <&ov5640_ep>;
		};
	};
};

设备树部分就差不多了,接下来是驱动的部分,驱动代码比较多,这里截取一部分的,完整驱动代码请到本文后面的源代码获取。参考正点原子的配套代码!!!

(2)驱动部分
驱动程序需要知道自己该为哪个设备服务。这里通过 I2C 驱动框架来实现,重点在于 id_table 里的名字必须和我们在设备树里写的 compatible 属性相匹配。

/* 1. I2C 匹配表:名字必须和设备树节点对应 */
static const struct i2c_device_id ov5640_id[] = {
	{"ov5640", 0},
	{},
};
MODULE_DEVICE_TABLE(i2c, ov5640_id);

/* 2. I2C 驱动结构体 */
static struct i2c_driver ov5640_i2c_driver = {
	.driver = {
		  .owner = THIS_MODULE,
		  .name  = "ov5640",
	},
	.probe  = ov5640_probe,    /* 匹配成功后执行的 probe 函数 */
	.remove = ov5640_remove,
	.id_table = ov5640_id,
};

/* 3. 注册驱动 */
module_i2c_driver(ov5640_i2c_driver);

当驱动和设备树匹配成功后,就会进入核心的 ov5640_probe 函数。在这里,驱动会去解析设备树,拿到我们在上一节配置的电源(PWDN)和复位(RST)引脚,并对摄像头进行硬件复位。

static int ov5640_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	struct device *dev = &client->dev;
	int retval;

	// ... 省略部分代码 ...

	/* 1. 从设备树获取并请求电源控制引脚 (PWDN) */
	pwn_gpio = of_get_named_gpio(dev->of_node, "pwn-gpios", 0);
	retval = devm_gpio_request_one(dev, pwn_gpio, GPIOF_OUT_INIT_HIGH, "ov5640_pwdn");

	/* 2. 从设备树获取并请求复位引脚 (RST) */
	rst_gpio = of_get_named_gpio(dev->of_node, "rst-gpios", 0);
	retval = devm_gpio_request_one(dev, rst_gpio, GPIOF_OUT_INIT_HIGH, "ov5640_reset");

	/* 3. 获取时钟 (MCLK) 频率配置 */
	ov5640_data.sensor_clk = devm_clk_get(dev, "csi_mclk");
	retval = of_property_read_u32(dev->of_node, "mclk", &ov5640_data.mclk);

	// ... 省略部分代码 ...

	/* 4. 执行硬件复位时序 (拉高拉低引脚) */
	ov5640_reset();
	ov5640_power_down(0);

	/* 5. 通过 I2C 读取 OV5640 的芯片 ID,验证是否通信成功 */
	retval = ov5640_read_reg(OV5640_CHIP_ID_HIGH_BYTE, &chip_id_high);
	retval = ov5640_read_reg(OV5640_CHIP_ID_LOW_BYTE, &chip_id_low);
	if (chip_id_high != 0x56 || chip_id_low != 0x40) {
		pr_warning("camera ov5640 is not found\n");
		return -ENODEV;
	}
    
	// ... 接下一步:V4L2 注册 ...

读取到正确的 Chip ID 后,说明硬件通信没问题了。接下来要把这个摄像头注册成 V4L2 框架下的一个子设备,这样应用层(比如稍后我们要写的 Qt 程序)才能通过标准的 API 来控制它。

// ... 承接 probe 函数的后半部分 ...

	/* 6. 初始化 V4L2 子设备,并绑定操作集 (ov5640_subdev_ops) */
	v4l2_i2c_subdev_init(&ov5640_data.subdev, client, &ov5640_subdev_ops);

	/* 7. 异步注册 V4L2 子设备 */
	retval = v4l2_async_register_subdev(&ov5640_data.subdev);
	if (retval < 0)
		dev_err(&client->dev, "%s--Async register failed\n", __func__);

	pr_info("camera ov5640, is found\n");
	return retval;
}

这里的 ov5640_subdev_ops 是极其重要的结构体,它里面定义了上层应用修改分辨率、帧率、图像格式时,底层驱动具体应该执行的回调函数(比如 ov5640_s_fmt)。当应用层请求改变分辨率时,驱动就会去庞大的寄存器数组(例如 ov5640_setting_30fps_VGA_640_480)里抓取对应的数据,通过 I2C 写入摄像头内部,从而改变摄像头的输出状态。

2.使用 V4L2 工具测试画面

insmod mx6s_capture.ko
insmod ov5640.ko

加载驱动后后会显示驱动代码让其输出的信息,lsmod可以查看到目前板子所加载的驱动(不包含内核编译的,这个是手动加载的):
在这里插入图片描述

ls /dev/video1

板子可以查看到摄像头的节点了:
在这里插入图片描述

v4l2-ctl --device=/dev/video1 --list-formats-ext

查看摄像头的格式参数等:
在这里插入图片描述

gst-launch-1.0 -v imxv4l2src device=/dev/video1 ! "video/x-raw, format=(string)YUY2, width=(int)1024, height=(int)600" ! videoconvert ! fbdevsink sync=false

输入这行指令就可以打开摄像头进行测试画面了,终端会有以下信息出现即可成功,然后看触摸屏是否有界面:
请添加图片描述

画面出现了:
请添加图片描述

二、NCNN推理框架移植与模型准备

1.交叉编译 NCNN 源码

为了让 NCNN 能在我们的开发板上运行,同时又能在 PC 上使用其配套的模型转换工具,我们需要分别编译 x86(主机端) 和 ARM(设备端) 两个版本。

(1)首先编写交叉编译工具链,在 NCNN 源码根目录下,我们需要创建一个供 CMake 识别的工具链文件 imx6ull.toolchain.cmake,指明我们的交叉编译器路径:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# --- 请根据你的实际情况修改编译器命令 ---
set(CMAKE_C_COMPILER "arm-linux-gnueabihf-gcc")
set(CMAKE_CXX_COMPILER "arm-linux-gnueabihf-g++")

# --- 针对 i.MX6ULL (Cortex-A7) 的核心优化参数 ---
# -mfpu=neon: 开启 NEON 指令集加速 (必须开启,否则慢10)
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# -----------------------------------------------------------------------
# 【关键修复】必须使用绝对路径,确保 gcc 和 g++ 版本一致 (都是 4.9.4)
# 请仔细核对下面的路径是否就在你的电脑上
# -----------------------------------------------------------------------
set(CMAKE_C_COMPILER "/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc")
set(CMAKE_CXX_COMPILER "/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-g++")

# 核心优化参数:开启 NEON 和 VFPV4
set(CMAKE_C_FLAGS "-march=armv7-a -mfloat-abi=hard -mfpu=neon-vfpv4")
set(CMAKE_CXX_FLAGS "-march=armv7-a -mfloat-abi=hard -mfpu=neon-vfpv4")

# 跳过运行测试 (解决 TRY_RUN 报错)
set(THREADS_PTHREAD_ARG "2" CACHE STRING "Result from TRY_RUN" FORCE)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)# -mfloat-abi=hard: 硬件浮点运算
set(CMAKE_C_FLAGS "-march=armv7-a -mfloat-abi=hard -mfpu=neon")
set(CMAKE_CXX_FLAGS "-march=armv7-a -mfloat-abi=hard -mfpu=neon")

# 查找路径配置 (标准写法,不用改)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

(2)编译 ARM 目标板版本:由于 i.MX6ULL 没有 GPU 支持 Vulkan 计算,我们需要在 CMake 配置时关闭 Vulkan 支持,以精简库体积并避免编译报错。在 ncnn 根目录下编译 ARM :

mkdir build_imx6 && cd build_imx6

执行 cmake 配置,指定工具链并关闭 Vulkan:

cmake -DCMAKE_TOOLCHAIN_FILE=../imx6ull.toolchain.cmake \
      -DNCNN_VULKAN=OFF \
      -DCMAKE_INSTALL_PREFIX=../install_imx6 ..

编译并安装:

make -j4
make install

(3)同样的编译 PC 主机版本,我们在 PyTorch 等框架训练出来的模型(通常是 .pt 或 .pth),一般需要先导出为通用的 ONNX 格式,然后再转成 NCNN 格式。这一步是为了获取 NCNN 的模型转换工具(如 onnx2ncnn),将 ONNX 转换为 NCNN 格式。当然了主要为了方便在虚拟机QT上面测试NCNN调用AI,所以这步使用X86的NCNN还是必需的。
在 ncnn 根目录下编译X86:

mkdir build_x86 && cd build_x86

cmake -DCMAKE_INSTALL_PREFIX=../install_x86 ..

编译并安装:

make -j4
make install

(4)最后NCNN文件目录下,编译成功后有install_imx6和install_x86文件:
在这里插入图片描述
后续这两个文件里面的包在QT调用的时候需要用到!!!等下在QT界面设计章会提到!

2.AI 模型文件的获取与转换

NCNN 官方非常贴心地提供了一个名为 ncnn-assets 的仓库。这里面包含了大量已经转换好、经过优化且可以直接使用的前沿模型。

可以直接在你的开发环境(Ubuntu 主机端)拉取模型仓库:

git clone https://github.com/nihui/ncnn-assets.git
cd ncnn-assets/models/

若无法克隆模型也可以下载我的,都跟本文的源代码资料都放一起了!!!

这是我下载好的模型仓库:
在这里插入图片描述
由于正点原子 i.MX6ULL 是一颗基于 Cortex-A7 架构的入门级处理器,没有 NPU 或 GPU 加速,纯靠 CPU 算力进行推理。因此,在挑选模型时,我们必须最好使用轻量级。

对于图像分类任务,推荐使用:squeezenet_v1.1,mobilenet_yolo 系列

对于目标检测任务,推荐使用:nanodet_m (极其轻量,强烈推荐在 A7 处理器上尝试),yolov4-tiny-opt 或 yolov5s (帧率可能较低,适合做概念验证)

NCNN 的模型通常由两个文件组成,我们需要将选定的这两个文件提取出来,放到Qt 应用程序下调用:(虚拟机QT应用程序下的根目录,开发板QT应用程序下的根目录)

(1)xxxx.param:网络结构描述文件(如 nanodet_m.param)。

(2)xxxx.bin:网络权重参数文件(如 nanodet_m.bin)。

3.编译包和模型的存放

再调用NCNN之前需要先将在NCNN根目录下之前编译得到的install_imx6和install_x86文件(第二章1.编译源码得出)里面的包复制到QT源文件的目录下面,不然运行时会报错!!!

(1)这里先存放使用在虚拟机中的NCNN框架!!!

这是NCNN的目录编译出来install_imx6中的include路径下的所有包(x86跟arm使用include包都一样的,复制那个都可以,复制整个include):
在这里插入图片描述
这个camera是我的QT源目录,然后新建一个3rdparty文件夹作为存放NCNN的一些包来使用,models文件夹存放需要调用的模型,把刚才复制的include文件夹复制到QT源文件目录下里面的“3rdparty/ncnn/”中(我是再建了一个ncnn文件夹):
在这里插入图片描述
存放路径如下:
在这里插入图片描述
在“3rdparty/ncnn/”中复制存放include后,然后再新建一个lib包
在这里插入图片描述
然后lib文件夹中又新建2个文件夹,一个是arm分别存放之前编译后“install_imx6/lib”下的libncnn.a文件,另一个x86存放“install_x86/lib”下的libncnn.a文件,一定要一一对应!!!
注意注意!!!这2个libncnn.a文件对应架构是不一样的,一定要正确放好!!!

最后存放路径如下:
在这里插入图片描述

在界面初始化阶段,我们需要提前将 NCNN根目录里的模型放过来QT的目录,模型里有3个网络结构(.param)、权重参数(.bin)以及分类标签(synset_words.txt)放在QT这边目录里model的文件中(我这里使用yolov8的轻量化模型)。所有模型都可以存放这个“/camera/models”下去引用。
在这里插入图片描述

(2)然后是在imx6ull中的NCNN框架!!!

在这里插入图片描述

三、QT界面功能设计与测试

1.摄像头控制与拍摄

底层的摄像头驱动和 NCNN 运行环境都搭建好后,我们就可以在应用层开始操作了。我将摄像头的控制逻辑封装在一个独立的 Camera 类中,利用 OpenCV 读取视频流,再通过 Qt 的信号槽机制发送给主界面显示。

(1)首先利用 OpenCV 读取视频流并转换格式,Linux 下读取摄像头最简单的方式就是利用 OpenCV 的 VideoCapture 类。因为硬件性能有限,我们采用 QTimer 定时器来驱动画面的刷新,每 33 毫秒抓取一帧图像(约等于 30 帧/秒)。

void Camera::timerTimeOut()
{
    /* 1. 如果摄像头没有打开,停止定时器 */
    if (!capture->isOpened()) {
        timer->stop();
        return;
    }

    static cv::Mat frame;
    *capture >> frame; // 从 OpenCV 抓取一帧

    if (frame.empty()) {
        qDebug() << "Empty frame captured!";
        return;
    }

 	qDebug() << "Captured frame size:" << frame.cols << "x" << frame.rows;

    /* 2. 发送图片信号给主界面 */
    if (frame.cols)
        emit readyImage(matToQImage(frame));
}

/* 核心转换函数:OpenCV Mat 转 Qt QImage */
QImage Camera::matToQImage(const cv::Mat &img)
{
    /* USB摄像头和OV5640等都是RGB三通道,不考虑单/四通道摄像头 */
    if(img.type() == CV_8UC3) {
        /* 得到图像的的首地址 */
        const uchar *pimg = (const uchar*)img.data;

        /* 以img构造图片 */
        QImage qImage(pimg, img.cols, img.rows, img.step,
                      QImage::Format_RGB888);

        /* 在不改变实际图像数据的条件下,交换红蓝通道 */
        return qImage.rgbSwapped();
    }

    /* 返回QImage */
    return QImage();
}

(2)主界面动态挂载与显示

在嵌入式 Linux 设备(比如板子上的 /dev 目录)中,摄像头设备节点通常是 video1 或 video2。我们在主界面的初始化中,动态扫描这些节点并添加到下拉框中。

当用户点击“打开摄像头”时,启动定时器;当 Camera 类发出 readyImage 信号时,主界面负责将图像贴到 QLabel 上:

// 扫描设备节点
void Widget::scanCameraDevice()
{
#if win32
    ui->comboBox_2->addItem("windows摄像头0");
    connect(ui->comboBox_2, SIGNAL(currentIndexChanged(int)),
            camera, SLOT(selectCameraDevice(int)));
#else
    QFile file("/dev/video0");

    if (file.exists()) {
        ui->comboBox_2->addItem("video0");
    } else {
        ui->textBrowser->append("未检测到摄像头设备");
        return;
    }

    file.setFileName("/dev/video1");
    if (file.exists()) ui->comboBox_2->addItem("video1");

    file.setFileName("/dev/video2");
    if (file.exists()) ui->comboBox_2->addItem("video2");

#if !__arm__
    ui->comboBox_2->setCurrentIndex(0);
#endif

    connect(ui->comboBox_2, SIGNAL(currentIndexChanged(int)),
            camera, SLOT(selectCameraDevice(int)));
#endif
}

(3)实现拍照与本地留存

当画面正常预览后,点击拍照按钮

void Widget::on_start_clicked()
{
    QString video = ui->comboBox_2->currentText();
    //我的摄像头默认检测为video1
    if (video == "video1"){
        static bool isRunning = false;
        isRunning = !isRunning; // 每次点击切换真假状态,当关闭时候会变为false
        //qDebug() << isRunning;
        setButtonText(isRunning); // 更新按钮文字、选择摄像头等
        camera->cameraProcess(isRunning); // 启动或停止定时器采集
    }else {
        QMessageBox::warning(this, "提示", "请选择正确/已连接的摄像头!");
    }
}

我们需要将当前的 saveImage 以时间戳命名,存入开发板的本地目录中,供后续的 AI 识别使用:

void Widget::saveImageToLocal()
{
    if (saveImage.isNull())
        return;

    QDir dir("/home/root/growing/picture");
    if (!dir.exists())
        dir.mkpath(".");

    QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm");
    QString fileName = dir.filePath("pho_" + timestamp + ".png");

    qDebug() << "正在保存:" << fileName;

    ui->textBrowser->append("已保存在" + fileName);

    if (saveImage.save(fileName, "PNG", -1)) {
        ui->label_2->setPixmap(QPixmap::fromImage(QImage(fileName)));

        QString detectedColor = detectColor(saveImage);
        //QMessageBox::information(this, "颜色识别结果","识别到的颜色是: " + detectedColor);
        ui->label_3->setText("识别到的颜色是: " + detectedColor);


        //保存图片后,使下拉框能找到图片名,也就是刷新图片目录一次了
        scanImagesToComboBox("/home/root/growing/picture");


    } else {
        qDebug() << "保存失败!";
        ui->textBrowser->append("保存失败!");
    }
}

2.Qt 调用 NCNN 识别

这里有一个非常实用的跨平台小技巧:利用 #ifdef arm 宏定义,让代码能够自适应 PC 虚拟机和 i.MX6ULL 开发板的路径,省去了每次交叉编译都要改路径的麻烦。

void Widget::initNcnn()
{
    ncnn::Option opt;
    opt.lightmode = true;
    opt.num_threads = 1; // i.MX6ULL 必须设为1
    net.opt = opt;

    // 路径判断:虚拟机 vs 开发板
    QString modelPath;
#ifdef __arm__
    modelPath = "/home/root/growing/models/";
#else
    // 请改为你虚拟机实际存放模型的路径
    modelPath = "/home/deng10/grouping/camera/models/";
#endif

    // 加载 ShuffleNet (注意:文件名要跟你 models 目录里的一致)
    //int ret1 = net.load_param((modelPath + "shufflenet_v2_x0.5.param").toUtf8().constData());
    //int ret2 = net.load_model((modelPath + "shufflenet_v2_x0.5.bin").toUtf8().constData());

    int ret1 = net.load_param((modelPath + "yolov8n_cls.ncnn.param").toUtf8().constData());
    int ret2 = net.load_model((modelPath + "yolov8n_cls.ncnn.bin").toUtf8().constData());


    if (ret1 != 0 || ret2 != 0) {
        qDebug() << "现在识别的路径是:" << modelPath;
        qDebug() << "【错误】模型加载失败,请检查路径:" << modelPath;
        // 可以在界面上提示一下
        ui->textBrowser->append("AI模型加载失败!");
    } else {
        qDebug() << "AI模型加载成功!";
        ui->textBrowser->append("AI模型就绪。");
    }

    // 加载标签
    QFile file(modelPath + "synset_words.txt");
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream in(&file);
        while (!in.atEnd()) {
            QString line = in.readLine();
            // 简单解析:取第一个空格后的内容
            labels.push_back(line.section(' ', 1));
        }
        file.close();
    }
}

执行推理与结果解析
当我们在界面上点击“检测颜色/识别”按钮时,程序就会获取当前保存的图片路径,并送入 NCNN 网络中提取特征。

QString Widget::runInference(const QString& imagePath)
{
    // ... 基础检查和读取图片不变 ...

    // 1. 读取图片
    cv::Mat img = cv::imread(imagePath.toStdString());
    if (img.empty()) return "错误:图片读取失败";

    // 2. 预处理 (YOLOv8-cls 也是 224x224)
    ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data,
                                                 ncnn::Mat::PIXEL_BGR2RGB,
                                                 img.cols, img.rows,
                                                 224, 224);

    // =========================================================
    // 【修改点 1】 YOLOv8 的归一化参数不一样!
    // =========================================================
    // MobileNet/ShuffleNet 是 mean=127.5, norm=1/127.5
    // YOLOv8 通常只需要 norm=1/255.0 (把 0-255 映射到 0-1)
    const float mean_vals[3] = {0.0f, 0.0f, 0.0f};
    const float norm_vals[3] = {1/255.0f, 1/255.0f, 1/255.0f};
    in.substract_mean_normalize(mean_vals, norm_vals);

    // 4. 推理
    ncnn::Extractor ex = net.create_extractor();
    ex.input("in0", in); // 【注意】YOLOv8 ncnn 输入节点通常叫 "in0"

    ncnn::Mat out;

    // =========================================================
    // 【修改点 2】 输出节点通常叫 "out0"
    // =========================================================
    // 建议先用 tail 命令查一下 param 文件确认
    // 大概率是 "out0"
    int ret = ex.extract("out0", out);

    if (ret != 0) {
        qDebug() << "推理失败,找不到节点 out0";
        // 如果失败,试一下 "output"
        return "错误";
    }

    // =========================================================
    // 【修改点 3】 它的输出是 w=1000 的一维数组
    // =========================================================
    float* values = (float*)out.data;
    int max_idx = 0;
    float max_score = values[0];

    for (int i = 0; i < out.w; ++i) {
        if (values[i] > max_score) {
            max_score = values[i];
            max_idx = i;
        }
    }

    // 5. 返回结果
    QString resultName = "Unknown";
    if (max_idx < (int)labels.size()) {
        resultName = labels[max_idx];
    }

    return QString("%1 (Score: %2)").arg(resultName).arg(max_score * 100, 0, 'f', 1);
}

经过以上步骤,底层摄像头采集的照片就被成功转换为了矩阵数据,经过 YOLOv8 分类网络的计算,最终得出了具体的类别名称和置信度,并显示在了界面的 Label 上。


总结与演示

本文详细介绍了在i.MX6ULL嵌入式Linux平台上,使用OV5640摄像头采集图像,并通过NCNN推理框架进行AI识别的完整流程。以下是项目最终效果的演示视频:

轻量化ai识别图片视频

QT源代码链接: link

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐