在基于 Android 相机预览的 CV 应用程序中使用 OpenCL

如何使用 OpenCL 构建自定义 OpenCV Android SDK

  1. 组装和配置 Android OpenCL SDK。示例的 JNI 部分依赖于标准 Khornos OpenCL 标头,以及 OpenCL 和 libOpenCL.so 的C++包装器。标准 OpenCL 标头可以从 OpenCV 存储库中的第三方目录或 Linux 发行版包中复制。C++包装器在Github上的官方Khronos存储库中可用。按以下方式将头文件复制到专用目录:
    cd your_path/ && mkdir ANDROID_OPENCL_SDK && mkdir ANDROID_OPENCL_SDK/include && cd ANDROID_OPENCL_SDK/include
    cp -r path_to_opencv/opencv/3rdparty/include/opencl/1.2/CL 。&& cd CL
    wget https://github.com/KhronosGroup/OpenCL-CLHPP/raw/main/include/CL/opencl.hpp
    wget https://github.com/KhronosGroup/OpenCL-CLHPP/raw/main/include/CL/cl2.hpp
    libOpenCL.so 可以随 BSP 一起提供,也可以从任何具有相关结构的 OpenCL cabaple Android 设备下载。
    CD your_path/ANDROID_OPENCL_SDK & MKDIR 库 && CD 库
    adb 拉 /system/vendor/lib64/libOpenCL.so
    libOpenCL.so 的系统版本可能具有许多特定于平台的依赖项。 标志允许忽略在构建过程中未使用的第三方符号。以下 CMake 行允许将 JNI 部分链接到标准 OpenCL,但不能将 loadLibrary 包含在应用程序包中。系统 OpenCL API 在运行时使用。-Wl,--allow-shlib-undefined
    target_link_libraries(${target} -lOpenCL)
  2. 使用 OpenCL 构建自定义 OpenCV Android SDK。默认情况下,OpenCL 支持 (T-API) 在 Android 操作系统的 OpenCV 版本中处于禁用状态。但是可以在启用 OpenCL/T-API 的情况下为 Android 本地重建 OpenCV:使用 CMake 选项。您还需要指定 CMake 的 Android OpenCL SDK: use 选项的路径。如果您正在使用 OpenCV 构建,请按照 wiki 上的说明进行操作。在 中设置这些 CMake 参数,例如:-DWITH_OPENCL=ON-DANDROID_OPENCL_SDK=path_to_your_Android_OpenCL_SDKbuild_sdk.py.config.pyndk-18-api-level-21.config.py
    ABI(“3”, “arm64-v8a”None, 21, cmake_vars=dict('WITH_OPENCL': 'ON', 'ANDROID_OPENCL_SDK': 'path_to_your_Android_OpenCL_SDK'))
    如果你使用 cmake/ninja 构建 OpenCV,请使用这个 bash 脚本(设置你的NDK_VERSION和路径,而不是路径示例):
    CD path_to_opencv & & mkdir 构建 && cd 构建
    导出NDK_VERSION=25.2.9519653
    导出 ANDROID_SDK=/home/user/Android/Sdk/
    导出 ANDROID_OPENCL_SDK=/path_to_ANDROID_OPENCL_SDK/
    导出 ANDROID_HOME=$ANDROID_SDK
    导出ANDROID_NDK_HOME=$ANDROID_SDK/ndk/$NDK_VERSION/
    cmake -GNinja -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_shared -DANDROID_NATIVE_API_LEVEL=24
    -DANDROID_SDK=$ANDROID_SDK -DANDROID_NDK=$ANDROID_NDK_HOME -DBUILD_JAVA=ON -DANDROID_HOME=$ANDROID_SDK -DBUILD_ANDROID_EXAMPLES=ON
    -DINSTALL_ANDROID_EXAMPLES=ON -DANDROID_ABI=arm64-v8a -DWITH_OPENCL=ON -DANDROID_OPENCL_SDK=$ANDROID_OPENCL_SDK ..

前言

通过 OpenCL 使用 GPGPU 来增强应用程序性能现在已成为一种现代趋势。一些 CV 算法(例如图像过滤)在 GPU 上的运行速度比在 CPU 上快得多。最近,它在 Android 操作系统上成为可能。

Android 操作设备最流行的 CV 应用场景是在预览模式下启动相机,将一些 CV 算法应用于每一帧,并显示由该 CV 算法修改的预览帧。

让我们考虑一下在这种情况下如何使用 OpenCL。具体来说,让我们尝试两种方式:直接调用 OpenCL API 和最近引入的 OpenCV T-API(又名透明 API)——一些 OpenCV 算法的隐式 OpenCL 加速。

应用程序结构

从 Android API 级别 11 (Android 3.0) 开始,相机 API 允许使用 OpenGL 纹理作为预览帧的目标。Android API 级别 21 带来了一个新的 Camera2 API,它提供了对相机设置和使用模式的更多控制,它允许预览帧和 OpenGL 纹理的多个目标。

在 OpenGL 纹理中拥有预览帧对于使用 OpenCL 来说是一笔不错的选择,因为有一个 OpenGL-OpenCL 互操作性 API (cl_khr_gl_sharing),允许与 OpenCL 函数共享 OpenGL 纹理数据而无需复制(当然有一些限制)。

让我们为我们的应用程序创建一个基础,它只将 Android 相机配置为将预览帧发送到 OpenGL 纹理,并在显示时显示这些帧,而无需任何处理。

用于此目的的最小类如下所示:Activity

​
公共类 Tutorial4Activity 扩展 Activity {
私人MyGLSurfaceView mView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
mView = 新的 MyGLSurfaceView(这个);
setContentView(mView);
}
@Override
受保护的无效 onPause() {
mView.onPause();
super.onPause();
}
@Override
受保护的无效 onResume() {
super.onResume();
mView.onResume();
}
}
和最小类:View

public class MyGLSurfaceView 扩展了 CameraGLSurfaceView 实现 CameraGLSurfaceView.CameraTextureListener {
static final String LOGTAG = “MyGLSurfaceView”;
受保护的 int procMode = NativePart.PROCESSING_MODE_NO_PROCESSING;
static final String[] procModeName = new String[] {“无处理”, “CPU”, “OpenCL Direct”, “OpenCL via OpenCV” };
受保护的 int frameCounter;
保护时间长NanoTime;
TextView mFpsText = 空;
公共MyGLSurfaceView(上下文,属性集属性){
super(上下文,属性);
}
@Override
公共布尔值 onTouchEvent(MotionEvent e) {
if(e.getAction() == MotionEvent.ACTION_DOWN)
((活动)getContext()).openOptionsMenu();
返回 true;
}
@Override
公共空隙 surfaceCreated(SurfaceHolder holder) {
super.surfaceCreated(持有人);
NativePart.initCL();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
NativePart.closeCL();
super.surfaceDestroyed(持有人);
}
公共无效 setProcessingMode(int newMode) {
if(newMode>=0 && newMode<procModeName.length)
procMode = 新模式;
还
Log.e(LOGTAG, “忽略无效的处理模式: ” + newMode);
((活动) getContext()).runOnUiThread(new Runnable() {
公共无效运行() {
Toast.makeText(getContext(), “选择模式: ” + procModeName[procMode], Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onCameraViewStarted(int width, int height) {
((活动) getContext()).runOnUiThread(new Runnable() {
公共无效运行() {
Toast.makeText(getContext(), “onCameraViewStarted”, Toast.LENGTH_SHORT).show();
}
});
如果 (NativePart.builtWithOpenCL())
NativePart.initCL();
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
@Override
公共无效 onCameraViewStopped() {
((活动) getContext()).runOnUiThread(new Runnable() {
公共无效运行() {
Toast.makeText(getContext(), “onCameraViewStopped”, Toast.LENGTH_SHORT).show();
}
});
}
@Override
公共布尔值 onCameraTexture(int texIn, int texOut, int width, int height) {
转数快
frameCounter++;
if(frameCounter >= 30)
{
final int fps = (int) (frameCounter * 1e9 / (System.nanoTime() - lastNanoTime));
Log.i(LOGTAG, “drawFrame() FPS: ”+fps);
if(mFpsText != null) {
Runnable fpsUpdater = new Runnable() {
公共无效运行() {
mFpsText.setText(“FPS: ” + fps);
}
};
新增功能处理程序(Looper.getMainLooper()).post(fpsUpdater);
} 还 {
Log.d(LOGTAG, “mFpsText == null”);
mFpsText = (TextView)((活动) getContext()).findViewById(R.id.fps_text_view);
}
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
if(procMode == NativePart.PROCESSING_MODE_NO_PROCESSING)
返回 false;
NativePart.processFrame(texIn, texOut, width, height, procMode);
返回 true;
}
}
​

注意

我们使用两个渲染器类:一个用于旧版 Camera API,另一个用于现代 Camera2

最小的类可以在 Java 中实现(OpenGL ES 2.0 在 Java 中可用),但由于我们将使用 OpenCL 修改预览纹理,因此让我们将 OpenGL 内容移动到 JNI。下面是 JNI 内容的简单 Java 包装器:Renderer

公共类 NativePart {
静态的
{
System.loadLibrary(“opencv_java4”);
System.loadLibrary(“JNIpart”));
}

public static final int PROCESSING_MODE_NO_PROCESSING = 0;
公共静态最终 int PROCESSING_MODE_CPU = 1;
public static final int PROCESSING_MODE_OCL_DIRECT = 2;
public static final int PROCESSING_MODE_OCL_OCV = 3;

公共静态原生布尔值 builtWithOpenCL();
公共静态原生 int initCL();
公共静态原生 void closeCL();
public static native void processFrame(int tex1, int tex2, int w, int h, int mode);
}

由于 和 API 在相机设置和控制方面存在显著差异,因此让我们为两个相应的渲染器创建一个基类:CameraCamera2

​
公共抽象类 MyGLRendererBase 实现 GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
受保护的最终字符串 LOGTAG = “MyGLRendererBase”;
保护表面纹理 mSTex;
保护MyGLSurfaceView mView;
受保护的布尔值 mGLInit = false;
受保护的布尔值 mTexUpdate = false;
MyGLRendererBase(MyGLSurfaceView 视图) {
mView = 视图;
}
受保护的抽象 void openCamera();
受保护的抽象 void closeCamera();
受保护的抽象 void setCameraPreviewSize(int width, int height);
公共无效 onResume() {
Log.i(LOGTAG, “onResume”);
}
公共无效 onPause() {
Log.i(LOGTAG, “onPause”);
mGLInit = 假;
mTexUpdate = 假;
closeCamera();
if(mSTex != 空) {
mSTex.release();
mSTex = 空;
NativeGLRenderer.closeGL();
}
}
@Override
公共同步 void onFrameAvailable(SurfaceTexture surfaceTexture) {
Log.i(LOGTAG, “onFrameAvailable”);
mTexUpdate = 真;
mView.requestRender();
}
@Override
公共无效 onDrawFrame(GL10 gl) {
Log.i(LOGTAG, “onDrawFrame”);
如果 (!mGLInit)
返回;
已同步(此) {
如果 (mTexUpdate) {
mSTex.updateTexImage();
mTexUpdate = 假;
}
}
NativeGLRenderer.drawFrame();
}
@Override
公共空隙 onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
Log.i(LOGTAG, “onSurfaceChanged(”+surfaceWidth+“x”+surfaceHeight+")");
NativeGLRenderer.changeSize(surfaceWidth, surfaceHeight);
setCameraPreviewSize(surfaceWidth, surfaceHeight);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig 配置) {
Log.i(LOGTAG, “onSurfaceCreated”);
字符串 strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
if (strGLVersion != null)
Log.i(LOGTAG, “OpenGL ES 版本: ” + strGLVersion);
int hTex = NativeGLRenderer.initGL();
mSTex = 新 SurfaceTexture(hTex);
mSTex.setOnFrameAvailableListener(这个);
openCamera();
mGLInit = 真;
}
}
​

如您所见,和 API 的继承者应该实现以下抽象方法:CameraCamera2

受保护的抽象 void openCamera();
受保护的抽象 void closeCamera();
受保护的抽象 void setCameraPreviewSize(int width, int height);

让我们在本教程之外留下它们实现的细节,请参考源代码查看它们。

预览帧修改

OpenGL ES 2.0 初始化的细节也相当简单和嘈杂,但这里重要的一点是,作为相机预览目标的 OpeGL 纹理应该是类型(不是),内部它以 YUV 格式保存图片数据。这使得无法通过 CL-GL 互操作 () 共享它并通过 C/C++ 代码访问其像素数据。为了克服这个限制,我们必须使用 FrameBuffer 对象(又名 FBO)将 OpenGL 渲染从这个纹理到另一个常规纹理。GL_TEXTURE_EXTERNAL_OESGL_TEXTURE_2Dcl_khr_gl_sharingGL_TEXTURE_2D

C/C++ 代码

之后,我们可以通过 从 C/C++ 读取(复制)像素数据,并在修改后通过 将它们写回纹理。glReadPixels()glTexSubImage2D()

直接 OpenCL 调用

此外,该纹理可以在不复制的情况下与 OpenCL 共享,但我们必须为此以特殊方式创建 OpenCL 上下文:GL_TEXTURE_2D

​
int initCL()
{
dumpCLinfo();
LOGE(“initCL:启动initCL”);
EGLDisplay mEglDisplay = eglGetCurrentDisplay();
如果 (mEglDisplay == EGL_NO_DISPLAY)
LOGE(“initCL: eglGetCurrentDisplay() returned 'EGL_NO_DISPLAY', error = %x”, eglGetError());
EGLContext mEglContext = eglGetCurrentContext();
如果 (mEglContext == EGL_NO_CONTEXT)
LOGE(“initCL: eglGetCurrentContext() returned 'EGL_NO_CONTEXT', error = %x”, eglGetError());
cl_context_properties props[] =
{ CL_GL_CONTEXT_KHR, (cl_context_properties) mEglContext,
CL_EGL_DISPLAY_KHR、(cl_context_properties) mEglDisplay、
CL_CONTEXT_PLATFORM、0、
0 };
尝试
{
haveOpenCL = 假;
cl::P latform p = cl::P latform::getDefault();
std::string ext = p.getInfo<CL_PLATFORM_EXTENSIONS>();
if(ext.find(“cl_khr_gl_sharing”) == std::string::npos)
LOGE(“警告:PLATFORM不支持CL-GL共享”);
道具[5] = (cl_context_properties) p();
theContext = cl::Context(CL_DEVICE_TYPE_GPU, props);
std::vector<cl::D evice> devs = theContext.getInfo<CL_CONTEXT_DEVICES>();
LOGD(“上下文返回了 %d 个设备,取第一个设备”, devs.size());
ext = devs[0].getInfo<CL_DEVICE_EXTENSIONS>();
if(ext.find(“cl_khr_gl_sharing”) == std::string::npos)
LOGE(“警告:DEVICE不支持CL-GL共享”);
theQueue = cl::CommandQueue(theContext, devs[0]);
cl::P rogram::Sources src(1, std::make_pair(oclProgI2I, sizeof(oclProgI2I)));
theProgI2I = cl::P rogram(theContext, src);
progI2I.build(devs);
cv::ocl::attachContext(p.getInfo<CL_PLATFORM_NAME>(), p(), theContext(), devs[0]());
if( cv::ocl::useOpenCL() )
LOGD(“OpenCV+OpenCL 工作正常!”);
还
LOGE(“无法使用 OpenCL TAPI 初始化 OpenCV”);
haveOpenCL = 真;
}
catch(const cl::Error&e)
{
LOGE(“cl::错误:%s (%d)”, e.what(), e.err());
返回 1;
}
catch(const std::exception&e)
{
LOGE(“std::exception: %s”, e.what());
返回 2;
}
捕获(...)
{
LOGE( “OpenCL 信息:初始化 OpenCL 内容时出现未知错误” );
返回 3;
}
LOGD(“initCL 已完成”);
如果(haveOpenCL)
返回 0;
还
返回 4;
}
然后,纹理可以被对象包装,并通过 OpenCL 调用进行处理:cl::ImageGL

cl::ImageGL imgIn (theContext, CL_MEM_READ_ONLY, GL_TEXTURE_2D, 0, texIn);
cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, texOut);
std::vector < cl::Memory >图像;
images.push_back(imgIn);
images.push_back(imgOut);
int64_t t = getTimeMs();
theQueue.enqueueAcquireGLObjects(&images);
theQueue.finish();
LOGD(“enqueueAcquireGLObjects() 成本 %d ms”, getTimeInterval(t));
t = getTimeMs();
cl::Kernel Laplacian(theProgI2I, “拉普拉斯”);TODO:可以做一次
拉普拉斯.setArg(0, imgIn);
Laplacian.setArg(1, imgOut);
theQueue.finish();
LOGD(“Kernel() 成本 %d ms”, getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueNDRangeKernel(拉普拉斯, cl::NullRange, cl::NDRange(w, h), cl::NullRange);
theQueue.finish();
LOGD(“enqueueNDRangeKernel() 成本 %d 毫秒”, getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueReleaseGLObjects(&images);
theQueue.finish();
LOGD(“enqueueReleaseGLObjects() 成本 %d ms”, getTimeInterval(t));
​

OpenCV T-API

但是,您可能希望使用隐式调用 OpenCL 的 OpenCV T-API,而不是自己编写 OpenCL 代码。您只需要将创建的 OpenCL 上下文传递给 OpenCV(通过 ),并以某种方式将 OpenGL 纹理包装成 .不幸的是,OpenCL 缓冲区在内部保留,无法包裹在 OpenGL 纹理或 OpenCL 图像上 - 因此我们必须在此处复制图像数据:cv::ocl::attachContext()cv::UMatUMat

int64_t t = getTimeMs();
cl::ImageGL imgIn (theContext, CL_MEM_READ_ONLY, GL_TEXTURE_2D, 0, texIn);
std::vector < cl::Memory > images(1, imgIn);
theQueue.enqueueAcquireGLObjects(&images);
theQueue.finish();
cv::UMat uIn、uOut、uTmp;
cv::ocl::convertFromImage(imgIn(), uIn);
LOGD(“将纹理数据加载到 OpenCV UMat 成本 %d ms”, getTimeInterval(t));
theQueue.enqueueReleaseGLObjects(&images);
t = getTimeMs();
cv::blur(uIn, uOut, cv::Size(5, 5));
cv::拉普拉斯(uIn, uTmp, CV_8U);
cv乘法(uTmp, 10, uOut);
LOGD(“OpenCV 处理成本 %d ms”, getTimeInterval(t));
t = getTimeMs();
cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, texOut);
图像.clear();
images.push_back(imgOut);
theQueue.enqueueAcquireGLObjects(&images);
cl_mem clBuffer = (cl_mem)uOut。手柄cv::ACCESS_READ);
cl_command_queue q = (cl_command_queue)cv::ocl::Queue::getDefault()。PTR的();
size_t偏移量 = 0;
size_t origin[3] = { 0, 0, 0 };
size_t region[3] = { (size_t)w, (size_t)h, 1 };
CV_Assert(clEnqueueCopyBufferToImage (q, clBuffer, imgOut(), offset, origin, region, 0, NULL, NULL) == CL_SUCCESS);
theQueue.enqueueReleaseGLObjects(&images);
LOGD(“将结果上传到纹理成本 %d ms”, getTimeInterval(t));

注意

当通过 OpenCL 图像包装器将修改后的图像放回原始 OpenGL 纹理时,我们必须再复制一个图像数据。

性能说明

为了比较我们在索尼Xperia Z3上以720p相机分辨率测量的相同预览帧修改(拉普拉斯)的性能,这些修改是通过C / C++代码(调用with),直接OpenCL调用(使用OpenCL图像进行输入和输出)和OpenCV T-API(调用with)完成的:cv::Laplaciancv::Matcv::Laplaciancv::UMat

  • C/C++ 版本显示 3-4 fps
  • 直接 OpenCL 调用显示 25-27 fps
  • OpenCV T-API 显示 11-13 fps(由于额外的往返复制)cl_imagecl_buffer

   在线教程

有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓

请添加图片描述

人工智能书籍

第一阶段:零基础入门(3-6个月)

新手应首先通过少而精的学习,看到全景图,建立大局观。 通过完成小实验,建立信心,才能避免“从入门到放弃”的尴尬。因此,第一阶段只推荐4本最必要的书(而且这些书到了第二、三阶段也能继续用),入门以后,在后续学习中再“哪里不会补哪里”即可。

第二阶段:基础进阶(3-6个月)

熟读《机器学习算法的数学解析与Python实现》并动手实践后,你已经对机器学习有了基本的了解,不再是小白了。这时可以开始触类旁通,学习热门技术,加强实践水平。在深入学习的同时,也可以探索自己感兴趣的方向,为求职面试打好基础。

第三阶段:工作应用

这一阶段你已经不再需要引导,只需要一些推荐书目。如果你从入门时就确认了未来的工作方向,可以在第二阶段就提前阅读相关入门书籍(对应“商业落地五大方向”中的前两本),然后再“哪里不会补哪里”。

 有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓

在这里插入图片描述

 

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值