融媒体服务中PBO进行多重采样抗锯齿(MSAA)

如果不理解pbo 那先去了解概念,在此不再解释,这是我为了做融合服务器viewpointserver做的一部分工作,融合服务器的功能是将三维和流媒体,AI融合在一起,viewpointserver会直接读取三维工程的文件,同时融合rtsp视频流,将视频流作为纹理给材质,最后赋值给三维模型如fbx的表面。由于没有窗口,三维作为服务运行,里面有一项工作就是三维的抗锯齿采集。

PBO(Pixel Buffer Object)进行多重采样抗锯齿(MSAA)的一般步骤

1 初始化 OpenGL 和 MSAA 帧缓冲:

1.1开启 MSAA:

在初始化 OpenGL 时,通过设置相关参数开启多重采样抗锯齿。例如,使用 glEnable(GL_MULTISAMPLE) 函数来启用 OpenGL 的多重采样功能。

1.2 创建帧缓冲:

创建一个帧缓冲对象(FBO),用于存储多重采样后的图像数据。这可以通过 glGenFramebuffers 和 glBindFramebuffer 等函数来完成。

1.3 配置 FBO 的附件:

为 FBO 配置颜色附件和深度附件。对于颜色附件,创建一个纹理,并将其绑定到 FBO 上;对于深度附件,可以创建一个渲染缓冲对象(RBO)并绑定到 FBO。确保纹理和 RBO 的尺寸与渲染窗口的尺寸相匹配。

2 渲染到 MSAA 帧缓冲:

2.1 绑定 MSAA 帧缓冲:

在渲染场景之前,使用 glBindFramebuffer 函数将之前创建的 MSAA FBO 绑定为当前的渲染目标。这样,后续的渲染操作将把图像数据渲染到 MSAA FBO 中。

2.2 进行常规渲染:

按照正常的 OpenGL 渲染流程绘制场景中的物体。由于启用了 MSAA,OpenGL 会在每个像素内进行多个子采样,以实现抗锯齿效果。

3 使用 PBO 读取 MSAA 数据:

创建 PBO:使用 glGenBuffers 函数创建一个 PBO,并使用 glBindBuffer 函数将其绑定到 GL_PIXEL_PACK_BUFFER 目标上。然后,使用 glBufferData 函数为 PBO 分配足够的内存空间,以存储从 MSAA FBO 读取的像素数据。

读取像素数据到 PBO:在渲染完成后,使用 glReadPixels 函数将 MSAA FBO 中的像素数据读取到 PBO 中。由于 PBO 的存在,这个操作可以在后台异步进行,减少对 CPU 的阻塞。

4 处理和解析 PBO 中的数据:

4.1 映射 PBO:

使用 glMapBuffer 函数将 PBO 映射到 CPU 可访问的内存空间,以便读取和处理像素数据。这将返回一个指向 PBO 内存的指针,可以通过该指针访问像素数据。

4.2 解析像素数据:

根据需要,对 PBO 中的像素数据进行处理和解析。例如,可以将像素数据转换为图像格式,以便保存为文件或进行其他操作。在处理像素数据时,需要考虑 MSAA 的子采样信息,通常需要对多个子采样点的颜色值进行合并或平均,以得到最终的抗锯齿效果。

4.3 取消映射 PBO:

完成对 PBO 数据的处理后,使用 glUnmapBuffer 函数取消对 PBO 的映射,释放 CPU 对 PBO 内存的访问。

5 显示或使用抗锯齿后的图像:

5.1 将处理后的像素数据显示在屏幕上:

如果需要在屏幕上显示抗锯齿后的图像,可以使用 glDrawPixels 或其他相关的 OpenGL 函数将处理后的像素数据绘制到默认的帧缓冲中,然后通过交换缓冲区来显示在屏幕上。

5.2 保存为图像文件:

可以将处理后的像素数据保存为图像文件,以便后续使用。这可以通过使用图像库(如 stb_image 库)来实现,将像素数据写入图像文件中。

使用深度缓冲的例子

以下使用qt来做,qt的优点是很多都是封装好的,比较容易实现,不用引入额外的库,当然我们也可以使用glfw来做,后面会给出例子

c 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QtWidgets/QWidget>
#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLTexture>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include "ui_MainWindow.h"

QT_BEGIN_NAMESPACE
namespace Ui {
	class CMainWindow;
}
QT_END_NAMESPACE

class CMainWindow : public QOpenGLWidget, protected QOpenGLFunctions_4_5_Core
{
	Q_OBJECT

public:
	CMainWindow(QWidget *parent = Q_NULLPTR);
	~CMainWindow();

	void initFBO();

protected:
	void initializeGL();
	void paintGL();
	void resizeGL(int w, int h);

private:
	QImage m_img;
	GLsizei m_width = 0;
	GLsizei m_height = 0;
  GLsizeiptr m_dataSize = 0;
	GLuint m_VBO = 0;
	GLuint m_VAO = 0;
	GLuint m_EBO = 0;
	GLuint m_texture = 0;
	GLuint m_frameBuffer = 0;
	GLuint m_RBO = 0;
	GLuint m_fVBO = 0;
	GLuint m_fVAO = 0;
	GLuint m_textureFBO = 0;
	QOpenGLShaderProgram *m_programScreen = nullptr;
	QOpenGLShaderProgram *m_shaderProgram = nullptr;
private:
	Ui::CMainWindow *ui;
};
#endif //MAINWINDOW_H
c 复制代码
#include "MainWindow.h"

CMainWindow::CMainWindow(QWidget* parent)
	: QOpenGLWidget(parent), ui(new Ui::CMainWindow)
{
	ui->setupUi(this);
	m_img = QImage("2.jpg");//picture
	m_width = m_img.width();
	m_height = m_img.height();
	m_dataSize = m_width * m_height * 3;
}

CMainWindow::~CMainWindow()
{
	delete ui;
	glDeleteVertexArrays(1, &m_VAO);
	glDeleteBuffers(1, &m_VBO);
	glDeleteBuffers(1, &m_EBO);
	glDeleteVertexArrays(1, &m_fVAO);
	glDeleteBuffers(1, &m_fVBO);
	glDeleteBuffers(1, &m_frameBuffer);
	glDeleteBuffers(1, &m_RBO);
}
void CMainWindow::initFBO()
{
	glGenFramebuffers(1, &m_frameBuffer);
	glBindFramebuffer(GL_FRAMEBUFFER, m_frameBuffer);

	glGenTextures(1, &m_textureFBO);
	glBindTexture(GL_TEXTURE_2D, m_textureFBO);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_width, m_height, 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);//TODO
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glBindTexture(GL_TEXTURE_2D, 0);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_textureFBO, 0);

	glGenRenderbuffers(1, &m_RBO);
	glBindRenderbuffer(GL_RENDERBUFFER, m_RBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, m_width, m_height);
	glBindRenderbuffer(GL_RENDERBUFFER, 0);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_RBO);
	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
		qDebug() << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" ;
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	char vertexShaderSource[] =
		"#version 450 core\n"
		"layout (location = 0) in vec2 aPos;\n"
		"layout (location = 1) in vec2 aTexCoords;\n"
		"out vec2 TexCoords;\n"
		"void main()\n"
		"{\n"
		"   gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);\n"
		"TexCoords = aTexCoords;\n"
		"}\n";
	char fragmentShaderSource[] =
		"#version 450 core\n"
		"out vec4 FragColor;\n"
		"in vec2 TexCoords;\n"
		"uniform sampler2D screenTexture;\n"
		"void main()\n"
		"{\n"
		"   FragColor = texture(screenTexture, TexCoords);\n"
		"}\n";

	m_programScreen = new QOpenGLShaderProgram;
	m_programScreen->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
	m_programScreen->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
	m_programScreen->link();

	m_programScreen->bind();
	m_programScreen->setUniformValue("screenTexture", 1);
	m_programScreen->release();

	float fb_Vertices[] = {
		// positions   // texCoords
		-1.0f,  1.0f,  0.0f, 1.0f,
		-1.0f, -1.0f,  0.0f, 0.0f,
		 1.0f, -1.0f,  1.0f, 0.0f,

		-1.0f,  1.0f,  0.0f, 1.0f,
		 1.0f, -1.0f,  1.0f, 0.0f,
		 1.0f,  1.0f,  1.0f, 1.0f
	};

	glGenVertexArrays(1, &m_fVAO);
	glBindVertexArray(m_fVAO);
	glGenBuffers(1, &m_fVBO);
	glBindBuffer(GL_ARRAY_BUFFER, m_fVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(fb_Vertices), fb_Vertices, GL_STATIC_DRAW);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)(2 * sizeof(float)));
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

}

void CMainWindow::initializeGL()
{
	this->initializeOpenGLFunctions();

	char vertexShaderSource[] =
		"#version 450 core\n"
		"layout(location = 0) in vec3 aPos;\n"
		"layout(location = 1) in vec3 aColor;\n"
		"layout(location = 2) in vec2 aTexcood;\n"
		"out vec3 vertexColor;\n"
		"out vec2 m_tex;\n"
		"void main()\n"
		"{\n"
		"   m_tex = aTexcood;\n"
		"   vertexColor = aColor;\n"
		"   gl_Position = vec4(aPos, 1.0);\n"
		"}\n";

	char fragmentShaderSource[] =
		"#version 450 core\n"
		"out vec4 FragColor;\n"
		"in vec3 vertexColor;\n"
		"in vec2 m_tex;\n"
		"uniform sampler2D ourTexture;\n"
		"void main()\n"
		"{\n"
		"   FragColor = texture2D(ourTexture, m_tex) * vec4(vertexColor, 1.0f);\n"
		"}\n";

	m_shaderProgram = new QOpenGLShaderProgram;
	m_shaderProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
	m_shaderProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
	m_shaderProgram->link();

	m_shaderProgram->bind();
	m_shaderProgram->setUniformValue("ourTexture", 0);
	m_shaderProgram->release();

	GLfloat vertices[] = {
		-1.0f, -1.0f,0.0f, 0.0f,0.0f,1.0f, 0.0f,0.0f,
		 1.0f, -1.0f,0.0f, 0.0f,1.0f,0.0f, 1.0f,0.0f,
		 1.0f,  1.0f,0.0f, 1.0f,0.0f,0.0f, 1.0f,1.0f,
		-1.0f,  1.0f,0.0f, 0.0f,1.0f,1.0f, 0.0f,1.0f,
	};

	GLuint indices[] = {
		0,1,2,
		0,2,3
	};

	glGenVertexArrays(1, &m_VAO);
	glBindVertexArray(m_VAO);
	glGenBuffers(1, &m_VBO);
	glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	glGenBuffers(1, &m_EBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void*)(6 * sizeof(float)));
	glEnableVertexAttribArray(2);

	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	glGenTextures(1, &m_texture);
	glBindTexture(GL_TEXTURE_2D, m_texture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_width, m_height, 0, GL_BGRA, GL_UNSIGNED_BYTE, m_img.bits());//TODO
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glGenerateMipmap(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, 0);
	
	//初始化FBO
	initFBO();
	
}

void CMainWindow::paintGL()
{
	glBindFramebuffer(GL_FRAMEBUFFER, m_frameBuffer);
	glEnable(GL_DEPTH_TEST);

	glClearColor(0.5f, 0.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	m_shaderProgram->bind();
	glBindVertexArray(m_VAO);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_texture);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL);
	glBindVertexArray(0);
	glBindTexture(GL_TEXTURE_2D, 0);
	m_shaderProgram->release();

	/********** 关键之处 ***********/
	GLuint fb = context()->defaultFramebufferObject();
	glBindFramebuffer(GL_FRAMEBUFFER, fb);

	glDisable(GL_DEPTH_TEST);
	glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	glViewport(0, 0, m_width, m_height);

	m_programScreen->bind();
	glBindVertexArray(m_fVAO);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, m_textureFBO);
	glDrawArrays(GL_TRIANGLES, 0, 6);
	glBindVertexArray(0);
	glBindTexture(GL_TEXTURE_2D, 0);
	m_programScreen->release();

}

void CMainWindow::resizeGL(int w, int h)
{
	glViewport(0, 0, w, h);
}

qt中采集场景如图

下面给出glfw的例子

glfw

首先初始化 OpenGL、GLFW 和 GLEW,也可以用glad,并创建了 MSAA FBO、颜色纹理、深度 RBO 和 PBO。然后,在渲染循环中,先将场景渲染到 MSAA FBO 中,再将 FBO 中的像素数据读取到 PBO 中,并对 PBO 中的像素数据进行处理。最后,清理资源并退出程序。当然实际应用中确实需要根据具体需求进行更多的错误处理和优化。

c 复制代码
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cstdlib>

// 窗口尺寸
const int WIDTH = 800;
const int HEIGHT = 600;

// 创建 PBO
GLuint pbo;
// 创建 MSAA FBO
GLuint fbo;
// 颜色纹理
GLuint colorTexture;
// 深度 RBO
GLuint depthRBO;

void init() {
    // 初始化 GLFW
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        exit(EXIT_FAILURE);
    }

    // 创建窗口
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "MSAA with PBO", nullptr, nullptr);
    if (!window) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        exit(EXIT_FAILURE);
    }

    // 将窗口设置为当前上下文
    glfwMakeContextCurrent(window);

    // 初始化 GLEW
    glewExperimental = GL_TRUE;
    if (glewInit()!= GLEW_OK) {
        std::cerr << "Failed to initialize GLEW" << std::endl;
        exit(EXIT_FAILURE);
    }

    // 开启多重采样抗锯齿
    glEnable(GL_MULTISAMPLE);

    // 创建 FBO
    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);

    // 创建颜色纹理
    glGenTextures(1, &colorTexture);
    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture);
    glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGBA8, WIDTH, HEIGHT, GL_TRUE);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0);

    // 创建深度 RBO
    glGenRenderbuffers(1, &depthRBO);
    glBindRenderbuffer(GL_RENDERBUFFER, depthRBO);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRBO);

    // 检查 FBO 是否完整
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE) {
        std::cerr << "Failed to create FBO" << std::endl;
        exit(EXIT_FAILURE);
    }

    // 创建 PBO
    glGenBuffers(1, &pbo);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    glBufferData(GL_PIXEL_PACK_BUFFER, WIDTH * HEIGHT * 4, nullptr, GL_STREAM_READ);
}

void renderScene() {
    // 绑定 MSAA FBO 并渲染场景
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glViewport(0, 0, WIDTH, HEIGHT);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 在这里进行场景的绘制操作

    // 解除绑定 FBO
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void processPixels() {
    // 将 MSAA FBO 中的像素数据读取到 PBO
    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    glReadPixels(0, 0, WIDTH, HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

    // 映射 PBO 到 CPU 可访问的内存
    GLubyte* pixels = (GLubyte*)glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
    if (pixels) {
        // 在这里处理像素数据,例如保存为图像或进行其他操作
        // 为了简单起见,这里只是打印一些像素信息
        for (int i = 0; i < 10; i++) {
            std::cout << "Pixel " << i << ": ("
                      << (int)pixels[4 * i] << ", "
                      << (int)pixels[4 * i + 1] << ", "
                      << (int)pixels[4 * i + 2] << ", "
                      << (int)pixels[4 * i + 3] << ")" << std::endl;
        }

        // 取消映射 PBO
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    }
}

int main() {
    init();

    // 渲染循环
    while (!glfwWindowShouldClose(glfwGetCurrentContext())) {
        renderScene();
        processPixels();

        // 交换缓冲区并处理事件
        glfwSwapBuffers(glfwGetCurrentContext());
        glfwPollEvents();
    }

    // 清理资源
    glDeleteBuffers(1, &pbo);
    glDeleteTextures(1, &colorTexture);
    glDeleteRenderbuffers(1, &depthRBO);
    glDeleteFramebuffers(1, &fbo);
    glfwTerminate();

    return 0;
}

存文件测试

c 复制代码
static void saveImage(GLuint& pbo, const std::string& filename, int width, int height) {

    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);

    // 将帧缓冲区内容读取到 PBO
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

    // 映射 PBO 获取数据指针
    GLubyte* pixels = (GLubyte*)glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
    if (pixels)
    {
        stbi_write_png(filename.c_str(), width, height, 4, pixels, width * 4);
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    }
    else
    {
        std::cerr << "Failed to map." << std::endl;
    }
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
}

如下图所示,存的图像是倒立镜像的,实际上是正立的三角形,还没有将视频附上去,工作量有些大,等到下一个文章再写。

相关推荐
新加坡内哥谈技术11 分钟前
OpenAI发布全新AI模型 o3 与 o3-mini:推理与编码能力迎来重大突破. AGI 来临
大数据·人工智能·语言模型·自然语言处理
bylander15 分钟前
【AI学习】OpenAI推出o3,向AGI迈出关键一步
人工智能·学习·agi
SomeB1oody16 分钟前
获取OpenAI官方给ChatGPT的系统定义Prompt
人工智能·语言模型·chatgpt·prompt
三月七(爱看动漫的程序员)17 分钟前
Knowledge Graph Prompting for Multi-Document Question Answering
人工智能·gpt·学习·语言模型·自然语言处理·机器人·知识图谱
sp_fyf_202419 分钟前
【大语言模型】ACL2024论文-28 TTM-RE: 增强记忆的文档级关系抽取
人工智能·深度学习·机器学习·计算机视觉·语言模型·自然语言处理·数据挖掘
sp_fyf_202423 分钟前
【大语言模型】ACL2024论文-33 Johnny 如何说服大型语言模型越狱:通过人性化 LLMs 重新思考挑战 AI 安全性的说服技巧
人工智能·深度学习·机器学习·语言模型·自然语言处理·数据挖掘
三月七(爱看动漫的程序员)23 分钟前
LEAST-TO-MOST PROMPTING ENABLES COMPLEX REASONING IN LARGE LANGUAGE MODELS---正文
人工智能·gpt·学习·机器学习·语言模型·自然语言处理·llama
XianxinMao23 分钟前
低比特语言模型 是一种利用较少比特数进行语言建模的技术
人工智能
合方圆~小文34 分钟前
工业摄像机基于电荷耦合器件的相机
人工智能·深度学习·数码相机·目标检测
正在走向自律1 小时前
AI Agent案例全解析:百度营销智能体(8/30)
人工智能·百度·ai agent·工作流模式