본문 바로가기

Lucene

[Lucene] Lucene을 활용한 간단한 이미지 검색 구현

오픈되어 있는 소스중에 이미지로부터 50자리의 0과 1로 이루어진 hashcode를 추출해주는 소스가 있다.

(https://code.google.com/p/ironchef-team21/source/browse/ironchef_team21/src/ImagePHash.java?r=75856e07bb89645d0e56820d6e79f8219a06bfb7)


이미지 프로세싱에 대해서 잘 알고 있지 못 하기 때문에 정확하게 내용을 알고있지는 못 하지만

이 50자리의 해시코드가 완전히 같은 경우 동일한 이미지로 인식이 된다. 

소스중 distance를 구하는 메서드가 있는데 0~49 index까지 charAt 메서드를 사용하여 같은 숫자를 가지고 있는지 

파악하여, 다른 숫자의 개수를 return해 주는 메서드이다. 이 distance가 0이면 완전히 같은 이미지이고, distance의 값이 클수록 (거리가 멀수록) 다른 이미지가 된다.


이런저런 테스트를 해본결과 동일 이미지에 약간의 배너마크가 들어가거나 한 경우 5이내의 distance를 보였고, 특이한것은 이미지의 색감이 완전히 달라도 모양(패턴)이 비슷하면 distance의 값이 작게 나왔다.


위 소스를 보면 hashcode 추출시 흑백이미지로 변환시키는 부분이 있는데 그 부분에 의해서 나오는 현상이지 않을까 싶다.


그냥 짐작하건데 이미지를 50분할해서 각 부분의 해시를 추출하는게 아닐까 싶기도...


아무튼... 완전히 distance가 0이 아니더라도 5이내의 distance 값 안에서는 비슷한 (모양)패턴의 이미지가 찾아지기도 한다는 것을 확인하게 된 이후 (정말 운 좋게 절묘한 sample test에서 찾아냈다..) 티셔츠, 신발등 비슷한 모양의 이미지 검색과 동일한 이미지 검색을 사용 할 수 있겠다.. 싶은 생각이 들었다. 


완전히 같은 해시코드만을 select하여 동일 이미지를 찾는 것은 DB로도 충분히 가능하겠으나, distance가 5이내의 이미지 검색... 과 같은 형태가 된다면 Lucene의 CustomScoreQuery를 사용하면 좋지 않을까 싶은 생각이 들어서... 구현을 해보았다.


옛날에 작성했던 포스트를 참조해서 구현하려고 하는데.. Lucene 4.4는 예전과는 또 형태가 완전히 바뀌어서 ..

그리고 옛날에 작성했던 포스트의 내용이 제대로 기억나지 않아서 겨우겨우... 모양만 작성하였다 . -.-


일단, 이를 위해서 FunctionQuery와 CustomScoreQuery를 작성하였는데, FunctionQuery에서는 이미지의 hash코드를 사용하여 distance를 계산해 이를 float 값으로 리턴하여준다. 그러면, CustomScoreQuery에서는 이 값을 받아서 정렬을 위해 음수로 변환한 후 return하도록 간단히 구현하였다.


1. ImageHashDistanceFunction.java




ValueSource와 String을 생성자에서 인자로 받는다. 여기서 이 두개의 값을 사용하여 각 hash의 distance를 계산하게 되는데, String hash는 검색시 넘겨주는 hash값이다. (즉, 검색 쿼리가 되는 이미지의 hash 값이 되겠다.)

그리고 ValueSource로는 IndexReader로부터 색인되어 있는 이미지들의 hash 값을 가지고와서 이를 가지고 검색 쿼리로 들어온 hash코드와 distance 계산을 하게 된다.


그리고 그 값을 FloatDocValues의 인스턴스로 넘겨주고 있다.




2. ImageHashDistanceScore.java


customScore 메서드가 핵심인데.. 앞의 ImageHashDistanceFunction에서 넘어온 FloatDocValues의 값이 이 메서드의 subQueryScore로 넘어오게 된다. 이 메서드를 사용해서 예전에 포스팅한것처럼 여러가지 형태의 검색을 구현 할 수 있다. 여기서는 단순히 distance가 작을수록 높은 점수를 받게 하도록 하기 위해서 넘어온 FloatDocValues의 값을 음수로 만들어주는 형태로만 간단하게 구현을 하였다.


3. Test



searchFunctionDistance 메서드를 보면, 위에서 만든 두개의 클래스를 사용해서 Query를 만들고 이를 사용해서 검색을 하고있다. 예제에서는 "12345"와 가장 비슷한 hash값을 갖는 문서들을 순서대로 검색하게 된다.


hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0

hashCode : 12345, str1 : abcde, distance  : 5.0

fieldValue : abcde, subQueryScore : 5.0

hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0


id : null, hash : 12345, score : -0.0

id : null, hash : 12345, score : -0.0

id : null, hash : abcde, score : -5.0

위 결과를 보면 맨 마지막 3줄에서 같은 hash를 갖는 이미지들이 상위로 노출됨을 알 수 있다.


아직 제대로 파악을 하지 못 했을 수도 있지만 ImageHashDistanceFunction 클래스를 통해서 특정 distance이상의 이미지를 제거하지 못하였다. 값을 0을 줘도 음수를 줘도 어쨎든 값이 ImageHashDistanceScore로 넘어가버린다. 일단 이 클래스로 넘어오게 되면 필터링이 아니라 정렬순서에만 영향을 주기 때문에 그 이전에 Filtering 시키는 방법이 필요할듯한다. 


그 방법으로 Filter와 Collector 클래스를 확장하는 예제가 있는것 같은데.. Filter는 아직 학습이 부족하여 제대로 구현하지 못 하였다. Collector로는 아주 단순하게 한번 구현을 해보았는데..


4. HashDistanceCollector.java



collect 메서드가 그 기능에 맞춰서 구현된 메서드이다. 나머지는 루씬에서 구현된 부분들을 그대로 가지고왔다. 점수가 하드코딩 되어있기는 하지만, 특정 점수 이상(혹은 이하)의 경우에만 collect가 되도록 수정한 것이다.


5. Test


맨 마지막 메서드가 테스트 메서드인데.. 돌려보면 아래와 같은 결과가 나온다.

hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0

doc : 0, score : -0.0

hashCode : 12345, str1 : abcde, distance  : 5.0

fieldValue : abcde, subQueryScore : 5.0

doc : 1, score : -5.0

hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0

doc : 2, score : -0.0

id : null, hash : 12345, score : -0.0

id : null, hash : 12345, score : -0.0

-1보다 큰 점수를 갖는 이미지만 collect하도록 되어있기 때문에, 위와는 달리 -5.0을 갖는 이미지는 검색에서 제외되었음을 알 수 있다. 또한, 로그를 통해서 FunctionQuery와 CustomScoreQuery, Collector가 어떤 순서로 작동하는지도 대략적으로 확인이 가능하다.