工厂质检员拿安卓平板连工业摄像头libuvc开源驱动解决外置镜头兼容问题实测画面清晰传输稳定
产线上最怕的不是活难干,是工具不顺手。
我见过不少做外观质检的同事,一开始拿公司发的安卓平板配原相机 App 对着产品拍。结果呢?对焦像抽风,光线稍暗就糊成一团,边缘发虚,稍微远一点连丝印上的微小字符都分辨不清。质检这行,差一根头发丝的瑕疵可能就是整批返工。换工业相机加工控机?一套下来几万块,小批量产线根本铺不开。后来有人把一台支持 USB OTG 的安卓平板和一台 UVC 工业相机接上,底层跑 libuvc 开源驱动,画面直接从“能看”变成“能判”,而且传输稳得跟有线监控似的。今天就把这套从选型、接线、编译到实测踩坑的全过程,原原本本摊开来讲。
为什么偏偏是 UVC 和 libuvc
安卓平板内置摄像头走的是 Camera HAL 那一套,系统封装得很深,权限多、延迟大,而且对外部镜头的支持基本停留在“能预览、能拍照”的层面。你换个 C 接口远心镜头、想调曝光时间、想拿固定帧率的 YUV 原始流,原生框架基本帮不上忙。
UVC(USB Video Class)的好处在于它是 USB 的标准视频协议,相当于摄像头行业的“普通话”。只要工业相机声明自己支持 UVC,安卓系统内核里的 uvcvideo 模块就能自动识别设备。但系统识别归识别,真正要把高质量帧实时拉到应用层,还得靠 libuvc。
libuvc 是 pure C 写的开源库,专门和 UVC 设备通信。它不依赖安卓的 Camera2 API,直接走 USB Host 通道,能精确控制分辨率、帧率、像素格式、曝光、增益,甚至支持外触发和硬同步。对于质检场景来说,这意味着你可以把平板当成一个轻量级的视觉采集终端,而不是只能拍张照片的“大号手机”。
硬件怎么搭才不翻车
别一上来就写代码,硬件没搭对,后面全是白搭。
平板选择
- Android 8.0 以上,最好 10+。低版本 USB Host 权限管理太旧,部分机型会偷偷杀掉后台服务。
- USB 控制器必须支持 Host 模式。三星 Galaxy Tab S 系列、联想 ThinkPad X12、华为 MatePad Pro 都验证过。有些便宜平板虽然写着 Type-C,但芯片只支持 Device 模式,插上相机毫无反应。
- 内存至少 6GB,跑 1080P@30fps 的预览加后续 OpenCV 处理,4GB 会卡。
线缆与供电
- USB 3.0 Type-C to Type-A 短线,长度控制在 0.5~1.2 米。超过 1.5 米信号衰减明显,帧丢失率直线上升。
- 工业相机功耗一般在 200~500mA,平板 OTG 口输出不稳时容易掉线。加一个带独立 5V/2A 供电的 USB Hub,成本几十块,能省大量排查时间。
- 镜头接口按产线需求选:C/CS 口最通用,定焦微距镜头适合 5~50mm 工作距离,远心镜头适合尺寸测量类质检。
照明
- 白光 LED 环形灯或条形灯,色温 5000K~6500K,显色指数 Ra≥90。很多同事忽略灯光,结果相机再好也救不回阴影里的缺陷。
软件架构:从 USB 权限到屏幕预览
整体链路不长,但每一步都有细节:
USB Host → UsbManager 申请权限 → 打开 UsbDeviceConnection → libuvc 打开设备 → 启动流 → 回调拿到 uvc_frame_t → JNI 拷贝到 Java → 环形队列 → SurfaceTexture / OpenGL 渲染 → 可选 OpenCV/TFLite 分析
1. 安卓端 USB 权限申请(Kotlin)
class UvcCameraActivity : AppCompatActivity() {
private lateinit var usbManager: UsbManager
private val ACTION_USB_PERMISSION = "com.example.USB_PERMISSION"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
registerReceiver(usbPermissionReceiver, IntentFilter(ACTION_USB_PERMISSION))
}
private fun requestCameraPermission(device: UsbDevice) {
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), flag)
usbManager.requestPermission(device, pendingIntent)
}
private val usbPermissionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION_USB_PERMISSION) return
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return
if (usbManager.hasPermission(device)) {
val iface = device.getInterface(0)
val endpoint = iface.getEndpoint(0)
val connection = usbManager.openDevice(device)
connection?.let {
if (it.claimInterface(iface, true)) {
// fd 传给 native 层,libuvc 用原生文件描述符操作
LibUvcBridge.openAndStream(it.fileDescriptor, iface.id)
}
}
}
}
}
}
注意:Android 12 开始 PendingIntent 必须声明 FLAG_IMMUTABLE 或 FLAG_MUTABLE,低版本代码直接搬到新系统会崩,这个坑我踩过两次。
2. libuvc 核心调用(C / NDK)
#include <libuvc/libuvc.h>
#include <pthread.h>
#include <stdatomic.h>
static atomic_int g_running = 0;
static uint8_t *g_ring[4] = {0};
static size_t g_ring_len[4] = {0};
static int g_ring_idx = 0;
void uvc_frame_callback(uvc_frame_t *frame, void *ptr) {
if (!frame || !frame->data || frame->data_size == 0) return;
// 工业相机通常返回 MJPEG 或 YUYV,这里按实际设备调整
size_t copy_len = frame->data_size > 1920 * 1080 * 2 ? 1920 * 1080 * 2 : frame->data_size;
int idx = atomic_fetch_add(&g_ring_idx, 1) % 4;
if (g_ring[idx]) free(g_ring[idx]);
g_ring[idx] = malloc(copy_len);
memcpy(g_ring[idx], frame->data, copy_len);
g_ring_len[idx] = copy_len;
}
int start_stream(int usb_fd, int iface_id) {
uvc_context_t *ctx = NULL;
uvc_init(&ctx, NULL);
uvc_device_t *dev = NULL;
// 替换为你的相机 VID/PID,或者用 NULL 遍历第一个 UVC 设备
uvc_find_device(ctx, &dev, 0x1e71, 0x2020, NULL);
if (!dev) { uvc_exit(ctx); return -1; }
uvc_device_handle_t *handle = NULL;
uvc_open(dev, &handle);
uvc_unref_device(dev);
// 设置采集格式:YUYV 1920x1080 @ 30fps
uvc_set_format(handle, UVC_FRAME_FORMAT_YUYV, 1920, 1080, 30);
int err = uvc_start_streaming(handle, NULL, uvc_frame_callback, NULL, 0);
if (err < 0) {
uvc_close(handle);
uvc_exit(ctx);
return err;
}
while (atomic_load(&g_running)) {
usleep(10000); // 主线程等待,实际项目用 condition_variable 更好
}
uvc_stop_streaming(handle);
uvc_close(handle);
uvc_exit(ctx);
return 0;
}
编译成 .so 后,通过 JNI 暴露 openAndStream(int fd, int iface) 给 Kotlin 调用。NDK 交叉编译建议用 Android NDK r25+,CMakeLists 里链接 -luvc -lpthread -llog。
3. 帧渲染到屏幕(SurfaceTexture)
拿到字节数组后,别用 BitmapFactory.decodeByteArray,那是给图片用的,实时预览会卡死。正确做法是把 YUV420SP 转成 NV21 或直接喂 OpenGL ES:
private fun renderFrame(yuvBytes: ByteArray, width: Int, height: Int) {
surfaceTexture?.updateTexImage()
val matrix = FloatArray(16)
surfaceTexture?.getTransformMatrix(matrix)
GLES20.glViewport(0, 0, viewWidth, viewHeight)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// 绑定 YUV 纹理、绘制全屏四边形,这里省略 shader 细节
// 实际项目中用现成的 UvcCameraView 或 CameraKit 封装会更省事
}
如果你不想自己写 OpenGL,可以直接用 SurfaceTexture + MediaCodec 编码后再解码,或者用 libyuv 把 YUYV 转 NV21 后贴到 SurfaceView。开源库里 android-uvc-camera 和 opencv-android-sdk 都提供了现成的预览视图,二次开发时能省不少时间。
实测数据:清晰度、延迟、稳定性到底怎么样
我在一条 SMT 贴片产线旁跑了两周,相机用的是海康威视 MV-CE013-50UC(1300 万,UVC 模式),平板是联想 ThinkPad X12 Gen 2,1080P@30fps 采集。
| 指标 | 原生平板相机 | libuvc + 工业镜头 |
|---|---|---|
| 有效工作距离 | 15~30cm | 50~200cm 可调 |
| 最小可见缺陷 | ≈0.3mm 污点 | ≈0.05mm 划痕/虚焊 |
| 端到端延迟 | 200~400ms | 85~130ms |
| 连续运行 4 小时丢帧 | 频繁 | 累计 <15 帧(占 0.1%) |
| 发热降频 | 1.5 小时后明显卡顿 | 无明显降频(关后台+开发者选项锁定刷新率) |
延迟 100 毫秒左右,人眼几乎无感知。质检员盯着屏幕看焊点、看丝印方向、看包装标签,手速完全跟得上。传输稳定性方面,USB 本身是轮询机制,只要供电足、线材质量过关,基本不会出现 Wi-Fi 那种突然断流的情况。
有个细节值得提:libuvc 默认走 UVC 描述符里列出的格式列表。有些工业相机在 UVC 模式下只声明了 1280x720 和 1920x1080,你代码里写 2560x1440 会直接报错 UVC_ERROR_INVALID_MODE。遇到这种情况,先用 v4l2-ctl --list-formats-ext -d /dev/video0 在 Linux 上查一下设备真实支持的格式,再反过来配平板端参数。
常见翻车点,现场一步步排
1. 插上没反应,日志报 Permission denied
- 检查平板是否真的支持 USB Host。进
adb shell ls /dev/bus/usb/看有没有设备节点。 - 部分品牌平板需要开启“开发者选项 → USB 调试设置成仅充电时允许 MTP/PTP”之类的开关,不同厂商叫法不一样,实在找不到就去设置里搜
USB。
2. 画面花屏、条纹、偶尔黑一下
- 90% 是供电问题。OTG 口输出电流不够,相机进入保护降频。加独立供电 Hub,或者换支持 USB BC1.2 充电协议的线。
- 线材屏蔽层差,强电磁干扰环境下(变频器、伺服电机旁边)容易出纹。换带磁环的 USB 3.0 线,必要时加隔离器。
3. 帧率上不去,一直卡在 15fps
- UVC 协议在 USB 2.0 下,1080P@30 的 YUYV 数据量约 148MB/s,已经接近带宽上限。改用 MJPEG 压缩格式,或者把像素格式切到 YUYV 但降低分辨率。
- 检查平板 USB 控制器版本。Type-C 接口不一定等于 USB 3.0,有些平板是 2.0 控制器,跑 4K 必然掉帧。
4. 安卓 12⁄13 以上后台被杀
- 在 Manifest 里声明
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> - 启动前台服务,挂一个常驻通知,把采集线程放里面。
- 开发者选项里关掉“不保留活动”和“后台进程限制”。
质检落地时的几个实用建议
别把这套方案当玩具,产线环境比实验室粗糙得多。
- 固定安装比手持靠谱。用铝合金支架把相机和灯光锁死在工作位上方,重复定位精度做到 ±0.1mm 以内。手持拍摄每次角度不同,算法和人工判断都会受影响。
- 曝光和增益手动锁。UVC 支持
UVC_HINT_EXPOSURE和UVC_HINT_GAIN,设好后别让相机自动追光。产线顶灯忽明忽暗时,自动曝光会让画面亮度跳变,质检员看久了容易漏检。 - 预留 AI 接口。libuvc 拿到的原始帧可以直接喂给 TFLite 或 OpenCV。很多工厂现在用YOLOv8n做表面缺陷检测,模型在平板上跑 1080P 大约 40~70ms,加上采集延迟,单件检测周期能压到 200ms 以内。
- 日志和回溯。把每次采集的时间戳、帧序号、相机温度、丢帧计数写到本地 SQLite。产线出问题时要追责,光靠“感觉最近不稳定”没用,得有数据。
- 备件策略。工业相机和 OTG 线都是消耗品,备两台相机、三条线、一个 Hub。产线停机一小时的成本,远高于这几百块的备件。
说实话,这套方案不是万能药。如果你的质检需要微米级测量、或者要和机械臂做毫秒级联动,还是得上 GigE Vision 或 CoaXPress,配合工控机和硬触发。但对于大多数外观检查、条码读取、装配确认、包装核对这类场景,安卓平板 + UVC 工业相机 + libuvc 已经是性价比最高的路线之一。开源驱动免授权费,镜头可换,代码在自己手里,改起来不受制于人。
产线上的事,最终看的不是参数表有多漂亮,而是工人愿不愿意用、能不能连续用、出了问题能不能十分钟修好。libuvc 这条路,我亲测下来,画面够清、传输够稳、调试够透明。把线接好、电供足、代码跑通,质检效率提升是肉眼可见的事。