OpenCV(四)—— 车牌号识别

news2025/1/14 1:23:05

本节是车牌识别的最后一部分 —— 车牌字符识别,从一个完整的车牌图片到识别出车牌上的字符大致需要如下几步:

  • 预处理:将车牌图片灰度化、二值化,并去除识别时的干扰因素,比如车牌铆钉
  • 字符分割:将整个车牌图片按照每个字符分割成 7 个单独的字符图片保存到集合中
  • 字符识别:使用经过训练的数字、英文字符、中文字符的特征集合对字符图片进行识别,得到最终的车牌号

下面详解以上步骤。

1、预处理

图像识别的预处理工作一般都是灰度化、二值化这些图像“降噪”处理,当然我们这里还有一个特殊的处理,就是要祛除车牌图像上的铆钉,它是对识别准确度影响较大的一个因素。

1.1 AnnPredictor 预处理

新建一个类 AnnPredictor 用于进行字符识别:

#ifndef ANNPREDICTOR_H
#define ANNPREDICTOR_H

#define ANNPREDICTOR_DEBUG

#include <opencv2/opencv.hpp>
#include <string>
#include <opencv2/ml.hpp>

using namespace std;
using namespace cv;
using namespace ml;

class AnnPredictor {
public:
	AnnPredictor(const char* ann_model, const char* ann_zh_model);
	~AnnPredictor();
	// 字符识别
	string predict(Mat plate);

private:
	// 用于数字和英文字符识别
	Ptr<ANN_MLP> ann;
	// 用于中文字符识别
	Ptr<ANN_MLP> ann_zh;
	// ANN 的 HOG 特征
	HOGDescriptor* annHog = nullptr;
	// 与 SvmPredictor 中的函数相同
	void getHOGFeatures(HOGDescriptor* svmHog, Mat src, Mat& dst);
	// 去除铆钉
	bool clearRivet(Mat &plate);
	// 验证单个字符尺寸
	bool verifyCharSize(Mat src);
	// 获取城市汉字字符在排序后的矩形集合中的索引
	int getCityIndex(vector<Rect> rects);
	// 获取中文字符所在的矩形
	void getChineseRect(Rect cityRect, Rect& chineseRect);
	// 识别车牌字符保存到 str_plate 中
	void predict(vector<Mat> plateCharMats, string& str_plate);
	// 汉字字符集合
	static string ZHCHARS[];
	// 数字与英文字符集合
	static char CHARS[];
};

#endif // !ANNPREDICTOR_H

这样在 LicensePlateRecognizer 中调用 predict() 即可获取到车牌字符串:

LicensePlateRecognizer::LicensePlateRecognizer(const char* svm_model, const char* ann_model, const char* ann_zh_model)
{
	...
	annPredictor = new AnnPredictor(ann_model, ann_zh_model);
}

LicensePlateRecognizer::~LicensePlateRecognizer()
{
	...
	if (annPredictor)
	{
		delete annPredictor;
		annPredictor = nullptr;
	}
}

string LicensePlateRecognizer::recognize(Mat src)
{
	// 1.车牌定位,使用 Sobel 算法定位
	// 2.精选车牌定位得到的候选车牌图,找到最有可能是车牌的图
	...

	// 3.对车牌图进行字符识别
	string str_plate = annPredictor->predict(plate);
	plate.release();

	return str_plate;
}

predict() 内,先进行预处理,像灰度化和二值化这些操作前面已出现过多次就不再赘述了:

string AnnPredictor::predict(Mat plate)
{
	// 1.预处理 
	// 1.1 灰度化
	Mat gray;
	cvtColor(plate, gray, COLOR_BGR2GRAY);

	// 1.2 二值化(非黑即白,对比更强烈)
	Mat shold;
	threshold(gray, shold, 0, 255, THRESH_OTSU + THRESH_BINARY);

	// 1.3 去铆钉
	if (!clearRivet(shold))
	{
		return string("未识别到车牌");
	}
    ...
}

主要说一下如何去掉车牌图片上的铆钉。

1.2 去掉车牌上的铆钉

二值化后的图片仍能看到车牌上的铆钉,这是影响识别准确程度的一个干扰因素:

去柳钉前

因此我们要去掉它,去掉后的效果:

去柳钉后

去除铆钉的思路是,对车牌图像进行逐行扫描,如果这一行是铆钉,那么颜色跳变的次数应该为 4 次,远远少于正常字符的颜色跳变次数:

2024-4-4.去柳钉前颜色跳变示意图1

上面红线是扫描到铆钉的行,先是黑色,扫描到铆钉变为白色,离开铆钉再变为黑色,第二颗铆钉重复上述过程,因此有 4 次黑白之间的颜色跳变。而第二条红线扫描到正常字符,跳变次数远远大于 4,我们就用这个思路去除铆钉:

/**
* 通过一行的颜色跳变次数判断是否扫描到了铆钉,
* 一行最小跳变次数为 12,最大为 12 + 8 * 6 = 60。
* 如果该行是铆钉行,则将该行所有像素都涂成黑色(像素值为 0)
*/
bool AnnPredictor::clearRivet(Mat &plate)
{
	// 1.逐行扫描统计颜色跳变次数保存到集合中
	int minChangeCount = 12;
	vector<int> changeCounts;
	int changeCount;
	for (int i = 0; i < plate.rows; i++)
	{
		for (int j = 0; j < plate.cols - 1; j++)
		{
			int pixel_front = plate.at<char>(i, j);
			int pixel_back = plate.at<char>(i, j + 1);
			if (pixel_front != pixel_back)
			{
				changeCount++;
			}
		}
		changeCounts.push_back(changeCount);
		changeCount = 0;
	}

	// 2.计算字符高度,即满足像素跳变次数的行数
	int charHeight = 0;
	for (int i = 0; i < plate.rows; i++)
	{
		if (changeCounts[i] >= 12 && changeCounts[i] <= 60)
		{
			charHeight++;
		}
	}

	// 3.判断字符高度 & 面积占整个车牌的高度 & 面积的百分比,排除不符合条件的情况
	// 3.1 高度占比小于 0.4 则认为无法识别
	float heightPercent = float(charHeight) / plate.rows;
	if (heightPercent <= 0.4)
	{
		return false;
	}
	// 3.2 面积占比小于 0.15 或大于 0.5 则认为无法识别
	float plate_area = plate.rows * plate.cols;
	// countNonZero 返回非 0 像素点(即白色)个数,或者自己遍历找像素点为 255 的个数也可
	float areaPercent = countNonZero(plate) * 1.0 / plate_area;
	// 小于 0.15 就是蓝背景白字车牌确实达不到识别标准,大于 0.5 是因为
	// 黄背景黑子二值化会把背景转化为白色,由于前面的处理逻辑只能处理
	// 蓝背景车牌,所以黄色车牌的情况也直接认为不可识别
	if (areaPercent <= 0.15 || areaPercent >= 0.5)
	{
		return false;
	}

	// 4.将小于最小颜色跳变次数的行全部涂成黑色
	for (int i = 0; i < plate.rows; i++)
	{
		if (changeCounts[i] < minChangeCount)
		{
			for (int j = 0; j < plate.cols; j++)
			{
				plate.at<char>(i, j) = 0;
			}
		}
	}

	return true;
}

2、字符分割

接下来开始分割字符,主要可以分为三部分:

  1. 找到各个字符的轮廓,并生成轮廓对应的图片
  2. 对汉字字符的轮廓图片进行特殊处理
  3. 将所有 7 个字符的轮廓图片保存到集合中为字符识别做准备

下面详解。

2.1 生成字符轮廓图片

先通过寻找轮廓的函数 findContours() 找到轮廓,生成轮廓矩形集合 vec_ann_rects:

string AnnPredictor::predict(Mat plate)
{
	// 1.预处理 
	...

	// 2.字符分割
	// 2.1 找轮廓
	// vector<Point>是点的集合,可以连成线,线的集合就是轮廓了
	vector<vector<Point>> contours;
	findContours(shold, // 输入的图像
		contours, // 轮廓,接收结果
		RETR_EXTERNAL, // 轮廓检索模式:外轮廓
		CHAIN_APPROX_NONE // 轮廓近似算法模式:不进行轮廓近似,保留所有的轮廓点
	);

	vector<Rect> vec_ann_rects;
	// 在原图上克隆一个用来画矩形
	Mat src_clone = plate.clone();
	for each (vector<Point> points in contours) {
		Rect rect = boundingRect(points);
		Mat rectMat = shold(rect);
		// rectangle(src_clone, rect, Scalar(0, 0, 255));
		// 尺寸判断,符合规格的放入 vec_sobel_rects 集合中
		if (verifyCharSize(rectMat)) {
			vec_ann_rects.push_back(rect);
		}
	}
	...
}

遍历 vec_ann_rects 生成轮廓矩形并生成与矩形对应的图片,这就是分割的字符图片。当然,在将它们存入集合前你需要过滤一下,因为不是所有轮廓都刚好是一个完整的字符。比如“渝”字,由于汉字相比于英文字符和数字,结构复杂,因此无法识别为整个字,而是识别出“渝”字中点或者某个局部部分:

2024-4-8.字符找轮廓效果图2

因为我们导出的车牌宽度只有 136 像素,所以放大后并不清晰,但是能看出来,英文字母和数字的轮廓只有一个,而“渝”的轮廓有多个。面对这种情况,我们先用 verifyCharSize() 进行尺寸校验,将不符合规格的字符图片过滤掉:

bool AnnPredictor::verifyCharSize(Mat src)
{
	// 最理想情况 车牌字符的标准宽高比
	float aspect = 45.0f / 90.0f;
	// 当前获得矩形的真实宽高比
	float realAspect = (float)src.cols / (float)src.rows;
	// 最小的字符高
	float minHeight = 10.0f;
	// 最大的字符高
	float maxHeight = 35.0f;
	// 1、判断高符合范围  2、宽、高比符合范围
	// 最大宽、高比 最小宽高比
	float error = 0.7f;
	float maxAspect = aspect + aspect * error;//0.85
	float minAspect = 0.05f;

	int plate_area = src.cols * src.rows;
	float areaPercent = countNonZero(src) * 1.0 / plate_area;

	if (areaPercent <= 0.8 && realAspect >= minAspect && realAspect <= maxAspect
		&& src.rows >= minHeight &&
		src.rows <= maxHeight) {
		return true;
	}
	return false;
}

汉字被识别为多个部分,无法通过 verifyCharSize() 的校验,会被过滤掉。也就是说,这一步中,我们只获取了英文和数字图片,汉字图片暂时还未获取。

2.2 获取汉字轮廓

思路是,先定位到汉字后面表示城市的那一位英文字符,再向左侧推导获取汉字轮廓。

首先,根据图片的横坐标对 vec_ann_rects 集合内的字符图片进行从左至右的排序:

string AnnPredictor::predict(Mat plate)
{
    ...
    // 2.2 对矩形轮廓从左至右排序
	sort(vec_ann_rects.begin(), vec_ann_rects.end(), [](const Rect& rect1, const Rect& rect2) {
		return rect1.x < rect2.x;
		});
    ...
}

然后获取到表示城市字符的轮廓索引:

string AnnPredictor::predict(Mat plate)
{
    ...
    // 2.3 获取城市字符轮廓的索引
	int cityIndex = getCityIndex(vec_ann_rects);
    ...
}

getCityIndex() 获取城市字符索引的思路是:车牌上共有 7 个字符,那么城市字符位于第 2 位,该字符中间横坐标一定在车牌水平方向的 1/7 ~ 2/7 之间:

/**
* 寻找城市字符(7 位字符中的第 2 位)轮廓索引
*/
int AnnPredictor::getCityIndex(vector<Rect> rects)
{
	int cityIndex = 0;
	for (int i = 0; i < rects.size(); i++)
	{
		Rect rect = rects[i];
		int midX = rect.x + rect.width / 2;
		// 如果字符水平方向中点坐标在整个车牌水平坐标的
		// 1/7 ~ 2/7 之间,就认为是目标索引。136 是我们
		// 训练车牌使用的素材的车牌宽度
		if (midX < 136 / 7 * 2 && midX > 136 / 7)
		{
			cityIndex = i;
			break;
		}
	}
	return cityIndex;
}

获取城市字符索引后,可以根据其横坐标推断出汉字字符的轮廓:

string AnnPredictor::predict(Mat plate)
{
    ...
    // 2.4 推导汉字字符的轮廓
	Rect chineseRect;
	getChineseRect(vec_ann_rects[cityIndex], chineseRect);
    ...
}

getChineseRect() 会将推断出的矩形保存到 chineseRect 中:

/**
* 通过城市字符的矩形,确定汉字字符的矩形
*/
void AnnPredictor::getChineseRect(Rect cityRect, Rect& chineseRect)
{
	// 把宽度稍微扩大一点以包含完整的汉字字符
	// 还有一层理解,就是汉字与城市字符之间的空隙也要计算进去
	float width = cityRect.width * 1.15;

	// 城市轮廓矩形的横坐标
	int x = cityRect.x;

	// 用城市矩形的横坐标减去汉字宽度得到汉字矩形的横坐标
	int newX = x - width;
	chineseRect.x = newX > 0 ? newX : 0;
	chineseRect.y = cityRect.y;
	chineseRect.width = width;
	chineseRect.height = cityRect.height;
}

2.3 保存所有字符图片

最后将 7 个字符的图片保存到 plateCharMats 集合中等待字符识别:

string AnnPredictor::predict(Mat plate)
{
    ...
	// 2.5 将字符图像保存到集合中
	// 先保存汉字字符图像
	vector<Mat> plateCharMats;
	plateCharMats.push_back(shold(chineseRect));

	// 再获取汉字之后的 6 个字符并保存
	int count = 6;
	if (vec_ann_rects.size() < 6)
	{
		return string("未识别到车牌");
	}

	for (int i = cityIndex; i < vec_ann_rects.size() && count; i++, count--)
	{
		plateCharMats.push_back(shold(vec_ann_rects[i]));
	}
    ...
}

3、字符识别

3.1 识别过程

调用 predict() 传入字符图片集合 plateCharMats,识别的字符结果保存在 str_plate 中:

string AnnPredictor::predict(Mat plate)
{
    ...
    // 3.字符识别
	string str_plate;
	predict(plateCharMats, str_plate);
	for (Mat m : plateCharMats) {
		m.release();
	}

	// 4.释放 Mat
	gray.release();
	shold.release();
	src_clone.release();
	
	return str_plate;
}

predict() 内遍历 plateCharMats,提取 HOG 特征后对汉字和数字英文分开识别,注意 ZHCHARS 与 CHARS 内的字符顺序要和训练样本存放顺序相同:

string AnnPredictor::ZHCHARS[] = { "川", "鄂", "赣", "甘", "贵", "桂", "黑", "沪", "冀", "津", "京", "吉", "辽", "鲁", "蒙", "闽", "宁", "青", "琼", "陕", "苏", "晋", "皖", "湘", "新", "豫", "渝", "粤", "云", "藏", "浙" };
char AnnPredictor::CHARS[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

void AnnPredictor::predict(vector<Mat> plateCharMats, string& result)
{
	for (int i = 0; i < plateCharMats.size(); i++)
	{
		Mat mat_plate_char = plateCharMats[i];

		// 提取 HOG 特征
		Mat features;
		getHOGFeatures(annHog, mat_plate_char, features);

		Mat sample = features.reshape(1, 1);
		Mat response;
		Point maxLoc;
		Point minLoc;

		if (i)
		{
			// 字母和数字
			ann->predict(sample, response);
			minMaxLoc(response, 0, 0, &minLoc, &maxLoc);
			int index = maxLoc.x;
			result += CHARS[index];
		}
		else
		{
			// 汉字
			ann_zh->predict(sample, response);
			minMaxLoc(response, 0, 0, &minLoc, &maxLoc);
			int index = maxLoc.x;
			result += ZHCHARS[index];
		}
	}
}

至此代码结束,先来看一下效果:

2024-4-8.最终识别结果1

对于【渝G 83666】的识别结果为【渝G 80666】,错了一位,这与识别的算法,还有训练样本的数量都有关系。总的来说,Demo 提供了一种车牌识别的思路,但是准确度还是有限的。

3.2 样本制作

最后来说说样本是如何制作的。

识别车牌字符,需要所有字符训练的特征集合,即首位的汉字共 31 个字符、第二位英文字符 24 个(刨除 I 和 O 两个容易被误识别为 1 和 0)、以及后续位数中需要用到的 10 个数字。我们将训练样本分为两类:英文字符和数字的训练样本放入 ann 文件夹,汉字字符放入 ann_zh 文件夹。两个文件夹内都需要对字符进行编号:

  • ann 中 0 号文件夹是数字 0 的训练素材,1 号文件夹是数字 1 的训练素材,以此类推,总共是 10 + 24 = 24 个文件夹
  • ann_zh 中 1 号文件夹是“川”字的训练素材,2 号文件夹是“鄂”字的训练素材,共计 31 个文件夹

这些字符的编号顺序需要记录在一个额外的文档中,内容如下:

"川", "鄂", "赣", "甘", "贵", "桂", "黑", "沪", "冀", "津", "京", "吉", "辽", "鲁", "蒙", "闽", "宁", "青", "琼", "陕", "苏", "晋", "皖", "湘", "新", "豫", "渝", "粤", "云", "藏", "浙"
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'

训练后可以得到两个特征集合文件 ann.xml(数字和英文字符)和 ann_zh.xml(汉字字符),正是初始化 AnnPredictor 的 ann 和 ann_zh 所加载的文件。目录结构如下:

2024-4-8.车牌字符样本训练目录

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

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

相关文章

基于springboot+vue+Mysql的影城管理系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

CATO原理中的数学与魔术(七)——Baby Hummer的拓展二

在上一篇文章中&#xff0c;我们介绍了《Baby Hummer》的两个拓展&#xff0c;从表演的形式台词&#xff0c;到呈现的策略&#xff0c;都有一定的改进&#xff0c;相关内容请戳&#xff1a; CATO原理中的数学与魔术&#xff08;六&#xff09;——Baby Hummer的拓展一 CATO原理…

WebSocket 多屏同显和异显

介绍 多屏同显:通过在一个应用上进行操作之后,另一个应用也能跟着一起发生改变,例如app1播放了晴天这首音乐,那么app2也要同步播放这首音乐,确保所有屏幕显示的内容完全相同。多屏异显:每个屏幕可以显示不同的内容,或者在内容更新时存在一定的延迟,而不需要严格保持同步…

MLP手写数字识别(1)-MNIST数据集下载与可视化(tensorflow)

1.下载与查看MNIST数据集 from keras.datasets import mnist(x_train_image,y_train_label),(x_test_image,y_test_label) mnist.load_data() print("train images:",x_train_image.shape) print("test images:",x_test_image.shape) print("train …

83、动态规划-打家劫舍

思路&#xff1a; 首先使用递归方式求出最优解。从每个房屋开始&#xff0c;分别考虑偷与不偷两种情况&#xff0c;然后递归地对后续的房屋做同样的决策。这种方法确保了可以找到在不触发警报的情况下可能的最高金额。 代码如下&#xff1a; public static int rob(int[] nu…

【多变量控制系统 Multivariable Control System】(2)给定系统转换方程,绘制奈奎斯特图【新加坡南洋理工大学】

一、系统转换方程 系统的转换方程 G(s) 和回馈矩阵 K(s) 由下2式给出&#xff1a; 二、从转换方程推出系统矩阵A和B from scipy.signal import tf2ss# Convert to state-space representation A, B, C, D tf2ss([1], [1, -2, 1])print("A matrix:", A) print(&quo…

【Unity】在空物体上实现 IPointerClickHandler 不起作用

感谢Unity接口IPointerClickHandler使用说明_哔哩哔哩_bilibiliUnity接口IPointerClickHandler使用说明, 视频播放量 197、弹幕量 0、点赞数 3、投硬币枚数 2、收藏人数 2、转发人数 0, 视频作者 游戏创作大陆, 作者简介 &#xff0c;相关视频&#xff1a;在Unity多场景同时编辑…

基于Springboot的教学资源共享平台(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的教学资源共享平台&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构…

VsCode | 修改首页启动页 Logo

VsCode | 修改首页启动页 Logo 最终效果&#xff1a; 插件的安装 先安装插件 Custom CSS and JS Loader 插件配置 Ctrl Shift P 输入 打开用户设置&#xff0c;在末尾添加 "vscode_custom_css.imports": [""]下载 Logo 下载 Logo 点我下载 引入…

使用D3.js进行数据可视化

D3.js介绍 D3.js是一个流行的JavaScript数据可视化库&#xff0c;全称为Data-Driven Documents&#xff0c;即数据驱动文档。它以数据为核心&#xff0c;通过数据来驱动文档的展示和操作。D3.js提供了丰富的API和工具&#xff0c;使得开发者能够创建出各种交互式和动态的数据可…

LeetCode 110.平衡二叉树(Java/C/Python3/Go实现含注释说明,Easy)

标签 树深度优先搜索递归 题目描述 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡的二叉树定义为&#xff1a; 一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。 原题&#xff1a;LeetCode 110.平衡二叉树 思路及…

设计模式之组合实体模式

在编程的奇幻森林里&#xff0c;树木与枝叶错综复杂&#xff0c;如何让代码世界井然有序&#xff1f;组合实体模式&#xff08;Composite Pattern&#xff09;就像一位高明的园艺师&#xff0c;它以一种巧妙的方式&#xff0c;将个体与整体统一管理&#xff0c;让无论是单个对象…

Android使用kts上传aar到JitPack仓库

Android使用kts上传aar到JitPack 之前做过sdk开发&#xff0c;需要将仓库上传到maven、JitPack或JCenter,但是JCenter已停止维护&#xff0c;本文是讲解上传到JitPack的方式,使用KTS语法&#xff0c;记录使用过程中遇到的一些坑. 1.创建项目(library方式) 由于之前用鸿神的w…

关于Clion开发stm32printf重定向问题简单解决问题方法

title: 关于Clion开发stm32printf重定向问题简单解决问题方法 tags: STM32Clion 参考来源1 这是另一种方法 在printf 重定向的基础上加上 一句 setbuf(stdout,NULL); 参考来源2 自己写的笔记啦

深入理解vector 【C++】

一、vector的介绍&#xff1a; 1.vector是表示可变大小的顺序容器。 2.就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素 进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是可以动态改变的&am…

国产化改造之容器迁移指导(未完)

一、背景 信创即信息技术应用创新的简称,涵盖了国产软件、国产芯片以及云计算等各个方向,也可以理解为常说的“ZZKK(自主可控)”, ZZKK是指对国内企事业单位应用系统中关键软硬件部件的安全性、可靠性、性能稳定性、安全接入等方面进行评估和测试的过程。信创的发展核心就…

一对一WebRTC视频通话系列(二)——websocket和join信令实现

本系列博客主要记录WebRtc实现过程中的一些重点&#xff0c;代码全部进行了注释&#xff0c;便于理解WebRTC整体实现。 一对一WebRTC视频通话系列往期博客&#xff1a; 一对一WebRTC视频通话系列&#xff08;一&#xff09;—— 创建页面并显示摄像头画面 websocket和join信令…

下载Node.js及其他环境推荐nvm

文章目录 项目场景&#xff1a;下载Node.js环境配置配置环境变量 安装脚手架安装依赖安装淘宝镜像安装 cnpm&#xff08;我需要安装&#xff09;nvm 安装 Node.js &#xff08;推荐&#xff09; 项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 项目…

Java毕业设计 基于SSM SpringBoot vue宠物领养平台

Java毕业设计 基于SSM SpringBoot vue宠物领养平台 SSM 宠物领养平台 功能介绍 首页 图片轮播 新闻信息 新闻类型 新闻详情 宠物百科 宠物百科类型 宠物百科详情 宠物 宠物类型 宠物详情 立即领养 留言 论坛 发布帖子 登录 个人中心 宠物收藏 宠物领养订单 后台管理 登录注…

pymeshlab创建给定水平半径、垂直半径和水平细分以及垂直细分的圆环并保存(torus)

一、关于环境 请参考&#xff1a;pymeshlab遍历文件夹中模型、缩放并导出指定格式-CSDN博客 二、关于代码 本文所给出代码仅为参考&#xff0c;禁止转载和引用&#xff0c;仅供个人学习。 # pymeshlab需要导入&#xff0c;其一般被命名为ml import pymeshlab as ml# 首先需…