Opencv学习笔记
# Opencv学习笔记
Day1
图像读取与显示
#include<bits/stdc++.h>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc,char** argv)
{
Mat a = imread("E:/壁纸/5.jpg");
//创建一个Windows窗口
namedWindow("输入窗口", WINDOW_FREERATIO);
//显示图像
imshow("输入窗口", a);
//持续10000ms
waitKey(10000);
//关闭所有窗口
destroyAllWindows();
}
图像色彩空间转换
头文件
#pragma once
#include<opencv2/opencv.hpp>
using namespace cv;
class QuickDemo {
public:
void colorSpace_Demo(Mat &image);
};
源文件
#include<bits/stdc++.h>
#include<QuickDemo.h>
void QuickDemo::colorSpace_Demo(Mat& image)
{
Mat gray, hsv;
cvtColor(image, hsv, COLOR_BGR2HSV); //转换色彩空间
cvtColor(image, gray, COLOR_BGR2GRAY);
imshow("HSV", hsv);
imshow("灰度", gray);
imwrite("E:/hsv.png", hsv);
imwrite("E:/gray.png", gray);
}
#include<bits/stdc++.h>
#include<opencv2/opencv.hpp>
#include<QuickDemo.h>
using namespace std;
using namespace cv;
int main(int argc,char** argv)
{
Mat a = imread("E:/壁纸/5.jpg");
namedWindow("输入窗口", WINDOW_FREERATIO);
imshow("输入窗口", a);
QuickDemo qd;
qd.colorSpace_Demo(a);
waitKey(10000);
destroyAllWindows();
}
图像对象的创建与赋值
void QuickDemo::mat_creation_demo(Mat& image)
{
Mat m1, m2;
m1 = image.clone(); //克隆
image.copyTo(m2); //拷贝
//创建图像
//创建一个全是0的8*8的8位的ugsined无符号的单通道的图像
Mat m3 = Mat::zeros(Size(8, 8), CV_8UC1);
//输出m3图像
std::cout << m3 << std::endl;
//创建一个50*50 8位的ugsined无符号的三通道的图像 每三个数表示一个像素
Mat m4 = Mat::zeros(Size(50, 50), CV_8UC3);
//输出m4的宽度 高度 通道数
std::cout << "width:" << m4.cols << "height:" << m4.rows << "channels:" << m4.channels() << std::endl;
//创建一个全是1的图像 之能用于单通道
Mat m5 = Mat::ones(Size(8, 8), CV_8UC1);
std::cout << m5 << std::endl;
//第一通道全部赋值127
m5 = 127;
std::cout << m5 << std::endl;
//三个通道分别赋值255 0 0
m4 = Scalar(255, 0, 0);
std::cout << m4 << std::endl;
//显示图像
//imshow("创建图像", m4);
//改变m6颜色 m4颜色也改变
//说明m6指向m4
Mat m6 = m4;
m6 = Scalar(0, 255, 255);
imshow("创建图像", m4);
//克隆会产生一个独立的个体
//两者没有关系
Mat m7 = m4.clone();
m7 = Scalar(0, 255, 255);
imshow("图像1", m4);
imshow("图像2", m7);
//copyTo操作也会产生一个独立的个体
Mat m8;
m4.copyTo(m8); //将m4 copy给m8
m8 = Scalar(0, 255, 255);
imshow("图像3", m8);
}
图像像素的读写操作
void QuickDemo::pixel_visit_demo(Mat& image)
{
int w = image.cols;
int h = image.rows;
int dims = image.channels();
//遍历每个像素点
for (int row = 0; row < h; row++)
{
for (int col = 0; col < w; col++)
{
if (dims == 1) //灰度图像
{
//将当前这个像素点的颜色变为255-pv;
int pv = image.at<uchar>(row, col);
image.at<uchar>(row, col) = 255 - pv;
}
if (dims == 3) //彩色图像
{
//获取该像素点的三个值rgb
Vec3b bgr = image.at<Vec3b>(row, col);
image.at<Vec3b>(row, col)[0] = 255 - bgr[0];
image.at<Vec3b>(row, col)[1] = 255 - bgr[1];
image.at<Vec3b>(row, col)[2] = 255 - bgr[2];
}
}
}
imshow("像素读写演示", image);
}
也可以使用指针 更简洁
//遍历每个像素点
for (int row = 0; row < h; row++)
{
uchar* current_row = image.ptr<uchar>(row);
for (int col = 0; col < w; col++)
{
if (dims == 1) //灰度图像
{
*current_row++ = 255 - *current_row;
}
if (dims == 3) //彩色图像
{
//不用麻烦的分三通道 指针会出手
*current_row++ = 255 - *current_row;
*current_row++ = 255 - *current_row;
*current_row++ = 255 - *current_row;
}
}
}
Day2
图像像素的算术操作
void QuickDemo::operators_demo(Mat& image)
{
//加法操作
Mat dst;
dst = image + Scalar(50, 50, 50);
imshow("加法操作", dst);
//除法操作
Mat dst;
dst = image / Scalar(2, 2, 2);
imshow("除法操作", dst);
//矩阵乘法要符合A(n,m) B(m,n)的规定 但可以直接乘常数
Mat dst;
dst = image * 2;
imshow("乘法操作", dst);
//用multiply函数可以做到矩阵点乘
Mat dst;
Mat m = Mat::zeros(Size(8 * 8), CV_8UC3);
m = Scalar(2, 2, 2);
multiply(image, m, dst);
imshow("乘法操作", dst);
Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());
m = Scalar(50, 50, 50);
//遍历加法操作
int w = image.cols;
int h = image.rows;
int dims = image.channels();
for (int row = 0; row < h; row++)
{
for (int col = 0; col < w; col++)
{
Vec3b p1 = image.at<Vec3b>(row, col);
Vec3b p2 = m.at<Vec3b>(row,col);
//saturate_cast<uchar>将该值固定在uchar的范围内
dst.at<Vec3b>(row, col)[0] = saturate_cast<uchar>(p1[0] + p2[0]);
dst.at<Vec3b>(row, col)[1] = saturate_cast<uchar>(p1[1] + p2[1]);
dst.at<Vec3b>(row, col)[2] = saturate_cast<uchar>(p1[2] + p2[2]);
}
}
imshow("加法操作", dst);
//add实现加法操作
add(image, m, dst);
imshow("加法操作", dst);
//subtract实现减法操作
subtract(image, m, dst);
imshow("减法操作", dst);
//divide实现除法操作
divide(image, m, dst);
imshow("除法操作", dst);
}
滚动条操作演示-调整图像亮度
//b代表lightness
static void on_track(int b, void* userdata)
{
Mat image = *((Mat*)userdata);
Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());
m = Scalar(b, b, b);
add(image, m, dst);
imshow("亮度调整", dst);
}
void QuickDemo::tracking_bar_demo(Mat& image)
{
namedWindow("亮度调整", WINDOW_AUTOSIZE);
int max_value = 100;
int lightness = 50;
//进度条名称 作用的窗口名称 改变的变量的地址 改变的最大值 回调函数 给回调函数传的void指针
createTrackbar("Value Bar:", "亮度调整", &lightness, max_value, on_track, (void*)(&image));
}
融合两个图像
//将两张相同大小,相同类型的图片融合
void QuickDemo::fuse_demo(Mat& image1, Mat& image2)
{
Mat dst = Mat::zeros(image1.size(), image1.type());
//第一个矩阵 权重 第二个矩阵 权重 相加后再加上的值 最后存到的变量
addWeighted(image1, 0.5, image2, 0.5, 0, dst);
namedWindow("图片合成", WINDOW_AUTOSIZE);
imshow("图片合成", dst);
}
键盘响应操作
waitKey函数的功能是不断刷新图像,频率时间为delay,单位为ms
返回值为当前键盘按键值。
void QuickDemo::key_demo(Mat& image)
{
Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());
m = Scalar(50, 50, 50);
image.copyTo(dst);
while (1)
{
int c = waitKey(100);
if (c == 27) //按esc退出
break;
if (c == 49) { //按1变成灰度图像
cvtColor(dst, dst, COLOR_BGR2GRAY);
}
if (c == 50) { //按2变成hsv图像
cvtColor(dst, dst, COLOR_BGR2HSV);
}
if (c == 51) { //按3变亮
add(dst, m, dst);
}
imshow("键盘响应", dst);
}
}
OpenCV自带颜色表操作
void QuickDemo::color_style_demo(Mat& image)
{
//颜色表
int color_map[] = {
COLORMAP_AUTUMN,
COLORMAP_BONE,
COLORMAP_CIVIDIS,
COLORMAP_COOL,
COLORMAP_DEEPGREEN,
COLORMAP_HOT,
COLORMAP_HSV,
COLORMAP_INFERNO,
COLORMAP_JET,
COLORMAP_MAGMA,
COLORMAP_OCEAN,
COLORMAP_PARULA,
COLORMAP_PINK,
COLORMAP_PLASMA,
COLORMAP_RAINBOW,
COLORMAP_SPRING,
COLORMAP_SUMMER,
COLORMAP_TURBO,
COLORMAP_TWILIGHT,
COLORMAP_TWILIGHT_SHIFTED,
COLORMAP_VIRIDIS,
COLORMAP_WINTER
};
Mat dst;
int index = 0;
while (1)
{
int c = waitKey(2000);
if (c == 27) //退出
break;
//变换颜色
applyColorMap(image, dst, color_map[index]);
index++;
index %= 19;
imshow("颜色风格", dst);
}
}
图像像素的逻辑操作
void QuickDemo::bitwise_demo(Mat& image)
{
Mat m1 = Mat::zeros(Size(256, 256), CV_8UC3);
Mat m2 = Mat::zeros(Size(256, 256), CV_8UC3);
//图像 左上角坐标 矩形大小 颜色 小于0表示填充大于0表示绘制 周围8个像素提供支援 坐标点的小数点位数
rectangle(m1, Rect(100, 100, 80, 80), Scalar(255, 255, 0), -1, LINE_8, 0);
rectangle(m2, Rect(150, 150, 80, 80), Scalar(0, 255, 255), -1, LINE_8, 0);
//2表示边的线宽
rectangle(m1, Rect(100, 100, 80, 80), Scalar(255, 255, 0), 5,LINE_8, 0);
rectangle(m2, Rect(150, 150, 80, 80), Scalar(0, 255, 255), 5,LINE_8, 0);
imshow("m1", m1);
imshow("m2", m2);
Mat dst;
//和操作
bitwise_and(m1, m2, dst);
//或操作
bitwise_or(m1, m2, dst);
//取反操作
bitwise_not(m1, dst);
//也可以这样取反
dst = ~m1;
//异或操作
bitwise_xor(m1, m2, dst);
imshow("像素位操作", dst);
}
通道分离与合并
void QuickDemo::channels_demo(Mat& image)
{
//分离三通道图像为三个单通道图像
std::vector<Mat>mv;
split(image, mv);
imshow("蓝色", mv[0]);
imshow("绿色", mv[1]);
imshow("红色", mv[2]);
////呈现蓝色图像只要把绿色和红色清零
Mat dst;
mv[1] = 0;
mv[2] = 0;
merge(mv, dst);
imshow("蓝色", dst);
//通道混合
Mat dst = Mat::zeros(image.size(), image.type());
//原图像的0通道复制到输出图像的2通道 以此类推
int from_to[] = { 0,2,1,1,2,0 };
//原图像的地址 矩阵个数 输出图像的地址 矩阵个数 索引 索引对个数
mixChannels(&image, 1, &dst, 1, from_to, 3);
imshow("通道混合", dst);
}
Day3
图像色彩空间转换
void QuickDemo::inrange_demo(Mat& image)
{
Mat hsv;
cvtColor(image, hsv, COLOR_BGR2HSV);
Mat mask;
//把白色的区域都赋为1其余为0 具体见表
inRange(hsv, Scalar(0, 0, 221), Scalar(180, 30, 255), mask);
imshow("mask1", mask);
Mat redback = Mat::zeros(image.size(), image.type());
//红色
redback = Scalar(40, 40, 200);
//取反
bitwise_not(mask, mask);
imshow("mask2", mask);
//把image赋给redback 但只赋值mask上为1的点
image.copyTo(redback, mask);
imshow("redback", redback);
}
图像像素值统计
void QuickDemo::pixel_statisitc_demo(Mat& image)
{
double minv, maxv;
Point minLoc, maxLoc;
std::vector<Mat>mv;
split(image, mv);
for (int i = 0; i < mv.size(); i++)
{
//单通道图像 最小值 最大值 最小值坐标 最大值坐标
minMaxLoc(mv[i], &minv, &maxv, &minLoc, &maxLoc);
std::cout << "min value:" << minv << "max value:" << maxv << std::endl;
}
Mat mean, stddev;
//均值 标准
meanStdDev(image, mean, stddev);
std::cout << "means:" << mean << "stddev" << stddev << std::endl;
}
图像几何形状绘制
void QuickDemo::drawing_demo(Mat& image)
{
//定义一个矩形
Rect rect;
//左上角坐标
rect.x = 200;
rect.y = 200;
rect.width = 100;
rect.height = 100;
//在image上绘制一个矩形
//rectangle(image, rect, Scalar(0, 0, 255), 5, 8, 0);
//绘制圆 圆心坐标 半径 颜色 线宽 锯齿 小数点
//circle(image, Point(200, 200), 15, Scalar(255, 0, 0), 1, 8, 0);
Mat dst, bg = Mat::zeros(image.size(), image.type());
rectangle(bg, rect, Scalar(0, 0, 255), -1, 8, 0);
//绘制一条线 左上角坐标 右下角坐标 颜色 线宽 锯齿 小数点
line(bg, Point(100, 100), Point(350, 400), Scalar(0, 255, 0), 2, 8, 0);
//椭圆
RotatedRect rrt;
//中心坐标
rrt.center = Point(200, 200);
//长宽
rrt.size = Size(100, 200);
//角度
rrt.angle = 90, 0;
ellipse(bg, rrt, Scalar(0, 255, 255), 2, 8);
//将image和bg融合权值分配7:3
addWeighted(image, 0.7, bg, 0.3, 0, dst);
imshow("绘制演示", bg);
}
Day4
随机数与随机颜色
void QuickDemo::random_drawing(Mat& image)
{
Mat canvas = Mat::zeros(Size(1024, 1024), CV_8UC3);
//RNG类 随机数种子
RNG rng(12345);
while (true) {
int c = waitKey(10);
if (c == 27)
break;
//[0,1024)中的随机值
int x1 = rng.uniform(0, 1024);
int y1 = rng.uniform(0, 1024);
int x2 = rng.uniform(0, 1024);
int y2 = rng.uniform(0, 1024);
int r = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int b = rng.uniform(0, 255);
line(canvas, Point(x1, y1), Point(x2, y2), Scalar(r, g, b), 4, LINE_AA, 0);
imshow("随即绘制演示", canvas);
}
}
多边形填充与绘制
void QuickDemo::polyline_drawing_demo()
{
Mat canvas = Mat::zeros(Size(512, 512), CV_8UC3);
std::vector<Point>pts;
pts.push_back({ 100,100 });
pts.push_back({ 350,100 });
pts.push_back({ 450,280 });
pts.push_back({ 320,450 });
pts.push_back({ 80,400 });
//填充多边形
fillPoly(canvas, pts, Scalar(255, 255, 0), 8, 0);
//绘制一个多边形 顶点坐标集合 是否把绘制的多条线段首尾相连,显示,如果要绘制多边形,则这个参数值该置为true
polylines(canvas, pts, true, Scalar(0, 0, 255), 1, 8, 0);
imshow("多边形绘制", canvas);
std::vector<std::vector<Point> >contours;
contours.push_back(pts);
//绘制第几个轮廓 颜色 线宽 -1为填充
drawContours(canvas, contours, -1, Scalar(255, 0, 0), -1);
imshow("多边形绘制", canvas);
}
鼠标操作与响应
Point sp(-1, -1);
Point ep(-1, -1);
Mat dst;
//鼠标事件 鼠标坐标 鼠标事件2 传递参数
static void on_draw(int event, int x, int y, int flags, void* userdata)
{
//把传递的图像传给image
Mat image = *((Mat*)userdata);
//左键按下
if (event == 1)
{
sp.x = x;
sp.y = y;
std::cout << "start point:" << sp << std::endl;
}
//左键升起
else if (event == 4)
{
ep.x = x;
ep.y = y;
int dx = ep.x - sp.x;
int dy = ep.y - sp.y;
if ((dx > 0) and (dy > 0))
{
Rect box(sp.x, sp.y, dx, dy);
rectangle(image, box, Scalar(0, 0, 255), 2, 8, 0);
imshow("鼠标绘制", image);
imshow("截图", image(box));
sp.x = -1;
sp.y = -1;
}
}
//鼠标移动
else if (event == 0)
{
if ((sp.x > 0) and (sp.y > 0))
{
ep.x = x;
ep.y = y;
int dx = ep.x - sp.x;
int dy = ep.y - sp.y;
if ((dx > 0) and (dy > 0))
{
Rect box(sp.x, sp.y, dx, dy);
//覆盖 只显示一个矩形
dst.copyTo(image);
rectangle(image, box, Scalar(0, 0, 255), 2, 8, 0);
imshow("鼠标绘制", image);
}
}
}
}
void QuickDemo::mouse_drawing_demo(Mat &image)
{
namedWindow("鼠标绘制", WINDOW_AUTOSIZE);
//鼠标指令 窗体 回调函数 传递参数
setMouseCallback("鼠标绘制", on_draw, (void*) (&image));
dst = image.clone();
}
event:
#defineCV_EVENT_MOUSEMOVE 0 移动
#defineCV_EVENT_LBUTTONDOWN 1 左键按下
#defineCV_EVENT_RBUTTONDOWN 2 右键按下
#defineCV_EVENT_MBUTTONDOWN 3 中键按下
#defineCV_EVENT_LBUTTONUP 4 左键升起
#defineCV_EVENT_RBUTTONUP 5 右键升起
#defineCV_EVENT_MBUTTONUP 6 中键升起
#defineCV_EVENT_LBUTTONDBLCLK 7 左键双击
#defineCV_EVENT_RBUTTONDBLCLK 8 右键双击
#defineCV_EVENT_MBUTTONDBLCLK 9 中键双击
#defineCV_EVENT_MOUSEHWHEEL 10 滚轮事件
flag:
#defineCV_EVENT_FLAG_LBUTTON 1 左键拖曳
#defineCV_EVENT_FLAG_RBUTTON 2 右键拖曳
#defineCV_EVENT_FLAG_MBUTTON 4 中键拖曳
#defineCV_EVENT_FLAG_CTRLKEY 8 (8~15)按Ctrl不放事件
#defineCV_EVENT_FLAG_SHIFTKEY 16 (16~31)按Shift不放事件
#defineCV_EVENT_FLAG_ALTKEY 32 (32~39)按Alt不放事件
Day5
图像像素类型转换与归一化
void QuickDemo::norm_demo(Mat& image)
{
Mat dst;
std::cout << image.type() << std::endl;
//转换图像像素类型
image.convertTo(image, CV_32F);
std::cout << image.type() << std::endl;
//归一化
//NORM_MINMAX:将数组的数值归一化到[alpha,beta]内,常用。
//NORM_L1:归一化数组的L1 - 范数(绝对值的和)
//NORM_L2 : 归一化数组的(欧几里德)L2 - 范数
normalize(image, dst, 1.0, 0, NORM_MINMAX);
std::cout << dst.type() << std::endl;
imshow("图像数据归一化", dst);
}
归一化作用
归一化就是要把需要处理的数据经过处理后(通过某种算法)限制在你需要的一定范围内。首先归一化是为了后面数据处理的方便,其次是保证程序运行时收敛加快
归一化的具体作用是归纳统一样本的统计分布性。归一化在0-1之间是统计的概率分布,归一化在某个区间上是统计的坐标分布。归一化有同一、统一和合一的意思。
此外,归一化还有其他作用:
1.无量纲化
例如房子数量和收入,从业务层知道这两者的重要性一样,所以把它们全部归一化,这是从业务层面上作的处理。
2.避免数值问题
不同的数据在不同列数据的数量级相差过大的话,计算起来大数的变化会掩盖掉小数的变化。
3.一些模型求解的需要
例如梯度下降法,如果不归一化,当学习率较大时,求解过程会呈之字形下降。学习率较小,则会产生直角形路线,不管怎么样,都不会是好路线。
4.时间序列
进行log分析时,会将原本绝对化的时间序列归一化到某个基准时刻,形成相对时间序列,方便排查。
5.收敛速度
加快求解过程中参数的收敛速度。
归一化目的
归一化的目的简而言之,是使得没有可比性的数据变得具有可比性,同时又保持相比较的两个数据之间的相对关系,如大小关系;或是为了作图,原来很难在一张图上作出来,归一化后就可以很方便的给出图上的相对位置等。
图像放缩与插值
void QuickDemo::resize_demo(Mat& image)
{
Mat zoomin, zoomout;
int h = image.rows;
int w = image.cols;
//缩放图像
/*src - 输入图像。
dst - 输出图像;它的大小为 dsize(当它非零时)或从 src.size()、fx 和 fy 计算的大小;dst 的类型与 src 的类型相同。
dsize - 输出图像大小;如果它等于零,则计算为:dsize = Size(round(fx * src.cols), round(fy * src.rows))。dsize 或 fx 和 fy 必须为非零。
fx - 沿水平轴的比例因子;当它等于 0 时,它被计算为(double)dsize.width / src.cols
fy - 沿垂直轴的比例因子;当它等于 0 时,它被计算为(double)dsize.height / src.rows
interpolation 内插方式 内插方式有
CV_INTER_NEAREST 最邻近插值点法
CV_INTER_LINEAR 双线性插值法
CV_INTER_AREA 邻域像素再取样插补
CV_INTER_CUBIC 双立方插补,4 * 4大小的补点*/
resize(image, zoomin, Size(w / 2, h / 2), 0, 0, INTER_LINEAR);
imshow("zoomin", zoomin);
}
滚轮控制图像放缩
Mat dst;
static void on_draw(int event, int x, int y, int flags,void*userdata)
{
if (event == 10)
{
int op;
if (flags > 0)
op = 10;
else op = -10;
int h = dst.rows;
int w = dst.cols;
resize(dst, dst, Size(w + op, h + op), 0, 0, INTER_LINEAR);
imshow("图像缩放", dst);
}
}
void QuickDemo::mouse_resize_demo(Mat& image)
{
namedWindow("图像缩放", WINDOW_AUTOSIZE);
setMouseCallback("图像缩放", on_draw, NULL);
dst = image;
}
Day6
图像翻转
void QuickDemo::flip_demo(Mat& image)
{
Mat dst;
//0 上下翻转 -1上下左右翻转 1左右翻转
flip(image, dst, 1);
imshow("图像翻转", dst);
}
图像旋转
void QuickDemo::rotate_demo(Mat& image)
{
Mat dst, M;
int w = image.cols;
int h = image.rows;
//中心点,角度,放缩比
M = getRotationMatrix2D(Point2f(w / 2, h / 2), 45, 1.0);
double cos = abs(M.at<double>(0, 0)); //旋转角度的cos
double sin = abs(M.at<double>(0, 1)); //旋转角度的sin
int nw = cos * w + sin * h;
int nh = sin * w + cos * h;
//中心点偏移
M.at<double>(0, 2) += (nw / 2 - w / 2);
M.at<double>(1, 2) += (nh / 2 - h / 2);
//旋转矩阵 大小 插值方式的组合 边缘像素模式 颜色
warpAffine(image, dst, M, Size(nw, nh), INTER_LINEAR, 0, Scalar(255, 0, 0));
imshow("旋转演示", dst);
}
嘻嘻
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程