在数字图像处理中,均值滤波和中值滤波是常见的空间域处理方法,可以用于过滤图像中的噪声。本文主要介绍数字图像均值滤波与中值滤波的基本原理,并记录在紫光同创 PGL22G FPGA 平台的布署与实现过程。

目录
1. 均值滤波与中值滤波
2. FPGA 布署与实现
2.1 功能与指标定义
2.2 模块设计
2.3 上板调试
1. 均值滤波与中值滤波
均值滤波是一种常用的图像处理技术,属于线性滤波方式,它的基本思想是用当前像素的邻域内所有像素的平均值来替换当前像素,从而达到抑制图像噪声的目的。
具体来说,均值滤波首先选择一个邻域大小,通常是 3 × 3,或者 5 × 5,然后遍历图像上的每个点。以图像上每个点作为中心,对邻域内的像素值求均值,最后令计算结果为对应点处的响应。通过多次应用这种操作,实现对整个图像进行均值滤波处理。

均值滤波可以有效地去除图像中的噪声,然而,均值滤波也存在一些缺点,例如可能会产生图像边缘模糊和细节损失等问题。因此,在实际应用中需要根据具体需求和场景选择合适的滤波方法。例如当图像中存在椒盐噪声(salt and pepper noise)时,可以采取中值滤波的方法。
中值滤波属于非线性滤波方式,与均值滤波不同,中值滤波使用邻域内像素值的中间值,作为对应点的响应。由于中间值不受极端值的影响,因此中值滤波可以有效地过滤椒盐噪声。
使用 Matlab 实现均值滤波,代码如下:
clc, clear% 读取图像
im = imread('./img_noisy.png');
im = im2double(im);% 均值滤波
h = [1,1,1; 1,1,1; 1,1,1] .*1/9;
im_new = imfilter(im, h);subplot(121)
imshow(im2uint8(im))
title('原图像')
subplot(122)
imshow(im2uint8(im_new))
title('处理后图像')

使用 Matlab 实现中值滤波,代码如下:
clc, clear% 读取图像
im = imread('./img_noisy.png');
im = im2double(im);% 中值滤波
im_r = medfilt2(im(:,:,1), [3,3]);
im_g = medfilt2(im(:,:,2), [3,3]);
im_b = medfilt2(im(:,:,3), [3,3]);
im_new(:,:,1) = im_r;
im_new(:,:,2) = im_g;
im_new(:,:,3) = im_b;subplot(121)
imshow(im2uint8(im))
title('原图像')
subplot(122)
imshow(im2uint8(im_new))
title('处理后图像')

2. FPGA 布署与实现
2.1 功能与指标定义
使用紫光同创 FPGA 平台实现 Gamma 变换功能,FPGA 需要实现的功能与指标如下:
(1)与电脑的串口通信,用于接收上位机下发的图像数据,波特率为 256000 Bd/s;
(2)均值滤波或中值滤波,使用 FPGA 内部的 RAM 资源缓存 3 行图像数据,对 3 × 3 邻域内的数据进行均值滤波或中值滤波;
(3)DDR3 读写控制,将处理前后的图像数据分别写入 DDR3 的不同区域,实现图像的拼接;
(4)HDMI 输出,输出一路 HDMI 信号源,用于将拼接后的图像显示在外接显示器上,分辨率为 1024×768。
2.2 模块设计
均值滤波工程主要的设计模块层次与功能说明如下:
| 模块名称 | 功能说明 | |
| top_uart | uart_rx_slice | 串口接收驱动模块 |
| uart_rx_parse | 串口数据解析模块,从上位机接收 8bit 原始图像,以及 Gamma 曲线数据 | |
| top_vidin | vidin_pipeline | pipeline 单元模块,缓存两行图像数据,并将数据提交到 ddr3 数据调度模块 |
| vidin_pipeline_s | pipeline 单元简化模块,只缓存两行图像数据,无需产生与提交 ddr3 写命令 | |
| mean_filter | 均值滤波处理模块,计算 3 × 3 领域内数据的均值 | |
| merge_out | dvi_timing_gen | HDMI 视频时序产生模块 |
| dvi_ddr_rd | 根据 HDMI 控制信号,提交读指令到 ddr3 数据调度模块 | |
| dvi_encoder | HDMI 输出编码(8b10b 编码)与输出驱动模块 | |
其中,mean_filter 模块实现中值滤波处理功能,需要例化 3 个 mean_filter 模块,分别对 R、G、B 三个分量作均值滤波处理。mean_filter 实现均值滤波的思路为:
(1)使用累加电路,计算 N × 3 区域内数据的和;
(2)对上述累加值进行打拍,得到 (N-1) × 3,(N-2) × 3 和 (N-3) × 3 区域内数据的和;
(3)将 N × 3 结果与 (N-3) × 3 结果相减,得到的结果乘以 1/9,即得到最终结果。
mean_filter 模块代码如下:
`timescale 1 ns/ 1 psmodule mean_filter (input reset,input clock,input load,input [23:0] ina,input [23:0] inb,input [23:0] inc,output [23:0] outd
);// internal signal declarations
reg load_r1;
reg load_r2;
reg [71:0] rgb_in;
wire [95:0] rgb_sum;
reg [95:0] d_reg;
reg [95:0] pipeline_data1;
reg [95:0] pipeline_data2;
reg [95:0] pipeline_data3;wire [53:0] mult_data_in;
wire [80:0] mult_data_out;// load 信号打两拍,用于边沿检测
always @(posedge reset or posedge clock) beginif (reset) beginload_r1 <= 1'b0;load_r2 <= 1'b0;endelse beginload_r1 <= load;load_r2 <= load_r1;end
end// 异步复位,同步置数
always @(posedge reset or posedge clock) beginif (reset) rgb_in <= {3{24'h000000}};else if (~load) rgb_in <= {3{24'h000000}};else rgb_in <= {inc, inb, ina};
endinteger k;// 对 ina,inb,inc 进行求和与寄存
always @(posedge reset or posedge clock) beginif (reset) d_reg <= {3{32'd0}};else if (~load_r1 && load) d_reg <= {3{32'd0}};else beginfor (k=0; k<3; k=k+1) // for r,g,b componentd_reg[k*32+:32] <= d_reg[k*32+:32] + rgb_in[k*8+:8]+ rgb_in[24+k*8+:8] + rgb_in[48+k*8+:8];end
endalways @(posedge reset or posedge clock) beginif (reset) beginpipeline_data1 <= {3{32'd0}};pipeline_data2 <= {3{32'd0}};pipeline_data3 <= {3{32'd0}};endelse if (~load_r1 && load) beginpipeline_data1 <= {3{32'd0}};pipeline_data2 <= {3{32'd0}};pipeline_data3 <= {3{32'd0}};endelse beginpipeline_data1 <= d_reg;pipeline_data2 <= pipeline_data1;pipeline_data3 <= pipeline_data2;end
endassign rgb_sum[0*32+:32] = d_reg[0*32+:32] - pipeline_data3[0*32+:32];
assign rgb_sum[1*32+:32] = d_reg[1*32+:32] - pipeline_data3[1*32+:32];
assign rgb_sum[2*32+:32] = d_reg[2*32+:32] - pipeline_data3[2*32+:32];// mult_18x9_uaub_inst: Simple multiplier instantiation
generate
genvar i;
for (i=0; i<3; i=i+1) begin: mult_18x9_uaub_genemult_18x9_uaub mult_18x9_uaub_inst (.a (mult_data_in[i*18+:18] ), // input 18-bit.b (9'd57 ), // input 9-bit.clk (clock ), // input 1-bit.rst (reset ), // input 1-bit.ce (1'b1 ), // input 1-bit.p (mult_data_out[i*27+:27]) // output 27-bit);
end
endgenerate
// End of mult_18x9_uaub_inst instantiationassign mult_data_in[0*18+:18] = rgb_sum[0*32+:18];
assign mult_data_in[1*18+:18] = rgb_sum[1*32+:18];
assign mult_data_in[2*18+:18] = rgb_sum[2*32+:18];assign outd[0*8+:8] = mult_data_out[0*27+9+:8];
assign outd[1*8+:8] = mult_data_out[1*27+9+:8];
assign outd[2*8+:8] = mult_data_out[2*27+9+:8];endmodule
中值滤波工程主要的设计模块层次与功能说明如下:
| 模块名称 | 功能说明 | |
| top_uart | uart_rx_slice | 串口接收驱动模块 |
| uart_rx_parse | 串口数据解析模块,从上位机接收 8bit 原始图像,以及 Gamma 曲线数据 | |
| top_vidin | vidin_pipeline | pipeline 单元模块,缓存两行图像数据,并将数据提交到 ddr3 数据调度模块 |
| vidin_pipeline_s | pipeline 单元简化模块,只缓存两行图像数据,无需产生与提交 ddr3 写命令 | |
| median_filter | 中值滤波处理模块,包含 median_calc 模块,计算 3 × 3 领域内数据的中值 | |
| merge_out | dvi_timing_gen | HDMI 视频时序产生模块 |
| dvi_ddr_rd | 根据 HDMI 控制信号,提交读指令到 ddr3 数据调度模块 | |
| dvi_encoder | HDMI 输出编码(8b10b 编码)与输出驱动模块 | |
其中,median_filter 模块实现中值滤波处理功能,同样需要例化 3 个 median_filter 模块,分别对 R、G、B 三个分量作中值滤波处理。median_filter 实现中值滤波的思路为:
(1)对每一行的 3 个数据进行排序,得到当前行的最大值、中间值和最小值;
(2)比较不同行的最大值、中间值和最小值,依次取最小、中间和最大的值;
(3)比较最小最大值、中间中间值和最大最小值,取中间的值,即得到最终结果。
median_filter 模块代码如下:
`timescale 1 ns/ 1 psmodule median_filter (input reset,input clock,input load,input [7:0] ina,input [7:0] inb,input [7:0] inc,output [7:0] outd
);// internal signal declarations
reg [23:0] ina_r;
reg [23:0] inb_r;
reg [23:0] inc_r;wire [23:0] out_a;
wire [23:0] out_b;
wire [23:0] out_c;wire [15:0] max_nc;
wire [15:0] min_nc;
wire [7:0] mid_nc1;
wire [7:0] mid_nc2;
wire [7:0] max_min;
wire [7:0] mid_mid;
wire [7:0] min_max;wire [7:0] out_nc1;
wire [7:0] out_nc2;// 异步复位,同步置数
always @(posedge reset or posedge clock) beginif (reset) beginina_r <= {3{8'd0}};inb_r <= {3{8'd0}};inc_r <= {3{8'd0}};endelse if (~load) beginina_r <= {ina_r[0+:16], 8'd0};inb_r <= {inb_r[0+:16], 8'd0};inc_r <= {inc_r[0+:16], 8'd0};endelse beginina_r <= {ina_r[0+:16], ina};inb_r <= {inb_r[0+:16], inb};inc_r <= {inc_r[0+:16], inc};end
end// 对每一行进行排序
median_calc median_calc_insta (.reset (reset ), // input 1-bit.clock (clock ), // input 1-bit.ina (ina_r[0*8+:8] ), // input 8-bit.inb (ina_r[1*8+:8] ), // input 8-bit.inc (ina_r[2*8+:8] ), // input 8-bit.outd (out_a ) // output 24-bit
);median_calc median_calc_instb (.reset (reset ), // input 1-bit.clock (clock ), // input 1-bit.ina (inb_r[0*8+:8] ), // input 8-bit.inb (inb_r[1*8+:8] ), // input 8-bit.inc (inb_r[2*8+:8] ), // input 8-bit.outd (out_b ) // output 24-bit
);median_calc median_calc_instc (.reset (reset ), // input 1-bit.clock (clock ), // input 1-bit.ina (inc_r[0*8+:8] ), // input 8-bit.inb (inc_r[1*8+:8] ), // input 8-bit.inc (inc_r[2*8+:8] ), // input 8-bit.outd (out_c ) // output 24-bit
);// 比较最大值,取其中的最小值
median_calc median_calc_inst_max (.reset (reset ), // input 1-bit.clock (clock ), // input 1-bit.ina (out_a[2*8+:8] ), // input 8-bit.inb (out_b[2*8+:8] ), // input 8-bit.inc (out_c[2*8+:8] ), // input 8-bit.outd ({min_nc, min_max} ) // output 24-bit
);// 比较中间值, 取其中的中间值
median_calc median_calc_inst_mid (.reset (reset ), // input 1-bit.clock (clock ), // input 1-bit.ina (out_a[1*8+:8] ), // input 8-bit.inb (out_b[1*8+:8] ), // input 8-bit.inc (out_c[1*8+:8] ), // input 8-bit.outd ({mid_nc1, mid_mid, mid_nc2} ) // output 24-bit
);// 比较最小值, 取其中的最大值
median_calc median_calc_inst_min (.reset (reset ), // input 1-bit.clock (clock ), // input 1-bit.ina (out_a[0*8+:8] ), // input 8-bit.inb (out_b[0*8+:8] ), // input 8-bit.inc (out_c[0*8+:8] ), // input 8-bit.outd ({max_min, max_nc} ) // output 24-bit
);// 比较min_max, mid_mid, max_min
median_calc median_calc_inst_out (.reset (reset ), // input 1-bit.clock (clock ), // input 1-bit.ina (min_max ), // input 8-bit.inb (mid_mid ), // input 8-bit.inc (max_min ), // input 8-bit.outd ({out_nc1, outd, out_nc2} ) // output 24-bit
);endmodule
median_calc 模块代码如下:
`timescale 1 ns/ 1 psmodule median_calc (input reset,input clock,input [7:0] ina,input [7:0] inb,input [7:0] inc,output [23:0] outd
);// internal signal declarations
reg [7:0] out_min;
reg [7:0] out_mid;
reg [7:0] out_max;always @(posedge reset or posedge clock) beginif (reset) beginout_min <= 8'd0;out_mid <= 8'd0;out_max <= 8'd0;endelse beginif (ina <= inb && ina <= inc) out_min <= ina;else if (inb <= ina && inb <= inc) out_min <= inb;elseout_min <= inc;if (ina >= inb && ina >= inc) out_max <= ina;else if (inb >= ina && inb >= inc) out_max <= inb;elseout_max <= inc;if ((ina >= inb && ina <= inc) || (ina <= inb && ina >= inc)) out_mid <= ina; else if ((inb >= ina && inb <= inc) || (inb <= ina && inb >= inc)) out_mid <= inb;elseout_mid <= inc;end
endassign outd = {out_max, out_mid, out_min};endmodule
2.3 上板调试
使用 PyQt5 和 OpenCV 库编写上位机程序,通过串口发送原始图像数据。由于 FPGA 使用了 pipeline 结构,处理后的图像高度会减小 2 行,因此上位机需要多发送 2 行图像数据,python 代码中对行计数器 row_cnt 进行求余运算即可。
代码如下:
# -*- Coding: UTF-8 -*-
import cv2
import sys
import struct
import numpy as np
import pyqtgraph as pg
from PyQt5 import Qt, QtGui, QtCore, QtWidgets, QtSerialPortclass mainWindow(Qt.QWidget):def __init__(self, com_port, parent=None):super(mainWindow, self).__init__(parent)#self.setGeometry(720, 300, 512, 384)self.setFixedSize(530, 384)self.setWindowTitle("PGL OpenCV Tool")# 创建标签与按钮self.img_widget = QtWidgets.QLabel()self.btn1 = QtWidgets.QPushButton("打开")self.btn1.clicked.connect(self.getfile)self.btn2 = QtWidgets.QPushButton("关闭")self.btn2.clicked.connect(self.close)# 创建布局centralLayout = QtWidgets.QVBoxLayout()centralLayout.addWidget(self.img_widget)bottomLayout = QtWidgets.QHBoxLayout()bottomLayout.addWidget(self.btn1)bottomLayout.addWidget(self.btn2)centralLayout.addLayout(bottomLayout)self.setLayout(centralLayout)# 串口对象self.COM = QtSerialPort.QSerialPort()self.COM.setPortName(com_port)self.COM.setBaudRate(256000)self.open_status = Falseself.row_cnt = 0self.img = Noneself.timer = QtCore.QTimer()self.timer.timeout.connect(self.sendImage)self.startup()def startup(self):"""Write code here to run once"""for com_port in QtSerialPort.QSerialPortInfo.availablePorts():print(com_port.portName())# Try open serial portif not self.COM.open(QtSerialPort.QSerialPort.ReadWrite):self.open_status = Falseprint("Open Serial Port failed.")else:self.open_status = Truedef getfile(self):"""获取图像路径"""fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file','C:\\Users\\Administrator\\Pictures', "Image files(*.jpg *.png)")self.clipImage(fname[0])self.updateImage()self.sendImage()self.slider_window.updatePlot(self.img)def clipImage(self, fname):"""读取并裁剪图片至512x384大小"""if fname:img = cv2.imread(fname, cv2.IMREAD_COLOR)img_roi = img[:384,:512,:]print(img_roi.shape)cv2.imwrite('./img_roi.png', img_roi)def updateImage(self):"""显示裁剪后的图像"""self.img = cv2.imread('./img_roi.png')# 显示原始图像self.img_widget.setPixmap(QtGui.QPixmap('./img_roi.png'))if self.open_status:self.timer.start(100)def sendImage(self):"""通过串口发送图片"""pattern = ">2H{:d}B".format(512*3)if self.open_status:if self.row_cnt == 384+2:self.row_cnt = 0self.timer.stop()else:args1 = [0x5500, self.row_cnt]args2 = [rgb for rgb in self.img[(self.row_cnt % 384),:].reshape(-1)]send_data = struct.pack(pattern, *(args1+args2))self.row_cnt += 1self.COM.write(send_data)def closeEvent(self, event):super().closeEvent(event)# 定时器停止self.timer.stop()if self.open_status:self.COM.close() # 关闭串口def main():app = QtWidgets.QApplication(sys.argv)window = mainWindow('COM21')window.show()sys.exit(app.exec_())if __name__ == "__main__":main()
连接 HDMI 线和串口线,选择带椒盐噪声的图像,观察 FPGA 处理前后的效果。
以下是均值滤波的处理结果。

以下是中值滤波的处理结果。

可以看出,均值滤波可以在一定程度上降低椒盐噪声的干扰,但同时会导致图像模糊;而中值滤波可以 100% 去除图像中的椒盐噪声,同时很好地保持图像的细节。