GStreamer开发笔记(三):测试gstreamer/v4l2+sdl2/v4l2+QtOpengl打摄像头延迟和内存

news2025/5/10 20:55:39

若该文为原创文章,转载请注明原文出处
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/147714800

长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中…

FFmpeg、SDL和流媒体开发专栏


上一篇:《GStreamer开发笔记(二):GStreamer在ubnutn平台部署安装,测试gstreamer/cheese/ffmpeg/fmplayer打摄像头延迟和内存》
下一篇:持续补充中…


前言

  前面测试了多种技术路线,本篇补全剩下的2种主流技术,v4l2+sdl2(偏底层),v4l2+QtOpengl(应用),v4l2+ffmpeg+QtQImage(Image的方式转图低于1ms,但是从yuv格式转到rgb格式需要ffmpeg进行转码耗时)。


Demo

  在这里插入图片描述


注意

  存在色彩空间不准确,不进行细究。


延迟和内存对比

步骤一:v4l2代码测试延迟和内存

  没有找到命令行,只找到了v4l2-ctl可以查看和控制摄像头的参数。
  看gsteamer的源头就是v4l2src,随手写个代码使用v4l2打开摄像头查看延迟,其中v4l2是个框架负责操作和捕获,无法直接进行渲染显示,本次使用了SDL进行显示。
  注意:这里不对v4l2介绍,会有专门的专栏去讲解v4l2的多媒体开发,但是这里使用v4l2的代码写个简单的程序来打开。

sudo apt-get install libsdl2-dev libsdl2-2.0-0

  然后写代码,代码贴在Demo里面
  在这里插入图片描述

  在这里插入图片描述

步骤二:v4l2+QtOpenGL+memcpy复制一次

  在这里插入图片描述

  查看内存:
  在这里插入图片描述

步骤三:v4l2+QtOpenGL+共享内存

  在这里插入图片描述


最终总结

  到这里,我们得出结论,gstreamer基本是最优秀的框架之一了,初步测试不是特别严谨,但是基本能反应情况(比如ffmpeg得fmplay本轮测试是最差,但是ffmpeg写代码可以进行ffmpeg源码和编程代码的优化,达到150ms左右,诸如这类情况不考虑)。
  V4l2+SDL优于gstreamer优于ffmplayer优于v4l2+QtOpenGL优于cheese优于ffmpeg。
  其中v4l2+SDL、gstreamer、fmplayer在内存占用上有点区别,延迟差不多130ms左右。Cheese和v4l2+QtOpenGL延迟差不多
到170ms。Ffmpeg的播放器延迟到500ms左右。


扩展

  这里要注意,大部分低延迟内窥镜笔者接触的都是buffer叠显存的方式,少数厂家使用v4l2+QtOpenGL的方式,经过测试慢了一帧左右。


Demo:V4l2+SDL

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <errno.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_pixels.h>

#define WIDTH 640
#define HEIGHT 480

int main() {

    setbuf(stdout, NULL);

    int fd;
    struct v4l2_format fmt;
    struct v4l2_requestbuffers req;
    struct v4l2_buffer buf;
    void *buffer_start;
    unsigned int buffer_length;

    // 打开摄像头设备
    fd = open("/dev/video0", O_RDWR);
    if (fd == -1) {
        perror("打开摄像头设备失败");
        return EXIT_FAILURE;
    }

    // 设置视频格式
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

    if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
        perror("设置视频格式失败");
        close(fd);
        return EXIT_FAILURE;
    }

    // 请求缓冲区
    memset(&req, 0, sizeof(req));
    req.count = 1;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
        perror("请求缓冲区失败");
        close(fd);
        return EXIT_FAILURE;
    }

    // 映射缓冲区
    memset(&buf, 0, sizeof(buf));
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    buf.index = 0;

    if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
        perror("查询缓冲区失败");
        close(fd);
        return EXIT_FAILURE;
    }

    buffer_length = buf.length;
    buffer_start = mmap(NULL, buffer_length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
    if (buffer_start == MAP_FAILED) {
        perror("映射缓冲区失败");
        close(fd);
        return EXIT_FAILURE;
    }

    // 将缓冲区放入队列
    if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
        perror("缓冲区入队失败");
        munmap(buffer_start, buffer_length);
        close(fd);
        return EXIT_FAILURE;
    }

    // 开始视频捕获
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) {
        perror("开始视频捕获失败");
        munmap(buffer_start, buffer_length);
        close(fd);
        return EXIT_FAILURE;
    }

    // 初始化 SDL
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "SDL 初始化失败: %s\n", SDL_GetError());
        munmap(buffer_start, buffer_length);
        close(fd);
        return EXIT_FAILURE;
    }

    SDL_Window *window = SDL_CreateWindow("V4L2 Camera", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, 0);
    if (!window) {
        fprintf(stderr, "创建 SDL 窗口失败: %s\n", SDL_GetError());
        SDL_Quit();
        munmap(buffer_start, buffer_length);
        close(fd);
        return EXIT_FAILURE;
    }

    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0);

    // SDL_PIXELFORMAT_YV12 =      /**< Planar mode: Y + V + U  (3 planes) */
    // SDL_PIXELFORMAT_IYUV =      /**< Planar mode: Y + U + V  (3 planes) */
    // SDL_PIXELFORMAT_YUY2 =      /**< Packed mode: Y0+U0+Y1+V0 (1 plane) */
    // SDL_PIXELFORMAT_UYVY =      /**< Packed mode: U0+Y0+V0+Y1 (1 plane) */
    // SDL_PIXELFORMAT_YVYU =      /**< Packed mode: Y0+V0+Y1+U0 (1 plane) */

//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YUY2, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_UYVY, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YVYU, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);

    int running = 1;
    SDL_Event event;
    while (running) {

        // 处理事件
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = 0;
            }
        }

        // 捕获帧
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
            perror("出队缓冲区失败");
            break;
        }

        // 更新 SDL 纹理
        SDL_UpdateTexture(texture, NULL, buffer_start, WIDTH);

        // 渲染纹理
        SDL_RenderClear(renderer);
        SDL_RenderCopy(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);

        // 将缓冲区重新入队
        if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
            perror("缓冲区入队失败");
            break;
        }
    }

    // 清理资源
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    munmap(buffer_start, buffer_length);
    close(fd);

    return EXIT_SUCCESS;
}

Demo:V4l2+QtOpenGL+共享内存

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <errno.h>
#include "DisplayOpenGLWidget.h"
#include <QApplication>
#include <QElapsedTimer>

#define WIDTH 640
#define HEIGHT 480

#include <QDebug>
#include <QDateTime>
//#define LOG qDebug()<<__FILE__<<__LINE__
//#define LOG qDebug()<<__FILE__<<__LINE__<<__FUNCTION__
//#define LOG qDebug()<<__FILE__<<__LINE__<<QThread()::currentThread()
//#define LOG qDebug()<<__FILE__<<__LINE__<<QDateTime::currentDateTime().toString("yyyy-MM-dd")
#define LOG qDebug()<<__FILE__<<__LINE__<<QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz")


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    DisplayOpenGLWidget displayOpenGLWidget;
    displayOpenGLWidget.show();
    setbuf(stdout, NULL);

    int fd;
    struct v4l2_format fmt;
    struct v4l2_requestbuffers req;
    struct v4l2_buffer buf;
    void *buffer_start;
    unsigned int buffer_length;

    // 打开摄像头设备
    fd = open("/dev/video0", O_RDWR);
    if (fd == -1) {
        perror("打开摄像头设备失败");
        return EXIT_FAILURE;
    }

    // 设置视频格式
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

    if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
        perror("设置视频格式失败");
        close(fd);
        return EXIT_FAILURE;
    }

    // 请求缓冲区
    memset(&req, 0, sizeof(req));
    req.count = 1;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
        perror("请求缓冲区失败");
        close(fd);
        return EXIT_FAILURE;
    }

    // 映射缓冲区
    memset(&buf, 0, sizeof(buf));
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    buf.index = 0;

    if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
        perror("查询缓冲区失败");
        close(fd);
        return EXIT_FAILURE;
    }

    buffer_length = buf.length;
    buffer_start = mmap(NULL, buffer_length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
    if (buffer_start == MAP_FAILED) {
        perror("映射缓冲区失败");
        close(fd);
        return EXIT_FAILURE;
    }
    displayOpenGLWidget.initDrawBuffer(WIDTH, HEIGHT, true, (char *)buffer_start);

    // 将缓冲区放入队列
    if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
        perror("缓冲区入队失败");
        munmap(buffer_start, buffer_length);
        close(fd);
        return EXIT_FAILURE;
    }

    // 开始视频捕获
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) {
        perror("开始视频捕获失败");
        munmap(buffer_start, buffer_length);
        close(fd);
        return EXIT_FAILURE;
    }

    int running = 1;
    while (running) {
        // 捕获帧
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
            perror("出队缓冲区失败");
            break;
        }
        // 渲染
//        memcpy(drawBuffer, buffer_start, buffer_length);

        displayOpenGLWidget.displayVideoFrame();
        QApplication::processEvents();
        QApplication::processEvents();
        QApplication::processEvents();

        // 将缓冲区重新入队
        if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
            perror("缓冲区入队失败");
            break;
        }

    }

    // 清理资源
    munmap(buffer_start, buffer_length);
    close(fd);

    return EXIT_SUCCESS;
}

入坑

入坑一:v4l2打开视频代码不对

问题

  V4l2打开视频代码数据错位
  在这里插入图片描述

原因

  纹理格式不同,但是笔者测试了SDL所有支持的,都不行,不钻了,是需要进行色彩空间转换下才可以(会额外消耗一定延迟,预估10ms以内),我们选个可以的,测试延迟内存即可。

// SDL_PIXELFORMAT_YV12 =      /**< Planar mode: Y + V + U  (3 planes) */
// SDL_PIXELFORMAT_IYUV =      /**< Planar mode: Y + U + V  (3 planes) */
// SDL_PIXELFORMAT_YUY2 =      /**< Packed mode: Y0+U0+Y1+V0 (1 plane) */
// SDL_PIXELFORMAT_UYVY =      /**< Packed mode: U0+Y0+V0+Y1 (1 plane) */
// SDL_PIXELFORMAT_YVYU =      /**< Packed mode: Y0+V0+Y1+U0 (1 plane) */

//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YUY2, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_UYVY, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
//    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YVYU, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);

解决

  不解决,选择能看清楚的就好了。


上一篇:《GStreamer开发笔记(二):GStreamer在ubnutn平台部署安装,测试gstreamer/cheese/ffmpeg/fmplayer打摄像头延迟和内存》
下一篇:持续补充中…


本文章博客地址:https://blog.csdn.net/qq21497936/article/details/147714800

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

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

相关文章

科技成果鉴定测试有哪些内容?又有什么作用?

科技成果鉴定测试是评价科技成果质量和水平的方法之一&#xff0c;通过测试&#xff0c;可以对科技成果的技术优劣进行评估&#xff0c;从而为科技创新提供参考和指导。 一、科技成果鉴定测试的内容   1.技术评审&#xff1a;通过技术专家对项目进行详细的技术分析&#xff…

基于Spring Boot + Vue 项目中引入deepseek方法

准备工作 在开始调用 DeepSeek API 之前&#xff0c;你需要完成以下准备工作&#xff1a; 1.访问 DeepSeek 官网&#xff0c;注册一个账号。 2.获取 API 密钥&#xff1a;登录 DeepSeek 平台&#xff0c;进入 API 管理 页面。创建一个新的 API 密钥&#xff08;API Key&#x…

A2A与MCP定义下,User,Agent,api(tool)间的交互流程图

官方图&#xff1a; 流程图&#xff1a; #mermaid-svg-2smjE8VYydjtLH0p {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-2smjE8VYydjtLH0p .error-icon{fill:#552222;}#mermaid-svg-2smjE8VYydjtLH0p .error-tex…

蓝桥杯2025年第十六届省赛真题-水质检测

C语言代码&#xff1a; #include <stdio.h> #include <string.h>#define MAX_LEN 1000000int main() {char a[MAX_LEN 1], b[MAX_LEN 1];// 使用 scanf 读取字符数组scanf("%s", a);scanf("%s", b);int ans 0;int pre -1;int state -1;i…

[Windows] 东芝存储诊断工具1.30.8920(20170601)

[Windows] 东芝存储诊断工具 链接&#xff1a;https://pan.xunlei.com/s/VOPpMjGdWZOLceIjxLNiIsIEA1?pwduute# 适用型号 东芝消费类存储产品&#xff1a; 外置硬盘&#xff1a;Canvio 系列 内置硬盘&#xff1a;HDW****&#xff08;E300 / N300 / P300 / S300 / V300 / X30…

Linux网络编程day7 线程池and UDP

线程池 typedef struct{void*(*function)(void*); //函数指针&#xff0c;回调函数void*arg; //上面函数的参数 }threadpool_task_t; //各子线程任务的结构体/*描述线程池相关信息*/struct threadpool_t{pthread_mutex_t lock; …

ABB电机保护单元通过Profibus DP主站转Modbus TCP网关实现上位机通讯

ABB电机保护单元通过Profibus DP主站转Modbus TCP网关实现上位机通讯 在工业自动化领域&#xff0c;设备之间的通信至关重要。Profibus DP是一种广泛应用的现场总线标准&#xff0c;而Modbus TCP则是一种基于以太网的常见通信协议。将Profibus DP主站转换为Modbus TCP网关&…

迪士尼机器人BD-X 概况

这些机器人代表着迪士尼故事叙述与非凡创新的完美结合。它们不仅栩栩如生&#xff0c;还配备了先进的技术。 -迪士尼幻想工程研发部高级副总裁凯尔劳克林 幕景 BDX 机器人是由华特迪士尼公司的研究和幻想工程部门利用NVIDIA人工智能技术 (AI)开发的现实世界机器人&#xff0c;…

UE5骨骼插槽蓝图

首先在人物骨骼处添加插槽并命名&#xff0c;然后再选择添加预览资产把你要的模型&#xff08;静态网格体&#xff09;放上去。 选择绑定的骨骼再去右边相对位置、旋转等调整物体。 再去人物蓝图里面写就ok了

绘制拖拽html

<!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8" /> <meta name"viewport" content"widthdevice-width, initial-scale1" /> <title>拖拽绘制矩形框 - 可移动可调整大小</ti…

ggplot2 | GO barplot with gene list

1. 效果图 2. 代码 数据是GO的输出结果&#xff0c;本文使用的是 metascape 输出的excel挑选的若干行。 # 1. 读取数据 datread.csv("E:\\research\\scPolyA-seq2\\GO-APA-Timepoint\\test.csv", sep"\t") head(dat)# 2. 选择所需要的列 dat.usedat[, c(…

系统思考:短期困境与长期收益

最近在项目中&#xff0c;一直有学员会提到一个议题&#xff0c;如何平衡当前困境和长期收益&#xff1f; 我的思考是在商业和人生的路上&#xff0c;我们常常听到“鱼和熊掌不可兼得”的说法&#xff0c;似乎短期利益和长期目标注定是对立的。但事实上&#xff0c;鱼与熊掌是…

Cjson格式解析与接入AI大模型

JSON格式的解析与构造 基本概念 JSON是JavaScript Object Notation的简称&#xff0c;中文含义为“JavaScript 对象表示法”&#xff0c;它是一种数据交换的文本格式&#xff0c;而不是一种编程语言。 JSON 是一种轻量级的数据交换格式&#xff0c;采用完全独立于编程语言的…

基于英特尔 RealSense D455 结构光相机实现裂缝尺寸以及深度测量

目录 一&#xff0c;相机参数规格 二&#xff0c;结合YOLO实例分割实现裂缝尺寸以及深度测量 2.1 应用场景 2.2 实现流程 2.3 效果展示 2.4 精度验证 2.5 实物裂缝尺寸以及深度测量效果展示 一&#xff0c;相机参数规格 英特尔 RealSense D455 是英特尔 RealSense D400 系…

Nacos源码—7.Nacos升级gRPC分析四

大纲 5.服务变动时如何通知订阅的客户端 6.微服务实例信息如何同步集群节点 6.微服务实例信息如何同步集群节点 (1)服务端处理服务注册时会发布一个ClientChangedEvent事件 (2)ClientChangedEvent事件的处理源码 (3)集群节点处理数据同步请求的源码 (1)服务端处理服务注册…

TIME - MoE 模型代码 3.2——Time-MoE-main/time_moe/datasets/time_moe_dataset.py

源码&#xff1a;GitHub - Time-MoE/Time-MoE: [ICLR 2025 Spotlight] Official implementation of "Time-MoE: Billion-Scale Time Series Foundation Models with Mixture of Experts" 这段代码定义了一个用于时间序列数据处理的 TimeMoEDataset 类&#xff0c;支…

【某OTA网站】phantom-token 1004

新版1004 phantom-token 请求头中包含phantom-token 定位到 window.signature 熟悉的vmp 和xhs一样 最新环境检测点 最新检测 canvas 下的 toDataURL方法较严 过程中 会用setAttribute给canvas 设置width height 从而使toDataURL返回不同的值 如果写死toDataURL的返回值…

OrangePi Zero 3学习笔记(Android篇)2 - 第一个C程序

目录 1. 创建项目文件夹 2. 创建c/cpp文件 3. 创建Android.mk/Android.bp文件 3.1 Android.mk 3.2 Android.bp 4. 编译 5. adb push 6. 打包到image中 在AOSP里面添加一个C或C程序&#xff0c;这个程序在Android中需要通过shell的方式运行。 1. 创建项目文件夹 首先需…

DeepResearch深度搜索实现方法调研

DeepResearch深度搜索实现方法调研 Deep Research 有三个核心能力 能力一&#xff1a;自主规划解决问题的搜索路径&#xff08;生成子问题&#xff0c;queries&#xff0c;检索&#xff09;能力二&#xff1a;在探索路径时动态调整搜索方向&#xff08;刘亦菲最好的一部电影是…

【论文阅读】基于客户端数据子空间主角度的聚类联邦学习分布相似性高效识别

Efficient distribution similarity identification in clustered federated learning via principal angles between client data subspaces -- 基于客户端数据子空间主角度的聚类联邦学习分布相似性高效识别 论文来源TLDR背景与问题两个子空间之间的主角&#xff08;Principa…