ffmpeg AVFilter介绍
一. ffmpeg AVFilter介绍
利用ffmpeg做图像的pixel format转换你还在用libswscale吗?嘿嘿,过时啦!
ffmpeg中有了新东西:libavfilter.使用它,可以完全代替libswscale,并且可以自动完成一些复杂的转换操作呢.libavfilter啊,用了都说好!但就是太复杂...
如果你仅仅是做图像的pixel format处理,用libswscale是相当简单,可以看看最新的ffplay.c中的代码,被#if CONFIG_AVFILTER #endif包围的代码量非常大,而且让人一上来看得一头雾水,但为了赶潮流,我们还是得学习它啊...
先弄清楚avfilter中的几个相关的概念(注意:如果没有directShow基础的同学看不懂以下解释,请先学DirectShow的基本概念):
1 AVFilterGraph:几乎完全等同与directShow中的fitlerGraph,代表一串连接起来的filter们.
AVFilter:代表一个filter.
AVFilterPad:代表一个filter的输入或输出口,等同于DShow中的Pin.只有输出pad的filter叫source,只有输入pad的tilter叫sink.
AVFilterLink:代表两个连接的fitler之间的粘合物.
其实总体看起来,libavfitler跟DShow几乎一样了.
下面看一下AVFilter是如何被使用的,我们以ffplay.c为例吧,分析一下其中AVFilter相关的代码.
1 产生graph:
AVFilterGraph *graph = avfilter_graph_alloc();
2 创建source
AVFilterContext *filt_src;
avfilter_graph_create_filter(&filt_src, &input_filter, "src",NULL, is, graph);
第一个参数是生成的filter(是一个source),第二个参数是一个AVFilter结构的实例,第三个参数是要创建的fitler的名字,第四个参数是不知道什么用,第五个参数是user data(调用者的私有数据),第六个参数是graph的指针.其中第二个参数的实例必须由调用者自己实现,才能将帧送到graph中.
3 创建sink
AVFilterContext *filt_out;
ret = avfilter_graph_create_filter(&filt_out, avfilter_get_by_name("buffersink"), "out", NULL, pix_fmts, graph);
参数同上,不解释.所创建的这个sink是一个buffersink,可参考libavfitler的源码文件sink_buffer.c看看它是个什么玩意.sink_buffer其实是一个能通过buffer输出帧的sink,当然它的输出不是通过pad,因为它后面没有fitler了.用它做sink,可以让使用这个graph的代码轻松取得graph处理后的帧.
4 连接source和sink
avfilter_link(filt_src, 0, filt_out, 0);
第一个参数是接在前面的filter,第二个参数是前fitler的要连接的pad的序号,第三个参数是后面的filter,第四个参数是后filter的要连接的pad.
4 对graph做最后的检查
avfilter_graph_config(graph, NULL);
我们是从sink中取出处理完成的帧,所以最好把sink的引用保存下来,比如:
AVFilterContext *out_video_filter=filt_out;
6实现input_filter
由于input_filter是个source,所以只为它分配output pad,并且只有一个pad.
- static AVFilter input_filter =
- {
- .name = "ffplay_input",
- .priv_size = sizeof(FilterPriv),
- .init = input_init,
- .uninit = input_uninit,
- .query_formats = input_query_formats,
- .inputs = (AVFilterPad[]) {{ .name = NULL }},
- .outputs = (AVFilterPad[]) {{ .name = "default",
- .type = AVMEDIA_TYPE_VIDEO,
- .request_frame = input_request_frame,
- .config_props = input_config_props, },
- { .name = NULL }},
- };
再实现AVFilter的回调函数们:init()和uninit()--用于初始化/销毁所用到的资源.
看一下ffplay.c中的实现:
- static int input_init(AVFilterContext *ctx, const char *args, void *opaque)
- {
- FilterPriv *priv = ctx->priv;
- AVCodecContext *codec;
- if(!opaque) return -1;
- priv->is = opaque;
- codec = priv->is->video_st->codec;
- codec->opaque = ctx;
- if((codec->codec->capabilities & CODEC_CAP_DR1)) {
- av_assert0(codec->flags & CODEC_FLAG_EMU_EDGE);
- priv->use_dr1 = 1;
- codec->get_buffer = input_get_buffer;
- codec->release_buffer = input_release_buffer;
- codec->reget_buffer = input_reget_buffer;
- codec->thread_safe_callbacks = 1;
- }
- priv->frame = avcodec_alloc_frame();
- return 0;
- }
FilterPriv是ffplay实现的filter(也就是input_filter)的私有数据结构.主要的工作是分配了一个AVFrame,用于保存从设备取得的帧.uninit()更简单,就不用看了.
还需实现output pad的request_frame(),才能使input_filter后面的filter获取到帧
- static int input_request_frame(AVFilterLink *link)
- {
- FilterPriv *priv = link->src->priv;
- AVFilterBufferRef *picref;
- int64_t pts = 0;
- AVPacket pkt;
- int ret;
- while (!(ret = get_video_frame(priv->is, priv->frame, &pts, &pkt)))
- av_free_packet(&pkt);
- if (ret < 0)
- return -1;
- if(priv->use_dr1 && priv->frame->opaque) {
- picref = avfilter_ref_buffer(priv->frame->opaque, ~0);
- } else {
- picref = avfilter_get_video_buffer(link, AV_PERM_WRITE, link->w, link->h);
- av_image_copy(picref->data, picref->linesize,
- priv->frame->data, priv->frame->linesize,
- picref->format, link->w, link->h);
- }
- av_free_packet(&pkt);
- avfilter_copy_frame_props(picref, priv->frame);
- picref->pts = pts;
- avfilter_start_frame(link, picref);
- avfilter_draw_slice(link, 0, link->h, 1);
- avfilter_end_frame(link);
- return 0;
- }
调用者从sink中获取处理后的帧:
av_buffersink_get_buffer_ref(filt_out, &picref, 0);
获取后的帧保存在picref中.这个函数会引起graph中的filter从后向前依次调用上一个filter的outpad的request_frame(),最后调用到source的request_frame(),也就是input_request_frame(),input_request_frame()调用get_video_frame()(见ffplay.c)从设备获取一帧(可能需要解码),然后将这帧数据复制到picref中,filter们处理的帧是用AVFilterBufferRef表示的.然后将帧的一些属性也复制到picref中,最后调用avfilter_start_frame(link, picref);avfilter_draw_slice(link, 0, link->h, 1);avfilter_end_frame(link);来处理这一帧.这三个函数对应着pad上的三个函数指针:start_frame,draw_slice,end_frame.以start_frame为例,其调用过程是这样的:首先是source的start_frame被调用,做一些必要的处理后,再调用连接到source之后的filter的start_frame.每个filter的output pad都负责在这个函数中向下传递这个调用.当sink调用完start_frame()时再一层层返回到source的output pad.当这三个函数都被source的output pad调用完成后,这一帧的最终结果就出来了.于是可以用sink上获得.
与DShow比较起来,avfilter没有那些推模式,拉模式的概念,没有在source的output pad上实现线程,整个graph的运转都是由调用者驱动.
转自http://blog.csdn.net/nkmnkm/article/details/7219641
二. FFmpeg filter HOWTO
定义一个滤镜
AVFilter
所有我们写的滤镜都要用一个AVFilter结构体讲给ffmpeg听。 这个结构体里描述了ffmpeg从哪个方法进入我们的滤镜。 这个结构体在libavfilter/avfilter.h里如下定义:
01
|
typedef struct
|
02
|
{
|
03
|
char *name; ///< 滤镜名称
|
04
|
05
|
int priv_size; ///< 给滤镜分配的内存大小
|
06
|
07
|
int (*init)(AVFilterContext *ctx, const char *args, void *opaque);
|
08
|
void (*uninit)(AVFilterContext *ctx);
|
09
|
10
|
int (*query_formats)(AVFilterContext *ctx);
|
11
|
12
|
const AVFilterPad *inputs; ///< 一系列输入 NULL terminated list of inputs. NULL if none
|
13
|
const AVFilterPad *outputs; ///< 一系列输出 NULL terminated list of outputs. NULL if none
|
14
|
} AVFilter;
|
“query_formats”方法用于设置可以接受的输入图像格式和输出的图像格式(用于滤镜链分辨哪些滤镜可以组合在一起用)。
AVFilterPad
这个滤镜用于描述滤镜的输入输出,在libavfilter/avfilter.h中定义如下:
01
|
typedef struct AVFilterPad
|
02
|
{
|
03
|
char *name;
|
04
|
int type;
|
05
|
06
|
int min_perms;
|
07
|
int rej_perms;
|
08
|
09
|
void (*start_frame)(AVFilterLink *link, AVFilterPicRef *picref);
|
10
|
AVFilterPicRef *(*get_video_buffer)(AVFilterLink *link, int perms);
|
11
|
void (*end_frame)(AVFilterLink *link);
|
12
|
void (*draw_slice)(AVFilterLink *link, int y, int height);
|
13
|
14
|
int (*request_frame)(AVFilterLink *link);
|
15
|
16
|
int (*config_props)(AVFilterLink *link);
|
17
|
} AVFilterPad;
|
头文件里有十分具体的描述,这里大概解释如下:
输入输出pad都可以用的元素:
name pad的名字,所有的输入pad名字不能重复,所有的输出名字不能重复;
type 此元素目前只能为“AV_PAD_VIDEO”值
config_props 链接此pad的配置方法的函数指针
仅限输入pad使用的元素:
min_perms 接受输入需要的最小权限
rej_perms 不接受的输入权限
start_frame 一帧传入时引用的方法的函数指针
draw_slice 每个slice已经传入后引用的方法的函数指针
end_frame 一帧完整结束后引用的方法的函数指针
get_video_buffer 前一个滤镜调用,用以为一个图像请求内存
仅限输出pad使用的元素:
request_frame 请求滤镜输出一帧
图像缓冲
引用计数
滤镜系统使用引用计数。意味着存在一个buffer里面存放着图像数据,而所有的滤镜都各自保有一个指向这个buffer的引用。当每一个滤镜完事,它就释放自己的那个引用。这样当所有的引用都释放以后,滤镜系统就会自动把buffer释放掉。
权限
由于可能有多个滤镜都保有了buffer的指针,它们可能会同时操作buffer而造成冲突,因此ffmpeg引入了权限系统。
大多数情况下,当一个滤镜准备输出一帧时,它调用滤镜链上下一个滤镜的一个方法来请求一个buffer。这指定了它对这个buffer需要的最低权限,不过可能实际被赋予的权限可能比要求的要高。
在想要把buffer输出给另一个滤镜时,会新建一个新的指向这个图像的引用,可能是一个权限标记的子集。这个新的引用属于接受buffer的滤镜。
举例说:一个丢帧的滤镜在输出最后一帧时,他可能在输出之后还是想要保持一个指向图像的引用,以确保没有其它滤镜同时修改这个buffer。为了达到这个目的,他可能请给自己求AV_PERM_READ|AV_PERM_WRITE|AV_PERM_PRESERVE权限,然后在给予其它滤镜的引用里去掉AV_PERM_WRITE权限。
可用的权限有:
AV_PERM_READ 可以读取图像数据
AV_PERM_WRITE 可以写入图像数据
AV_PERM_PRESERVE 保证图像数据不会被其它滤镜修改,意味着不会有其它滤镜拿到AV_PERM_WRITE权限
AV_PERM_REUSE 滤镜可能往一段buffer多次输出,但图像数据不得切换到不同的输出
AV_PERM_REUSE2 滤镜可能往一段buffer多次输出,可能在不同的输出之间修改图像数据
滤镜链
滤镜的输入输出用“AVFilterLink”结构体和其它滤镜相连接:
01
|
typedef struct AVFilterLink
|
02
|
{
|
03
|
AVFilterContext *src; ///< source filter
|
04
|
unsigned int srcpad; ///< index of the output pad on the source filter
|
05
|
06
|
AVFilterContext *dst; ///< dest filter
|
07
|
unsigned int dstpad; ///< index of the input pad on the dest filter
|
08
|
09
|
int w; ///< agreed upon image width
|
10
|
int h; ///< agreed upon image height
|
11
|
enum PixelFormat format; ///< agreed upon image colorspace
|
12
|
13
|
AVFilterFormats *in_formats; ///< formats supported by source filter
|
14
|
AVFilterFormats *out_formats; ///< formats supported by destination filter
|
15
|
16
|
AVFilterPicRef *srcpic;
|
17
|
18
|
AVFilterPicRef *cur_pic;
|
19
|
AVFilterPicRef *outpic;
|
20
|
};
|
成员“src”和“dst”分别指出滤镜在链上的输入和输出的结束。“srcpad”指向链条上一个“源滤镜”的输出面的索引;类似的,“dstpad”指向目标滤镜的输入面的索引。
成员“in_formats”指向“源滤镜”定义的它支持的格式,“out_formats”指向“目标滤镜”支持的格式。结构体“AVFilterFormats”用于存储支持格式的列表,它使用引用计数,跟踪它的引用(参见libavfilter/avfilter.h中关于AVFilterFormats结构体的注释,了解色度空间的协商机制是怎么工作的,以及为什么协商是必要的)。结果就是一个滤镜如果为它之前和之后的滤镜提供了指向相同支持格式的列表的指针,就意味着这个链条上的滤镜就只能使用相同的格式了。
两个滤镜相连时,它们需要在它们处理的图像数据的尺寸和图像格式上达成一致。达成一致后,这些会作为参数存储在link结构体中。
成员“srcpic”是滤镜系统内部使用的,不应该直接存取。
成员“cur_pic”是给目标滤镜用的。当一个帧正在通过滤镜链时(开始于start_frame(),结束于end_frame),这个成员包含了目标滤镜对此帧的引用。
成员“outpic”会在接下来一个小教程中详细介绍。
写一个简单的滤镜
默认的滤镜入口点
因为大多数滤镜都只有一个输入一个输出,且每接受一个帧只输出一个帧,ffmpeg的滤镜系统提供了一系列默认的切入点以简化这种滤镜的开发,以下是切入点和默认实现的作用:
request_frame() 从滤镜链中前一个滤镜那请求一个帧
query_formats() 设置所有面上都支持的格式列表,这样别人就要按照这个列表来。默认包含大多数的YUV和RGB/BGR格式
start_frame() 请求一个buffer来保存输出帧。一个指向此buffer的引用存储在hook到滤镜的输出的link的“outpic”成员中。下一个滤镜的start_frame()回调会被调用,传入一个此buffer的引用。
end_frame() 调用下一个滤镜的end_frame()回调函数。释放指向输出link的“outpic”成员的引用,如果那成员被设置了(比如说使用了默认的start_frame()方法)。释放输入link的“cur_pic”引用
get_video_buffer() 返回一个在要求的权限上加一个AV_PERM_READ权限的buffer
config_props() on output pad 把输出的图像尺寸设置成和输入一样
“vf_negate”滤镜
介绍了数据结构和回调函数,让我们来看一个真实的滤镜。vf_negate滤镜的效果是反转视频中的色彩。它就一个输入一个输出,并且每个输入帧都输出一个帧。非常典型,可以使用滤镜系统那些默认的回调实现。
首先,让我们看一眼在“libavfilter/vf_negate.c”文件最下面的“AVFilter”结构体:
01
|
AVFilter avfilter_vf_negate =
|
02
|
{
|
03
|
.name = "negate" ,
|
04
|
05
|
.priv_size = sizeof (NegContext),
|
06
|
07
|
.query_formats = query_formats,
|
08
|
09
|
.inputs = (AVFilterPad[]) {{ .name = "default" ,
|
10
|
.type = AV_PAD_VIDEO,
|
11
|
.draw_slice = draw_slice,
|
12
|
.config_props = config_props,
|
13
|
.min_perms = AV_PERM_READ, },
|
14
|
{ .name = NULL}},
|
15
|
.outputs = (AVFilterPad[]) {{ .name = "default" ,
|
16
|
.type = AV_PAD_VIDEO, },
|
17
|
{ .name = NULL}},
|
18
|
};
|
可以看到滤镜的名字是“negate”,需要sizeof(NegContext)字节的空间存储上下文。在input和output列表的最后,都有一个name设置为NULL的pad。可以看出这个滤镜确实只有一个输入一个输出。如果你仔细观察pad的定义,你会发现好多回调函数已经被指定好了。因为我们这个滤镜很简单,所以大多数保持默认的就可以。
让我们看看它自己定义的回调函数。
query_formats()
01
|
static int query_formats(AVFilterContext *ctx)
|
02
|
{
|
03
|
avfilter_set_common_formats(ctx,
|
04
|
avfilter_make_format_list(10,
|
05
|
PIX_FMT_YUV444P, PIX_FMT_YUV422P, PIX_FMT_YUV420P,
|
06
|
PIX_FMT_YUV411P, PIX_FMT_YUV410P,
|
07
|
PIX_FMT_YUVJ444P, PIX_FMT_YUVJ422P, PIX_FMT_YUVJ420P,
|
08
|
PIX_FMT_YUV440P, PIX_FMT_YUVJ440P));
|
09
|
return 0;
|
10
|
}
|
这个函数调用了avfilter_make_format_list()。这个方法第一个函数指定后面要列举多少个格式,后面就把格式列出来。返回是一个包含指定格式的AVFilterFormats结构体。把这个结构体传给avfilter_set_common_formats()方法把格式给设置上。如同你看到的,这个滤镜支持一堆YUV平面的色彩空间格式,包括JPEG YUV色彩空间(其中那些包含字母J的)。
config_props() on an input pad
input填充的config_props()负责验证是否支持输入pad的属性,也负责更新滤镜的属性上下文。
TODO: 快速解释一下YUV色彩空间,色读采样,YUV和JEPG YUV范围的不同。
让我们看看滤镜是怎么存储它的上下文的:
1
|
typedef struct
|
2
|
{
|
3
|
int offY, offUV;
|
4
|
int hsub, vsub;
|
5
|
} NegContext;
|
AVFilter结构体中的成员“priv_size”告诉滤镜系统它需要多少字节来存储这个结构体。成员“hsub”和“vsub”用于色度采样,成员“offY”和“offUV”用于YUV和JPEG间范围的不同。让我们看看这些在输入pad的config_props中是咋设置的:
01
|
static int config_props(AVFilterLink *link)
|
02
|
{
|
03
|
NegContext *neg = link->dst->priv;
|
04
|
05
|
avcodec_get_chroma_sub_sample(link->format, &neg->hsub, &neg->vsub);
|
06
|
07
|
switch (link->format) {
|
08
|
case PIX_FMT_YUVJ444P:
|
09
|
case PIX_FMT_YUVJ422P:
|
10
|
case PIX_FMT_YUVJ420P:
|
11
|
case PIX_FMT_YUVJ440P:
|
12
|
neg->offY =
|
13
|
neg->offUV = 0;
|
14
|
break ;
|
15
|
default :
|
16
|
neg->offY = -4;
|
17
|
neg->offUV = 1;
|
18
|
}
|
19
|
20
|
return 0;
|
21
|
}
|
它只是简单调用了avcodec_get_chroma_sub_sample()方法去得到色度采样的位移因子,然后把它们存到上下文中。然后它存储了一些JPEG YUV的亮度/色度范围不同的偏移补偿。返回0表明成功,因为没有这个滤镜无法处理的输入格式。
draw_slice()
最后,滤镜中最重要的方法,它实际处理图像,draw_slice():
01
|
static void draw_slice(AVFilterLink *link, int y, int h)
|
02
|
{
|
03
|
NegContext *neg = link->dst->priv;
|
04
|
AVFilterPicRef *in = link->cur_pic;
|
05
|
AVFilterPicRef *out = link->dst->outputs[0]->outpic;
|
06
|
uint8_t *inrow, *outrow;
|
07
|
int i, j, plane;
|
08
|
09
|
/* luma plane */
|
10
|
inrow = in-> data[0] + y * in-> linesize[0];
|
11
|
outrow = out->data[0] + y * out->linesize[0];
|
12
|
for (i = 0; i < h; i ++) {
|
13
|
for (j = 0; j < link->w; j ++)
|
14
|
outrow[j] = 255 - inrow[j] + neg->offY;
|
15
|
inrow += in-> linesize[0];
|
16
|
outrow += out->linesize[0];
|
17
|
}
|
18
|
19
|
/* chroma planes */
|
20
|
for (plane = 1; plane < 3; plane ++) {
|
21
|
inrow = in-> data[plane] + (y >> neg->vsub) * in-> linesize[plane];
|
22
|
outrow = out->data[plane] + (y >> neg->vsub) * out->linesize[plane];
|
23
|
24
|
for (i = 0; i < h >> neg->vsub; i ++) {
|
25
|
for (j = 0; j < link->w >> neg->hsub; j ++)
|
26
|
outrow[j] = 255 - inrow[j] + neg->offUV;
|
27
|
inrow += in-> linesize[plane];
|
28
|
outrow += out->linesize[plane];
|
29
|
}
|
30
|
}
|
31
|
32
|
avfilter_draw_slice(link->dst->outputs[0], y, h);
|
33
|
}
|
“y”参数是当前slice的顶部,“h”参数是slice的高度。在这个区域以外的图像被假设为是无意义的(可能在未来的一些滤镜中这个假设会被打破)。
变量“inrow”指向输入slice的第一行,“outrow”指向输出的第一行。然后,它先遍历每一行,然后在每一行中遍历每一个像素,用255去减像素值,加上在config_props()中为不同格式的范围做出的修正值。
然后它在色度平面上做了同样的事。注意宽度和高度是调整到合适色度采样的。
在图像修改结束后,调用calling avfilter_draw_slice()方法把slice送给下一个滤镜去处理。
翻译自
http://wiki.multimedia.cx/index.php?title=FFmpeg_filter_HOWTO
ffmpeg AVFilter介绍相关推荐
- 音视频篇 - FFmpeg 的介绍和使用
本文章是阅读<音视频开发进阶指南基于android与ios平台的实践>一书的学习笔记. 目录: FFmpeg 的介绍 FFmpeg Android 编译库 FFmpeg 的结构 FFmpe ...
- ffmpeg函数介绍
本文对在使用ffmpeg进行音视频编解码时使用到的一些函数做一个简单介绍,我当前使用的ffmpeg版本为:0.8.5,因为本人发现在不同的版本中,有些函数名称会有点小改动,所以在此有必要说明下ffmp ...
- FFMPEG AvFilter使用实例(实现视频缩放,裁剪,水印等)
FFMPEG官网给出了FFMPEG 滤镜使用的实例,它是将视频中的像素点替换成字符,然后从终端输出.我在该实例的基础上稍微的做了修改,使它能够保存滤镜处理过后的文件.在上代码之前先明白几个概念: Fi ...
- 使用ffmpeg AVfilter 中的amix实现混音
官方文档: 6.16 amix Mixes multiple audio inputs into a single output. Note that this filter only support ...
- ffmpeg avfilter
前言 FFmpeg的优秀在于它的功能强大和良好的系统框架,而滤镜就是其中之一.ffmpeg的自带滤镜不但能对视频进行裁剪,添加logo,还能将多个滤镜组全使用. 更妙之处在于它还可以方便地添加自己定义 ...
- FFMPEG,vlc介绍和视频直播,obs(zz)
点击打开链接 1. 有用的资料 http://blog.csdn.net/fireroll/article/details/18903843 http://blog.csdn.net/leixiaoh ...
- FFmpeg命令介绍
FFmpeg 简介 轶闻 常用命令 视频相关 音频相关 录制命令 简介 FFmpeg是一套可以用来记录.转换数字音频.视频,并能将其转化为流的开源计算机程序.使用C语言进行开发,采用LGPL或GPL许 ...
- 最简单的基于FFmpeg的AVfilter的例子-修正版
代码是参考雷神的博客的代码,不过由于ffmpeg版本不同,记录使用中遇到的问题. 1.调用avfilter_get_by_name("ffbuffersink")时在新版本的ffm ...
- c++ ffmpeg内存推流_最简单的基于FFmpeg的AVfilter的例子
此前libavfilter一直是结合着libavcodec等类库的接口函数使用的,因此我一直以为libavfilter库与libavcodec等类库是高度耦合的(也就是如果想使用libavfilter ...
- 最简单的基于FFmpeg的AVfilter例子(水印叠加)
2019独角兽企业重金招聘Python工程师标准>>> FFMPEG中有一个类库:libavfilter.该类库提供了各种视音频过滤器.之前一直没有怎么使用过这个类库,最近看了一下它 ...
最新文章
- 活下去,是一种信念 !
- 使用Numpy和Scipy处理图像
- Nova Conductor 与 Versioned Object Model 机制
- 2020-12-28 Matlab自动化控制-Adrc自抗扰控制
- Latex学习(脚注)
- MySQL记住密码_技术分享 | mysqlsh 命令行模式 密码保存
- windows网络编程第二版 第三章 Internet Protocol 读书笔记
- html在线编辑器 asp.net,ASP.NET网站使用Kindeditor富文本编辑器配置步骤
- python指定范围内加法代码解析
- html有几个文件夹,关于webpack打包问题,怎么打包成多个文件夹,每个文件夹下有相应的html,js和css?...
- 【SQL那些事】事务
- Ubuntu安装搜狗拼音和金山快盘
- CNNIC报告:中国网民超8亿,人工智能取得突出成果
- Adobe Audition 基本使用
- C#代码实现矢量画图
- Java中的ConcurrentModificationException
- html跑马灯代码大全(图片文字移动代码)
- 激光雷达互动交互大屏全息互动投影Tuio多点触摸检测驱动引擎
- 无人驾驶传感器融合系列(五)——毫米波雷达测速原理(77GHz FMCW)
- 分享6种时间管理方法,不要再说时间不够用了!