前几天突然想到了这个,因此写了一些东西。不敢说实现,因为这个方法的有效性并没有相应的证明,只是看起来像是有效的。我只拿了三个小矩阵测试,似乎可行。时间较少,非常粗糙。是否有不成立的地方或者有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 // 结束
代码有很多的冗余,可以修改一下,但是道理基本上就是这么个道理。