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

 查看:OpenCV系列文章目录(持续更新中......)

上一篇:OpenCV4.9.0在Android 开发简介

下一篇:在 MacOS 中安装

本指南旨在帮助您在基于 Android 相机预览的 CV 应用程序中使用 OpenCL ™。教程是为 Android Studio 2022.2.1 编写的。它已使用 Ubuntu 22.04 进行了测试。

本教程假定您已安装并配置了以下内容:

  • Android Studio (2022.2.1.+)
  • JDK 17
  • Android SDK
  • Android NDK (25.2.9519653+)
  • 从 github 或发布版下载 OpenCV 源代码,并按照 wiki 上的指令构建。

它还假定您熟悉 Android Java 和 JNI 编程基础知识。如果您需要上述任何方面的帮助,可以参考我们的 Android 开发简介指南。

本教程还假设您有一个启用了 OpenCL 的 Android 操作设备。

相关源代码位于 opencv/samples/android/tutorial-4-opencl 目录下的 OpenCV 示例中。

如何使用 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

    wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

    libOpenCL.so 可以随 BSP 一起提供,也可以从任何具有相关架构的 OpenCL-cabaple Android 设备下载
    cd your_path/ANDROID_OPENCL_SDK && mkdir lib && cd lib
    adb pull /system/vendor/lib64/libOpenCL.so

    wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

     libOpenCL.so 的系统版本可能有很多特定于平台的依赖关系。-Wl,--allow-shlib-undefined 标志允许忽略在构建过程中未使用的第三方符号。以下 CMake 行允许将 JNI 部件链接到标准 OpenCL,但不能将 loadLibrary 包含在应用程序包中。系统 OpenCL API 用于运行时。

target_link_libraries(${target} -lOpenCL)

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

使用 OpenCL 构建自定义 OpenCV Android SDK。默认情况下,OpenCL 支持 (T-API) 在 Android 操作系统的 OpenCV 构建中处于禁用状态。但可以在启用 OpenCL/T-API 的情况下在本地重建适用于 Android 的 OpenCV:CMake 的 use 选项。您还需要为 CMake 指定 Android OpenCL SDK: use 选项的路径。如果您正在使用 OpenCV 构建 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 build && cd build
export NDK_VERSION=25.2.9519653
export ANDROID_SDK=/home/user/Android/Sdk/
export ANDROID_OPENCL_SDK=/path_to_ANDROID_OPENCL_SDK/
export ANDROID_HOME=$ANDROID_SDK
export 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如下所示:Activity

public class Tutorial4Activity extends Activity {
private 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 = new MyGLSurfaceView(this);
setContentView(mView);
}
@Override
protected void onPause() {
mView.onPause();
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
mView.onResume();
}
}

和最小的类View分别是

​
public class MyGLSurfaceView extends CameraGLSurfaceView implements CameraGLSurfaceView.CameraTextureListener {
static final String LOGTAG = "MyGLSurfaceView";
protected int procMode = NativePart.PROCESSING_MODE_NO_PROCESSING;
static final String[] procModeName = new String[] {"No Processing", "CPU", "OpenCL Direct", "OpenCL via OpenCV"};
protected int frameCounter;
protected long lastNanoTime;
TextView mFpsText = null;
public MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if(e.getAction() == MotionEvent.ACTION_DOWN)
((Activity)getContext()).openOptionsMenu();
return true;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
super.surfaceCreated(holder);
//NativePart.initCL();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//NativePart.closeCL();
super.surfaceDestroyed(holder);
}
public void setProcessingMode(int newMode) {
if(newMode>=0 && newMode<procModeName.length)
procMode = newMode;
else
Log.e(LOGTAG, "Ignoring invalid processing mode: " + newMode);
((Activity) getContext()).runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getContext(), "Selected mode: " + procModeName[procMode], Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onCameraViewStarted(int width, int height) {
((Activity) getContext()).runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getContext(), "onCameraViewStarted", Toast.LENGTH_SHORT).show();
}
});
if (NativePart.builtWithOpenCL())
NativePart.initCL();
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
@Override
public void onCameraViewStopped() {
((Activity) getContext()).runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getContext(), "onCameraViewStopped", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public boolean onCameraTexture(int texIn, int texOut, int width, int height) {
// FPS
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() {
public void run() {
mFpsText.setText("FPS: " + fps);
}
};
new Handler(Looper.getMainLooper()).post(fpsUpdater);
} else {
Log.d(LOGTAG, "mFpsText == null");
mFpsText = (TextView)((Activity) getContext()).findViewById(R.id.fps_text_view);
}
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
if(procMode == NativePart.PROCESSING_MODE_NO_PROCESSING)
return false;
NativePart.processFrame(texIn, texOut, width, height, procMode);
return true;
}
}

注意

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

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

public class NativePart {
static
{
System.loadLibrary("opencv_java4");
System.loadLibrary("JNIpart");
}
public static final int PROCESSING_MODE_NO_PROCESSING = 0;
public static final int PROCESSING_MODE_CPU = 1;
public static final int PROCESSING_MODE_OCL_DIRECT = 2;
public static final int PROCESSING_MODE_OCL_OCV = 3;
public static native boolean builtWithOpenCL();
public static native int initCL();
public static native void closeCL();
public static native void processFrame(int tex1, int tex2, int w, int h, int mode);
}

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

​
public abstract class MyGLRendererBase implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
protected final String LOGTAG = "MyGLRendererBase";
protected SurfaceTexture mSTex;
protected MyGLSurfaceView mView;
protected boolean mGLInit = false;
protected boolean mTexUpdate = false;
MyGLRendererBase(MyGLSurfaceView view) {
mView = view;
}
protected abstract void openCamera();
protected abstract void closeCamera();
protected abstract void setCameraPreviewSize(int width, int height);
public void onResume() {
Log.i(LOGTAG, "onResume");
}
public void onPause() {
Log.i(LOGTAG, "onPause");
mGLInit = false;
mTexUpdate = false;
closeCamera();
if(mSTex != null) {
mSTex.release();
mSTex = null;
NativeGLRenderer.closeGL();
}
}
@Override
public synchronized void onFrameAvailable(SurfaceTexture surfaceTexture) {
//Log.i(LOGTAG, "onFrameAvailable");
mTexUpdate = true;
mView.requestRender();
}
@Override
public void onDrawFrame(GL10 gl) {
//Log.i(LOGTAG, "onDrawFrame");
if (!mGLInit)
return;
synchronized (this) {
if (mTexUpdate) {
mSTex.updateTexImage();
mTexUpdate = false;
}
}
NativeGLRenderer.drawFrame();
}
@Override
public void 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 config) {
Log.i(LOGTAG, "onSurfaceCreated");
String strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
if (strGLVersion != null)
Log.i(LOGTAG, "OpenGL ES version: " + strGLVersion);
int hTex = NativeGLRenderer.initGL();
mSTex = new SurfaceTexture(hTex);
mSTex.setOnFrameAvailableListener(this);
openCamera();
mGLInit = true;
}
}

如您所见, Camera 和 Camera2  APIs的继承者应实现以下抽象方法:

protected abstract void openCamera();
protected abstract void closeCamera();
protected abstract void setCameraPreviewSize(int width, int height);

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

预览帧修改

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

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

C/C++ code

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

直接 OpenCL 调用

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

​
int initCL()
{
dumpCLinfo();
LOGE("initCL: start initCL");
EGLDisplay mEglDisplay = eglGetCurrentDisplay();
if (mEglDisplay == EGL_NO_DISPLAY)
LOGE("initCL: eglGetCurrentDisplay() returned 'EGL_NO_DISPLAY', error = %x", eglGetError());
EGLContext mEglContext = eglGetCurrentContext();
if (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 };
try
{
haveOpenCL = false;
cl::Platform p = cl::Platform::getDefault();
std::string ext = p.getInfo<CL_PLATFORM_EXTENSIONS>();
if(ext.find("cl_khr_gl_sharing") == std::string::npos)
LOGE("Warning: CL-GL sharing isn't supported by PLATFORM");
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("Context returned %d devices, taking the 1st one", devs.size());
ext = devs[0].getInfo<CL_DEVICE_EXTENSIONS>();
if(ext.find("cl_khr_gl_sharing") == std::string::npos)
LOGE("Warning: CL-GL sharing isn't supported by DEVICE");
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]());
if( cv::ocl::useOpenCL() )
LOGD("OpenCV+OpenCL works OK!");
else
LOGE("Can't init OpenCV with OpenCL TAPI");
haveOpenCL = true;
}
catch(const cl::Error& e){
LOGE("cl::Error: %s (%d)", e.what(), e.err());
return 1;
}
catch(const std::exception& e)
{
LOGE("std::exception: %s", e.what());
return 2;
}
catch(...)
{
LOGE( "OpenCL info: unknown error while initializing OpenCL stuff" );
return 3;
}
LOGD("initCL completed");
if (haveOpenCL)
return 0;
else
return 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() costs %d ms", getTimeInterval(t));
t = getTimeMs();
cl::Kernel Laplacian(theProgI2I, "Laplacian"); //TODO: may be done once
Laplacian.setArg(0, imgIn);
Laplacian.setArg(1, imgOut);
theQueue.finish();
LOGD("Kernel() costs %d ms", getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueNDRangeKernel(Laplacian, cl::NullRange, cl::NDRange(w, h), cl::NullRange);
theQueue.finish();
LOGD("enqueueNDRangeKernel() costs %d ms", getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueReleaseGLObjects(&images);
theQueue.finish();
LOGD("enqueueReleaseGLObjects() costs %d ms", getTimeInterval(t));

OpenCV T-API

但是,与其自己编写 OpenCL 代码,不如使用隐式调用 OpenCL 的 OpenCV T-API。您只需要将创建的 OpenCL 上下文传递给 OpenCV(通过cv::ocl::attachContext() ),并以某种  cv::UMat.方式将 OpenGL 纹理包装起来。不幸的是,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("loading texture data to OpenCV UMat costs %d ms", getTimeInterval(t));
theQueue.enqueueReleaseGLObjects(&images);
t = getTimeMs();
//cv::blur(uIn, uOut, cv::Size(5, 5));
cv::Laplacian(uIn, uTmp, CV_8U);
cv:multiply(uTmp, 10, uOut);
cv::ocl::finish();
LOGD("OpenCV processing costs %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);
cv::ocl::finish();
LOGD("uploading results to texture costs %d ms", getTimeInterval(t));

注意

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

性能说明

为了比较在具有720p相机分辨率的Sony Xperia Z3上,通过C / C++代码(调用cv::Laplacian与cv::Mat),直接OpenCL调用(使用OpenCL图像进行输入和输出)和OpenCV T-API(调用cv::Laplacian与cv::UMat)完成的相同预览帧修改(Laplacian)的FPS:

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

参考文献:

1、《Use OpenCL in Android camera preview based CV application》  Andrey Pavlenko, Alexander Panov

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/760441.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用图解说明mysql 行锁加锁规则

加锁原则 原则 1&#xff1a;加锁的基本单位是 next-key lock。希望你还记得&#xff0c;next-key lock 是前开后闭区间。原则 2&#xff1a;查找过程中访问到的对象才会加锁。优化 1&#xff1a;索引上的等值查询&#xff0c;给唯一索引加锁的时候&#xff0c;next-key lock …

【Java】Oracle发布Java22最新版本

甲骨文&#xff08;ORACLE&#xff09;已经于2023年3月19日正式发布了最新版本的JDK&#xff0c;版本号&#xff1a;22 根据官方声明&#xff0c;Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进&#xff0c;包括对Java 语言、其API 和性能&#xff0c;以…

基于stable diffusion的IP海报生成

【AIGC】只要10秒&#xff0c;AI生成IP海报&#xff0c;解放双手&#xff01;&#xff01;&#xff01;在AIGC市场发展的趋势下&#xff0c;如何帮助设计工作者解放双手。本文将从图像生成方向切入&#xff0c;帮助大家体系化的学习Stable diffusion的使用&#xff0c;完成自有…

php 对接IronSource海外广告平台收益接口Reporting API

今天对接的是IronSource广告reporting api接口&#xff0c;拉取广告收益回来自己做统计。记录分享给大家 首先是文档地址,进入到IronSource后台就能看到文档地址以及参数&#xff1a; 文档地址&#xff1a;https://developers.is.com/ironsource-mobile/air/reporting/ 在这里插…

基于Python3的数据结构与算法 - 16 链表

目录 链表 1. 创建链表 2. 链表的插入和删除 3. 双链表 4. 链表总结 链表 链表是由一系列节点组成的元素集合。每个节点包含两部分&#xff0c;数据域item和指向下一个节点得指针next。通过节点之间的相互连接&#xff0c;最终串联成一个链表。 class Node:def __init…

如何利用人工智能技术实现企业营销效率提升10倍(上)

01. 品牌营销面临越来越大的挑战 在当前行业下行周期&#xff0c;品牌营销正面临着前所未有的挑战。首当其冲的是高昂的营销费用&#xff0c;这使得企业在投入资源时更加谨慎&#xff0c;同时也需要寻求更加高效的营销手段来确保投入产出比的最大化。其次&#xff0c;由于缺乏…

Linux系统本地部署Docker Compose UI服务结合内网穿透实现公网访问

文章目录 1. 安装Docker2. 检查本地docker环境3. 安装cpolar内网穿透4. 使用固定二级子域名地址远程访问 Docker Compose UI是Docker Compose的web界面。这个项目的目标是在Docker Compose之上提供一个最小的HTTP API&#xff0c;同时保持与Docker Compose CLI的完全互操作性。…

探讨苹果 Vision Pro 的空间视频(术语辨析、关键技术、思考)

背景:一位资深视频技术从业者在 Pixvana 工作,积累了丰富的捕获、处理、编码、流传和播放空间媒体经验。 一、术语 空间视频:传统的 3D 视频,呈矩形,包含左右眼视图,如 iPhone15 Pro 和 Vision Pro 可录制。沉浸式视频:非矩形的环绕式视频体验,通常由两个或多个传感器…

Unity 学习笔记 5.控制飞机飞行

目录 1.摄像机跟随的方法 2.鼠标按键响应 3.键盘按键响应 4.导入素材 5.让飞机向前飞 6.摄像机跟随飞机移动 7.鼠标控制飞机倾斜 8.键盘控制飞机飞行 下载源码 UnityPackage 1.摄像机跟随的方法 2.鼠标按键响应 3.键盘按键响应 4.导入素材 下载素材 步骤&#xff1a; 将…

蓝桥杯 第3217题 简单的异或难题 C++ Java Python

题目 思路和解题方法 计算给定数组中子数组异或和的问题。它采用了前缀异或的方法来预处理数组&#xff0c;然后对于每个查询&#xff0c;通过异或操作计算子数组的异或和。 读取输入的数组&#xff0c;并计算每个位置的前缀异或和。对于每个查询&#xff0c;读取查询的左右边界…

css使用变量

vue3单文件SFC新特性在css里可以使用变量&#xff0c;具体使用如下&#xff1a; <template><div class"home-view"><span>测试</span><p>测试2</p></div> </template><script setup lang"ts"> imp…

C++关键字:const

文章目录 一、const的四大作用1.修饰 变量、数组2.修饰 函数的形参、修饰 引用 (最常用&#xff09;3.修饰 指针&#xff1a;常量指针、指针常量 、只读指针4.修饰 类的成员函数、修饰 类的对象 一、const的四大作用 1.修饰 变量、数组 1.const修饰变量&#xff1a; 被const修…

基于Spring Boot的煤矿信息管理系统

摘 要 系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对煤矿信息管理的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上…

一分钟了解自动化测试【建议收藏】

引子 写在最前面&#xff1a;目前自动化测试并不属于新鲜的事物&#xff0c;或者说自动化测试的各种方法论已经层出不穷&#xff0c;但是&#xff0c;能够明白自动化测试并很好落地实施的团队还不是非常多&#xff0c;我们接来下用通俗的方式来介绍自动化测试…… 本文共有2410…

Web Service接口测试

Web service 接口测试 一. web Service概念 Web service使用与平台和编程语言无关的方式进行通讯的一项技术, web service 是一个接口, 他描述了一组可以在网络上通过标准的XML消息传递访问的操作,它基于xml语言协议来描述要执行的操作或者要与另外一个web 服务交换数据, 一组…

C语言每日一题06

一、题目 二、解析 void main &#xff08;&#xff09; { char c1&#xff0c;c2&#xff1b; int a1&#xff0c;a2&#xff1b; c1 getchar &#xff08;&#xff09;&#xff1b;//读取第一个输入&#xff0c;c11 scanf &#xff08;“%3d”&#xff0c;&a1&#xff…

2024上海慕尼黑正运动展位现场直击

3月20日&#xff0c;备受业界关注的、3C电子行业规模最大的会展活动“2024慕尼黑上海电子生产设备展”在上海新国际博览中心盛大开幕。 正运动技术本次以“打卡正运动&#xff0c;共同探讨国产全自主运动控制新发展”为主题&#xff0c;现场精心策划了一系列激动人心的有奖互动…

数据库:基本操作与用户授权

一 基本操作 1 SQL分类 数据库&#xff1a;database 表&#xff1a;table&#xff0c;行&#xff1a;row 列&#xff1a;column 索引&#xff1a;index 视图&#xff1a;view 存储过程&#xff1a;procedure 存储函数&#xff1a;function 触发器&#xff1a;trigger 事…

【开发环境搭建篇】MySQL服务器端安装和配置

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0c;产…

高效低压电源——FP7195升降压芯片打造专属电源方案

目录 方案背景 产品介绍 升降压LED驱动电源参数介绍 FP7195芯片介绍 产品应用领域 电源板详解 总结 方案背景 目前市面上的低压LED驱动电源五花八门&#xff0c;升压&#xff0c;降压都包含在内&#xff0c;不过在实际使用时总会有一定限制要求&#xff0c;比如要求电源…