简洁高效的相机预览

将相机预览的视频帧转发给AI模块识别并添加tag后生成的新图像预览在屏幕上的功能实现。

在android框架中,移植的ai处理框架一般opencv框架做图像处理。opencv吐出的图像数据接入android app可以通过jni给到java,但是处理起来很麻烦。之前做其他平台的时候,见到过一个直接通过c代码实现的相机预览功能。记录下实现的方法,做个笔记下次使用方便查阅。

main.cpp

关于格式转换,根据相机输出的视频格式做转换就行,我的相机输出的是yuyv。

cpp 复制代码
#define LOG_TAG "V4L2CameraPreview"
#include <utils/Log.h>
#include <gui/SurfaceComposerClient.h>
#include <gui/Surface.h>
#include <system/window.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>

using namespace android;

void yuyv_to_rgba(const uint8_t* yuyv, uint32_t* rgba, int width, int height, int out_stride) {
    if (!yuyv || !rgba || width <= 0 || height <= 0) return;

    for (int i = 0; i < height; ++i) {
        for (int j = 0; j < width; j += 2) {
            int src_idx = (i * width + j) * 2;  // 每像素 2 字节,每 2 像素 4 字节
            if (src_idx + 3 >= width * height * 2) break; // 防越界

            uint8_t y0 = yuyv[src_idx + 0];
            uint8_t u  = yuyv[src_idx + 1];
            uint8_t y1 = yuyv[src_idx + 2];
            uint8_t v  = yuyv[src_idx + 3];

            // 转换 Y0
            int c0 = y0 - 16;
            int d = u - 128;
            int e = v - 128;
            int r0 = (298 * c0 + 409 * e + 128) >> 8;
            int g0 = (298 * c0 - 100 * d - 208 * e + 128) >> 8;
            int b0 = (298 * c0 + 516 * d + 128) >> 8;
            r0 = r0 < 0 ? 0 : (r0 > 255 ? 255 : r0);
            g0 = g0 < 0 ? 0 : (g0 > 255 ? 255 : g0);
            b0 = b0 < 0 ? 0 : (b0 > 255 ? 255 : b0);

            // 转换 Y1
            int c1 = y1 - 16;
            int r1 = (298 * c1 + 409 * e + 128) >> 8;
            int g1 = (298 * c1 - 100 * d - 208 * e + 128) >> 8;
            int b1 = (298 * c1 + 516 * d + 128) >> 8;
            r1 = r1 < 0 ? 0 : (r1 > 255 ? 255 : r1);
            g1 = g1 < 0 ? 0 : (g1 > 255 ? 255 : g1);
            b1 = b1 < 0 ? 0 : (b1 > 255 ? 255 : b1);

            // 写入两个像素(注意 stride)
            int idx0 = i * out_stride + j;
            int idx1 = i * out_stride + j + 1;
            rgba[idx0] = 0xFF000000 | (r0 << 16) | (g0 << 8) | b0;
            if (j + 1 < width) {
                rgba[idx1] = 0xFF000000 | (r1 << 16) | (g1 << 8) | b1;
            }
        }
    }
}
struct Buffer {
    void* start;
    size_t length;
};

int main() {
    const char* device = "/dev/video69";
    int width = 1024, height = 600;
    int fd = open(device, O_RDWR | O_NONBLOCK);
    if (fd < 0) {
        ALOGE("Failed to open %s", device);
        return -1;
    }

    // 设置格式
	struct v4l2_format fmt = {};
	fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	fmt.fmt.pix.width = 1024;   // 选一个支持的尺寸,比如 640x480
	fmt.fmt.pix.height = 600;
	fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;  // ← 改为 YUYV
	fmt.fmt.pix.field = V4L2_FIELD_NONE;

    if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
        ALOGE("VIDIOC_S_FMT failed");
        close(fd);
        return -1;
    }

	ioctl(fd, VIDIOC_G_FMT, &fmt); // 重新读取
	ALOGI("Actual format: %c%c%c%c, size=%dx%d",
			fmt.fmt.pix.pixelformat & 0xFF,
			(fmt.fmt.pix.pixelformat >> 8) & 0xFF,
			(fmt.fmt.pix.pixelformat >> 16) & 0xFF,
			(fmt.fmt.pix.pixelformat >> 24) & 0xFF,
			fmt.fmt.pix.width, fmt.fmt.pix.height);
	int v4l_wid = fmt.fmt.pix.width;
	int v4l_height = fmt.fmt.pix.height;

    // 请求缓冲区
    struct v4l2_requestbuffers req = {};
    req.count = 4;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
        ALOGE("VIDIOC_REQBUFS failed");
        close(fd);
        return -1;
    }

    Buffer buffers[4];
    for (uint32_t i = 0; i < req.count; ++i) {
        struct v4l2_buffer buf = {};
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
            ALOGE("VIDIOC_QUERYBUF failed");
            close(fd);
            return -1;
        }
        buffers[i].length = buf.length;
        buffers[i].start = mmap(nullptr, buf.length,
                                PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
        if (buffers[i].start == MAP_FAILED) {
            ALOGE("mmap failed");
            close(fd);
            return -1;
        }
        ioctl(fd, VIDIOC_QBUF, &buf); // 入队
    }

    // 启动流
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
        ALOGE("VIDIOC_STREAMON failed");
        close(fd);
        return -1;
    }

    // 创建 Surface
    sp<SurfaceComposerClient> client = SurfaceComposerClient::getDefault();
    sp<SurfaceControl> surfaceCtrl = client->createSurface(
        String8("V4L2Preview"), width, height,
        PIXEL_FORMAT_RGBA_8888,
		0
    );
    sp<Surface> surface = surfaceCtrl->getSurface();
	// === 新增:计算缩放并设置 Matrix ===
    float src_w = v4l_wid;   // 实际摄像头宽度(如 640)
    float src_h = v4l_height; // 实际摄像头高度(如 480)
    float dst_w = width;     // Surface 宽度(1024)
    float dst_h = height;    // Surface 高度(600)

    // 计算等比缩放因子(保证覆盖全屏)
    float scale = fmaxf(dst_w / src_w, dst_h / src_h); // 1024/640=1.6, 600/480=1.25 → scale=1.6

    // 缩放后图像尺寸
    float scaled_w = src_w * scale; // 1024
    float scaled_h = src_h * scale; // 768

    // 计算需要裁剪的偏移(居中裁剪)
    float crop_x = (scaled_w - dst_w) / 2.0f; // 0
    float crop_y = (scaled_h - dst_h) / 2.0f; // (768-600)/2 = 84

    // 构建变换:先缩放,再平移(负值表示向上/向左裁剪)
    SurfaceComposerClient::Transaction t2;
    t2.setMatrix(surfaceCtrl, scale, 0, 0, scale)
       .setPosition(surfaceCtrl, -crop_x, -crop_y)
       .setLayer(surfaceCtrl, 0x7FFFFFFF)
       .show(surfaceCtrl)
       .apply();
    // === 新增结束 ===
    ANativeWindow* window = surface.get();

    //SurfaceComposerClient::Transaction t;
    //t.setLayer(surfaceCtrl, 0x7FFFFFFF).show(surfaceCtrl).apply();

    ALOGI("Streaming from V4L2...");
    //for (int frame = 0; frame < 300; ++frame) {
    while(1) {
        struct v4l2_buffer buf = {};
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        // 出队一帧
        if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) continue;

        // 转换并渲染
        ANativeWindow_Buffer anb;
        if (ANativeWindow_lock(window, &anb, nullptr) == 0) {
			ALOGI("Streaming from V4L2...wid:%d,hei:%d", anb.width, anb.height);
			yuyv_to_rgba(
					(const uint8_t*)buffers[buf.index].start,
					(uint32_t*)anb.bits,
					v4l_wid,      // 从 VIDIOC_G_FMT 获取的实际宽
					v4l_height,     // 实际高
					anb.stride       // Surface 的 stride
					);
			ANativeWindow_unlockAndPost(window);
        }

        // 重新入队
        ioctl(fd, VIDIOC_QBUF, &buf);
        usleep(33000); // ~30 FPS
    }

    // 停止流
    ioctl(fd, VIDIOC_STREAMOFF, &type);
    for (int i = 0; i < 4; ++i) {
        munmap(buffers[i].start, buffers[i].length);
    }
    close(fd);

    ALOGI("Done.");
    return 0;
}

Android.bp

bash 复制代码
cc_binary {
    name: "camera_preview_hal_bp",

    // 源文件
    srcs: ["main.cpp"],

    // 编译选项
    cflags: [
        "-Wall",
        "-Werror",
    ],
    cppflags: [
        "-std=c++17",
        "-fexceptions",
        "-frtti",
    ],

    // STL:使用 AOSP 默认的 libc++
    stl: "libc++",

    // 链接系统库
    shared_libs: [
        "libgui",           // SurfaceComposerClient, SurfaceControl
        "libui",            // GraphicBuffer, PixelFormat
        "libbinder",        // IPC
        "libutils",         // RefBase, String8, SortedVector (内部)
        "libcutils",        // compatibility
        "libnativewindow",  // ANativeWindow_lock/unlockAndPost
        "liblog",           // ALOGI/ALOGE
    ],

    // 可选:仅在 eng/userdebug 版本编译
    owner: "your_team",  // 可删除
    //vendor_available: true, // 如果要放 vendor 分区
	//vendor_specific: true,   // ← Android 13+ 新属性
	//vendor: true,
    //proprietary: true,      // 绕过 private: true 限制
}
相关推荐
ask_baidu2 小时前
Doris笔记
android·笔记
SongJX_2 小时前
DHCP服务
linux·运维·服务器
[J] 一坚2 小时前
Shell 脚本解锁 curl/iptables/Nginx 日志分析等实战用法
linux·服务器·正则表达式·系统架构·自动化
点亮一颗LED(从入门到放弃)2 小时前
字符设备驱动(5)
linux·运维·服务器
hqk2 小时前
鸿蒙ArkUI:状态管理、应用结构、路由全解析
android·前端·harmonyos
知识分享小能手2 小时前
CentOS Stream 9入门学习教程,从入门到精通,Linux日志分析工具及应用 —语法详解与实战案例(17)
linux·学习·centos
ZXF_H3 小时前
Linux tcpdump抓包实践(以http为例)
linux·http·wireshark·tcpdump
悄悄敲敲敲3 小时前
Linux:信号(二)
linux·操作系统·信号
消失的旧时光-19433 小时前
从 C 链表到 Android Looper:MessageQueue 的底层原理一条线讲透
android·数据结构·链表