H.264帧结构和RTSP协议源码框架

目录

 

1、H264编码原理和基本概念

1.1、h.264编码原理

1.2、h.264编码相关的一些概念

2、H264的NAL单元详解

2.1、VCL和NAL的关系

2.2、H.264视频流分析工具

2.3、h264视频流总体分析

2.4、相关概念

3、H264的NAL单元---sps和pps

3.1、sps和pps详解

3.2、H264的profile和level

3.3、sequence

4、rtsp协议和源码框架

4.1、源码框架函数调用关系

4.2、直接发送与环状buffer发送

4.3、RTP发送一帧数据的两种不同发送格式->整发送和分包发送


 

1、H264编码原理和基本概念

1.1、h.264编码原理

(1)图像冗余信息:空间冗余、时间冗余

(2)视频编码关键点:压缩比高、算法复杂度小、还原度高

(3)H.264的2大组成部分:VCL和NAL

        VCL关心如何进行视频压缩

        NAL关心压缩后的视频流如何被网络传输和解码播放

1.2、h.264编码相关的一些概念

(1)宏块 MB(macroblock):多个像素组成的一块,宏块是视频压缩算法的最基本单位

由于图像本身在局部,颜色具有相似性,所以可以把一幅图像分成若干个宏块

(2)片 slice:构成帧的一部分

        一帧图像=若干个slice(可以是一个slice)

        一个slice=若干个MB

        一个MB=多个像素

(3)帧 frame:一整幅完整的图像

(4)I帧、B帧、P帧:帧有好几种类型

I帧:非参考帧,这一帧图像的内容只和本身有关,和前一帧后一帧图像的内容无关,一般作为起始帧,因为这一帧没有任何参考,所以只能对这一帧进行帧内压缩(空间冗余上的优化)

B帧:参考帧,这一帧图像的内容不光和本身有关,还和前一帧或后一帧图像的内容有关(空间冗余+时间冗余的优化)

P帧:参考帧,这一帧图像的内容不光和本身有关,还和前一帧图像的内容有关(空间冗余+时间冗余的优化)

(5)帧率 fps:一秒中有多少帧,帧率高(慢动作),帧率低(快动作)

(6)像素->宏块->片->帧->序列->码流

2、H264的NAL单元详解

2.1、VCL和NAL的关系

(1)VCL只关心编码部分,重点在于编码算法以及在特定硬件平台的实现,VCL输出的是编码后的纯视频流信息,没有任何头信息

(2)NAL关心的是VCL的输出纯视频流如何被表达和封包以利于网络传输

(3)SODB:String Of Data Bits(VCL输出的纯视频流)

(4)RBSP:Raw Byte Sequence Payload

(5)NALU:Network Abstraction Layer Units        NALU是H264文件的基本组成单元

(6)关系:

SODB + RBSP trailing bits = RBSP

NALU header(1 byte) + RBSP = NALU

H264文件由若干个序列组成 -> 序列由若干个帧/slice组成 -> 帧/slice由分隔符和NALU单元组成 -> 去掉NALU header得到RBSP -> 去掉RBSP trailing得到SODB-> VLC播放器解码播放SODB

(7)总结:做编码器的人关心的是VCL部分;做视频传输和解码播放的人关心的是NAL部分

2.2、H.264视频流分析工具

(1)雷神作品:SpecialVH264.exe

(2)国外工具:Elecard StreamEye Tools

(3)二进制工具:winhex

(4)网络抓包工具:wireshark

(5)播放器:vlc

2.3、h264视频流总体分析

(1)h264标准有多个版本,可能会有差异,具体差异不详

(2)网上看的资料有时讲法会有冲突,或者无法验证的差异

(3)这里以海思平台为主、为准、为案例,不能保证其他平台也完全一样

(4)海思平台编码出来的H.264码流(就是一个二进制文件)都是一个一个序列组成,序列包含:1sps+1pps+1sei+1I帧+若干p帧

3a52b471bc984eb98d57f907f9d2e64d.png

2.4、相关概念

(1)序列 sequence,一般一秒发一个sequence,也就是说sequence(除去sps、pps、sei)和帧率相等

(2)分隔符:00 00 00 01的四字节组合,是用来做识别的,不是有效数据,表示一个slice的开始,表示我们有一个新的片到来。

d8fcfe76a46d42b884f6c5575dcaa3a8.png

帧的有效数据部分是不会出现(00 00 00 01)的,h264的标准规定有效数据不允许出现连续的3个00,会在第二个00后面,第三个00前面添加一个03进去,用(00 00 03 00)来表示(00 00 00)。

(3)sps

(4)pps

(5)sei

(6)NALU header:分隔符后的第一个字节

914df4ea696f4168ab46dd2c24380990.png

NALU header有8个位

bit7(forbidden_zero_bit):禁止位,默认位0,值为1表示语法出错

bit6~5(nal_ref_idc):重要性,代表这一帧的内容在视频流中的重要程度,重要程度由大到小11>10>01>00

bit4~1(nal_unit_type):NAL单元类型

f4574dd7343640338fde646a0dbde61b.png

1:表示 非I帧,具体是P还是B帧不知道

5:表示 I帧

6:表示 SEI

7:表示 SPS

8:表示 PPS

参考博文:

H264(NAL简介与I帧判断)_h264 判断i帧_jefry_xdz的博客-CSDN博客

 

3、H264的NAL单元---sps和pps

参考博文:

https://www.cnblogs.com/wainiwann/p/7477794.html

3.1、sps和pps详解

在H.264标准协议中规定了多种不同的NAL Unit类型,其中类型7表示该NAL Unit内保存的数据为Sequence Paramater Set(序列参数集),描述这个序列的参数信息都在这里。在H.264的各种语法元素中,SPS中的这些参数信息至关重要。如果其中的数据丢失或出现错误,那么解码过程很可能会失败。SPS和PPS中的信息会用于播放器的初始化使用。

SPS中保存了一组视频序列的全局参数。所谓的视频序列即原始视频的一帧一帧的图像数据经过编码之后组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集(PPS)中,例如P帧需要参考前面的帧进行编码,那么是怎么参考的,这个参考数据就保存在PPS中。

一般情况SPS和PPS位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:

  • 解码器需要在码流中间开始解码;
  • 编码器在编码的过程中改变了码流的参数(如图像分辨率等);

视频播放器为了让后续的解码过程可以使用SPS中包含的参数,必须对SPS其中的数据进行解析。其中H.264标准协议中规定的SPS格式位于文档的7.3.2.1.1部分,如下图所示:

2c6060b688da4e33b1b62e52c1f37853.png

(1) profile_idc:

标识当前H.264码流的profile。我们知道,H.264中定义了三种常用的档次profile:

基准档次:baseline profile;

主要档次:main profile;

扩展档次:extended profile;

在H.264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:

profile_idc = 66(0x42) → baseline profile;

profile_idc = 77 → main profile;

profile_idc = 88 → extended profile;

在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。

当前码流中,profile_idc = 0x42 = 66 ,因此profile档次为 baseline profile;

8eb9eae6d74d47e98d2ac45987d27d6e.png

(2) level_idc

标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。

455fbb5138304c76a06574a73ec7fb0b.png

当前码流中,level_idc = 0x1F = 31,因此码流的级别为3.1。

Level为3.1的特性:

c99f45c9897d4b54b2e145f722a362e6.png

3.2、H264的profile和level

参考博文:

h264中profile和level的含义_xiaojun11-的博客-CSDN博客

Profile是对视频压缩特性的描述(CABAC呀、颜色采样数等等),简而言之就是压缩算法的档次。Level是对视频本身特性的描述(码率、分辨率、fps)。

简单来说,Profile越高,就说明采用了越高级的压缩特性(越高级的压缩算法)。Level越高,视频的码率、分辨率、fps越高。

一些移动设备(手机、游戏机、PMP)由于性能有限,不支持全部高级视频压缩特性和高分辨率图像,只支持基础压缩特性和分辨率低一些的图像。为了让这个限制更加清晰明了,H264从低到高划分了很多Profile和Level。

profile主要是定义了编码工具(压缩算法)的集合,不同的profile,包含了不同的编码技术;

h.264的profile有三种Baseline Profile(基本的)、Main Profile(主线的)、High Profile(高等级的)

3.3、sequence

(1)一段h.264的码流其实就是多个sequence组成的

(2)每个sequence均有固定结构:1sps+1pps+1sei+1I帧+若干p帧

(3)sps和pps和sei描述该sequence的图像信息,这些信息有利于网络传输或解码

(4)I帧是关键,丢了I帧整个sequence就废了,因为I帧是自参考的,其它的帧都参考于它,每个sequence有且只有1个I帧

(5)p帧的个数等于fps-1

(6)I帧越大则P帧可以越小,反之I帧越小则P帧会越大

I帧越大,说明I帧本身压缩比例不高,参考信息比较多,比较详细,所以P帧就越好参考,就可以越小

(7)I帧的大小取决于图像本身内容,和压缩算法的空间压缩部分

如果图像本身色彩丰富、画面复制,那么I帧就会很大

(8)P帧的大小取决于图像变化的剧烈程度

(9)CBR和VBR下P帧的大小策略会不同,CBR时P帧大小基本恒定,VBR时变化会比较剧烈

CBR:固定码率

VBR:可变码率

4、rtsp协议和源码框架

rtsp协议参考:

链接:https://pan.baidu.com/s/1IHU8vaFShMq3mOUz3c2iDw?pwd=0000 
提取码:0000

4.1、源码框架函数调用关系

main
	RtspServer_init
		RtspServerListen								    //线程函数---达到PLAY状态
			socket                            		        //创建TCP套接字
			setsockopt                                 	    //设置端口复用
			bind                                      	    //绑定服务器IP和端口
			listen								            //监听、并告诉linux系统这是个服务器
			while(accept)							        //不断等待客户端连接
				setsockopt
				for                                         //实际只执行一次
					RtspClientMsg                           //此函数是重点,创建一个线程去处理这此连接
					    pthread_detach                      //分离线程,子线程结束后自动回收资源
					    while                               //如果当前不是未连接状态
					    {
						    recv                            //读1024个字节
						    if(没读到)
						    {
							    退出线程
						    }
						    ParseRequestString              //解析客户端发来的Requst请求并填充字符串数组    

						    if(是OPTIONS请求)
							    OptionAnswer                //返回服务器提供的可用方法
								    sprintf                 //组包
								    strcat                  //连接RTSP头和sdp信息
								    send                    //发送
						    if(是DESCRIBE请求)
							    DescribeAnswer              //返回sdp包给播放器client去配置解码
								    GetLocalIP              //获取本地IP
     								    	ioctl
								    sprintf                 //组包
								    send                    //发送
						    if(是SETUP请求)
							    SetupAnswer
								    ParseTransportHeader    //解析客户端发来的SETUP Request,然后填充到变量中
								    GetLocalIP              //获取本地IP
								    sprintf                 //组包
								    send                    //发送
						    if(是PLAY请求)
							    PlayAnswer
								    sprintf                 //组包
								    send                    //发送
								    socket                  //创建一个UDPsocket
								    //由此可见,之前的TCPsocket是一个命令通道,这里的UDPsocket是一个传输数据的通道
						    if(是PAUSE请求)                 //暂停请求
							    PauseAnswer                 //源码分析得知,这里并没有实现暂停码流的传输
								    sprintf                 //组包
								    send                    //发送
						    if(是TEARDOWN请求)
							    TeardownAnswer
								    sprintf                 //组包
								    send                    //发送
								    close(udp)              //关闭数据传输通道UDPsocket
							    close(tcp)                  //关闭命令传输通道TCPsocket
					      }   
		vdRTPSendThread                                     //线程函数---发送流媒体的数据
			while(1)                                        //每5ms判断当前是否有数据要发送
			{
				if(!list_empty)                             //链表为空,则表明当前没有数据要发送
				{
					get_first_item                          //取出链表中第一个非空节点
					VENC_Sent//发送码流数据
						sendto                              //UDP发送
					list_del                                //从链表中把节点去掉
				}
				usleep(5000);                               //延时5ms
			}
	SAMPLE_VENC_720P_CLASSIC
		SAMPLE_COMM_VENC_StartGetStream
			SAMPLE_COMM_VENC_GetVencStreamProc              //线程函数
				HI_MPI_VENC_GetChnAttr
				SAMPLE_COMM_VENC_GetFilePostfix
				HI_MPI_VENC_GetFd
				while
				{
					HI_MPI_VENC_Query
					malloc
					HI_MPI_VENC_GetStream                   //获取一帧编码完的码流

					//以下两种方式二选一
					SAMPLE_COMM_VENC_Sentjin                //此函数是重点---编码完后直接发送
						VENC_Sent                           //直接发送    
					saveStream                              //此函数是重点---编码完后保存到环状buf中
						malloc                              //生成一个链表节点
						INIT_LIST_HEAD                      //初始化这个节点的next指针和prev指针都指向自己
						填充节点内容
						list_add_tail                       //将节点加入链表
					//当我将编码完的数据保存到环形链表后,此时链表就有节点数据了
					//然后就可以在 vdRTPSendThread 线程中发送了

					HI_MPI_VENC_ReleaseStream
					free
				}

4.2、直接发送与环状buffer发送

直接发送:编码完直接发送,编多少发多少

代码体现在:

SAMPLE_COMM_VENC_GetVencStreamProc线程函数:
    HI_MPI_VENC_GetStream            //获取编码完的码流
    SAMPLE_COMM_VENC_Sentjin         //发送函数

缺陷:视频采集和编码速度 与 网络传输速度 都是变动的,速度不一定一致,会导致系统进入漫长的等待,不能更好的实时视频监控

 

环状buffer发送:编码完放到环状buffer中,待发送

环状buffer即环形链表,当生产者(获取码流的线程)编码完一帧数据就将这帧数据作为节点加入链表,消费者(发送线程)只管从中取。

对比代码:SAMPLE_COMM_VENC_GetVencStreamProc线程函数中,调用HI_MPI_VENC_GetStream获取编码完的码流,调用saveStream将码流数据节点加入链表,vdRTPSendThread线程从链表中取走节点,然后调用VENC_Sent发送。

当链表中没有节点,则消费者要阻塞,等待生产者编码完将数据节点加入链表。环形链表相比数组的优点是使用内存可以足够大,直到内存撑爆。

代码体现在:

SAMPLE_COMM_VENC_GetVencStreamProc线程函数:
        HI_MPI_VENC_GetStream         //获取编码完的码流
        saveStream                    //编码完后保存到环状buf中
vdRTPSendThread线程函数:
        get_first_item                //取出链表中第一个非空节点
        VENC_Sent                     //发送码流数据
        list_del                      //从链表中把节点去掉

接下来就只用关注VENC_Sent函数内是怎么组包,然后通过UDP传输码流数据了。。

4.3、RTP发送一帧数据的两种不同发送格式->整发送和分包发送

关于rtp头的timestamp位:
RTP timestamp与帧率及时钟频率的关系_帧率 时钟频率_jasonhwang的博客-CSDN博客

关于RTP发送一帧数据的两种不同发送格式:整包发送和分包发送

https://www.cnblogs.com/yjg2014/p/6144977.html

VENC_Sent函数全解:

HI_S32 VENC_Sent(char *buffer,int buflen)
{
    HI_S32 i;
    int is=0;
    int nChanNum=0;

    for(is=0;is<MAX_RTSP_CLIENT;is++)
    {
        if(g_rtspClients[is].status!=RTSP_SENDING)//判断是否是 可发送 状态
        {
            continue;
        }
        int heart = g_rtspClients[is].seqnum % 10000;//seqnum:序列号,对10000取余
        
        char* nalu_payload;
        int nAvFrmLen = 0;
        int nIsIFrm = 0;
        int nNaluType = 0;
        char sendbuf[500*1024+32];

    
        nAvFrmLen = buflen;//音频或视频数据一帧的长度

        struct sockaddr_in server;
        server.sin_family=AF_INET;
           server.sin_port=htons(g_rtspClients[is].rtpport[0]);   //对方的端口       
           server.sin_addr.s_addr=inet_addr(g_rtspClients[is].IP);//对方的IP
        int    bytes=0;
        unsigned int timestamp_increse=0;
        
        timestamp_increse=(unsigned int)(90000.0 / 25);    //  两帧之间RTP timestamp的增量 = 时钟频率 / 帧率

        rtp_hdr =(RTP_FIXED_HEADER*)&sendbuf[0];         //填充RTP包头
    
        rtp_hdr->payload     = RTP_H264;   
        rtp_hdr->version     = 2;                          //RTP版本号
        rtp_hdr->marker    = 0;           
        rtp_hdr->ssrc      = htonl(10);                   //信源标记

        if(nAvFrmLen<=nalu_sent_len)    //如果这一帧数据的长度<=1400
        {
            rtp_hdr->marker=1;                            //标记一帧的结束
            rtp_hdr->seq_no     = htons(g_rtspClients[is].seqnum++);     //下一帧的序列号
            nalu_hdr =(NALU_HEADER*)&sendbuf[12]; 
            nalu_hdr->F=0; 
            nalu_hdr->NRI=  nIsIFrm; 
            nalu_hdr->TYPE=  nNaluType;
            nalu_payload=&sendbuf[13];
            memcpy(nalu_payload,buffer,nAvFrmLen);
                    g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;            
            rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);
            bytes=nAvFrmLen+ 13 ;                
            sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
        }
        else if(nAvFrmLen>nalu_sent_len)    //如果这一帧数据的长度>1400
        {
            int k=0,l=0;
            k=nAvFrmLen/nalu_sent_len;                    //这一帧数据的长度/1400
            l=nAvFrmLen%nalu_sent_len;                    //这一帧数据的长度对1400取余
            int t=0;        

            g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;    //计算timestamp
            rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);                 //把timestamp放入RTP包头       
            while(t<=k)            //循环k+1次,也就是发k+1个包,代表这一帧发完
            {
                rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++);
                if(t==0)        //第一包
                {
                    rtp_hdr->marker=0;            //填充完RTP_header的12个字节后,接下来填充的是NALU的分包形式
                    fu_ind =(FU_INDICATOR*)&sendbuf[12];//fu_indicato是NALU头的分包形式的第一部分
                    fu_ind->F= 0; 
                    fu_ind->NRI= nIsIFrm;
                    fu_ind->TYPE=28;                    //Type为FU-A
        
                    fu_hdr =(FU_HEADER*)&sendbuf[13];    //fu_header     是NALU头的分包形式的第二部分
                    fu_hdr->E=0;
                    fu_hdr->R=0;
                    fu_hdr->S=1;                        //表示帧开始
                    fu_hdr->TYPE=nNaluType;                //NALU type
    
                    nalu_payload=&sendbuf[14];    //填充完NALU的分包形式后,接下来开始填充有效数据
                    memcpy(nalu_payload,buffer,nalu_sent_len);
    
                    bytes=nalu_sent_len+14;        //发送的长度是有效数据长度+14(14就是前面添加的这些信息)                
                    sendto( udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
    
                }
                else if(k==t)    //最后一包
                {
                    rtp_hdr->marker=1;                    //标记一帧的结束
                    fu_ind =(FU_INDICATOR*)&sendbuf[12]; 
                    fu_ind->F= 0 ;
                    fu_ind->NRI= nIsIFrm ;
                    fu_ind->TYPE=28;

                    fu_hdr =(FU_HEADER*)&sendbuf[13];
                    fu_hdr->R=0;
                    fu_hdr->S=0;
                    fu_hdr->TYPE= nNaluType;
                    fu_hdr->E=1;                        //表示帧结束,和marker一样
                    nalu_payload=&sendbuf[14];
                    memcpy(nalu_payload,buffer+t*nalu_sent_len,l);
                    bytes=l+14;        
                    sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
                }
                else if(t<k && t!=0)    //中间的包
                {

                    rtp_hdr->marker=0;

                    fu_ind =(FU_INDICATOR*)&sendbuf[12]; 
                    fu_ind->F=0; 
                    fu_ind->NRI=nIsIFrm;
                    fu_ind->TYPE=28;
                    fu_hdr =(FU_HEADER*)&sendbuf[13];
                    //fu_hdr->E=0;
                    fu_hdr->R=0;
                    fu_hdr->S=0;
                    fu_hdr->E=0;                        //S=0,E=0表示既不是开头也不是结尾
                    fu_hdr->TYPE=nNaluType;
                    nalu_payload=&sendbuf[14];
                    memcpy(nalu_payload,buffer+t*nalu_sent_len,nalu_sent_len);
                    bytes=nalu_sent_len+14;    
                    sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
                }
            }
        }

    }

    //------------------------------------------------------------
}

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

InnoDB的三种行锁(提供具体sql执行案例分析)

InnoDB存储引擎有3种行锁的算法&#xff0c;其分别是&#xff1a; Record Lock&#xff08;记录锁&#xff09;&#xff1a;单个行记录上的范围 (锁住某一行记录)Gap Lock&#xff08;间隙锁&#xff09;&#xff1a;间隙锁&#xff0c;锁定一个范围&#xff0c;但不包含记录本…

人工智能(pytorch)搭建模型14-pytorch搭建Siamese Network模型(孪生网络),实现模型的训练与预测

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能(pytorch)搭建模型14-pytorch搭建Siamese Network模型(孪生网络)&#xff0c;实现模型的训练与预测。孪生网络是一种用于度量学习&#xff08;Metric Learning&#xff09;和比较学习&#xff08;Compariso…

基于深度学习的人脸面部表情识别系统【含Python源码+PyqtUI界面+原理详解】

功能演示 摘要&#xff1a;面部表情识别&#xff08;Facial Expression Recognition&#xff09;是一种通过技术手段识别人物图像中人脸面部表情的技术。本文详细介绍了其实现的技术原理&#xff0c;同时给出完整的Python实现代码、训练好的深度学习模型&#xff0c;并且通过Py…

GO语言使用最简单的UI方案govcl

接触go语言有一两年时间了。 之前用Qt和C#写过桌面程序&#xff0c;C#会被别人扒皮&#xff0c;极度不爽&#xff1b;Qt默认要带一堆dll&#xff0c;或者静态编译要自己弄或者找库&#xff0c;有的库还缺这缺那&#xff0c;很难编译成功。 如果C# winform可以编译成二进制原生…

商品减库在Redis中的运用

一.商品减库中存在问题 1.传统的代码 1.1引入jar包 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.…

基于tensorflow深度学习的猫狗分类识别

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

机器学习之K-means聚类算法

目录 K-means聚类算法 算法流程 优点 缺点 随机点聚类 人脸聚类 旋转物体聚类 K-means聚类算法 K-means聚类算法是一种无监督的学习方法&#xff0c;通过对样本数据进行分组来发现数据内在的结构。K-means的基本思想是将n个实例分成k个簇&#xff0c;使得同一簇内数据相…

基于小程序的用户服务技术研究

目录 1. 小程序开发技术原理 2. 用户服务设计3. 数据库设计和管理4. 安全和隐私保护5. 性能优化和测试总结 关于基于小程序的用户服务技术研究&#xff0c;这是一个非常广泛和复杂的领域&#xff0c;需要涉及多个方面的知识和技术。一般来说&#xff0c;基于小程序的用户服务技…

怎么学习数据库连接与操作? - 易智编译EaseEditing

学习数据库连接与操作可以按照以下步骤进行&#xff1a; 理解数据库基础知识&#xff1a; 在学习数据库连接与操作之前&#xff0c;首先要了解数据库的基本概念、组成部分和工作原理。 学习关系型数据库和非关系型数据库的区别&#xff0c;了解常见的数据库管理系统&#xff…

HTTP协议

HTTP协议专门用于定义浏览器与服务器之间交互数据的过程以及数据本身的格式 HTTP概述 HTTP是一种客户端&#xff08;用户&#xff09;请求和服务器&#xff08;网站&#xff09;应答的标准&#xff0c;它作为一种应用层协议&#xff0c;应用于分布式、协作式和超媒体信息系统…

【springboot】—— 后端Springboot项目开发

后端Springboot项目开发 步骤1 先创建数据库&#xff0c;并在下面创建一个user表&#xff0c;插入数据&#xff0c;sql如下&#xff1a; CREATE TABLE user (id int(11) NOT NULL AUTO_INCREMENT COMMENT ID,email varchar(255) NOT NULL COMMENT 邮箱,password varchar(255)…

王益分布式机器学习讲座~Random Notes (1)

0 并行计算是什么&#xff1f;并行计算框架又是什么 并行计算是一种同时使用多个计算资源&#xff08;如处理器、计算节点&#xff09;来执行计算任务的方法。通过将计算任务分解为多个子任务&#xff0c;这些子任务可以同时在不同的计算资源上执行&#xff0c;从而实现加速计…

ChatGLM2-6B发布,位居C-Eval榜首

ChatGLM-6B自2023年3月发布以来&#xff0c;就已经爆火&#xff0c;如今6月25日&#xff0c;清华二代发布&#xff08;ChatGLM2-6B&#xff09;&#xff0c;位居C-Eval榜单的榜首&#xff01; 项目地址&#xff1a;https://github.com/THUDM/ChatGLM2-6B HuggingFace&#xf…

Sequential用法

目录 1.官方文档解释 1.1原文参照 1.2中文解释 2.参考代码 3.一些参考使用 3.1生成网络 3.2 感知机的实现 3.3组装网络层 1.官方文档解释 1.1原文参照 A sequential container. Modules will be added to it in the order they are passed in the constructor. A…

【书】《Python全栈测试开发》——浅谈我所理解的『自动化』测试

目录 1. 自动化测试的What and Why?1.1 What1.2 Why2. 自动化的前戏需要准备哪些必备技能?3. 自动化测试类型3.1 Web自动化测试3.1.1 自动化测试设计模式3.1.2 自动化测试驱动方式3.1.3 自动化测试框架3.2 App自动化测试3.3 接口自动化测试4. 自动化调优《Python全栈测试开发…

Springboot钉钉免密登录集成(钉钉小程序和H5微应用)

欢迎访问我的个人博客:www.ifueen.com RT&#xff0c;因为业务需要把我们系统集成到钉钉里面一个小程序和一个H5应用&#xff0c;并且在钉钉平台上面实现无感登录&#xff0c;用户打开我们系统后不需要再输入密码即可登录进系统&#xff0c;查阅文档实际操作过之后记录一下过程…

Qt6.2教程——4.QT常用控件QPushButton

一&#xff0c;QPushButton简介 QPushButton是Qt框架中的一种基本控件&#xff0c;它是用户界面中最常见和最常用的控件之一。QPushButton提供了一个可点击的按钮&#xff0c;用户可以通过点击按钮来触发特定的应用程序操作。比如&#xff0c;你可能会在一个对话框中看到"…

VMware Tools安装“保熟“技巧

网上关于如何安装VMware Tools也有很多帖子,但是基本很难对症下药。下面笔者给出两种情况&#xff0c;读者可根据自己概况定位自己的问题&#xff0c;从而进行解决。 如果读者安装操作系统时是如笔者如下截图 那么读者可参考这个解决方案 安装VMware Tools选项显示灰色的正确解…

高等数学下拾遗+与matlab结合

如何学好高等数学 高等数学是数学的一门重要分支&#xff0c;包括微积分、线性代数、常微分方程等内容&#xff0c;它是许多理工科专业的基础课程。以下是一些学好高等数学的建议&#xff1a; 扎实的基础知识&#xff1a;高等数学的内容很多&#xff0c;包括初等数学的一些基…

【数据库】关系型数据库与非关系型数据库解析

【数据库】关系型数据库与非关系型数据库解析 文章目录 【数据库】关系型数据库与非关系型数据库解析1. 介绍2. 关系型数据库3. 非关系型数据库4. 区别4.1 数据存储方式不同4.2 扩展方式不同4.3 对事务性的支持不同4.4 总结 参考 1. 介绍 一个通俗易懂的比喻&#xff1a;关系型…