【流媒体】基于libRTMP的H264推流器

news2024/9/23 21:24:58

目录

  • 1. 整体流程
  • 2. 代码
    • 2.1 头文件
    • 2.2 c文件
  • 3. 测试

RTMP协议相关:
【流媒体】RTMP协议概述
【流媒体】RTMP协议的数据格式
【流媒体】RTMP协议的消息类型
【流媒体】RTMPDump—主流程简单分析
【流媒体】RTMPDump—RTMP_Connect函数(握手、网络连接)
【流媒体】RTMPDump—RTMP_ConnectStream(创建流连接)
【流媒体】RTMPDump—Download(接收流媒体信息)
【流媒体】RTMPDump—AMF编码
【流媒体】基于libRTMP的H264推流器

参考:

  1. libRTMP推流开源工程(附带github工程链接)
    最简单的基于librtmp的示例:发布H.264(H.264通过RTMP发布)
  2. RTMP服务器的搭建
    Windows搭建RTMP服务器+OBS推流+VLC拉流
  3. RTMP封装H264头部的格式
    rtmp传输h.264视频的必备知识(一)

前面记录了RTMP协议相关的内容,本文记录如何使用libRTMP库进行推流,推流的内容是本地H264码流,向RTMP服务器(nginx)进行推流,并且在传输之后使用ffplay进行拉流播放,主要参考了雷博的实现方式,但进行了代码简化,加上了自己的理解和注释。在修改过程中,直接使用了雷博提供的libRTMP库,没有自己编译,后续再了解

1. 整体流程

工程代码结构:
在这里插入图片描述
代码实现的流程步骤为:
(1)创建RTMP文结构体(RTMP_Alloc)
(2)进行连接(librtmp_h264_connect)
 (a)初始化网络sockets(InitSockets)
 (b)初始化RTMP结构体(RTMP_Init)
 (c)设置URL(RTMP_SetupURL)
 (d)设置链接可写(RTMP_EnableWrite)
 (e)RTMP连接(RTMP_Connect)
 (f)RTMP流连接(RTMP_ConnectStream)
(3)发送数据报(librtmp_h264_send)
 (a)寻找H264码流中的单个NAL(find_nal_unit)
 (b)内部发送函数(h264_send_internal)
  (i)获取NAL到SPS或PPS(copy_data)
  (ii)发送SPS和PPS信息(h264_send_metadata)
  (iii)发送frame信息(h264_send_frame)
(4)关闭连接(librtmp_h264_close)
(5)释放RTMP结构体(RTMP_Close)

PS:在传输H264码流时,有一个小问题,发送H264码流时,雷博的代码中会将SEI信息跳过,暂时还不理解

2. 代码

2.1 头文件

#pragma once
#include <stdint.h>

typedef enum {
	NALU_TYPE_SLICE = 1,
	NALU_TYPE_DPA = 2,
	NALU_TYPE_DPB = 3,
	NALU_TYPE_DPC = 4,
	NALU_TYPE_IDR = 5,
	NALU_TYPE_SEI = 6,
	NALU_TYPE_SPS = 7,
	NALU_TYPE_PPS = 8,
	NALU_TYPE_AUD = 9,
	NALU_TYPE_EOSEQ = 10,
	NALU_TYPE_EOSTREAM = 11,
	NALU_TYPE_FILL = 12,
} NALU_TYPE;

typedef struct RTMPMetadata {
	int sps_size;
	uint8_t* sps_data;

	int pps_size;
	uint8_t* pps_data;
}RTMPMetadata_t;

2.2 c文件

c文件基本上是参考雷博的思想实现的,里面涉及到了如何封装SPS、PPS和frame的信息,可以看参考目录的第3条,有详细的解释。代码中关键位置有注解

#pragma warning(disable:4996)

#include <stdio.h>
#include <stdlib.h>
#include "librtmp_sendH264.h"
#include "librtmp\rtmp.h"   
#include "librtmp\rtmp_sys.h"   
#include "librtmp\amf.h"  
//#include "sps_decode.h" // 这里没有去解析fps

#ifdef WIN32     
#include <windows.h>  
#pragma comment(lib,"WS2_32.lib")   
#pragma comment(lib,"winmm.lib")  
#endif 

#define BASE_SIZE		1024
#define MAX_BUF_SIZE	32 * 1024 * 1024
#define RTMP_HEAD_SIZE   (sizeof(RTMPPacket) + RTMP_MAX_HEADER_SIZE)

void debug_byte(uint8_t* data, int size)
{
	for (int m = 0; m < size; m++)
	{
		printf("data[%d]:%x ", m, data[m]);
		if (m % 10 == 0 && m != 0)
		{
			printf("\n");
		}
	}
}

void debug_rtmp_packet(RTMPPacket* rtmp_packet)
{
	printf("m_headerType:%d\n", rtmp_packet->m_headerType);
	printf("m_packetType:%d\n", rtmp_packet->m_packetType);
	printf("m_hasAbsTimestamp:%d\n", rtmp_packet->m_hasAbsTimestamp);
	printf("m_nChannel:%d\n", rtmp_packet->m_nChannel);
	printf("m_nTimeStamp:%d\n", rtmp_packet->m_nTimeStamp);
	printf("m_nInfoField2:%d\n", rtmp_packet->m_nInfoField2);
	printf("m_nBodySize:%d\n", rtmp_packet->m_nBodySize);
	printf("m_nBytesRead:%d\n", rtmp_packet->m_nBytesRead);
	printf("m_nBody\n");
	debug_byte((uint8_t*)rtmp_packet->m_body, rtmp_packet->m_nBodySize);
}
// 查找起始码,能够找出一个NALU的起始位置和结束位置
int find_nal_unit(uint8_t* buf, int size, int* nal_start, int* nal_end)
{
	int i;
	// find start
	*nal_start = 0;
	*nal_end = 0;

	i = 0;
	while (   //( next_bits( 24 ) != 0x000001 && next_bits( 32 ) != 0x00000001 )
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) &&
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0 || buf[i + 3] != 0x01)
		)
	{
		i++; // skip leading zero
		if (i + 4 >= size) { return 0; } // did not find nal start
	}

	if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) // ( next_bits( 24 ) != 0x000001 )
	{
		i++;
	}

	if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) { /* error, should never happen */ return 0; }
	i += 3;
	*nal_start = i;

	while (   //( next_bits( 24 ) != 0x000000 && next_bits( 24 ) != 0x000001 )
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0) &&
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01)
		)
	{
		i++;
		// FIXME the next line fails when reading a nal that ends exactly at the end of the data
		if (i + 3 >= size) { *nal_end = size; return -1; } // did not find nal end, stream ended first
	}

	*nal_end = i;
	return (*nal_end - *nal_start);
}
// 初始化sockets
int InitSockets()
{
	WORD version;
	WSADATA wsaData;
	version = MAKEWORD(2, 2); // RTMPDump中使用1.1,目前应尽量使用2.2
	return (WSAStartup(version, &wsaData) == 0);
}

// 网络连接
int librtmp_h264_connect(RTMP* rtmp, const char* url)
{
	int ret = 0;
	// 初始化socket
	ret = InitSockets();
	if (ret < 0)
	{
		printf("Init sockets failed\n");
		return -1;
	}
	RTMP_Init(rtmp);

	ret = RTMP_SetupURL(rtmp, (char*)url);
	if (ret < 0)
	{
		printf("Setup URL failed\n");
		return -1;
	}

	// 设置可写
	RTMP_EnableWrite(rtmp);

	// RTMP连接
	ret = RTMP_Connect(rtmp, NULL);
	if (ret < 0)
	{
		printf("RTMP Connect failed\n");
		return -1;
	}
	// RTMP网络连接
	ret = RTMP_ConnectStream(rtmp, NULL);
	if (ret < 0)
	{
		printf("RTMP Connect Stream failed\n");
		return -1;
	}

	return 0;
}

int copy_data(uint8_t** dst, uint8_t* src, int size)
{
	*dst = (uint8_t*)malloc(size * sizeof(uint8_t));
	memcpy(*dst, src, size);
	return size;
}

// 发送metadata(SPS和PPS)
int h264_send_metadata(RTMP* rtmp, RTMPMetadata_t* rtmp_meta)
{
	RTMPPacket* packet = NULL;
	unsigned char* body = NULL;
	int i;
	uint8_t* sps = rtmp_meta->sps_data;
	uint8_t* pps = rtmp_meta->pps_data;
	int sps_len = rtmp_meta->sps_size;
	int pps_len = rtmp_meta->pps_size;

	packet = (RTMPPacket*)malloc(RTMP_HEAD_SIZE + 1024);
	if (!packet)
	{
		printf("malloc packet failed\n");
		return -1;
	}
	memset(packet, 0, RTMP_HEAD_SIZE + 1024);
	packet->m_body = (char*)packet + RTMP_HEAD_SIZE;
	body = (unsigned char*)packet->m_body;
	i = 0;
	/*
		下面对SPS和PPS信息进行了封装,封装的格式参考
		https://blog.csdn.net/dqxiaoxiao/article/details/94820599
	*/
	body[i++] = 0x17;	// 1: keyframe, 7: AVC
	body[i++] = 0x00;	// AVPacketType

	body[i++] = 0x00;	// Composition Time (3 Bytes)
	body[i++] = 0x00;
	body[i++] = 0x00;

	/*AVCDecoderConfigurationRecord*/ // 
	body[i++] = 0x01;	// 版本号固定为1
	body[i++] = sps[1];	// AVCProfileIndication
	body[i++] = sps[2];	// profile_compability
	body[i++] = sps[3];	// AVCLevelIndication
	body[i++] = 0xff;	// reserved

	/*sps*/
	body[i++] = 0xe1;					// 0xe1 : 1110 0001,高3位为保留位,默认全为1,低5位表示SPS的数量,这里设置为1
	body[i++] = (sps_len >> 8) & 0xff;	// 下面两个字节都是,SPS的长度
	body[i++] = sps_len & 0xff;			// SPS的长度
	memcpy(&body[i], sps, sps_len);		// 将长度为sps_len的SPS信息赋值到body中
	i += sps_len;

	/*pps*/
	body[i++] = 0x01;					// PPS的个数
	body[i++] = (pps_len >> 8) & 0xff;	// 下面两个字节都是PPS的长度
	body[i++] = (pps_len) & 0xff;
	memcpy(&body[i], pps, pps_len);		// 将长度为pps_len的PPS信息赋值到body中
	i += pps_len;

	// 处理头信息
	packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
	packet->m_nBodySize = i;
	packet->m_nChannel = 0x04;
	packet->m_nTimeStamp = 0;
	packet->m_hasAbsTimestamp = 0;
	packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
	packet->m_nInfoField2 = rtmp->m_stream_id;

	//printf("own test\n");
	//debug_byte(body, i);
	//debug_rtmp_packet(packet);

	/*调用发送接口*/
	int ret = RTMP_SendPacket(rtmp, packet, TRUE);
	free(packet);    //释放内存
	return ret;
}

int rtmp_send_packet(RTMP* rtmp, uint8_t* data, int size, int timestamp)
{
	RTMPPacket* packet;
	packet = (RTMPPacket*)malloc(RTMP_HEAD_SIZE + size);
	if (!packet)
	{
		printf("malloc packet failed\n");
		return -1;
	}
	memset(packet, 0, RTMP_HEAD_SIZE);
	packet->m_body = (char*)packet + RTMP_HEAD_SIZE;
	packet->m_nBodySize = size;
	memcpy(packet->m_body, data, size);
	packet->m_hasAbsTimestamp = 0;
	packet->m_packetType = RTMP_PACKET_TYPE_VIDEO; // Video
	packet->m_nInfoField2 = rtmp->m_stream_id;
	packet->m_nChannel = 0x04;
	packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
	packet->m_nTimeStamp = timestamp;
	int ret = 0;
	if (RTMP_IsConnected(rtmp))
	{
		ret = RTMP_SendPacket(rtmp, packet, TRUE); /*TRUE为放进发送队列,FALSE是不放进发送队列,直接发送*/
	}
	/*释放内存*/
	free(packet);
	return ret;
}

int h264_send_frame(RTMP* rtmp, RTMPMetadata_t* rtmp_meta, int is_keyframe, uint8_t* data, int size, int timestamp)
{
	unsigned char* body = (unsigned char*)malloc(size + 9);
	if (!body)
	{
		printf("malloc body failed\n");
		return -1;
	}
	memset(body, 0, size + 9);
	int i = 0;
	/*
		下面对frame进行了封装,封装的格式参考
		https://blog.csdn.net/dqxiaoxiao/article/details/94820599
	*/
	if (is_keyframe)
	{
		body[i++] = 0x17;// 1: keyframe  7:AVC
	}
	else
	{
		body[i++] = 0x27;// 2: p-frame  7:AVC
	}
	body[i++] = 0x01;// AVC NALU   
	body[i++] = 0x00;
	body[i++] = 0x00;
	body[i++] = 0x00;

	// NALU size
	body[i++] = size >> 24 & 0xff;
	body[i++] = size >> 16 & 0xff;
	body[i++] = size >> 8 & 0xff;
	body[i++] = size & 0xff;
	// NALU data
	memcpy(&body[i], data, size);

	int ret = rtmp_send_packet(rtmp, body, i + size, timestamp);
	free(body);

	return ret;
}

int h264_send_internal(RTMP* rtmp, RTMPMetadata_t* rtmp_meta, uint8_t* data, int nal_start, int nal_end, int timestamp)
{
	int nal_size = nal_end - nal_start;
	int nal_type = data[nal_start] & 0x0F;
	int is_keyframe = (nal_type == NALU_TYPE_IDR) ? TRUE : FALSE;
	switch (nal_type)
	{
	case NALU_TYPE_SPS:
		rtmp_meta->sps_size = copy_data(&rtmp_meta->sps_data, data + nal_start, nal_size);
		break;
	case NALU_TYPE_PPS:
		rtmp_meta->pps_size = copy_data(&rtmp_meta->pps_data, data + nal_start, nal_size);
		break;
	case NALU_TYPE_SEI: // SEI信息不作处理,直接跳过
		break;
	case NALU_TYPE_IDR:
	case NALU_TYPE_SLICE:
		if (is_keyframe)
		{
			h264_send_metadata(rtmp, rtmp_meta); // 在发送keyframe之前必须发送metadata
			printf("send metadata\n");
		}
		h264_send_frame(rtmp, rtmp_meta, is_keyframe, data, nal_size, timestamp);
		printf("send frame\n");
		break;
	default:
		break;
	}
	

	return 0;
}

int librtmp_h264_send(RTMP* rtmp)
{
	FILE* file;
	file = fopen("test.h264", "rb");
	if (!file)
	{
		printf("read stream file failed\n");
		return -1;
	}

	uint8_t* buf = (uint8_t*)malloc(MAX_BUF_SIZE);
	if (!buf)
	{
		printf("malloc buf failed\n");
		return -1;
	}

	RTMPMetadata_t rtmp_meta = { 0 };
	size_t rsz = 0;
	size_t sz = 0;
	int64_t off = 0;
	uint8_t* p = buf;
	int nal_start = 0;
	int nal_end = 0;
	int timestamp = 0;
	int frame_rate = 25;

	while (1)
	{
		rsz = fread(buf + sz, 1, MAX_BUF_SIZE - sz, file);
		if (!rsz)
		{
			printf("EOF\n");
			break;
		}
		sz += rsz;
		while (find_nal_unit(p, sz, &nal_start, &nal_end) > 0)
		{
			h264_send_internal(rtmp, &rtmp_meta, p, nal_start, nal_end, timestamp);
			timestamp += 1000 / frame_rate;
			msleep(80); // 这里简化了一下,使用固定的时间间隔

			p += nal_end;
			sz -= nal_end;
		}

		memmove(buf, p, sz);
		off += p - buf;
		p = buf;
	}

	if (rtmp_meta.sps_data)
	{
		free(rtmp_meta.sps_data);
	}
	if (rtmp_meta.pps_data)
	{
		free(rtmp_meta.pps_data);
	}

	return 0;
}

int CleanupSockets()
{
	WSACleanup();
	return 0;
}

int librtmp_h264_close(RTMP* rtmp)
{
	CleanupSockets();
	return 0;
}

int main(int argc, char* argv[])
{
	RTMP* rtmp = RTMP_Alloc();
	int ret;

	const char* url = "rtmp://127.0.0.1:1935/live/stream";
	// 1. 网络连接
	librtmp_h264_connect(rtmp, url);
	// 2. 发送数据报
	librtmp_h264_send(rtmp);
	// 3. 关闭连接
	librtmp_h264_close(rtmp);

	RTMP_Close(rtmp);
	return 0;
}

3. 测试

测试的步骤为:
(1)启动RTMP服务器(nginx)
(2)使用libRTMP进行推流
 (a)码流为Crew序列
 (b)因为是将本机作为RTMP服务器,可以在浏览器中输入如下地址来查看服务器的情况

http://localhost/stat

如下所示,这里因为来回测试了很多遍,所以持续Time比较久。从stream中一栏可以看到video的相关信息,主要包括codec和size
在这里插入图片描述

(3)使用ffplay进行拉流查看,命令行为

.\ffplay.exe rtmp://127.0.0.1:1935/live/stream

在这里插入图片描述

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

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

相关文章

智云-一个抓取web流量的轻量级蜜罐docker一键启动

智云-一个抓取web流量的轻量级蜜罐docker安装教程 github地址 https://github.com/xiaoxiaoranxxx/POT-ZHIYUN docker快速启动(v1.4) git clone https://github.com/xiaoxiaoranxxx/POT-ZHIYUN.git cd POT-ZHIYUN docker-compose up -d默认映射到80和8080端口 mysql不对外开放…

leetcode67. 二进制求和,简单模拟

leetcode67. 二进制求和 给你两个二进制字符串 a 和 b &#xff0c;以二进制字符串的形式返回它们的和。 示例 1&#xff1a; 输入:a “11”, b “1” 输出&#xff1a;“100” 示例 2&#xff1a; 输入&#xff1a;a “1010”, b “1011” 输出&#xff1a;“10101” …

【Java】 力扣 最大子数组和

目录 题目链接题目描述思路代码 题目链接 53.最大子数组和 题目描述 思路 动态规划解析&#xff1a; 状态定义&#xff1a; 设动态规划列表 dp &#xff0c;dp[i] 代表以元素 nums[i] 为结尾的连续子数组最大和。 为何定义最大和 dp[i] 中必须包含元素 nums[i] &#xff1a;…

一款免费的文件锁定占用解除工具,绿色免安装版

IObit Unlocker是一款由IObit公司开发的免费文件解锁工具&#xff0c;旨在解决用户在删除、重命名、移动或复制文件和文件夹时遇到的“无法删除”或“访问被拒绝”的问题。该软件体积小巧&#xff0c;不到3MB&#xff0c;非常易于使用&#xff0c;并且不需要安装&#xff0c;可…

【现代操作系统】3. 中断、异常、系统调用

通用概念 中断&#xff08;Interrupt&#xff09; 外部硬件设备所产生的信号异步&#xff1a;产生原因和当前执行指令无关&#xff0c;如程序被磁盘读打断 异常&#xff08;Exception&#xff09; 软件的程序执行而产生的事件包括系统调用同步&#xff1a;产生和当前执行或试图…

【AI学习】LLaMA模型的微调成本有几何?

在前面文章《LLaMA 系列模型的进化&#xff08;二&#xff09;》中提到了Stanford Alpaca模型。 Stanford Alpaca 基于LLaMA (7B) 进行微调&#xff0c;通过使用 Self-Instruct 方法借助大语言模型进行自动化的指令生成&#xff0c;Stanford Alpaca 生成了 52K 条指令遵循样例数…

【数据结构与算法】穷举搜索

穷举搜索目录 一.穷举搜索的原理二.穷举问题的引入三.穷举搜索的实现四.穷举搜索的高效版 一.穷举搜索的原理 列出所有可能出现的情况,逐个判断有那些是符合问题要求的条件. 通常可以从两方面分析: 问题所涉及的情况答案需要满足的条件 二.穷举问题的引入 有20枚硬币&#…

电价预测 | TSOA-TCN-Attention凌日算法优化时序卷积神经网络电价预测

目录 效果一览基本介绍程序设计 效果一览 基本介绍 电价预测 | TSOA-TCN-Attention凌日算法优化时序卷积神经网络电价预测 电价预测需求&#xff1a;随着能源市场的开放和电力交易的增加&#xff0c;准确的电价预测对于市场参与者的决策至关重要。而时序数据中的规律和趋势对于…

中小型制造企业质量管理设计与实现

文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家&#xff0c;服务很多代码文档&#xff0c;百分百好评&#xff0c;战绩可查&#xff01;&#xff01;入职于互联网大厂&#xff0c;可以交流&#xff0c;共同进步。有保障的售后 代码参考数据库参…

多线程并行

多线程并行、所有线程结束后输出任务完成 示例 package com.fd;import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger;public class Test3 {public static void main(String[] args) throws InterruptedException {AtomicInteger counter…

【数据结构入门】二叉树之堆的实现

文章目录 前言一、树1.1 树的概念1.2 树的相关概念 二、二叉树2.1 二叉树的概念2.2 特殊的二叉树2.3 二叉树的性质 三、堆3.1 堆的概念3.2 堆的性质3.3 堆的存储3.4 堆的实现3.4.1 堆的初始化3.4.2 堆的销毁3.4.1 堆向上调整算法3.4.2 堆向下调整算法3.4.3 堆的创建3.4.4 堆的插…

MT2523AS 原边10瓦无外围方案PCB设计要点

MT2523AS 是自供电原边反馈5V2A(10瓦)电源芯片。MT2523AS 内置功率三极管&#xff0c;采用脉冲频率调制&#xff08;PFM&#xff09;建立非连续导电模式&#xff08;DCM&#xff09;的反激式电源&#xff0c;外围设计极简化。MT2523AS 具有可变原边峰值电流&#xff0c;通过最大…

AI技术在招聘人才笔试测评中的作用

一、引言 在快速变化的商业环境中&#xff0c;企业之间的竞争日益激烈&#xff0c;而人才作为企业发展的核心驱动力&#xff0c;其选拔与培养显得尤为重要。传统的人才招聘流程&#xff0c;尤其是笔试测评环节&#xff0c;往往依赖于人工阅卷、主观判断&#xff0c;不仅效率低…

每日一问:深入理解JVM——结构与类的加载过程解析

每日一问&#xff1a;深入理解JVM——结构与类的加载过程解析 在Java的世界中&#xff0c;JVM&#xff08;Java Virtual Machine&#xff0c;Java虚拟机&#xff09;是一个核心概念。它是Java程序能够跨平台运行的基础&#xff0c;负责执行Java字节码&#xff0c;并为Java应用程…

成为Python砖家(3): 何时产生字节码 .pyc 文件

好奇&#xff1a;.pyc和 __pycache__是啥&#xff1f; 你是否好奇&#xff0c;在某些 Python 工程中&#xff0c;当执行了 xxx.py脚本后&#xff0c;多出了 __pycache__目录&#xff1f;这个目录下存放的是一些 .pyc结尾的文件。 这些文件&#xff0c;叫做 python bytecode。 …

电子信息工程专业学习路线的制定与实践

电子信息工程专业是一个多学科交叉、技术更新迅速的领域。对于大学生来说&#xff0c;制定合适的学习路线并有效学习专业知识至关重要。 目录 一、明确学习目标 二、构建知识体系 三、掌握基础知识 四、深入专业课程 五、实践与理论相结合 六、学习编程语言 七、参与科研…

345345

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

数学建模起步感受(赛前15天)

0基础直接上手数模&#xff0c;因为大一&#xff01;年轻就是无所畏惧&#xff01;开个玩笑&#xff0c;因为数模比赛比一年少一年… 抱着不打也是浪费的态度&#xff0c;我开始着手准备 首先python啥也不会&#xff0c;知道有元组这玩意… 仅仅在刷软考题的时候遇到python选择…

[数据集][目标检测]竹子甘蔗发芽缺陷检测数据集VOC+YOLO格式2953张3类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2953 标注数量(xml文件个数)&#xff1a;2953 标注数量(txt文件个数)&#xff1a;2953 标注…

计算机Java项目|基于SpringBoot的医疗报销系统的设计与实现

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参…