Verilog实时的图像多连通域检测

前几天突然想到了这个,因此写了一些东西。不敢说实现,因为这个方法的有效性并没有相应的证明,只是看起来像是有效的。我只拿了三个小矩阵测试,似乎可行。时间较少,非常粗糙。是否有不成立的地方或者有bug,权当看个乐呵。
优点是实时的,缺点是需要ram,且这个ram需要无时钟读取数据,且ram的大小和连通域的个数无相应关系,这使得在设计过程中无法具体确定需要多大的ram,这是较大的问题。

在进行图像处理的过程中,可能会用到多连通域检测,例如在进行移动目标检测的过程中,通常情况下物体会有多个,此时可能需要一种较快的标记算法。在软件层面是容易实现的,可以很方便的用dfs或者bfs,追求时间复杂度可能需要并查集。但是硬件上可能不太有那么富裕的时间来进行复杂的递归或者迭代。
现在假设前面已经对图像做完了处理,拿到了连续的0、1图像数据来表示此像素是否有东西,目标是实时将同一连通域的图像分组编号并输出,中间不能有停顿,那么该如何处理。
很容易想到对输入数据编号,相邻的数据在进来的同时赋予相同的编号。但是需要考虑上一层的数据是否相连,因此需要一个行FIFO;需要哈希表记录每一个编号和其实际上代表编号,这可以用ram代替;在处理完一副图像需要读取哈希表来确定到底输出什么,因此需要对数据缓存,需要一个数据FIFO;为了保证实时性,实现index_ram的pingpong操作,因此还需要一块ram。

编号的分配原则:
首选跟随同一行的前一个,之后左上角有数据,那么就跟随左上角编号,上方有数据就跟随上方,右上方有数据就跟随右上方。
编号合并原则:
如果产生了相连,那么选择小的编号来代表大的编号。(其实并不准确,应该说用小的编号的根来代表大的编号,根就是这一个群的代表编号,学过树和并查集的可能更能理解)。直觉上像是对的,但是我不会证明。

如果有同志发现这种方法的漏洞,请联系我删除,避免误导他人。废话结束,直接上代码。:w

module	connect_dec #(
	parameter	HEIGHT	=	360,
	parameter	WIDTH	=	480
)(
	input				pix_i,
	input				pix_valid_i,

	output		[31:0]	data_o,		// 输出的数据编号
	output				data_valid_o,


	input		clk,
	input		rst_n
);

localparam	NUM	=	HEIGHT * WIDTH;		// 总共的像素数



reg		[15:0]	pix_count_col;	// 计数输入的像素
reg		[15:0]	pix_count_row;

reg		[31:0]	now_idx;	// 当前排列的编号

reg		[31:0]	left_up_data;	// FIFO中的左上方数据,同时也是写入data ram的数据
reg		[31:0]	midd_up_data;	// FIFO正上方数据
reg		[31:0]	last_pix;		// 上一个像素,骑士就是赋值的元素
reg		[31:0]	now_pix;		// 赋值完成的这一个像素


reg		[31:0]	wr_addr0;
reg				wr0;
reg		[31:0]	din0;

//wire	wr_en0;	// 两个ram轮流工作,轮流写入
reg		wr_en0;	// 为1表示写入第一个,为0表示写入第2个

reg		pix_valid_d;	// 延迟pix_valid_i
reg		end_first_image;	// 结束完了第一幅图之后就一直置1

wire	[31:0]	fifo_dout;	// 行FIFO中输出的数据,也就是右上角的数据
wire	hang_fifo_rd;	// 行FIFO读写
wire	hang_fifo_wr;

wire	[31:0]	data_fifo_dout;	// 数据FIFO中输出的数据
wire	data_fifo_rd;	// data_fifo读写
wire	data_fifo_wr;

wire	[31:0]	ram1_dout;
wire	[31:0]	ram2_dout;
wire	[31:0]	ram_dout_mux;	// ram1 ram2读出的数据选择哪个输出

wire	[63:0]	com_data;	// 比较数据
wire	[31:0]	com_idx;	// 对比完后,小的编号代表的实际集合
wire	[31:0]	index_ram1_rdaddr;	// index_ram 1的读地址
wire	[31:0]	index_ram2_rdaddr;	// index_ram 2的读地址

//assign	hang_fifo_wr	=	(pix_count_row >= 0 && pix_count_row < HEIGHT-1) && pix_valid_i;
assign	hang_fifo_wr	=	((pix_count_row >= 1 && pix_count_row <= HEIGHT-2) ||
							(pix_count_row == 0 && pix_count_col >= 1 && pix_count_col <= WIDTH-1) ||
							(pix_count_row == HEIGHT-1 && pix_count_col == 0)) && pix_valid_i;
//assign	hang_fifo_rd	=	(pix_count_row >= 1 && pix_count_row <= HEIGHT-1) && pix_valid_i;
assign	hang_fifo_rd	=	((pix_count_row >= 1 && pix_count_row <= HEIGHT-2) ||
							(pix_count_row == 0 && pix_count_col >= WIDTH-2 && pix_count_col <= WIDTH-1) ||
							(pix_count_row == HEIGHT-1 && pix_count_col >= 0 && pix_count_col < WIDTH-2)) && pix_valid_i;

assign	data_fifo_wr	=	pix_valid_d;	// 有数据来就写data_fifo,pix_valid_i的延时?
assign	data_fifo_rd	=	end_first_image & pix_valid_i;	// 第二幅图像进来的时候才开始读data_fifo

assign	ram_dout_mux	=	wr_en0 ? ram2_dout : ram1_dout;	// 写第一个那么就是读第二个,写第二个就是读第一个
assign	data_o	=	ram_dout_mux;
assign	data_valid_o	=	pix_valid_i & end_first_image;

assign	com_data	=	max_val(fifo_dout, now_pix);	// 两个对比大小的编号
assign	index_ram1_rdaddr	=	wr_en0 ? com_data[31:0] : data_fifo_dout;
assign	index_ram2_rdaddr	=	~wr_en0 ? com_data[31:0] : data_fifo_dout;



function	[63:0]	max_val;
	input	[31:0]	a;
	input	[31:0]	b;
	begin
		if(a <= b)	max_val	=	{b, a};
		else	max_val	=	{a, b};
	end
endfunction




always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	end_first_image	<=	1'b0;
	else	end_first_image	<=	(pix_count_row == HEIGHT-1 && pix_count_col == WIDTH-1 && pix_valid_i) | end_first_image;
end

always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	pix_valid_d	<=	1'b0;
	else	pix_valid_d	<=	pix_valid_i;
end

always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	wr_en0	<=	1'b0;
	else if(pix_count_row == 0 && pix_count_col == 0 && pix_valid_i)begin
		wr_en0	<=	~wr_en0;
	end
end





always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	now_idx	<=	32'h1;
	else if(pix_valid_i)begin
		if(pix_count_col == WIDTH-2 && pix_count_row == HEIGHT-1)	now_idx	<=	32'h1;	// 图像结束,重置idx
		else if(pix_i != 0)begin
			if(pix_count_row == 0)begin	// 第一行
				if(pix_count_col == 16'hffff || pix_count_col == WIDTH-1)	now_idx	<=	now_idx + 1;	// 第一个数据
				else if(pix_count_col == WIDTH-2)begin	// 最后一个数据
					if(last_pix == 0)	now_idx	<=	now_idx + 1;
				end
				else begin	// 中间数据
					if(last_pix == 0)	now_idx	<=	now_idx + 1;
				end
			end
			else begin		// 不是第一行
				if(pix_count_col == WIDTH-1)begin	// 第一个数据
					if(left_up_data == 0 && fifo_dout == 0)	now_idx	<=	now_idx + 1;	// 重新分配编号
				end
				else if(pix_count_col == WIDTH-2)begin	// 最后一个数据
					if(last_pix == 0 && left_up_data == 0 && midd_up_data == 0)	now_idx	<=	now_idx + 1;
				end
				else begin		// 中间数据
					if(last_pix == 0 && left_up_data == 0 && midd_up_data == 0 && fifo_dout == 0)	now_idx	<=	now_idx + 1;
				end
			end
		end
	end
end

// 应该分类讨论
// 1、本行的第一个数据
// 2、本图的第一行数据
// 3、本行的最后一个数据

always@(*)begin
	now_pix	=	32'h0;
	if(pix_valid_i)begin
		if(pix_i != 0)begin
			// 第一行
			if(pix_count_row == 0)begin
				if(pix_count_col == 16'hffff || pix_count_col == WIDTH-1)	now_pix	=	now_idx;	// 第一个数据
				else if(pix_count_col == WIDTH-2)begin	// 最后一个数据
					if(last_pix != 0)	now_pix	=	last_pix;
					else	now_pix	=	now_idx;
				end
				else begin
					if(last_pix != 0)	now_pix	=	last_pix;
					else	now_pix	=	now_idx;
				end
			end
			else begin		// 不是第一行
				if(pix_count_col == WIDTH-1)begin	// 第一个数据
					if(midd_up_data != 0)	now_pix	=	midd_up_data;
					else if(fifo_dout != 0)	now_pix	=	fifo_dout;
					else	now_pix	=	now_idx;	// 重新分配编号
				end
				else if(pix_count_col == WIDTH-2)begin	// 最后一个数据
					if(last_pix != 0)	now_pix	=	last_pix;
					else if(left_up_data != 0)	now_pix	=	left_up_data;
					else if(midd_up_data != 0)	now_pix	=	midd_up_data;
					else	now_pix	=	now_idx;
				end
				else begin		// 中间数据
					if(last_pix != 0)	now_pix	=	last_pix;
					else if(left_up_data != 0)	now_pix	=	left_up_data;
					else if(midd_up_data != 0)	now_pix	=	midd_up_data;
					else if(fifo_dout != 0)	now_pix	=	fifo_dout;
					else	now_pix	=	now_idx;
				end
			end
		end
		else	now_pix	=	32'h0;	// 当前像素为0
	end
	else	now_pix	=	32'h0;	// 默认
end


// 规划更新
always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)begin
		wr0	<=	1'b0;
		wr_addr0	<=	32'h0;
		din0	<=	32'h0;
	end
	else if(pix_valid_i)begin
		if(pix_i != 0)begin
			// 第一行
			if(pix_count_row == 0)begin
				if(pix_count_col == 16'hffff || pix_count_col == WIDTH-1)begin	// 第一个像素
					wr_addr0	<=	now_pix;
					wr0	<=	1'b1;
					din0	<=	now_pix;
				end
				else if(pix_count_col == WIDTH-2)begin	// 最后一个像素
					wr_addr0	<=	now_pix;
					wr0	<=	1'b1;
					din0	<=	now_pix;
				end
				else begin		// 中间像素
					wr_addr0	<=	now_pix;
					wr0	<=	1'b1;
					din0	<=	now_pix;
				end
			end
			else begin		// 不是第一行
				if(pix_count_col == WIDTH-1)begin		// 第一个像素
					if(midd_up_data != 0)	// 被同化,不需要写入
						wr0	<=	1'b0;
					else if(fifo_dout != 0)	// 同理也不需要更新
						wr0	<=	1'b0;
					else begin
						wr_addr0	<=	now_pix;		// 初次赋予编号
						wr0	<=	1'b1;
						din0	<=	now_pix;
					end
				end
				else if(pix_count_col == WIDTH-2)begin	// 最后一个像素
					if(left_up_data != 0)begin	// 这个时候需要考虑此点是左上方像素同化而来,还是很早就被赋予了别的编号
						if(last_pix != 0)	wr0	<=	1'b0;
						else	wr0	<=	1'b0;
					end
					else if(midd_up_data != 0)begin	// 同上
						if(last_pix != 0)	wr0	<=	1'b0;
						else	wr0	<=	1'b0;
					end
					else begin	// 上面数据都是空的
						wr_addr0	<=	now_pix;
						wr0	<=	1'b1;
						din0	<=	now_pix;
					end
				end
				else begin		// 中间数据,不能简单的分三种情况,应该三种情况结合
					if(left_up_data != 0 && midd_up_data != 0 && fifo_dout != 0)begin	// 情况1 直接并	111
						if(last_pix != 0)begin	// 上一个有编号,说明已经合并过了,不论now_pix是否等于上面的编号
							wr0	<=	1'b0;
						end
						else begin	// 上一个没有编号,说明是头一个被赋予编号的
							wr0	<=	1'b1;
							wr_addr0	<=	now_pix;
							din0	<=	now_pix;
						end
					end
					else if(left_up_data != 0 && midd_up_data != 0 && fifo_dout == 0)begin	// 情况2 直接并	110
						if(last_pix != 0)	// 上一个有编号,说明已经合并过了
							wr0	<=	1'b0;
						else begin
							wr0	<=	1'b1;
							wr_addr0	<=	now_pix;
							din0	<=	now_pix;
						end
					end
					else if(left_up_data != 0 && midd_up_data == 0 && fifo_dout != 0)begin	// 情况3 可能是101 也可能是102
						if(last_pix != 0)	wr0	<=	1'b0;	// 如果是有标号的,说明已经合并过了
						else begin	// 如果没有编号,此pix是由左上角同化而来,因此无需合并左上角,只合并右上角
							wr0	<=	1'b1;
							wr_addr0	<=	com_data[63:32];
							din0	<=	wr_en0 ? ram1_dout : ram2_dout;
						end
					end
					else if(left_up_data == 0 && midd_up_data != 0 && fifo_dout == 0)begin	// 情况4 010 直接并
						if(last_pix != 0)begin	// 如果上一个点有编号,那么其实就是001的情况,已经并过了
							wr0	<=	1'b0;
						end
						else begin	// 如果前面没有编号,那就更不用并了
							wr0	<=	1'b0;
							//wr_addr0	<=	now_pix;
							//din0	<=	now_pix;
						end
					end
					else if(left_up_data == 0 && midd_up_data != 0 && fifo_dout != 0)begin	// 情况5 011直接并
						if(last_pix != 0)	wr0	<=	1'b0;
						else begin	// 前面没有编号
							wr0	<=	1'b0;
							//wr_addr0	<=	now_pix;
							//din0	<=	now_pix;
						end
					end
					else if(left_up_data != 0 && midd_up_data == 0 && fifo_dout == 0)begin	// 情况6 100直接并
						if(last_pix != 0)	wr0	<=	1'b0;
						else begin	// 前面有了编号,不一定和left_up_data相同,但是肯定已经合并过了
							wr0	<=	1'b0;
							//wr_addr0	<=	now_pix;
							//din0	<=	now_pix;
						end
					end
					else if(left_up_data == 0 && midd_up_data == 0 && fifo_dout != 0)begin	// 情况7 001
						if(last_pix != 0)begin	// 说明早有编号,且需要合并右上角的
							wr0	<=	1'b1;
							wr_addr0	<=	com_data[63:32];
							//din0	<=	com_data[31:0];
							din0	<=	wr_en0 ? ram1_dout : ram2_dout;
						end
						else begin	// 初次赋予编号,由右上角同化而来,无需合并
							wr0	<=	1'b0;
						end
					end
					else if(left_up_data == 0 && midd_up_data == 0 && fifo_dout == 0)begin	// 情况8 上面的全是0
						// 可以讨论前面是否是空白像素,如果是就不用再写入了,当然也可以直接写入,没有什么区别
						if(last_pix != 0)	wr0	<=	1'b0;
						else begin
							wr0	<=	1'b1;
							wr_addr0	<=	now_pix;
							din0	<=	now_pix;
						end
					end
					else begin
						wr0	<=	1'b0;
					end
				end
			end
		end
		else begin	// 像素为0
			wr0	<=	1'b0;
		end
	end
	else begin	// 正常情况下都不存在更新情况
		wr0	<=	1'b0;
	end
end



always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)begin
		left_up_data	<=	32'h00;
		midd_up_data	<=	32'h00;
	end
	else if(pix_valid_i)begin
		midd_up_data	<=	fifo_dout;
		left_up_data	<=	midd_up_data;
	end
end



always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	last_pix	<=	32'h0;
	else if(pix_valid_i)	last_pix	<=	now_pix;
end


//always@(posedge clk, negedge rst_n)begin
//	if(rst_n == 0)	now_idx	<=	32'h1;	// 开始为1
//	else if(pix_valid_i)begin
//		if(pix_count_col == WIDTH-2 && pix_count_row == HEIGHT-1)	now_idx	<=	32'h1;	// 一幅图结束了,那么重置now_idx
//		else if(pix_count_col == WIDTH-2 && pix_i != 0)	now_idx	<=	now_idx + 1;	// 某一行是以有效数据结束的,那么变更now_idx
//		else if(last_pix != 0 && pix_i == 0)	now_idx	<=	now_idx + 1;
//	end
//end




always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	pix_count_col	<=	16'hffff;
	else if(pix_valid_i)	pix_count_col	<=	pix_count_col == WIDTH-1 ? 0 : pix_count_col + 1;
end


always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	pix_count_row	<=	16'd0;
	else if(pix_valid_i)begin
		if(pix_count_col == WIDTH-1 && pix_count_row == HEIGHT-1)	pix_count_row	<=	0;
		else if(pix_count_col == WIDTH-1)	pix_count_row	<=	pix_count_row + 1;
	end
end









// data fifo
sync_fifo	#(
	.DP(NUM+4),
	.DW(32)
)data_fifo(
	.wr_en(data_fifo_wr),
	.din(last_pix),	// 处理完的数据进入data_fifo
	.rd_en(data_fifo_rd),
	.dout(data_fifo_dout),
	.full(),
	.empty(),

	.clk(clk),
	.rst_n(rst_n)
);


// 行FIFO
sync_fifo	#(
	.DP(WIDTH+2),
	.DW(32)
)hang_fifo(
	.din(last_pix),		// 赋值后的数据写入
	.dout(fifo_dout),
	.rd_en(hang_fifo_rd),
	.wr_en(hang_fifo_wr),
	.full(),
	.empty(),
	.clk(clk),
	.rst_n(rst_n)
);




index_ram #(
	.DEPTH(NUM),
	.WIDTH(32)
)ram1(
	.addr0(wr_addr0),
	.din0(din0),
	.wr0(wr0 & wr_en0),

	// 读取
	.addr1(index_ram1_rdaddr),
	.dout1(ram1_dout),
	
	.clk(clk),
	.rst_n(rst_n)
);

index_ram #(
	.DEPTH(NUM),
	.WIDTH(32)
)ram2(
	.addr0(wr_addr0),
	.din0(din0),
	.wr0(wr0 & ~wr_en0),

	// 读取
	.addr1(index_ram2_rdaddr),
	.dout1(ram2_dout),
	
	.clk(clk),
	.rst_n(rst_n)
);


endmodule

module	index_ram #(
	parameter	DEPTH	=	100,
	parameter	WIDTH	=	32
)(
	input		[31:0]	addr0,
	input		[31:0]	din0,
	input				wr0,

	input		[31:0]	addr1,
	output		[31:0]	dout1,

	input	clk,
	input	rst_n
);


reg		[31:0]	mem	[DEPTH-1:0];


integer	i	=	0;
always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)begin
		for(i = 0; i < DEPTH; i=i+1)begin
			mem[i] = 0;
		end
	end
	else if(wr0)	mem[addr0]	<=	din0;
end

assign	dout1	=	mem[addr1];

endmodule

module	sync_fifo#(
	parameter	DP	=	1024,
	parameter	DW	=	32
)(
	input		[DW-1:0]	din,
	input				wr_en,
	input				rd_en,

	output		[DW-1:0]	dout,
	output				full,
	output				empty,

	input				clk,
	input				rst_n
);

localparam	ADDR_W	=	$clog2(DP);

reg	[DW-1:0]	mem[DP-1:0];
reg	[ADDR_W:0]	write_ptr;
reg	[ADDR_W:0]	read_ptr;
reg	[DW-1:0]	dq;

wire	[ADDR_W-1:0]	write_addr;
wire	[ADDR_W-1:0]	read_addr;

assign	write_addr	=	write_ptr[ADDR_W-1:0];
assign	read_addr	=	read_ptr[ADDR_W-1:0];
assign	dout	=	dq;
assign	empty	=	(write_ptr == read_ptr);
assign	full	=	(write_addr == read_addr) & (write_ptr[ADDR_W] ^ read_ptr[ADDR_W]);

// write_ptr
always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	write_ptr	<=	0;
	else if(wr_en & ~full)	write_ptr	<=	write_addr == DP-1 ? {~write_ptr[ADDR_W], {ADDR_W{1'b0}}} : write_ptr + 1;
end

// read_ptr
always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	read_ptr	<=	0;
	else if(rd_en & ~empty)	read_ptr	<=	read_addr == DP-1 ? {~read_ptr[ADDR_W], {ADDR_W{1'b0}}} : read_ptr + 1;
end

// mem
always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	;
	else if(wr_en & ~full)	mem[write_addr]	<=	din;
end

// dq
always@(posedge clk, negedge rst_n)begin
	if(rst_n == 0)	dq	<=	8'h0;
	else if(rd_en & ~empty)	dq	<=	mem[read_addr];
end


endmodule

仿真。

module	tb_connect_dec();

localparam	WIDTH	=	32;
localparam	HEIGHT	=	12;

localparam	nums	=	3;
localparam	hang	=	nums * HEIGHT;


reg		clk;
reg		rst_n;
reg		pix;
reg		pix_valid;

wire	[31:0]	data_o;
wire	data_valid_o;


reg	[31:0]	image[0:499999];


initial begin
	$fsdbDumpfile("top.fsdb");
	$fsdbDumpvars();
end

always	#10	clk	=	~clk;


integer i = 0;
integer	j = 0;
//integer k = 0;
//integer fp;

initial begin
	$readmemb("./image.v", image);
	clk	=	0;
	rst_n	=	0;
	pix_valid	=	0;
	#20;
	rst_n	=	1;

	#11;
	for(j = 0; j < hang; j=j+1)begin
		for(i = WIDTH-1; i >= 0; i=i-1)begin
			pix	=	image[j][i];
			pix_valid	=	1'b1;
			#20;
		end
	end

	#5e4;

	#100;

	$finish;
end



connect_dec #(
	.HEIGHT(HEIGHT),
	.WIDTH(WIDTH)
)	udt(
	.pix_i(pix),
	.pix_valid_i(pix_valid),
	.data_o(data_o),
	.data_valid_o(data_valid_o),

	.clk(clk),
	.rst_n(rst_n)
);




endmodule

图像是我随便写的几个小矩阵。

0000000000000000000000000000000
0000011111111111111000000000000
0000000111111111111111100000000
0001111111111110000000000000000
1111100000000000000000000000000
0000011111111110000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000011111111111111000000000
0000000001010101010101000000000
0000000001111111111111111110000
0000000000011111111100000000000	// 结束
0000000000000000000000000000000
0000111111111111111111111110000
0000001111111111111111111000000
0000000011111111111111100000000
0000000000111111111110000000000
0000000000001111111000000000000
0000000000000011100000000000000
0000000000000001000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000	// 结束
0000000000000000000000000000000
0000110000000000000011110000000
0000110000000000000100001000000
0000110000000000000000110000000
0000110000000000000011000000000
0000110000000000000111111000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000	// 结束

代码有很多的冗余,可以修改一下,但是道理基本上就是这么个道理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值