1. Title
Image의 Edge를 검출할 수 있는 Sobel Filter를 구현한다.
2. Category
"Video Processing"
3. Key Concepts
Edge detection by Sobel Filter
4. 개념
Sobel 필터는 이미지 처리에서 경계(엣지)를 감지하는 데 사용되는 필터 중 하나이다. 이 필터는 주로 이미지의 수평(horizontal) 또는 수직(vertical) 방향의 변화율을 측정하여 경계를 감지한다.
수평(수직) 방향의 변화량을 계산하기 위해 각각 Sobel 필터를 사용한 두 개의 연산을 한다
1) 수평 필터 (Gx)
2) 수직 필터 (Gy)
Sobel 필터는 3x3 커널을 사용하여 각 픽셀에 대한 변화를 계산하게된다.
sobel_filter_x
| -1 0 1 |
| -2 0 2 |
| -1 0 1 |
sobel_filter_y
| -1 -2 -1 |
| 0 0 0 |
| 1 2 1 |
각 방향의 경계를 구한 후, 두 방향의 경계를 합성하여 전체 경계 크기를 구한다. 경계 크기는 피타고라스의 정리를 사용하여 계산하게 된다.
mag_sobel = sqrt(Gx^2 + Gy^2)
경계가 일정 강도 이상인 경우(예: mag_sobel > THRESH_HIGH), 해당 픽셀은 경계로 인식하고, 그렇지 않으면 배경으로 간주하게 된다.
5. BLOCK DIAGRAM / FSM
6.Code Review
원래는 소벨 필터의 결과값을 처리하여 BRAM에 따로 저장 후 출력하는 구조를 생각하였다. 하지만 Basys-3 보드의 메모리 부족으로 인해 해당 컨셉을 바꾸게 되었는데...
실시간으로 OV7670으로 부터 받는 이미지 데이터를 저장하는 Frame Buffer 하나만으로도 BRAM이 72%나 사용되고 있었기 때문이다. 따라서 라인버퍼 방식을 사용하여 실시간으로 이미지를 처리하고 내보내는 방식으로 다시 컨셉을 잡게 되었다.
1차 시도: 라인 버퍼 기반 소벨 필터
처음에는 Line Buffer를 입력받은 후, 해당 버퍼에 Sobel Filter를 적용하여 영상을 출력하는 구조로 설계하였다. 그러나 출력되는 영상과 VGA의 Sync 신호가 제대로 맞지 않는 문제가 발생하였고, 이를 해결하기 위해 버퍼 논리를 간소화하면 어떨까? 라는 생각을 했다.
module sobel_filter #(
parameter IMAGE_WIDTH = 320
)
(
clk,
reset,
enable,
pixel_in,
x_pixel,
y_pixel,
pixel_out
);
input logic clk;
input logic reset;
input logic enable;
input logic [11:0] pixel_in;
input logic [9:0] x_pixel;
input logic [9:0] y_pixel;
output logic [3:0] pixel_out;
logic [11:0] gray;
always_comb begin
gray = ((pixel_in[11:8] * 77 + pixel_in[7:4] * 150 + pixel_in[3:0] * 29) + 128) >> 8;
end
logic signed [3:0] sobel_filter_x[0:2][0:2] = '{
'{-1, 0, 1},
'{-2, 0, 2},
'{-1, 0, 1}
};
logic signed [3:0] sobel_filter_y[0:2][0:2] = '{
'{-1, -2, -1},
'{0, 0, 0},
'{1, 2, 1}
};
logic [7:0] line_buffer [0:2][0:IMAGE_WIDTH-1];
// Sequential line buffer filling logic
always_ff @(posedge clk, posedge reset) begin
if (reset) begin
for (int i = 0; i < 3; i = i + 1)
for (int j = 0; j < IMAGE_WIDTH; j = j + 1)
line_buffer[i][j] <= 0;
end
else if (enable) begin
for (int k = 0; k < IMAGE_WIDTH-1; k = k + 1) begin
line_buffer[0][k] <= line_buffer[0][k+1];
line_buffer[1][k] <= line_buffer[1][k+1];
line_buffer[2][k] <= line_buffer[2][k+1];
end
line_buffer[0][IMAGE_WIDTH -1] <= line_buffer[1][0];
line_buffer[1][IMAGE_WIDTH -1] <= line_buffer[2][0];
line_buffer[2][IMAGE_WIDTH-1] <= gray;
end
end
logic [1:0] pipeline_valid;
always_ff @(posedge clk, posedge reset) begin
if (reset) begin
pipeline_valid <= 0;
end else if (enable) begin
pipeline_valid <= {
pipeline_valid[0], (((x_pixel >= 2) && (y_pixel >= 2)) | (y_pixel >= 3))
};
end
end
logic signed [12:0] gx_sobel, gy_sobel;
logic [19:0] mag_sobel;
always_ff @(posedge clk or posedge reset) begin
if (reset) begin
gx_sobel <= 0;
gy_sobel <= 0;
mag_sobel <= 0;
pixel_out <= 0;
end else if (enable && pipeline_valid[1]) begin
gx_sobel <= (line_buffer[0][0] * sobel_filter_x[0][0]) +
(line_buffer[0][1] * sobel_filter_x[0][1]) +
(line_buffer[0][2] * sobel_filter_x[0][2]) +
(line_buffer[1][0] * sobel_filter_x[1][0]) +
(line_buffer[1][1] * sobel_filter_x[1][1]) +
(line_buffer[1][2] * sobel_filter_x[1][2]) +
(line_buffer[2][0] * sobel_filter_x[2][0]) +
(line_buffer[2][1] * sobel_filter_x[2][1]) +
(line_buffer[2][2] * sobel_filter_x[2][2]);
gy_sobel <= (line_buffer[0][0] * sobel_filter_y[0][0]) +
(line_buffer[0][1] * sobel_filter_y[0][1]) +
(line_buffer[0][2] * sobel_filter_y[0][2]) +
(line_buffer[1][0] * sobel_filter_y[1][0]) +
(line_buffer[1][1] * sobel_filter_y[1][1]) +
(line_buffer[1][2] * sobel_filter_y[1][2]) +
(line_buffer[2][0] * sobel_filter_y[2][0]) +
(line_buffer[2][1] * sobel_filter_y[2][1]) +
(line_buffer[2][2] * sobel_filter_y[2][2]);
mag_sobel <= (gx_sobel * gx_sobel + gy_sobel * gy_sobel) >> 6;
// mag_sobel <= (gx_sobel >= 0 ? gx_sobel : -gx_sobel) +
// (gy_sobel >= 0 ? gy_sobel : -gy_sobel);
pixel_out <= (mag_sobel > THRESH_HIGH) ? 4'hf : 0;
end
end
endmodule
2차 시도: 버퍼 논리 간소화
두 번째로는 라인 버퍼 구조를 보다 간단하게 설계하여 소벨 필터를 적용하였다. 그 결과, 출력 영상에서 노이즈가 크게 줄었고 실시간으로 안정적인 영상 처리가 가능하게 되었다.
module sobel_filter #(
parameter IMAGE_WIDTH = 320,
parameter THRESH_HIGH = 4500,
parameter THRESH_LOW = 5
) (
clk,
reset,
enable,
pixel_in,
x_pixel,
y_pixel,
pixel_out
);
input logic clk;
input logic reset;
input logic enable;
input logic [11:0] pixel_in;
input logic [9:0] x_pixel;
input logic [9:0] y_pixel;
output logic [3:0] pixel_out;
logic [7:0] gray;
always_comb begin
gray = (pixel_in[11:8] * 77 + pixel_in[7:4] * 150 + pixel_in[3:0] * 29) >> 7;
end
logic signed [3:0] sobel_filter_x[0:2][0:2] = '{
'{-1, 0, 1},
'{-2, 0, 2},
'{-1, 0, 1}
};
logic signed [3:0] sobel_filter_y[0:2][0:2] = '{
'{-1, -2, -1},
'{ 0, 0, 0},
'{ 1, 2, 1}
};
logic signed [7:0] window[0:2][0:2];
always_ff @(posedge clk or posedge reset) begin
if (reset) begin
for (int i = 0; i < 3; i = i + 1)
for (int j = 0; j < 3; j = j + 1)
window[i][j] <= 0;
end else if (enable) begin
window[0][0] <= window[0][1];
window[0][1] <= window[0][2];
window[0][2] <= window[1][0];
window[1][0] <= window[1][1];
window[1][1] <= window[1][2];
window[1][2] <= window[2][0];
window[2][0] <= window[2][1];
window[2][1] <= window[2][2];
window[2][2] <= gray;
end
end
logic [1:0] pipeline_valid;
always_ff @(posedge clk, posedge reset ) begin
if(reset) begin
pipeline_valid <= 0;
end else if(enable) begin
pipeline_valid <= {pipeline_valid[0], ((x_pixel >= 2) && (y_pixel >= 2) | (y_pixel >= 3))};
end
end
logic signed [11:0] gx_sobel, gy_sobel;
logic [11:0] mag_sobel;
always_ff @(posedge clk or posedge reset) begin
if (reset) begin
gx_sobel <= 0;
gy_sobel <= 0;
mag_sobel <= 0;
pixel_out <= 0;
end else if (enable && pipeline_valid[1]) begin
gx_sobel <= (window[0][0] * sobel_filter_x[0][0]) +
(window[0][1] * sobel_filter_x[0][1]) +
(window[0][2] * sobel_filter_x[0][2]) +
(window[1][0] * sobel_filter_x[1][0]) +
(window[1][1] * sobel_filter_x[1][1]) +
(window[1][2] * sobel_filter_x[1][2]) +
(window[2][0] * sobel_filter_x[2][0]) +
(window[2][1] * sobel_filter_x[2][1]) +
(window[2][2] * sobel_filter_x[2][2]);
gy_sobel <= (window[0][0] * sobel_filter_y[0][0]) +
(window[0][1] * sobel_filter_y[0][1]) +
(window[0][2] * sobel_filter_y[0][2]) +
(window[1][0] * sobel_filter_y[1][0]) +
(window[1][1] * sobel_filter_y[1][1]) +
(window[1][2] * sobel_filter_y[1][2]) +
(window[2][0] * sobel_filter_y[2][0]) +
(window[2][1] * sobel_filter_y[2][1]) +
(window[2][2] * sobel_filter_y[2][2]);
mag_sobel <= (gx_sobel >= 0 ? gx_sobel : -gx_sobel) +
(gy_sobel >= 0 ? gy_sobel : -gy_sobel);
pixel_out <= (mag_sobel > 50) ? 4'hf :
(mag_sobel > 20) ? mag_sobel >> 2 : 0;
end
end
endmodule
그러나, 코드를 다시 살펴보니 이미지를 불러와 윈도우를 업데이트하는 논리에 오류가 있는 것을 발견하게 되었다.
원래는 이런 방식으로 커널이 이동하며 Edge에 대한 결과를 구해야 하는데, 내가 짠 로직은 아래와 같이 괴상한? 형태로 커널이 이동하고 있었던 것이다.
따라서 최종적으로 생각한 컨셉은 Line Buffer의 업데이트 논리와 커널이 곱해지는 영역이 업데이트 되는 논리를 실시간으로 구현하는 것이었다.
3차 시도: Line Buffer와 Window의 동시 업데이트 방식
Line Buffer와 Window를 동시에 업데이트 하기 위해서는 파이프라인 구조를 도입하는 것이 필수적이었다. 아래와 같은 논리로 로직을 구현하면 어떨까 하고 생각하고 코드를 짜보았다.
- Line Buffer와 Window가 한 사이클 내에서 함께 갱신되면서, 새 pixel 입력(gray_in)이 들어오면
- Line Buffer는 shift하며 새 데이터를 저장.
- Window는 Line Buffer에서 데이터를 읽고 동시에 shift.
module sobel_filter #(
parameter IMAGE_WIDTH = 320,
parameter IMAGE_HEIGHT = 240,
parameter THRESH_HIGH = 4500,
parameter THRESH_LOW = 5
) (
clk,
reset,
enable,
pixel_in,
x_pixel,
y_pixel,
pixel_out
);
input logic clk;
input logic reset;
input logic enable;
input logic [11:0] pixel_in;
input logic [9:0] x_pixel;
input logic [9:0] y_pixel;
output logic [3:0] pixel_out;
logic [7:0] gray;
always_comb begin
gray = (pixel_in[11:8] * 77 + pixel_in[7:4] * 150 + pixel_in[3:0] * 29) >> 7;
end
logic signed [3:0] sobel_filter_x[0:2][0:2] = '{
'{-1, 0, 1},
'{-2, 0, 2},
'{-1, 0, 1}
};
logic signed [3:0] sobel_filter_y[0:2][0:2] = '{
'{-1, -2, -1},
'{ 0, 0, 0},
'{ 1, 2, 1}
};
// Line buffers for storing three complete rows of pixels
logic [7:0] line_buffer[0:2][0:IMAGE_WIDTH-1];
// 3x3 window for Sobel operation
logic signed [7:0] window[0:2][0:2];
logic window_valid;
// Update line buffers and window
always_ff @(posedge clk or posedge reset) begin
if (reset) begin
// Reset line buffers
for (int i = 0; i < IMAGE_WIDTH; i = i + 1) begin
line_buffer[0][i] <= 0;
line_buffer[1][i] <= 0;
line_buffer[2][i] <= 0;
end
// Reset window
for (int i = 0; i < 3; i = i + 1)
for (int j = 0; j < 3; j = j + 1)
window[i][j] <= 0;
window_valid <= 0;
end else if (enable) begin
line_buffer[y_pixel % 3][x_pixel] <= gray;
if (x_pixel % 3 >= 2 && y_pixel % 3 >= 2 && x_pixel < IMAGE_WIDTH && y_pixel < IMAGE_HEIGHT) begin
window_valid <= 1;
window[0][0] <= line_buffer[0][x_pixel-2];
window[0][1] <= line_buffer[0][x_pixel-1];
window[0][2] <= line_buffer[0][x_pixel];
// Middle row of window
window[1][0] <= line_buffer[1][x_pixel-2];
window[1][1] <= line_buffer[1][x_pixel-1];
window[1][2] <= line_buffer[1][x_pixel];
// Bottom row of window
window[2][0] <= line_buffer[2][x_pixel-2];
window[2][1] <= line_buffer[2][x_pixel-1];
window[2][2] <= line_buffer[2][x_pixel];
end
else begin
window_valid <= 0;
end
end
end
logic signed [11:0] gx_sobel, gy_sobel;
logic [11:0] mag_sobel;
always_ff @(posedge clk or posedge reset) begin
if (reset) begin
gx_sobel <= 0;
gy_sobel <= 0;
mag_sobel <= 0;
pixel_out <= 0;
end else if (enable && window_valid) begin
gx_sobel <= (window[0][0] * sobel_filter_x[0][0]) +
(window[0][1] * sobel_filter_x[0][1]) +
(window[0][2] * sobel_filter_x[0][2]) +
(window[1][0] * sobel_filter_x[1][0]) +
(window[1][1] * sobel_filter_x[1][1]) +
(window[1][2] * sobel_filter_x[1][2]) +
(window[2][0] * sobel_filter_x[2][0]) +
(window[2][1] * sobel_filter_x[2][1]) +
(window[2][2] * sobel_filter_x[2][2]);
gy_sobel <= (window[0][0] * sobel_filter_y[0][0]) +
(window[0][1] * sobel_filter_y[0][1]) +
(window[0][2] * sobel_filter_y[0][2]) +
(window[1][0] * sobel_filter_y[1][0]) +
(window[1][1] * sobel_filter_y[1][1]) +
(window[1][2] * sobel_filter_y[1][2]) +
(window[2][0] * sobel_filter_y[2][0]) +
(window[2][1] * sobel_filter_y[2][1]) +
(window[2][2] * sobel_filter_y[2][2]);
mag_sobel <= (gx_sobel >= 0 ? gx_sobel : -gx_sobel) +
(gy_sobel >= 0 ? gy_sobel : -gy_sobel);
pixel_out <= (mag_sobel > 45) ? 4'hf :
(mag_sobel > 20) ? mag_sobel >> 2 : 0;
end
end
endmodule
나름 파이프라인 구조라 생각하고 짰던 코드이다. 하지만 라인 버퍼에 대한 이해가 부족한 상태에서 머리 속에서 생각나는 그대로 코드를 적었더니... 각 연산의 논리가 명확하게 나눠져있지도 않고, 괴상한 놈이 탄생했다. 처음에는 원하는 결과가 나오지 않아 임계값 조정 등 파라미터를 수정하다가 결국 코드를 갈아엎고 처음 컨셉부터 명확하게 잡기로 결심하였다.
4차 시도: 명확한 파이프라인 구조 구축
앞서 말했듯, 나는 다시 초심으로 돌아와 처음부터 구조를 완벽히 계획하기로 결심하였다. 이를 위해 다음과 같은 부분을 확인해야겠다는 계획을 세웠다.
- 매 사이클마다 Line Buffer의 업데이트가 어떻게 되는지?
- 커널이 연산되는 구역(윈도우 사이즈라고 부르겠다)의 업데이트는 어떻게 되야하는지?
- Sobel Filter의 계산되는 시점은 어떻게 되야 하는지?
- 파이프 라인은 어떻게 구현할 것이며, 각 값이 업데이트 되는 시점을 정확하게 파악해야함!
해당 자료는 위의 내용을 바탕으로 IMAGE가 (8 X 8) 사이즈라고 가정하고, 매 사이클마다 각 레지스터의 값이 어떻게 업데이트 되는지 타임라인으로 분석한 자료이다. 중요한 몇 부분만 살펴보자면,
- 라인 버퍼 4개:
각 라인 버퍼는 한 라인씩 데이터를 저장, shift하며 새로운 입력을 넣음.
line_buffer_1 (IMAGE COL1) -> line_buffer_2 (IMAGE COL2) -> line_buffer_3 (IMAGE COL3) ->line_buffer_4 (IMAGE COL4) -> line_buffer_1 (IMAGE COL5) -> .... - 윈도우 구성:
매 사이클마다 윗 행은 이전 line buffer 값들로, 마지막 행은 실시간으로 들어온 입력 데이터로 채움.
Window의 값이 모두 채워지는, 즉 소벨 필터가 연산이 가능해지는 시점부터 소벨필터 연산을 시작함. 연산은 클록에 동기화 되어 이전 클럭의 Window 값을 바탕으로 계산 후, 그 다음 사이클에 값을 내보냄 (Sequential Logic)
Window의 값이 유효하지 않은 시점에서는 소벨필터의 Valid 신호를 0으로 만들어 쓰레기 값이 계산되지 않게함.
해당 시나리오를 바탕으로 다음과 같이 파이프라인을 짠 코드이다.
always @(posedge clk) begin
if (reset) begin
for (i = 0; i < IMG_WIDTH; i = i + 1) begin
line_buffer_4[i] <= 0;
line_buffer_3[i] <= 0;
line_buffer_2[i] <= 0;
line_buffer_1[i] <= 0;
end
end else if (display_enable) begin
line_buffer_4[x_pixel] <= line_buffer_3[x_pixel];
line_buffer_3[x_pixel] <= line_buffer_2[x_pixel];
line_buffer_2[x_pixel] <= line_buffer_1[x_pixel];
line_buffer_1[x_pixel] <= gray_8bit;
end
end
always @(posedge clk) begin
if (reset) begin
{w_0_0, w_1_0, w_2_0, w_3_0, w_4_0} <= 0;
{w_0_1, w_1_1, w_2_1, w_3_1, w_4_1} <= 0;
{w_0_2, w_1_2, w_2_2, w_3_2, w_4_2} <= 0;
{w_0_3, w_1_3, w_2_3, w_3_3, w_4_3} <= 0;
{w_0_4, w_1_4, w_2_4, w_3_4, w_4_4} <= 0;
valid_pipeline <= 0;
end else if (display_enable) begin
w_4_0 <= line_buffer_4[x_pixel];
w_4_1 <= line_buffer_3[x_pixel];
w_4_2 <= line_buffer_2[x_pixel];
w_4_3 <= line_buffer_1[x_pixel];
w_4_4 <= gray_8bit;
w_3_0 <= w_4_0;
w_3_1 <= w_4_1;
w_3_2 <= w_4_2;
w_3_3 <= w_4_3;
w_3_4 <= w_4_4;
w_2_0 <= w_3_0;
w_2_1 <= w_3_1;
w_2_2 <= w_3_2;
w_2_3 <= w_3_3;
w_2_4 <= w_3_4;
w_1_0 <= w_2_0;
w_1_1 <= w_2_1;
w_1_2 <= w_2_2;
w_1_3 <= w_2_3;
w_1_4 <= w_2_4;
w_0_0 <= w_1_0;
w_0_1 <= w_1_1;
w_0_2 <= w_1_2;
w_0_3 <= w_1_3;
w_0_4 <= w_1_4;
valid_pipeline <= {
valid_pipeline[1:0], (x_pixel >= 4 && y_pixel >= 4)
};
end else begin
valid_pipeline <= {valid_pipeline[1:0], 1'b0};
end
end
테스트 벤치 결과를 보면, 각 클록마다 라인 버퍼와 Window에는 값이 업데이트 되어지는 것을 확인할 수 있었다.
Image WIDTH 만큼의 시간이 끝날때마다 다음 라인 버퍼로 복사되는 로직이 올바르게 구현된 것을 확인
Valid (소벨 필터가 계산을 시작하는 시점, x_pixel >=5, y_pixel >=4) 부분부터 계산을 시작하여 다음 클록에 값을 내보내는 부분(mag_sobel) 도 구현이 잘 된 것을 확인할 수 있었다.
mag_sobel을 원래는 Combinational logic 으로 구현하여 바로 출력 픽셀이 나가도록 했었는데, 혹시나 있을 메타스테이블 상태가 우려되어 레지스터에 저장후 한 클럭 뒤에 실질적인 픽셀 값이 나가게 수정하였다.
설계한 시나리오대로 파이프라인 구조를 통해 영상이 출력되는 것을 확인할 수 있었다. 아직 Edge 검출은 완벽하게 하지 못하고 있고 있는 상황이여서 조정할 수 있는 파라미터들을 정리해보았다.
- Input Data의 크기(비트 수)
- 명암(밝기) 표현 범위: 비트수가 높을수록 (예: 8-bit → 16-bit) 더 많은 밝기 레벨을 표현할 수 있음.
- 노이즈 민감도: 비트수가 높으면 작은 변화도 민감하게 표현됨 → 미세한 노이즈까지 검출될 가능성 있음.
- 커널 사이즈 (5x5? 3x3?)
- 3x3 → 좁은 영역 기준으로 경계 검출 → 빠른 변화(세밀한 엣지)에 민감.
- 5x5 → 더 넓은 영역 평균 → 큰 구조, 직선성 강조, 노이즈 필터링 효과 더 강함.
- 노이즈 제거 효과: 커널이 클수록 노이즈가 평균화되어 사라지는 경향.
- Threshold 값
- 엣지 검출의 강도 판단: 그래디언트 크기 기준으로 엣지 여부 결정. Threshold가 낮으면 약한 엣지도 검출되고, 높으면 강한 엣지만 검출. 이 부분은 SW 입력을 통해 바꿀 수 있도록 설정
사실 파라미터를 찾는 부분이 가장 오래 걸렸었다. 적절한 비트 수를 선정하는 것부터 임계값을 어떻게 설정할 것인가. 그리고 커널 사이즈가 정말 노이즈 감소에 효과가 있는가? 판단을 내려야 할 요소가 너무나도 많았기 때문이다. 시행착오 끝에 내린 결론을 공유해본다.
우선 Input Data를 정하기 위해 카메라에서 받는 12비트의 데이터 중 주요 정보(영상의 큰 밝기 차이)가 담겨 있는 상위 비트를 추출하여 비교해보았다.
위에서부터 상위 8비트, 4비트, 2비트, 1비트의 출력 값인데, 이 중에서 Edge 검출도 어느정도 되면서 노이즈도 적당히 잡혀있는 상위 4비트를 선정하였다.
소벨 필터 관련 논문을 찾아보던 중 필터 사이즈를 높일 시, 더 명확한 라인을 표현할 수 있다는 내용을 찾게 되어 필터의 사이즈도 늘려서 설계를 하기로 결정하였다.
5x5 커널사이즈 를 사용하였을 때의 영상이다.
3x3 커널 사이즈를 사용하였을 때의 영상이다.
두 필터 사이의 눈에 띄는 차이는 발견하지 못했다.
사실 양쪽에 필터를 띄워놓고 동시에 비교하고 싶었지만, 두 필터를 인스턴스 하면 화면이 아예 출력되지 않는 버그가 발생을 해서... 디버깅을 위해 하루종일 매달려봤지만 결국 찾지 못하고 따로 비교하게 되었다.. 아래와 같이 Input Buffer를 추가하니 어느정도 화면이 출력은 되었지만, 결국 근본적인 원인은 찾지 못하였다.
/////////////////////////////////////////////////////////////////////
// 기존 Logic
/////////////////////////////////////////////////////////////////////
sobel_filter_5x5 sobel_5x5(
.clk(clk_25MHz),
.reset(reset),
.gray_8bit(gray_8bit),
.x_pixel(x_pixel),
.y_pixel(y_pixel),
.display_enable(display_enable),
.threshold(threshold),
.sobel_out(sobel_out_5x5)
);
sobel_filter_3x3 sobel_3x3(
.clk(clk_25MHz_1),
.reset(reset),
.gray_8bit(gray_8bit),
.x_pixel(x_pixel),
.y_pixel(y_pixel),
.display_enable(display_enable),
.threshold(threshold),
.sobel_out(sobel_out_3x3)
);
/////////////////////////////////////////////////////////////////////
// INPUT BUFFER 주가
/////////////////////////////////////////////////////////////////////
logic [7:0] gray_8bit_3x3;
assign gray_8bit_3x3 = gray_8bit;
logic [7:0] gray_8bit_5x5;
assign gray_8bit_5x5 = gray_8bit;
sobel_filter_5x5 sobel_5x5(
.clk(clk_25MHz),
.reset(reset),
.gray_8bit(gray_8bit_5x5),
.x_pixel(x_pixel),
.y_pixel(y_pixel),
.display_enable(display_enable),
.threshold(threshold),
.sobel_out(sobel_out_5x5)
);
sobel_filter_3x3 sobel_3x3(
.clk(clk_25MHz_1),
.reset(reset),
.gray_8bit(gray_8bit_3x3),
.x_pixel(x_pixel),
.y_pixel(y_pixel),
.display_enable(display_enable),
.threshold(threshold),
.sobel_out(sobel_out_3x3)
);
VGA 출력단에서 타이밍 문제가 있나 싶어 레지스터로 출력값을 받아봤지만 이 또한 실패..
/////////////////////////////////////////////////////////////////////
// 기존 Logic
/////////////////////////////////////////////////////////////////////
assign {red_port, green_port, blue_port} =
(display_enable && (x_pixel < 320 && y_pixel < 240)) ? {sobel_out_5x5, sobel_out_5x5, sobel_out_5x5} :
(display_enable && (x_pixel > 320 && x_pixel < 640 && y_pixel < 240)) ? {r, g, b} : 12'b0;
/////////////////////////////////////////////////////////////////////
// OUTPUT BUFFER 주가
/////////////////////////////////////////////////////////////////////
logic [3:0] r_out, g_out, b_out;
always_ff @(posedge clk_25MHz) begin
if (reset) begin
r_out <= 4'b0;
g_out <= 4'b0;
b_out <= 4'b0;
end else begin
if (display_enable && (x_pixel < 320 && y_pixel < 240)) begin
r_out <= sobel_out_5x5;
g_out <= sobel_out_5x5;
b_out <= sobel_out_5x5;
end else if (display_enable && (x_pixel > 320 && x_pixel < 640 && y_pixel < 240)) begin
r_out <= r;
g_out <= g;
b_out <= b;
end else begin
r_out <= 4'b0;
g_out <= 4'b0;
b_out <= 4'b0;
end
end
end
assign red_port = r_out;
assign green_port = g_out;
assign blue_port = b_out;
추가적으로, 위의 Sobel Filter를 보게 되면 컨볼루션 연산이 유효하지 않은 테두리 부분에 불필요한 출력이 발생하고 있는데, 이는 출력이 유효해지는 시점을 정하지 않고 내보내서 발생한 문제이다.
따라서 이런 쓰레기 값을 버리기 위해서는 픽셀을 출력하는 파이프라인 구조를 추가하거나, VGA의 x_pixel, y_pixel 값에 따라 픽셀 값을 출력하지 않게 하면 해결되게 된다.
쓰레기 값이 포함된 이미지
파이프라인 추가
VGA의 x_pixel, y_pixel 값에 따라 픽셀 값을 출력하지 않게 할 때
- 이 프로젝트를 진행하면서 실시간 이미지 처리를 하드웨어로 구현하는 것이 얼마나 힘든지 깨닫게 되었다.
- 처음에는 Basys-3 보드의 메모리 제약으로 인해 원래 구상했던 BRAM 기반 프레임 버퍼 설계를 포기하고 라인 버퍼 방식으로 전환해야 했던 경험은 이미지 파일의 크기가 생각보다 크다는 점을 깨닫게 해주었다.
- 단일 프레임 버퍼만으로도 BRAM의 72%를 사용하기 때문에 라인 버퍼를 구현하게 되었는데, 라인 버퍼 기반 소벨 필터를 설계하면서도 많은 시행착오를 겪게 되었다.
1차 시도에서는 VGA Sync 신호와 출력 영상의 타이밍 불일치로 인해 제대로 된 결과물을 얻지 못했고, 이를 해결하기 위해 버퍼 논리를 간소화한 2차 시도에서는 노이즈가 줄어드는 성과를 얻었지만, 원하는 윈도우 값을 얻지는 못하였다.
완전한 실시간 처리를 위해 3차 시도에서는 파이프라인 구조를 도입해 실시간 처리를 개선하려 했으나, 라인 버퍼와 윈도우의 동기화가 제대로 되지 않아 기대한 결과가 나오지 않게 되었다
4차 시도에서 초심으로 돌아가 파이프라인 구조를 명확히 설계하고, 타임라인 분석을 통해 각 레지스터와 연산의 동작을 꼼꼼히 정리한 것이 이번 프로젝트의 전환점이 되었다. 라인 버퍼와 윈도우가 매 사이클마다 어떻게 업데이트되는지, 소벨 필터 연산이 언제 유효해지는지를 시각화하며 설계하니 코드의 논리도 훨씬 명확해졌고, 테스트 벤치에서 기대한 대로 동작하는 것을 확인했을 때는 본 과정을 진행하며 가장 큰 보람을 느꼈던 순간이었다.
파라미터 튜닝 과정은 또 다른 도전이었다. 노이즈를 제거하고 사물의 Edge만을 추출하기 위해 입력 데이터의 비트 수, 커널 크기, 임계값 등 조정해야 할 요소가 많아 처음에는 막막했지만, 명확한 검증 계획을 세우면서 차근차근 해결해 나아갈 수 있었다.
3x3과 5x5 커널을 비교했을 때는 두 필터를 동시에 테스트하지 못한 버그를 디버깅을 계속한 결과, 의심되는 부분은 찾을 수 있었지만 결국 원인을 찾지 못한 것이 아쉬웠다.
이 프로젝트를 통해 느낀 가장 큰 점은 하드웨어를 통한 실시간 처리가 단순히 코드를 작성하는 것을 넘어, 리소스, 타이밍, 그리고 최적화까지 고려해야 하는 종합적인 작업이라는 것이다. 실패와 성공을 반복하며 얻은 경험은 앞으로의 설계에서는 초기 계획 단계부터 더 철저히 준비해야겠다는 교훈을 크게 느끼게 되었다.
'PROJECTS > 영상 처리' 카테고리의 다른 글
Video Course Project - Object Tracking (0) | 2025.03.30 |
---|---|
Video Course Project - Motion Detector (0) | 2025.03.28 |