当前位置: 首页 > article >正文

协议-ACLLite-ffmpeg

是什么?

  • FFmpeg是一个开源的多媒体处理工具包,它集成了多种功能,包括音视频的录制、转换和流式传输处理。
  • FFmpeg由一系列的库和工具组成,其中最核心的是libavcodec和libavformat库。
    • libavcodec是一个领先的音频/视频编解码器库,支持多种音频和视频格式的编码和解码操作。
    • libavformat库则用于处理各种不同的多媒体容器格式,如MP4、AVI、MKV等。它能够解析提取其中的音频和视频流,以便进行进一步的处理

为什么?

为什么有GStreamer的前提下,还要使用FFmpeg?

  • 在某些情况下,GStreamer 会比 FFmpeg 更适合特定的需求
    • FFmpeg 是一个功能强大的单体框架,主要专注于媒体的编解码、转码和流处理
    • GStreamer适合构建复杂的媒体处理管道,如视频会议、实时流媒体处理和视频编辑
功能FFmpegGStreamer
核心架构Monolithic模块化,基于管道线
易用性使用 CLI 更容易完成简单的媒体任务需要更多设置,但管道灵活
编解码器支持广泛的编解码器支持,几乎涵盖所有编解码器功能强大,但通常需要额外的插件
使用案例媒体转换、流媒体转码、实时流媒体、媒体应用
模块化模块化程度低,围绕特定命令构建模块化程度高,可定制组件
插件系统有限的插件系统,主要集中于编解码器广泛的插件系统,用于自定义处理
实时处理可以,但不直观专为实时流媒体和处理而设计
跨平台是(Linux、Windows、macOS 等)是(Linux、Windows、macOS 等)
复杂性对于简单的任务来说更简单对于复杂的自定义媒体工作流程来说更好
许可LGPL 或 GPL(取决于配置)LGPL
开发者社区庞大、活跃、被广泛采用活跃,但规模小于 FFmpeg

怎么做?

  • 官网Download FFmpeg,选择安装包Windows builds from gyan.dev
  • 找到release bulids部分,选择 ffmpeg-release-essentials.zip
  • 解压文件并检查目录结构
  • 配置环境变量 并检验 ffmpeg -version

核心本质

本质就四个名词

  • Demuxer:拆解多媒体文件,提取音频和视频流。
  • Decoder:将编码后的音频或视频数据解码为原始数据。
  • Encoder:将原始音频或视频数据编码为特定格式。
  • Muxer:将音频和视频流重新封装为多媒体文件。

![[Pasted image 20240704090349.png]]

  • Demuxer:拆解多媒体文件,提取音频和视频流。

![[Pasted image 20250206212037.png]]

  • 解码器接收音频、视频、 或字幕基本流,并将它们解码为原始帧( 视频的像素,音频的 PCM)

![[Pasted image 20250206212045.png]]

  • 编码器接收原始音频、视频或字幕帧并进行编码 它们被编码为数据包

![[Pasted image 20250206212106.png]]

  • Muxer:将音频和视频流重新封装为多媒体文件

![[Pasted image 20250206212115.png]]


大局观总览

  • 库总览

![[Pasted image 20240704161618.png]]

真正有用三库

  • libavcodec库包含多种音频、视频和字幕流的解码器和编码器,以及多种位流过滤器。
  • libavformat库用于将音频、视频和字幕流多路复用和解复用
  • libavfilter库用于处理音频/视频数据,例如进行视频的缩放、裁剪、旋转,音频的混音、音量调整

辅助类型库

  • libavutil包含了一些安全的、可移植的字符串函数、随机数生成器、数据结构、额外的数学函数、密码学和与多媒体相关的功能(如枚举像素和采样格式)

  • libswscale是一个用于图像缩放和颜色空间以及像素格式转换的高效库。

  • libswresample是一个高度优化的音频重采样、重矩阵和采样格式转换库。

  • libavdevice是一个通用的框架,用于抓取和渲染许多常见的多媒体输入/输出设备。

  • 命令总览

![[Pasted image 20240704161704.png]]

  • 功能总览

![[Pasted image 20240704161807.png]]

转换格式流程图

  • 输入文件拆解多媒体文件,提取音频和视频流 ;将编码后的音频或视频数据解码为原始数据;将原始音频或视频数据编码为特定格式;将音频和视频流重新封装为多媒体文件;

![[Pasted image 20250206170140.png]]

ffmpeg -i INPUT.mkv -map 0:v -map 0:a -c:v libx264 -c:a copy OUTPUT.mp4

简单的 filtergraph

  • 将编码后的音频或视频数据解码为原始数据;并对原始数据进行处理;将原始音频或视频数据编码为特定格式;

![[Pasted image 20250206170159.png]]

复杂 filtergraph 它可能有多个输入,多个输出,可能是不同类型的(音频或 video)

  • 第二个输入的帧将叠加在来自第一个输入的帧上。第三个 input 被重新缩放,然后被复制到两个相同的流中。其中之一 它们叠加在组合的前两个输入上,显示为 FilterGraph 的第一个输出。另一个是 filterGraph 的第二个输出

![[Pasted image 20250206172117.png]]


基本命令

ffmpeg -y -c:a libfdk_aac -c:v libx264 -i input.mp4 -c:v libvpx-vp9 -c:a libvorbis output.webm 
  • -y:不经过确认,输出时直接覆盖同名文件
  • -c:指定编码器
    • -c copy:直接复制,不经过重新编码(这样比较快)
    • -c:v:指定视频编码器
    • -c:a:指定音频编码器
  • -i:指定输入文件
  • -an:去除音频流
  • -vn: 去除视频流
  • -preset:指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow。
  • -f:fmt指定格式(音频或视频格式)
  • -t:duration 记录时长为t
  • -acodec: 音频选项, 一般后面加copy表示拷贝
  • -vcodec:视频选项,一般后面加copy表示拷贝
  • h264: 表示输出的是h264的视频裸流
  • mp4: 表示输出的是mp4的视频
  • mpegts: 表示ts视频流
ffmpeg -r 1 -i racing.mp4 -c:v libx264 -profile:v high -level:v 5.1 -c:a copy -r 60 racingoutput.mp4
  • -r 强制输入文件的帧速率(仅对 Raw 格式有效)为 1 fps,并且 输出文件的帧速率为 24 fps
  • -profile:v -level 设置H.264画质级别
ffmpeg -i input.avi output.mp4
  • 通过重新编码媒体流,将输入媒体文件转换为其他格式
ffmpeg -i input.avi -b:v 64k -bufsize 64k output.mp4
  • -b:v 64k 设置输出文件的视频码率为 64 kbit/s

Streamcopy 流复制 -map 的使用

![[Pasted image 20250207162306.png]]

ffmpeg -i INPUT.mkv -map 0:1 -c copy OUTPUT.mp4

![[Pasted image 20250207162209.png]]

ffmpeg -i INPUT0.mkv -i INPUT1.aac -map 0:0 -map 1:0 -c copy OUTPUT.mp4

![[Pasted image 20250207162215.png]]

ffmpeg -i INPUT.mkv -map 0:0 -c copy OUTPUT0.mp4 -map 0:1 -c copy OUTPUT1.mp4

推拉流RTSP命令

UDP推流

ffmpeg -re -i input.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/stream
  • -re 为以流的方式读取

TCP推流

ffmpeg -re -i input.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://127.0.0.1:8554/stream

循环推流

ffmpeg -re -stream_loop -1 -i input.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/stream
  • -stream_loop 为循环读取视频源的次数,-1为无限循环

拉流

ffplay rtsp://127.0.0.1:8554/stream

FFmpeg拉流保存成视频

ffmpeg -stimeout 30000000 -i rtsp://127.0.0.1:8554/stream -c copy output.mp4
  • -stimeout 30000000 为等待RTSP 流连接的时间,单位为us微秒,等待 30 秒如果连接失败则退出

过滤器(fliter)命令

  • 过滤器就是实现某一种视频功能的工具,FFmpeg自带开发了很多种filter用于实现不同的功能

  • 源视频宽度扩大两倍

ffmpeg -i jidu.mp4 -t 10 -vf pad=2 * iw output.mp4
  • 源视频水平翻转
ffmpeg -i jidu.mp4 -t 10 -vf hflip output2.mp4
  • 水平翻转视频覆盖output.mp4
ffmpeg -i output.mp4 -i output2.mp4 -filter_complex overlay=w compare.mp4

核心结构体

  • 围绕解协议,解封装,解码
  • libavcodec 的核心是 AVCodec 和 AVCodecContext

AVIOContext,URLProtocol,URLContext,AVFormatContext

解协议(http,rtsp,rtmp,mms)

  • AVIOContext,URLProtocol,URLContext
    • AVIOContext是FFMPEG管理输入输出数据的结构体,内含指针指向URLContext结构体
    • URLContext结构体中包含结构体URLProtocol
      • URLContext存储音频/视频使用的协议的类型以及状态
      • URLProtocol存储音频/视频使用的协议(rtp,rtmp,file等)操作函数接口

解封装(flv,avi,rmvb,mp4)

  • AVFormatContext存储音频/视频封装包含的数据和信息
  • AVInputFormat表示音频/视频输入格式

![[Pasted image 20250207121801.png]]

AVCodec AVCodecContext AVStream

架构类似于昇腾的device conent steam

![[Pasted image 20250207000401.png]]

解码(h264,mpeg2,aac,mp3)

  • AVCodecContext,存储该音视频流使用解码器的相关数据(所需的上下文环境)
  • AVCodec (该音视频的解码器)(h264 mpeg2 AAC mp3)

![[Pasted image 20250207091506.png]]

  • 实际使用时有可能会有多个 AVCodecContext 关联同一个 AVCodec 的情况。尤其是我们解码音频的时候。

音频文件时 5.1声道的 AVCodec AVCodecContext AVStream关系

  • 通常会对应 3 个 AVStream

    • 左右声道在一个 AVStream
    • 环绕声在一个 AVStream
    • 最后低音在另一个AVStream
  • 3 个AVStream的编码可能是相同的

  • 解码这个音频文件时就应该建立 3 个 AVCodecContext ,分别对应三个 AVStream。然后只需要有 1 个 AVCodec 。每个 AVCodecContext 都利用这一个 AVCodec 来解码。


AVPacket - AVFrame

  • 所谓解码就是把一个 AVPacket 中的数据解成 AVFrame
    • AVPacket是 编码压缩之后的数据
    • AVFrame 是原始的,没有编码、没有压缩的数据 对视频来说是YUV RGB,对音频来说是PCM

![[Pasted image 20250207114701.png]]

I帧 P帧 B帧 的影响

  • 我们会遇到前几个 AVPacket 解不出数据。

    • 到了某个 AVPacket ,可以连续解出多个 AVFrame 来的情况。
    • 这时这多个 AVFrame 就包括前面积压的 AVPacket 里的数据
  • avcodec_send_packet() 调用一次将一个 packet 推给Codec,

  • avcodec_receive_frame() 调用一次或多次来获得 frame


华为昇腾ACLLite库封装ffmpeg案例

什么是华为昇腾ACLLite库?

  • ACLLite库是对CANN提供的ACL接口进行的高阶封装,简化用户调用流程,为用户提供一组简易的公共接口。当前主要针对边缘场景设计

![[Pasted image 20250207153015.png]]

ACLLite\Media\CameraRead.cpp

void CameraRead::DecodeFrameThread(void* decoderSelf)

对照核心结构体 理解 具体解码流程

void CameraRead::DecodeFrameThread(void* decoderSelf)
{
    CameraRead* thisPtr = (CameraRead*)decoderSelf;
    int videoStreamIndex = -1;
    for (int i = 0; i < thisPtr->formatContext_->nb_streams; ++i) {
        if (thisPtr->formatContext_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIndex = i;
            break;
        }
    }
    if (videoStreamIndex == -1) {
        LOG_PRINT("[ERROR] usb camera %s index is -1", thisPtr->streamName_.c_str());
        thisPtr->isOpened_ = false;
        return;
    }
    AVCodecParameters* codecParameters = thisPtr->formatContext_->streams[videoStreamIndex]->codecpar;
    AVCodec* codec = avcodec_find_decoder(codecParameters->codec_id);
    if (codec == nullptr) {
        LOG_PRINT("[ERROR] Could not find ffmpeg decoder.");
        thisPtr->isOpened_ = false;
        return;
    }
    AVCodecContext* codecContext = avcodec_alloc_context3(codec);
    if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) {
        LOG_PRINT("[ERROR] Could not create decoder context.");
        thisPtr->isOpened_ = false;
        return;
    }
    if (avcodec_open2(codecContext, codec, nullptr) < 0) {
        LOG_PRINT("[ERROR] Could not open decoder context.");
        thisPtr->isOpened_ = false;
        return;
    }
    AVFrame* frame = av_frame_alloc();
    AVPacket packet;

    while (av_read_frame(thisPtr->formatContext_, &packet) >= 0 && !thisPtr->isStop_) {
        if (packet.stream_index == videoStreamIndex) {
            int response = avcodec_send_packet(codecContext, &packet);
            if (response < 0 || response == AVERROR(EAGAIN)) {
                continue;
            }
            while (response >= 0) {
                response = avcodec_receive_frame(codecContext, frame);
                if (response == AVERROR(EAGAIN)) {
                    break;
                } else if (response < 0) {
                    LOG_PRINT("[ERROR] Receive false frame from ffmpeg.");
                    thisPtr->isOpened_ = false;
                    return;
                }
                bool ret = thisPtr->SendFrame(packet.data, packet.size);
                if (!ret) {
                    thisPtr->isOpened_ = false;
                    LOG_PRINT("[ERROR] Send single frame from ffmpeg failed.");
                    return;
                }
            }
        }
        av_packet_unref(&packet);
    }
    av_frame_free(&frame);
    avcodec_close(codecContext);
    avformat_close_input(&thisPtr->formatContext_);
    thisPtr->isOpened_ = false;
    return;
}
  • avcodec_find_decoder 根据编解码参数的编解码器ID查找对应的编解码器
  • avcodec_alloc_context3 分配编解码器上下文
  • avcodec_open2 打开编解码器上下文
  • av_frame_alloc 分配帧结构体
  • av_read_frame 从输入文件中读取一个数据包(AVPacket),并将其存储到 packet 中
  • avcodec_send_packet 将一个数据包发送到解码器
  • avcodec_receive_frame 从解码器中接收一个解码后的帧
  • SendFrame 用于将解码后的帧发送到其他地方(例如显示或进一步处理)
  • av_frame_free avcodec_close avformat_close_input 最后释放资源

ACLLite\DVPPLite\src\VideoRead.cpp

void FFmpegDecoder::Decode(FrameProcessCallBack callback,void *callbackParam)

对照核心结构体 理解 具体解码流程

void FFmpegDecoder::Decode(FrameProcessCallBack callback,
                           void *callbackParam)
{
    LOG_PRINT("[INFO] Start ffmpeg decode video %s ...", streamName_.c_str());
    avformat_network_init(); // init network

    AVFormatContext* avFormatContext = avformat_alloc_context();

    // check open video result
    if (!OpenVideo(avFormatContext)) {
        return;
    }

    int videoIndex = GetVideoIndex(avFormatContext);
    if (videoIndex == kInvalidVideoIndex) { // check video index is valid
        LOG_PRINT("[ERROR] Rtsp %s index is -1", streamName_.c_str());
        return;
    }

    AVBSFContext* bsfCtx = nullptr;
    // check initialize video parameters result
    if (!InitVideoParams(videoIndex, avFormatContext, bsfCtx)) {
        return;
    }

    LOG_PRINT("[INFO] Start decode frame of video %s ...", streamName_.c_str());

    AVPacket avPacket;
    int processOk = true;
    // loop to get every frame from video stream
    while ((av_read_frame(avFormatContext, &avPacket) == 0) && processOk && !isStop_) {
        if (avPacket.stream_index == videoIndex) { // check current stream is video
          // send video packet to ffmpeg
            if (av_bsf_send_packet(bsfCtx, &avPacket)) {
                LOG_PRINT("[ERROR] Fail to call av_bsf_send_packet, channel id:%s",
                    streamName_.c_str());
            }

            // receive single frame from ffmpeg
            while ((av_bsf_receive_packet(bsfCtx, &avPacket) == 0) && !isStop_) {
                int ret = callback(callbackParam, avPacket.data, avPacket.size);
                if (ret != 0) {
                    processOk = false;
                    break;
                }
            }
        }
        av_packet_unref(&avPacket);
    }

    av_bsf_free(&bsfCtx); // free AVBSFContext pointer
    avformat_close_input(&avFormatContext); // close input video

    isFinished_ = true;
    LOG_PRINT("[INFO] Ffmpeg decoder %s finished", streamName_.c_str());
}

  • av_read_frame 从输入文件中读取一个数据包(AVPacket)
  • av_bsf_send_packet 数据包发送到解码器
  • av_bsf_receive_packet 从解码器中接收解码后的帧
  • callback 进一步处理
  • av_packet_unref av_bsf_free avformat_close_input 最后释放资源

根据对应类型初始化过滤器

void FFmpegDecoder::InitVideoStreamFilter(const AVBitStreamFilter*& videoFilter)
{
    if (videoType_ == AV_CODEC_ID_H264) { // check video type is h264
        videoFilter = av_bsf_get_by_name("h264_mp4toannexb"); // 目的是从Avcodec库中获取一个名为"h264_mp4toannexb"的视频过滤器。
    } else { // the video type is h265
        videoFilter = av_bsf_get_by_name("hevc_mp4toannexb");
    }
}

附录-h264基础概念

H264

![[Pasted image 20250206183105.png]]

为什么诞生

  • ⼀段分辨率为 1920 * 1080,每个像素点为 RGB 占⽤3 个字节,帧率是 25 的视频,对于传输带宽的要求是

  • 换成 bps 则意味着视频每秒带宽为 1186.523Mbps,这样的速率对于⽹络存储是不可接受的

  • H264 采⽤了 16 * 16 的分块⼤⼩对,视频帧图像进⾏相似⽐较和压缩编码

![[Pasted image 20250206184128.png]]

  • 一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了

![[Pasted image 20250206184103.png]]

H264有两种封装

  • H.264码流分Annex-B和mp4两种格式。

  • ⼀种是 annexb 模式,传统模式

  • ⼀种是 mp4 模式,⼀般 mp4 mkv 都是 mp4 模式

  • 很多解码器只⽀持 annexb 这种模式,因此需要将 mp4 做转换:

  • 在 ffmpeg 中⽤h264_mp4toannexb_filter 可以做转换

ffmpeg -i INPUT.mp4 -codec copy -bsf:v h264_mp4toannexb OUTPUT.ts
  • -bsf:v h264_mp4toannexb:指定视频过滤器为h264_mp4toannexb,这个过滤器的作用是将H.264流从长度前缀模式转换为开始代码前缀模式。

H.264有四种画质级别,分别是baseline, extended, main, high:

  • Baseline Profile:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;
  • Extended profile:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;(用的少)
  • Main profile:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced), 也支持CAVLC 和CABAC 的支持;
  • High profile:高级画质。在main Profile 的基础上增加了8x8内部预测、自定义量化、 无损视频编码和更多的YUV 格式;
ffmpeg -i input.mp4 -profile:v baseline -level 3.0 output.mp4
ffmpeg -i input.mp4 -profile:v main -level 4.2 output.mp4
ffmpeg -i input.mp4 -profile:v high -level 5.1 output.mp4

GOP - I帧 P帧 B帧

  • 编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures )

  • 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示

  • GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成

  • I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)

简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。

![[Pasted image 20250206174901.png]]

  • I帧表示关键帧,你可以理解为经过适度地压缩这一帧画面的完整保留
  • P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别

![[Pasted image 20250206173959.png]]

  • B帧记录的是本帧与前后帧的差别
  • B帧传送的是它与前面的I帧或P帧和后面的P帧之间的预测误差及运动矢量

![[Pasted image 20250206174047.png]]

有了 I帧,P帧, 为什么需要B帧

  • 因为B帧记录的是前后帧的差别,比P帧能节约更多的空间

IDR 图像(立即刷新图像

  • 一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像
  • H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,
    • 将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列
  • 如果前一个序列出现重大错误,在这里可以获得重新同步的机会
    • IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码

![[Pasted image 20250206180211.png]]

DTS PTS

为什么会有PTS和DTS的概念

  • P帧需要参考前面的I帧或P帧才可以生成一张完整的图片

  • B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片

  • 这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?

PTS和DTS的概念

  • DTS(Decoding Time Stamp):即解码时间戳
    • 这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
  • PTS(Presentation Time Stamp):即显示时间戳
    • 这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

[Pasted image 20250206175718.png]]![在这里插入图片描述

文档链接说明

  • 官方文档
    Documentation (ffmpeg.org)

  • 基本概念
    I帧、P帧、B帧、GOP、IDR 和PTS, DTS之间的关系 - 夜行过客 - 博客园

  • 参考文档
    音视频八股文 – h264 AnnexB_音视频开发面试八股文-CSDN博客

  • 参考文档
    FFmpeg基础知识之-—— H264编码profile & level控制_ffmpeg level-CSDN博客

  • 参考文档
    基于 FFMPEG 的视频解码(libavcodec ,致敬雷霄骅)-CSDN博客

  • 参考文档
    ffmpeg 结构体之间的关系_packet解码frame之间的对应-CSDN博客
    FFMPEG结构体分析:AVIOContext-CSDN博客

  • 参考文档
    ffmpeg 常用命令汇总_ffmpeg命令大全-CSDN博客
    基于FFmpeg进行rtsp推流及拉流(详细教程)_ffmpeg rtsp推流-CSDN博客

  • 华为昇腾ACLLite仓库
    Ascend/ACLLite

  • 华为昇腾对照概念
    AscendCL架构及基本概念-AscendCL应用开发概述-AscendCL应用开发(C&C++)-应用开发-开发指南-CANN社区版8.0.0.alpha003开发文档-昇腾社区


http://www.kler.cn/a/540665.html

相关文章:

  • 【JVM详解二】常量池
  • Linux下Gufw防火墙安装指南
  • 海量表格文字识别、PHP表格识别接口:从传统到智能
  • 数据集成实例分享:金蝶云星空对接旺店通实现库存管理自动化
  • 【Java基础】为什么不支持多重继承?方法重载和方法重写之间区别、Exception 和 Error 区别?
  • C++ 中信号转异常机制:在磁盘 I/O 内存映射场景下的应用与解析
  • C++ STL算法总结
  • salesforce 中 Account 转移给新 Owner 后如何仅转移 Case,而不转移 Opportunity
  • 怎么编写AI模型prompt(提问,表达需求)
  • ZooKeeper Watcher 机制详解:从注册到回调的全过程
  • Vue07
  • vi 是 Unix 和 Linux 系统中常用的文本编辑器
  • 易仓与金蝶云星空无缝集成:实现高效R调拨入库
  • 如何在浏览器中搭建开源Web操作系统Puter的本地与远程环境
  • Python 高阶函数(详解)
  • 主机安全:数字时代的基石
  • harmonyOS的路由跳转及数据请求
  • UNet-二维全景X射线图像牙齿分割(代码和模型修改)
  • DeepSeek神经网络:技术架构与实现原理探析
  • Harmony os router 使用详解
  • 基于UVM搭验证环境
  • 代码随想录_二叉树
  • 【多模态大模型】系列4:目标检测(ViLD、GLIP)
  • 因果推断与机器学习—特定领域的机器学习
  • 如何在 CSS Modules 中使用 Sass 或 Less?
  • stm32 deinit 函数作用