ffplay源码分析(五)包缓存队列和帧缓存队列

news2024/9/22 4:12:20

在音视频处理流程中,ffplay的有两种队列,包缓存队列(Packet Buffer Queue)和帧缓存队列(Frame Buffer Queue)。这两个队列的存在,是为了适应音视频数据处理过程中的多线程架构——包括收包线程、解码线程和渲染线程。具体来说,收包线程负责从网络或文件中读取数据并将其放入包缓存队列中;解码线程从包缓存队列中取出数据进行解码,然后将解码后的数据放入帧缓存队列中;最后,渲染线程从帧缓存队列中取出数据进行渲染。由于每个线程的处理速度不同,缓存队列在这一过程中起到了平衡各线程工作负荷和避免数据丢失的关键作用。

音频、视频和字幕都经历了类似的处理流程,因此设计出高效且适应音视频特性的缓存队列显得尤为重要。ffplay中对于包缓存队列和帧缓存队列的设计不仅确保了音视频数据的流畅处理,还有效地提升了播放体验。这种设计通过合理的缓存策略和线程同步机制,成功地解决了音视频处理中的各种挑战。

一、包缓存队列

包缓存队列的设计需要考虑多个因素,以确保其高效性和稳定性。因为数据包本身通常较小,因此没有必要将缓存队列设计为循环队列,采用常规的入队申请内存和出队释放内存的方式即可。

ffplay中的包缓存队列设计适配了音视频的特性,和普通的队列相比有如下差异

  1. 序列号处理功能

使用serial字段来追踪数据包的顺序,在某些多路流(如音视频同步)场景中非常有用。每次队列重启或刷新时,serial都会递增,有助于区分不同的播放段。

比如说发生跳转时,又解码到了跳转之前的数据,可能会有回跳的现象,

ffplay会在发生跳转的时候,更新包的序列号,当解码到老的序列号,就把数据给丢弃掉,直到解码到新的数据。

  1. 自动增长的fifo队列

使用av_fifo_alloc2创建自动增长的FIFO队列,避免了频繁内存分配,提高了性能。

  1. 阻塞和非阻塞设置

阻塞与非阻塞模式packet_queue_get函数通过block参数实现了阻塞和非阻塞模式的灵活切换,使得队列在不同的使用场景下能够适应需求。

1.1 PacketQueue结构体

//MyAVPacketList结构体的作用就是给包加上序列号
typedef struct MyAVPacketList {
    AVPacket *pkt;
    int serial;
} MyAVPacketList;

typedef struct PacketQueue {
    AVFifo *pkt_list;//fifo队列,
    int nb_packets;//packet数量
    int size;//packet大小(字节)
    int64_t duration;//持续时长
    int abort_request;//中断请求
    int serial;//序列号
    SDL_mutex *mutex;//锁
    SDL_cond *cond;//条件变量
} PacketQueue;

1.2 初始化

static int packet_queue_init(PacketQueue *q)
{
    memset(q, 0, sizeof(PacketQueue));
    //
    q->pkt_list = av_fifo_alloc2(1, sizeof(MyAVPacketList), AV_FIFO_FLAG_AUTO_GROW);
    if (!q->pkt_list)
        return AVERROR(ENOMEM);
    q->mutex = SDL_CreateMutex();
    if (!q->mutex) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    q->cond = SDL_CreateCond();
    if (!q->cond) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    q->abort_request = 1;
    return 0;
}

1.3 开始运行

static void packet_queue_start(PacketQueue *q)
{
    SDL_LockMutex(q->mutex);
    q->abort_request = 0;
    q->serial++;
    SDL_UnlockMutex(q->mutex);
}

1.4 放入packet

//内部调用
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList pkt1;
    int ret;
  
    if (q->abort_request)
       return -1;

    //给音视频包加上序列号
    pkt1.pkt = pkt;
    pkt1.serial = q->serial;

    //把包添加进队列
    ret = av_fifo_write(q->pkt_list, &pkt1, 1);
    if (ret < 0)
        return ret;
    //队列相关数据更新
    q->nb_packets++;
    q->size += pkt1.pkt->size + sizeof(pkt1);
    q->duration += pkt1.pkt->duration;
     //发出信号,表明当前队列中有数据了,通知等待中的读线程可以取数据了
    SDL_CondSignal(q->cond);
    return 0;
}


static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    AVPacket *pkt1;
    int ret;

    pkt1 = av_packet_alloc();
    if (!pkt1) {
        av_packet_unref(pkt);
        return -1;
    }
    //将pkt的内存转移到pkt1里,这样可以减少一次内存的拷贝。
    av_packet_move_ref(pkt1, pkt);

    //保护队列的线程安全
    SDL_LockMutex(q->mutex);
    ret = packet_queue_put_private(q, pkt1);
    SDL_UnlockMutex(q->mutex);

    if (ret < 0)
        av_packet_free(&pkt1);

    return ret;
}

1.5 获取队列中的数据

/* return < 0 if aborted, 0 if no packet and > 0 if packet.  */
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList pkt1;
    int ret;

    SDL_LockMutex(q->mutex);

    for (;;) {
        if (q->abort_request) {
            ret = -1;
            break;
        }
        //从队列中取出一个包
        if (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0) {
            q->nb_packets--;
            q->size -= pkt1.pkt->size + sizeof(pkt1);
            q->duration -= pkt1.pkt->duration;
            //移动这个包的数据
            av_packet_move_ref(pkt, pkt1.pkt);
            //更新序列号
            if (serial)
                *serial = pkt1.serial;
            //释放这个packet本身
            av_packet_free(&pkt1.pkt);
            ret = 1;
            break;
            //若非阻塞模式,直接返回
        } else if (!block) {
            ret = 0;
            break;
        } else {
            //若阻塞模式,等待数据
            SDL_CondWait(q->cond, q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);
    return ret;
}

1.6 清空队列

static void packet_queue_flush(PacketQueue *q)
{
    MyAVPacketList pkt1;

    SDL_LockMutex(q->mutex);
    while (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0)
        av_packet_free(&pkt1.pkt);
    q->nb_packets = 0;
    q->size = 0;
    q->duration = 0;
    q->serial++;
    SDL_UnlockMutex(q->mutex);
}

1.7 停止队列

static void packet_queue_abort(PacketQueue *q)
{
    SDL_LockMutex(q->mutex);
    q->abort_request = 1;
    //通知处理,避免packet_queue_get没有获取到数据,一直在等待
    SDL_CondSignal(q->cond);
    SDL_UnlockMutex(q->mutex);
}

1.8 销毁数据

static void packet_queue_destroy(PacketQueue *q)
{
    packet_queue_flush(q);
    av_fifo_freep2(&q->pkt_list);
    SDL_DestroyMutex(q->mutex);
    SDL_DestroyCond(q->cond);
}

二、帧缓存队列

帧缓存队列主要功能是用于视频解码器的帧管理,它提供了一个环形缓冲区,用于存储已经解码的帧,同时支持多线程的读写操作。它与包缓存队列类似,使用互斥锁和条件变量来同步访问,确保线程安全。

其设计要点如下,

环形缓冲区

  • 使用固定大小的数组实现环形缓冲区,通过读写索引(rindexwindex)管理帧的存取。

  • 通过模运算确保读写索引在队列范围内循环,提供高效的队列操作。

阻塞/非阻塞操作

  • 提供阻塞式的peek_writablepeek_readable方法,在队列满或空时等待条件满足。这种设计适合生产者-消费者模式,确保帧处理的同步性。

支持保存最后一帧

  • 通过keep_last标志控制是否保留最后一帧,适应不同的使用场景,例如在播放结束时显示最后一帧。

2.1 FrameQueue结构体

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];  // 存储帧的数组,容量为 FRAME_QUEUE_SIZE
    int rindex;                     // 读索引,指向下一个要读取的帧位置
    int windex;                     // 写索引,指向下一个要写入的帧位置
    int size;                       // 当前队列中的帧数量
    int max_size;                   // 队列的最大帧数量
    int keep_last;                  // 标志位,表示是否保留最后一帧以供显示
    int rindex_shown;               // 标志是否已经显示最后读取的帧
    SDL_mutex *mutex;               // 互斥锁,用于同步访问队列
    SDL_cond *cond;                 // 条件变量,用于线程之间的等待和通知
    PacketQueue *pktq;              // 指向关联的 PacketQueue,用于解码的输入数据
} FrameQueue;

2.1 初始化和销毁

/* 初始化 FrameQueue,设置队列大小,创建互斥锁和条件变量,并分配每个帧的内存 */
static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
    int i;
    // 初始化 FrameQueue 结构体
    memset(f, 0, sizeof(FrameQueue));
    
    // 创建互斥锁,用于线程同步
    if (!(f->mutex = SDL_CreateMutex())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM); // 内存分配错误
    }
    
    // 创建条件变量,用于线程间的信号传递
    if (!(f->cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM); // 内存分配错误
    }
    
    // 关联 PacketQueue,后续用于检查是否中止操作
    f->pktq = pktq;
    // 设置队列的最大容量,不能超过预定义的 FRAME_QUEUE_SIZE
    f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
    // f->keep_last = !!keep_last是将int取值的keep_last转换为boot取值(0或1)
    f->keep_last = !!keep_last;
    
    // 为队列中的每个 Frame 分配内存
    for (i = 0; i < f->max_size; i++)
        if (!(f->queue[i].frame = av_frame_alloc()))
            return AVERROR(ENOMEM); // 内存分配失败
    return 0; // 初始化成功
}

/* 销毁 FrameQueue,释放所有资源,包括互斥锁、条件变量和帧的内存 */
static void frame_queue_destroy(FrameQueue *f)
{
    int i;
    // 逐个释放队列中每个 Frame 的内存
    for (i = 0; i < f->max_size; i++) {
        Frame *vp = &f->queue[i];
        frame_queue_unref_item(vp); // 取消引用帧中的数据
        av_frame_free(&vp->frame); // 释放 AVFrame 结构体
    }
    // 销毁互斥锁
    SDL_DestroyMutex(f->mutex);
    // 销毁条件变量
    SDL_DestroyCond(f->cond);
}

2.2 读写帧

2.2.1 环状缓冲区

环状缓冲区是一种数据结构,用于高效管理固定大小的缓冲区,特别适用于需要频繁写入和读取的数据流,如音视频播放。它的核心思想是使用一个固定大小的数组,并使用两个指针(读指针和写指针)来跟踪读写位置。当写指针达到缓冲区末尾时,它会绕回到开始处,继续写入未被读指针消费的位置。

如下是一个生动的例子,蓝色是读指针,红色是写指针,读指针跟在写指针后边追着,
在这里插入图片描述

FrameQueue 中,环状缓冲区通过以下变量和逻辑实现:

  • 缓冲区数组Frame queue[FRAME_QUEUE_SIZE],它是一个定长的数组,保存实际的帧数据。

  • 写入索引 (windex):指示当前可以写入新帧的位置。

  • 读取索引 (rindex):指示当前读取的帧的位置。

  • 队列大小 (size):当前存储的帧的数量。

  • 最大队列大小 (max_size):队列的容量限制。

环状缓冲区的操作逻辑:
  • 写入操作 (frame_queue_push):将新帧写入queue[windex],然后递增windex。如果 windex 达到 max_size,则环绕回 0,即 windex = 0

  • 读取操作 (frame_queue_next):从queue[rindex]读取帧,然后递增rindex。如果 rindex 达到 max_size,同样环绕回 0,即 rindex = 0

这种结构的好处是高效地利用固定大小的数组,无需频繁地分配或释放内存,并能够高效地进行顺序的读取和写入操作。

2.2.2 线程安全

在查看 frame 源码时,我们注意到锁的主要作用是保护状态信息(如 sizerindexwindex 等),而不是直接保护实际的数据内容。这可能引发一个问题:是否会出现同一块内存在边读边写的情况?

答案是不会出现这样的情况,因为 FFplay 的队列设计采用了操作分离的策略。

具体来说,当 frame_queue_peek_writable 获取到一个可写的帧指针后,实际的数据写入操作是由调用者完成的。写入操作完成后,需要调用 frame_queue_push 告知队列写入已经完成,只有在这一步后,写指针 windex 才会被更新。由于队列的这种设计,即在写入操作完成并通知队列之前,读指针 rindex 不会去访问该帧的数据,因此不会出现同时对同一块内存进行读写的情况。

这种操作分离的设计确保了在没有锁保护实际数据的情况下,读写操作依然是安全的,避免了竞态条件的发生。

2.2.3 写数据


/* 获取可写入的新帧,如果队列已满则阻塞直到有空间可写 */
static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* 等待直到有空间可以写入新帧 */
    SDL_LockMutex(f->mutex); // 锁定互斥锁,进入临界区
    while (f->size >= f->max_size && !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex); // 等待条件变量信号
    }
    SDL_UnlockMutex(f->mutex); // 解锁互斥锁,离开临界区

    // 如果接收到中止请求,则返回 NULL
    if (f->pktq->abort_request)
        return NULL;

    // 返回当前可写入的新帧引用
    return &f->queue[f->windex];
}

/* 将写入索引移动到下一位置,并更新队列大小 */
static void frame_queue_push(FrameQueue *f)
{
    if (++f->windex == f->max_size) // 循环更新写索引
        f->windex = 0;
    SDL_LockMutex(f->mutex); // 锁定互斥锁
    f->size++; // 增加队列中帧的数量
    SDL_CondSignal(f->cond); // 发送信号通知可读
    SDL_UnlockMutex(f->mutex); // 解锁互斥锁
}

2.2.4 读数据

读队列引入了一种机制,用于保留已显示的最后一帧,即始终在队列中保留一帧。这样设计的好处是在播放结束时,画面能够停留在最后一帧。如果没有这种机制,就需要额外判断队列中的帧数量是否等于一,这会使代码变得复杂且难以维护。

虽然这个机制使得读队列的设计比写队列复杂得多,但它非常巧妙且实用。这种设计体现了对细节的用心,需要没有kpi压力的程序员才能打磨出来。

keep_last: 表示是否需要保留最后一个已显示的帧。如果设置为 1,则在调用 frame_queue_next 时会保留当前显示的帧,不会立即移动读索引。

rindex_shown: 用于标记当前是否已显示保留的最后一帧。默认是 0,如果设置为 1,表示当前的读索引位置对应的是已经显示的帧,并且该帧被保留用于后续显示。

简单理解,就是如果rindex_shown + rindex 才等于当前帧

/* 获取可读取的帧,如果队列为空则阻塞直到有帧可读 */
static Frame *frame_queue_peek_readable(FrameQueue *f)
{
    /* 等待直到有可读的帧 */
    SDL_LockMutex(f->mutex); // 锁定互斥锁
    //如果rindex_shown为1,说明当前读索引对应的是已读的帧,所以判断可读数量时需要减去这一帧。
    while (f->size - f->rindex_shown <= 0 && !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex); // 等待条件变量
    }
    SDL_UnlockMutex(f->mutex); // 解锁互斥锁

    // 如果接收到中止请求,则返回 NULL
    if (f->pktq->abort_request)
        return NULL;

    // 返回当前可读取的帧引用
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}


/* 获取下一个即将显示的帧 */
static Frame *frame_queue_peek_next(FrameQueue *f)
{
    // 通过计算得出下一个显示帧的索引
    return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}

/* 获取最后一个显示的帧*/
static Frame *frame_queue_peek_last(FrameQueue *f)
{
    // 返回最后一个已显示帧的引用
    return &f->queue[f->rindex];
}

/* 获取当前应该显示的帧*/
static Frame *frame_queue_peek(FrameQueue *f)
{
    //它使用 rindex(当前读取索引)加上 rindex_shown 来确定当前显示的帧。如果 rindex_shown 为 0,表示当前帧就是 rindex 指向的帧;如果 rindex_shown 为 1,则获取下一个帧
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}


/* 将读取索引移动到下一位置,并更新队列大小 */
static void frame_queue_next(FrameQueue *f)
{
    if (f->keep_last && !f->rindex_shown) {
        // 如果设置了 keep_last 并且当前没有显示帧,则保留当前帧,
        //如果满足这两个条件,rindex_shown 被设置为 1,函数直接返回,不移动读索引 rindex,也不减少 size。这样就实现了保留当前帧的功能。
        f->rindex_shown = 1;
        return;
    }
    // 取消引用当前显示的帧数据
    frame_queue_unref_item(&f->queue[f->rindex]);
    if (++f->rindex == f->max_size) // 循环更新读索引
        f->rindex = 0;
    SDL_LockMutex(f->mutex); // 锁定互斥锁
    f->size--; // 减少队列中帧的数量
    SDL_CondSignal(f->cond); // 发送信号通知可写
    SDL_UnlockMutex(f->mutex); // 解锁互斥锁
}

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

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

相关文章

win11+vscode+Flutter 开发环境配置

https://blog.csdn.net/Oven_maizi/article/details/126804404 1 vscode插件 安装 安装红框中的两个 2 flutter sdk 安装 dart sdk 包含在flutter sdk 里面&#xff0c;路径&#xff1a;flutter_windows_3.24.1-stable\flutter\bin\cache\dart-sdk 方式1&#xff1a; 通过…

[001-07-001].Redis7缓存双写一致性之更新策略探讨

1、面试题&#xff1a; 1.只要使用缓存&#xff0c;就可能会涉及到redis缓存与数据库双存储双写&#xff0c;只要是双写&#xff0c;就存在数据一致性问题&#xff0c;那么是如何解决数据一致性问题的2.双写一致性&#xff0c;你先动缓存redis还是数据库MySQL&#xff0c;哪一个…

Python爬虫案例四:爬取某个博主的所有文章保存成PDF格式

引入&#xff08;将图片保存成PDF格式&#xff09;&#xff1a; 测试链接&#xff1a; https://zq.bookan.com.cn/?tdetail&id21088&ct1&is31042341&rid4658&#xff08;图书馆图片保存PDF&#xff09;&#xff0c;前提是装库&#xff0c;pip install img2pdf…

IDEA如何将某个文件夹内的多个module合并到一个大的module内

效果&#xff1a; 初始 方法 Ctrl shift Alt S 打开 project structure 导入module 将这个大文件夹作为新的module导入 效果图 完事儿

C#上位机采用数据库操作方式对Excel或WPS表格进行读取操作

C#采用数据库操作方式对Excel或WPS表格进行读取操作 1、创建连接字符串并编写一个进行数据库操作的方法 public class OleDbHelper{//创建连接字符串private static string connString "ProviderMicrosoft.ACE.OLEDB.12.0;Data Source{0};" "Extended Propert…

如何在态势中嵌入感知?

态势感知是对事物发展的各种情况的估计、判断和理解&#xff0c;而感知是通过感官对事物的认识和了解。要在态势过程中嵌入感知过程&#xff0c;你可以考虑以下步骤&#xff1a; 1、确定态势和感知的要素&#xff1a;首先&#xff0c;明确态势和感知所涉及的关键要素。态势可能…

Lesson08---string类(1)

1. 为什么要学习string类 2. 标准库中的string类 3. string类的模拟实现 4. 扩展阅读 1. 为什么学习string类&#xff1f; 1.1 C语言中的字符串 在c语言中没有string类型只有char类型&#xff0c;char类型只能是一个字符非常的不好用&#xff0c;想要字符串还必须要弄一个c…

华溢艺术之花在社区绽放

华溢艺术之花在社区绽放 这是一个阳光明媚的周末&#xff0c;华溢艺术少儿艺术体验走进了夷陵区营盘社区碧桂园小区。这次活动不仅为社区的孩子们带来了丰富多彩的艺术体验&#xff0c;还让家长们目睹了孩子们在艺术成长道路上那些优美动人的闪光点。 活动现场&#xff0c;热闹…

线性代数|机器学习-P32循环矩阵的特征向量-傅里叶矩阵

文章目录 1. 大纲2. 循环矩阵2.1 移位矩阵P2.2 P的特征值和特征向量2.3 循环卷积矩阵2.4 循环卷积计算 3. 傅里叶矩阵 1. 大纲 循环矩阵在机器学习&#xff0c;图像处理中的应用循环卷积矩阵的特征值&#xff0c;特征向量&#xff0c;卷积规则循环卷积矩阵多项式表达&#xff…

kaggle平台free使用GPU

1、注册 请保证在【科学上网】条件下进入如下操作&#xff0c;只有在注册账户和手机号验证时需要。 step1&#xff1a;注册账户 进入kaggle官网&#xff1a;https://www.kaggle.com/&#xff0c;点击右上角【Register】进入注册页面 最好选择使用邮箱注册&#xff08;&#…

零域(微隔离)详述

一、什么是零信任微隔离&#xff1f; 微隔离的实现方式是将数据中心内部所有的业务按照特定的原则划分为数个微小的网络节点&#xff0c;根据动态策略分析对这些节点执行访问控制&#xff0c;在逻辑上将这些节点隔离开&#xff0c;限制用户横向移动&#xff0c;这就是微…

docker-compose 快速部署nacos-standalone单节点

一、nacos 介绍 官网&#xff1a; https://nacos.io/ 一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台 二、如何使用docker-compose 快速部署nacos2.3.1 ⚠️ &#xff1a; nacos-standalone 部署方式 依赖于 数据库&#xff0c;请先配置好数据库实例&…

日系编曲:了解日系音乐 日系和声特征 设计日系和声 和弦进行摘抄

了解日系音乐 日系音乐风格多样&#xff0c;涵盖流行、摇滚、民谣、古典等多种类型。以下是部分知名的日系音乐作品、歌手及乐队&#xff1a; 作品 《First Love》是宇多田光的代表作之一&#xff0c;旋律悠扬&#xff0c;情感真挚&#xff0c;展现了初恋的美好与青涩&#xf…

c++ 原型模式

文章目录 什么是原型模式为什么要使用原型模式使用场景示例 什么是原型模式 用原型实例指定创建对象的种类&#xff0c;并通过拷贝这些原型创建新的对象&#xff0c;简单理解就是“克隆指定对象” 为什么要使用原型模式 原型模式&#xff08;Prototype Pattern&#xff09;是…

C++学习笔记----6、内存管理(一)---- 使用动态内存(1)

当你使用现代结构&#xff0c;例如std::vector,std::string等等&#xff0c;从一开始到现在以及到未来&#xff0c;C是一个安全的编程语言。该语言提供了许多的道路&#xff0c;路线以及红绿灯&#xff0c;比如C核心指导&#xff0c;静态代码分析器来分析代码的正确性&#xff…

SQLite3 数据库

一、SQLite3 命令 1. 基本命令 .tables&#xff1a;用于查看数据库中的表。.headers on/off&#xff1a;开启或者关闭表头&#xff0c;方便查看表结构。.mode column&#xff1a;使列对齐&#xff0c;增强数据的可读性。.width 列宽1 列宽2&#xff1a;设置每一列的列宽&…

大量图片进行自适应处理

首先我们手里有一个视频&#xff0c;但是我们对视频进行图像处理的话视频很难暂停进行观察&#xff0c;我们可以写一个按键坚挺&#xff0c;但是小编这里介绍另一个办法&#xff0c;大致思路为&#xff0c;把视频进行截帧为图片的集合&#xff0c;再对该集合进行统一的图像处理…

基于UniApp+Springboot花店微信小程序

此花店小程序是多商家&#xff0c;多用户&#xff0c;以及后台管理员操作的花市集小程序系统&#xff0c;前端采用vue 和 UniApp&#xff0c;后端采用java Springboot 设计开发。前后端分离的花店微信小程序系统&#xff0c;数据库采用独立的数据库mysql 小程序端界面主要采用…

yolo数据集钢材表面缺陷v8下载适用yolov5等全版本已标注txt格式

钢材表面缺陷检测数据集介绍 数据集概述 本数据集专为钢材表面缺陷检测任务而设计&#xff0c;包含了大量的钢材表面图像&#xff0c;每张图像均带有详细的缺陷标注信息。数据集旨在帮助研究人员和开发人员训练高精度的目标检测模型&#xff0c;以应用于钢材制造和质量控制等多…

本地化云桌面系统环境VMware horizon搭建

前言&#xff1a;预准备使用空闲服务器&#xff0c;搭建轻办公云桌面环境&#xff0c;类似“华为云桌面FusionAccess”&#xff0c;或天翼云桌面系统。注意云桌面不是桌面云&#xff0c;云桌面的适用场景为团队化云上办公&#xff0c;可私有部署/云化部署。horizon是VMware旗下…