본문 바로가기

Verilog HDL 설계

Discrete Cosine Transform 구현

오늘은 K대학교 4학년 과정에 있는 프로젝트를 처음부터 구현해봤다.

 

DCT라고 불리는 녀석인데, JPEG 이미지 압축과정 중에 하나이다.

 

https://bskyvision.com/485

 

JPEG 압축 제대로 이해하기

아래 사진은 2019년 1월 5일, 14일에 찍은 사진들을 담은 폴더를 캡쳐한 것이다. 여기 보면 유형에 JPG 파일이라고 적혀있는 것을 확인할 수 있다. JPEG 이미지 파일 형식으로 이미지가 저장 및 표현

bskyvision.com

 

위의 링크는 JPEG 압축 과정을 다루고 있다.

 

해당 포스트에서 알 수 있듯이, DCT는 손실 압축 전에 전처리하는 과정이다. 저주파 성분은 보존하고 고주파 성분은 일부만 보존하여 데이터 압축률을 높인다.

 

https://github.com/pyong-1459/JPEG-DCT

 

GitHub - pyong-1459/JPEG-DCT: DCT implement with Verilog and Python

DCT implement with Verilog and Python. Contribute to pyong-1459/JPEG-DCT development by creating an account on GitHub.

github.com

 

내가 구현한 코드는 해당 깃허브 링크에 들어있다. 테스트용 이미지 및 테스트 벡터 파일도 포함되어 있다.

 

기존 프로젝트는 다음과 같은 과정을 진행한다.

 

1. 생성된 2진수 흑백 이미지 데이터 벡터 파일을 이용

2. Verilog에서 해당 파일을 통해 2d-DCT 구현

3. 2d-DCT 결과를 텍스트 파일로 저장

4. Matlab에서 해당 결과를 기존 결과처럼 데이터를 처리한 후 PSNR 비교

 

여기서 4번은 내가 Matlab이 없어서 고민하다가 python을 쓰기로 했다.

 

최종 시뮬레이션 결과는 아래와 같이 나타났다.

 

 

왼쪽부터 python으로 구현/python에서 transform_as_verilog 이용/베릴로그로 1d DCT 벡터를 생성 후 나머지 연산 수행/ 2d DCT 벡터를 생성후 나머지 연산 수행 이다.

 

얼핏보면 화질이 비슷한 것처럼 보이는게 특징이다.

 

DCT 돌아가는거 자체는 인터넷에 워낙 많이 알려져 있어서 python을 쓰는데는 문제가 없었다.

 

이 프로젝트에서 가장 힘든 과정은 베릴로그가 어떻게 돌아가는지를 확인하는 것인데, 베릴로그 시뮬레이션 툴이 그렇게 좋은 편이 아니기도 하고 데이터 양이 워낙 많아서 하나하나 살펴보기가 곤란하다.

 

베릴로그는 시간 단위로 데이터를 하나하나 보여주는데, 이미지 파일은 256x256x8bit이기 때문에 65536개의 데이터를 봐야 확인할 수 있다.

 

근데 이게 말도 안되므로 보통 Matlab에서 베릴로그처럼 fixed point 연산을 통해 돌아가도록 구현하고 시뮬레이션에서 볼 수 있는 벡터들을 베릴로그와 비교한다.

 

나는 python으로 벡터들을 비교했고, 시행착오 끝에 베릴로그로 구현에 성공했다.

 

이제 구현한 것을 알아보자.

 

파이썬

우선 dct에 대한 수학적 설명은 아래를 참고하면 좋을 것 같다.

 

https://en.wikipedia.org/wiki/Discrete_cosine_transform

 

Discrete cosine transform - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Technique used in signal processing and data compression A discrete cosine transform (DCT) expresses a finite sequence of data points in terms of a sum of cosine functions oscillating

en.wikipedia.org

 

이런 류의 주파수 변환들은 FFT(Fast Fourier Transform)의 경우에는 Butterfly 연산을 통해 연산 횟수를 줄인다.

 

DCT도 비슷한 방법으로 계산 횟수를 줄일 수 있다.

 

해당 사이트에서 연산횟수가 꽤 줄어든 알고리즘을 python으로 구현한 것이 있어서 해당 사이트의 코드를 이용했다.
 
python은 보통 floating point 연산을 하는데, 이를 integer 연산으로 변환시켜 구현하는 것이 이 프로젝트의 핵심이다.
 
해당 알고리즘은 scaling을 통해 DC 값을 줄여둔 채로 연산을 하는데, 이를 제거하여 활용하였다.
 
여러번의 시뮬레이션을 통해 곱하는 계수들을 최대한 단순화하면서 PSNR을 챙기기 위한 최소한의 비트수는 3비트였다. 
 
2비트로 하면 PSNR이 20대로 떨어진다
 
A의 경우 [0.707106..., 0.541196..., 0.707106..., 1.306562..., 0.382683...]이고
 
S의 경우 [0.353553..., 0.254897..., 0.270598..., 0.300672..., 0.353553..., 0.449988..., 0.653281..., 1.281457...]이다.
 
S는 2를 곱한 값을 사용하는게 연산을 줄일 수 있어서 2를 곱해서 사용했다.
 
이 A와 S를 fixed point 2진수로 나타내면 아래와 같이 나타난다.
 
A:
00.101101010 => .110
00.100010101 => .100
00.101101010 => .110
01.010011100 => 1.01
00.011000011 => .011
 
S(*2): (1~7)
00.100000101 => .100
00.100010101 => .100
00.100110011 => .101
00.101101010 => .110
00.111001100 => .111
01.010011100 => 1.01
10.100100000 => 10.1

 

S[0]의 경우는 scaling을 하지 않을 경우 필요없어서 제외했다. 

 

해당 2진수에서 상위 3비트만 챙기면 오른쪽과 같은데(반올림했다), 3비트 2진수의 경우 곱셈이 매우 간단해진다.

 

여기서 더 나아가서 곱셈을 덧셈으로 바꾸면(shift-and-add), 모든 곱셈은 shift와 한번의 덧셈으로 구현이 가능하다(111은 1000 - 1이므로 똑같이 덧셈을 한번 쓴다).

 

이를 파이썬에서 비슷하게 구현하기 위해서는 round를 사용하면 된다.

 

2진수 3비트이므로 8을 곱한 후 round하고, 이를 다시 8로 나누면 대충 2진수 소수점 3자리까지 나타낼 수 있는 값이 된다.

 

그래서 fastdct8.py에 round 및 8(유효비트를 3으로 설정했기 때문에 8이다)을 곱하고 나누는 과정을 transform_as_verilog에 나타냈다.

 

물론 베릴로그와 완벽히 같은 것은 아니라서(S[7] = 10.1이 아닌 10.101이 된다) 값이 완전히 같아지지는 않지만, 비교할만한 값이 된다. 

 

여기에 곱셈을 할때마다 소수점 자리수를 반올림하여 버리기 때문에 v0~v28까지의 값은 정수가 되어 이를 통해 베릴로그의 값과 비교할 수 있게 된다.

 

이제 베릴로그로 넘어가보자.

 

dct_1d.v 부터 보면, v0~v28까지 비트수를 각각 나눴다. 베릴로그에서 흔한 오버플로우를 방지하기 위해 비트수를 달리하였다. 

 

곱셈은 shift and add로 구현했으며, shift의 경우 arithmetic shift를 이용해 sign extension을 구현했다.

 

이때 bit shift는 우선 left shift 후에 right shift를 수행했다.

 

right shift한 채로 더하는것도 가능하지만, 유효숫자를 날려먹기 때문에 left shift를 통해 유효숫자를 최대한 챙기고 right shift를 취하여 연산했다.

 

v0~v28의 비트수는 lena.png에 맞게 고려된 비트수로, 다른 이미지라면 여러번의 테스트를 통해 각 wire 변수들을 몇비트로 정의해야 오버플로우가 일어나지 않게 되는지를 다시 고려해야 할 것이다.

 

최종 출력에서 v15는 하위 1비트를 자른 상태로 출력을 내보내기 때문에 검증 시에는 해당 인덱스의 값에 2를 곱해줘야 제대로된 비교가 된다.

 

v15의 하위 2비트를 자르는 것도 테스트해봤는데, PSNR이 미친듯이 떨어져서 관뒀다.

 

v15의 비트수에 따라 tp_mem의 크기가 정해지기 때문에 비트수를 최대한 적게 해줘야하는데, 그 한계가 하위 1비트를 자르는 것 밖에 안되는 것을 알 수 있었다.

 

v15는 DC 성분이기 때문에 해당 값을 제대로 처리 못하면 이미지가 회색조로 변하거나 아니면 완전히 망가지게 된다.

 

원래 이 프로젝트는 여러 이미지를 다루는데, 난 외부인이라 어떤 이미지를 쓰는지 모르기 때문에 유명한 이미지인 lena만을 사용했다.

 

dct_2d.v는 비트수가 다르다. 왜냐하면 1d-dct를 하면 값이 입력인 8비트보다 더 커지기 때문이다. 그리고 이에 맞춰 비트수를 재정의했다.

 

tp_mem.v는 원래 조교들이 주는 코드인데, 해당 코드를 얻을 방법이 없어서 내맘대로 만들었다.

 

행과 열을 바꾸는 역할을 하는데, 이를 최소한의 레지스터로 구현하기 위해 64개의 레지스터를 8x8로 나누고 레지스터 접근 방식을 한 사이클마다 행과 열이 바뀌게끔하여 구현했다.

 

원래는 총 4개를 쓴다고 알고 있는데, 이렇게 구현하면 2개로 계속 굴릴 수 있다.

 

dct_top.v에서는 탑 레벨 모듈을 구현했는데, 자세히 보면 tp_mem이 2개인데, 이는 입력 데이터가 매 클락마다 들어오기 때문에 folding을 할 수 없기 때문이다(원래는 2개를 2번 쓴다).

 

테스트벤치를 보면 최종 결과는 12비트인데, 이는 DC 성분의 범위를 고려한 비트수이다.

 

베릴로그 시뮬레이션을 다 돌리면 텍스트 파일이 하나 나오는데, 이를 python 폴더에 넣고 jpeg.py를 실행하면 검증이 시작된다(깃허브에서는 이미 폴더에 해당 출력 파일이 있다).

 

베릴로그와 python의 값을 어떻게 비교한 걸까?

 

방법은 조금 복잡하다.

 

우선 베릴로그 시뮬레이션을 돌려서 벡터파일을 출력시킨 후, python에서 연산된 값과 뺄셈을 수행하면 된다.

 

그 후 값 차이가 크게 나는 파트를 찾아야한다. 그 파트가 에러가 난 것이다.

 

보통 에러의 원인은 오버플로우/언더플로우인데, 베릴로그 덧셈 특성상 비트수를 충분히 확보하지 않으면 생긴다.

 

베릴로그는 덧셈을 결과 변수 비트수에 맞게 더하기 때문에 특정 조건에서 값이 오버플로우/언더플로우 된다.

 

그래서 모든 값에 오류가 생기는 것이 아닌 일부 부분에서만 오류가 생긴다.

 

일부 부분에서 오류가 생기는데 그 부분을 정확히 찾지 못하는 경우가 많아서 해당 대학교에서 많이 고생한다고 들었다.

 

파트를 찾게 되면(jpeg.py 75~78 line에 자세히 나타냈다) 이제 베릴로그 시뮬레이션으로 건너가서 해당 타이밍을 찾는다.

 

예를 들어 python에서 비교 대상의 3차원 인덱스가 10일 경우, 8*80*10 = 6400ns 쯤에 에러가 있다는 소리다.

 

물론 테스트벤치는 추가적인 시간을 요구하기 때문에 6400보다는 이후 타이밍에 에러가 있을 것이다.

 

이 과정을 1d DCT에서 실행하여 전반적인 에러를 잡고, 2d DCT에서 비트수를 조정하면서 같은 과정을 반복하여 디버깅하면 DCT를 성공적으로 구현할 수 있다.

 

내 최종 벡터 파일은 DC값이 1/4 되어있고, row나 col의 인덱스가 0일 경우 1/2가 되어있기 때문에, binary_read.py에서 해당 경우를 고려하여 값을 곱해주고 나서야 제대로 PSNR이 나왔다.

 

베릴로그 출력은 signed 값이긴 하지만 결국 2진수 값이기 때문에 음수가 양수로 취급된다.

 

binary_read.py에서는 이를 고치기 위해 최종 값의 비트수에서의 (최대값+1)/2 이상이면 음수로 취급하여 최대값+1로 빼줬다.

 

예를 들어 10비트면 1024가 최대값+1이므로 512 이상인 값을 음수로 취급하여 1024만큼 빼줘야한다.