public class MyGLSurfaceView extends CameraGLSurfaceView implements CameraGLSurfaceView.CameraTextureListener {
static final String LOGTAG =
"MyGLSurfaceView";
受保护的 int procMode = NativePart.PROCESSING_MODE_NO_PROCESSING;
静态 final String[] procModeName = new String[] {"无处理", "CPU", "OpenCL 直接", "OpenCL 通过 OpenCV"};
受保护的 int frameCounter;
受保护的 long lastNanoTime;
TextView mFpsText = null;
公共 MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
公共 boolean onTouchEvent(MotionEvent e) {
如果(e.getAction() == MotionEvent.ACTION_DOWN)
((Activity)getContext()).openOptionsMenu();
返回 true;
}
@Override
公共 void surfaceCreated(SurfaceHolder holder) {
super.surfaceCreated(holder);
}
@Override
公共 void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
}
公共 void setProcessingMode(int newMode) {
如果(newMode>=0 && newMode
procMode = newMode;
否则
Log.e(LOGTAG, "忽略无效的处理模式: " + newMode);
((Activity) getContext()).runOnUiThread(new Runnable() {
公共 void run() {
Toast.makeText(getContext(), "选择的模式: " + procModeName[procMode], Toast.LENGTH_LONG).show();
}
});
}
@Override
公共 void onCameraViewStarted(int width, int height) {
((Activity) getContext()).runOnUiThread(new Runnable() {
公共 void run() {
Toast.makeText(getContext(), "onCameraViewStarted", Toast.LENGTH_SHORT).show();
}
});
如果 (NativePart.builtWithOpenCL())
NativePart.initCL();
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
@Override
公共 void onCameraViewStopped() {
((Activity) getContext()).runOnUiThread(new Runnable() {
公共 void run() {
Toast.makeText(getContext(), "onCameraViewStopped", Toast.LENGTH_SHORT).show();
}
});
}
@Override
公共 boolean onCameraTexture(int texIn, int texOut, int width, int height) {
frameCounter++;
如果(frameCounter >= 30)
{
最终的 int fps = (int) (frameCounter * 1e9 / (System.nanoTime() - lastNanoTime));
Log.i(LOGTAG, "drawFrame() FPS: "+fps);
如果(mFpsText != null) {
Runnable fpsUpdater = new Runnable() {
公共 void run() {
mFpsText.setText("FPS: " + fps);
}
};
new Handler(Looper.getMainLooper()).post(fpsUpdater);
} 否则 {
Log.d(LOGTAG, "mFpsText == null");
mFpsText = (TextView)((Activity) getContext()).findViewById(R.id.fps_text_view);
}
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
如果(procMode == NativePart.PROCESSING_MODE_NO_PROCESSING)
返回 false;
NativePart.processFrame(texIn, texOut, width, height, procMode);
返回 true;
}
}
- 注意
- 我们使用了两个渲染器类:一个用于旧版 Camera API,另一个用于现代 Camera2 API。
可以在 Java 中实现一个最小的 Renderer 类(Java 中可以使用 OpenGL ES 2.0),但是由于我们将使用 OpenCL 修改预览纹理,因此让我们将 OpenGL 内容移到 JNI。这是一个简单的 Java 包装器,用于我们的 JNI 内容
公共类 NativePart {
静态
{
System.loadLibrary("opencv_java4");
System.loadLibrary("JNIpart");
}
公共静态 final int PROCESSING_MODE_NO_PROCESSING = 0;
公共静态 final int PROCESSING_MODE_CPU = 1;
公共静态 final int PROCESSING_MODE_OCL_DIRECT = 2;
公共静态 final int PROCESSING_MODE_OCL_OCV = 3;
公共静态 native boolean builtWithOpenCL();
公共静态 native int initCL();
公共静态 native void closeCL();
公共静态 native void processFrame(int tex1, int tex2, int w, int h, int mode);
}
由于 Camera 和 Camera2 API 在相机设置和控制方面存在很大差异,因此让我们为两个对应的渲染器创建一个基类
公共抽象类 MyGLRendererBase 实现 GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
受保护的 final String LOGTAG = "MyGLRendererBase";
受保护的 SurfaceTexture mSTex;
受保护的 MyGLSurfaceView mView;
受保护的 boolean mGLInit = false;
受保护的 boolean mTexUpdate = false;
MyGLRendererBase(MyGLSurfaceView view) {
mView = view;
}
受保护的抽象 void openCamera();
受保护的抽象 void closeCamera();
受保护的抽象 void setCameraPreviewSize(int width, int height);
公共 void onResume() {
Log.i(LOGTAG, "onResume");
}
公共 void onPause() {
Log.i(LOGTAG, "onPause");
mGLInit = false;
mTexUpdate = false;
closeCamera();
如果(mSTex != null) {
mSTex.release();
mSTex = null;
NativeGLRenderer.closeGL();
}
}
@Override
公共同步 void onFrameAvailable(SurfaceTexture surfaceTexture) {
mTexUpdate = true;
mView.requestRender();
}
@Override
公共 void onDrawFrame(GL10 gl) {
如果 (!mGLInit)
返回;
同步 (this) {
如果 (mTexUpdate) {
mSTex.updateTexImage();
mTexUpdate = false;
}
}
NativeGLRenderer.drawFrame();
}
@Override
公共 void onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
Log.i(LOGTAG, "onSurfaceChanged(" + surfaceWidth + "x" + surfaceHeight + ")");
NativeGLRenderer.changeSize(surfaceWidth, surfaceHeight);
setCameraPreviewSize(surfaceWidth, surfaceHeight);
}
@Override
公共 void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.i(LOGTAG, "onSurfaceCreated");
字符串 strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
如果 (strGLVersion != 空)
Log.i(LOGTAG, "OpenGL ES 版本: " + strGLVersion);
int hTex = NativeGLRenderer.initGL();
mSTex = 新 SurfaceTexture(hTex);
mSTex.setOnFrameAvailableListener(this);
openCamera();
mGLInit = 真;
}
}
std::string 字符串
**定义** cvstd.hpp:151
正如你所看到的,`Camera` 和 `Camera2` API 的继承者应该实现以下抽象方法
受保护的抽象 void openCamera();
受保护的抽象 void closeCamera();
受保护的抽象 void setCameraPreviewSize(int width, int height);
让我们将它们的实现细节留在这个教程之外,请参考 源代码 查看它们。
预览帧修改
OpenGL ES 2.0 初始化的细节也很直接,并且过于冗长,不适合在此引用,但此处重要的是,作为摄像机预览目标的 OpenGL 纹理应该是 `GL_TEXTURE_EXTERNAL_OES` 类型(而不是 `GL_TEXTURE_2D`),它在内部以 *YUV* 格式保存图像数据。这使得无法通过 CL-GL 互操作 ( `cl_khr_gl_sharing` ) 共享它,也无法通过 C/C++ 代码访问它的像素数据。为了克服这个限制,我们必须从这个纹理到另一个常规的 `GL_TEXTURE_2D` 纹理执行 OpenGL 渲染,使用 *帧缓冲区对象* (又名 FBO)。
C/C++ 代码
之后,我们可以通过 `glReadPixels()` 从 C/C++ 读取(复制)像素数据,并在修改后通过 `glTexSubImage2D()` 将它们写回纹理。
直接 OpenCL 调用
同样,`GL_TEXTURE_2D` 纹理可以与 OpenCL 共享而无需复制,但我们必须以特殊方式创建 OpenCL 上下文。
int initCL()
{
dumpCLinfo();
LOGE("initCL: 开始 initCL");
EGLDisplay mEglDisplay = eglGetCurrentDisplay();
如果 (mEglDisplay == EGL_NO_DISPLAY)
LOGE("initCL: eglGetCurrentDisplay() 返回 'EGL_NO_DISPLAY',错误 = %x", eglGetError());
EGLContext mEglContext = eglGetCurrentContext();
如果 (mEglContext == EGL_NO_CONTEXT)
LOGE("initCL: eglGetCurrentContext() 返回 'EGL_NO_CONTEXT',错误 = %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::Platform p = cl::Platform::getDefault();
std::string ext = p.getInfo<CL_PLATFORM_EXTENSIONS>();
如果 (ext.find("cl_khr_gl_sharing") == std::string::npos)
LOGE("警告:PLATFORM 不支持 CL-GL 共享");
props[5] = (cl_context_properties) p();
theContext = cl::Context(CL_DEVICE_TYPE_GPU, props);
std::vector<cl::Device> devs = theContext.getInfo<CL_CONTEXT_DEVICES>();
LOGD("上下文返回 %d 个设备,取第一个", devs.size());
ext = devs[0].getInfo<CL_DEVICE_EXTENSIONS>();
如果 (ext.find("cl_khr_gl_sharing") == std::string::npos)
LOGE("警告:DEVICE 不支持 CL-GL 共享");
theQueue = cl::CommandQueue(theContext, devs[0]);
cl::Program::Sources src(1, std::make_pair(oclProgI2I, sizeof(oclProgI2I)));
theProgI2I = cl::Program(theContext, src);
theProgI2I.build(devs);
cv::ocl::attachContext(p.getInfo<CL_PLATFORM_NAME>(), p(), theContext(), devs[0]());
如果 (cv::ocl::useOpenCL())
LOGD("OpenCV+OpenCL 工作正常!");
否则
LOGE("无法使用 OpenCL TAPI 初始化 OpenCV");
haveOpenCL = 真;
}
捕获 (const cl::Error& e)
{
LOGE("cl::Error: %s (%d)", e.what(), e.err());
返回 1;
}
捕获 (const std::exception& e)
{
LOGE("std::exception: %s", e.what());
返回 2;
}
捕获(...)
{
LOGE("OpenCL 信息:初始化 OpenCL 内容时出现未知错误");
返回 3;
}
LOGD("initCL 完成");
如果 (haveOpenCL)
返回 0;
否则
返回 4;
}
然后纹理可以被 `cl::ImageGL` 对象包装,并通过 OpenCL 调用进行处理
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;
images.push_back(imgIn);
images.push_back(imgOut);
int64_t t = getTimeMs();
theQueue.enqueueAcquireGLObjects(&images);
theQueue.finish();
LOGD("enqueueAcquireGLObjects() 耗时 %d 毫秒", getTimeInterval(t));
t = getTimeMs();
cl::Kernel Laplacian(theProgI2I, "Laplacian"); //TODO: 可能只执行一次
Laplacian.setArg(0, imgIn);
Laplacian.setArg(1, imgOut);
theQueue.finish();
LOGD("Kernel() 耗时 %d 毫秒", getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueNDRangeKernel(Laplacian, 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 毫秒", getTimeInterval(t));
OpenCV T-API
但是,与其自己编写 OpenCL 代码,不如使用隐式调用 OpenCL 的 **OpenCV T-API**。您只需要将创建的 OpenCL 上下文传递给 OpenCV(通过 `cv::ocl::attachContext()`),并以某种方式将 OpenGL 纹理与 `cv::UMat` 包装起来。不幸的是,`UMat` 在内部保留 OpenCL *缓冲区*,它不能包装在 OpenGL *纹理* 或 OpenCL *图像* 上——因此我们必须在此处复制图像数据
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::Laplacian(uIn, uTmp, CV_8U);
cv::multiply(uTmp, 10, uOut);
LOGD("OpenCV处理耗时 %d ms", getTimeInterval(t));
t = getTimeMs();
cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, texOut);
images.clear();
images.push_back(imgOut);
theQueue.enqueueAcquireGLObjects(&images);
cl_mem clBuffer = (cl_mem)uOut.handle(cv::ACCESS_READ);
cl_command_queue q = (cl_command_queue)cv::ocl::Queue::getDefault().ptr();
size_t offset = 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纹理时,我们必须再进行一次图像数据复制。
性能说明
为了比较性能,我们测量了在Sony Xperia Z3(720p摄像头分辨率)上,通过C/C++代码(调用带有cv::Mat的cv::Laplacian)、直接OpenCL调用(使用OpenCL图像作为输入和输出)以及OpenCV T-API(调用带有cv::UMat的cv::Laplacian)对相同预览帧进行修改(Laplacian)的FPS。
- C/C++版本显示3-4 fps
- 直接OpenCL调用显示25-27 fps
- OpenCV T-API显示11-13 fps(由于从cl_image到cl_buffer的额外复制)