LIRE Solrによる類似画像検索

この記事は[千 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/sen)の7日目の記事です。

## はじめに

最近、画像関連の調査をすることが多くなりました。
そういえば、以前から類似画像検索を検索エンジンでできないものかなと思ってまして、それらしいことができそうなLireSolrというApache Solr(以下、Solr)のプラグインが公開されていたため試してみました。
https://github.com/dermotte/liresolr

元となるプロジェクトはLIRE(Lucene Image Retrieval)で、luceneを利用して類似画像を検索できるもののようです。
http://www.lire-project.net/

## 環境

* macOS Catalina 10.15.1
* Docker Desktop for Mac 2.1.0.5

## dockerコンテナの設置

Docker Hub上にLireSolrのイメージは存在しますが、それだとブラックボックス過ぎるのでちゃんと構築してみます。
コンテナにはCentOS7系を利用しました。

```dockerfile:Dockerfile
FROM centos:centos7

RUN localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG="ja_JP.UTF-8" \
    LANGUAGE="ja_JP:ja" \
    LC_ALL="ja_JP.UTF-8"

RUN yum -y install curl git bzip2 unzip tar java-1.8.0-openjdk java-1.8.0-openjdk-devel

RUN useradd solr
RUN echo "export JAVA_HOME=/usr" >> /home/solr/.bashrc
CMD ["/bin/bash"]
```

特に依存コンテナはありませんが、docker buildのパタメータ指定が面倒なのでdocker-compose.ymlを作成します。
Solrを導入するため、デフォルトポートの8983は空けておきます。

```docker:docker-compose.yml
work_solr:
  container_name: work_solr
  build: .
  ports:
   - "8983:8983"
  tty: true
  privileged: true
  restart: always
```

コンテナを起動

```
docker-compose up -d
```

以降はコンテナ内で作業します。

```
docker exec -it work_solr /bin/bash
```

## LireSolrのビルド

LireSolrをビルドします。

```
su - solr
git clone https://github.com/dermotte/liresolr.git
cd liresolr
./gradlew distForSolr
```

## Solrの導入

2019/12/06現在のLireSolrのbuild.gradleを見ると、Solr 7.5.0のライブラリが指定されています。
経験上非互換はないだろうと思うので、Solr7系で最新のSolr 7.7.2を導入します。
ちなみにSolrの最新版はSolr 8.3.1です。

```
exit #←インストールのためrootユーザに戻る
cd ~
curl -LO http://ftp.riken.jp/net/apache/lucene/solr/7.7.2/solr-7.7.2.tgz
tar zxf solr-7.7.2.tgz
cd solr-7.7.2/bin/
./install_solr_service.sh ../../solr-7.7.2.tgz -n
```

## LireSolrの配置

先ほど作成したLireSolrのjarファイルをコピーします。

```
cp /home/solr/liresolr/dist/lire* /opt/solr/server/solr-webapp/webapp/WEB-INF/lib/
```

embedded ZooKeeperで起動したいため、initスクリプトに手を入れます。
(本来はZooKeeperを立てる必要があります)

```diff:/etc/init.d/solr
 esac

 if [ -n "$RUNAS" ]; then
-  su -c "SOLR_INCLUDE=\"$SOLR_ENV\" \"$SOLR_INSTALL_DIR/bin/solr\" $SOLR_CMD" - "$RUNAS"
+  su -c "SOLR_INCLUDE=\"$SOLR_ENV\" \"$SOLR_INSTALL_DIR/bin/solr\" $SOLR_CMD \"-cloud\"" - "$RUNAS"
 else
-  SOLR_INCLUDE="$SOLR_ENV" "$SOLR_INSTALL_DIR/bin/solr" "$SOLR_CMD"
+  SOLR_INCLUDE="$SOLR_ENV" "$SOLR_INSTALL_DIR/bin/solr" "$SOLR_CMD" "-cloud"
 fi
```

## LireSolrの設定を追加

Solrをembeddedモードで起動した場合、/opt/solr/server/solr/configsets/_default/conf配下の設定が利用されます。
こちらの設定ファイルにLireSolrの設定を追加します。

```diff:/opt/solr/server/solr/configsets/_default/conf/managed-schema
     <field name="_root_" type="string" indexed="true" stored="false" docValues="false" />
     <field name="_text_" type="text_general" indexed="true" stored="false" multiValued="true"/>

+    <!-- the title of the image, e.g. the file name, optional -->
+    <field name="title" type="text_general" indexed="true" stored="true" multiValued="true"/>
+    <!-- the url where the image is to be downloaded, optional  -->
+    <field name="imgurl" type="string" indexed="true" stored="true" multiValued="false"/>
+    <!-- Dynamic fields for LIRE Solr -->
+    <dynamicField name="*_ha" type="text_ws" indexed="true" stored="false"/> <!-- if you are using BitSampling -->
+    <dynamicField name="*_ms" type="text_ws" indexed="true" stored="false"/> <!-- if you are using Metric Spaces Indexing -->
+    <dynamicField name="*_hi" type="binaryDV" indexed="false" stored="true"/>
+    <fieldType name="binaryDV" class="net.semanticmetadata.lire.solr.BinaryDocValuesField"/>
+
     <!-- This can be enabled, in case the client does not know what fields may be searched. It isn't enabled by default
          because it's very expensive to index everything twice. -->
     <!-- <copyField source="*" dest="_text_"/> -->
```

```diff:/opt/solr/server/solr/configsets/_default/conf/solrconfig.xml
       -->
   </requestHandler>

+  <requestHandler name="/lireq" class="net.semanticmetadata.lire.solr.LireRequestHandler">
+      <lst name="defaults">
+          <str name="echoParams">explicit</str>
+          <str name="wt">json</str>
+          <str name="indent">true</str>
+      </lst>
+  </requestHandler>
+  <valueSourceParser name="lirefunc"
+    class="net.semanticmetadata.lire.solr.LireValueSourceParser" />
+
   <!-- A request handler that returns indented JSON by default -->
   <requestHandler name="/query" class="solr.SearchHandler">
     <lst name="defaults">
```

## Solrの起動

Solrを起動します。
initスクリプトに手を加えたため、systemctl経由ではなく直接実行しています。

```
/etc/init.d/solr start
```

## Collectionを作成

「liretest」というCollectionを作成して準備完了です。

```
su - solr
curl "http://localhost:8983/solr/admin/collections?action=CREATE&name=liretest&numShards=1&replicationFactor=1&maxShardsPerNode=1&collection.configName=_default"
```

## 画像をindexing

画像はpixaboyのフリー素材をいくつか利用させていただきました。
https://pixabay.com/ja/

画像を/home/solr/imagesに配置して、特徴量などを抽出します。

```
cd ~/liresolr/dist/
find /home/solr/images/ -name *.jpg > infile.txt
java -cp "liresolr.jar:lire.jar" net.semanticmetadata.lire.solr.indexing.ParallelSolrIndexer -i infile.txt -o outfile.txt
```

抽出したテキストデータをindexingします。

```
curl http://localhost:8983/solr/liretest/update -H "Content-Type: text/xml" --data-binary @outfile.xml
curl http://localhost:8983/solr/liretest/update -H "Content-Type: text/xml" --data-binary "<commit/>"
```

ブラウザから「localhost:8983」にアクセスして、検索できるか確認します。

<img width="967" alt="スクリーンショット 2019-12-07 19.28.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536396/1b4791c6-abdf-8490-6c54-7c036dbaff80.png">

## LireSolrで抽出する内容

* localimagefile / id / title
    * ファイルのパス 
* ph_hi / ph_ha
    * PHOG: Pyramid Histogram of Oriented Gradients)
* cl_hi / cl_ha
    * ColorLayout
* jc_hi / jc_ha
    * JCD (joined descriptor of CEDD and FCTH)
    * CEDD: Color and Edge Directivity Descriptor
    * FCTH: Fuzzy Color and Texture Histogram
* eh_hi / eh_ha
    * EdgeHistogram

...ちゃんと調べないとよく分かりませんね。。
画像の実際のタイトルや説明文などを含める場合には、ParallelSolrIndexerに実装を追加するか、作成後のXMLファイルをパースして改めて情報を追加する必要があります。

## 検索してみる

画像URLを指定して検索できるようです。
簡略化のため、/opt/solr/server/solr-webapp/webappに類似検索対象の画像を配置して、SolrのHTTPサーバ経由で画像にアクセスしています。

```
http://localhost:8983/solr/liretest/lireq?url=http%3A%2F%2Flocalhost%3A8983%2Fsolr%2F960a610bfc8914fdc7b549b7b6cfa1f9721a25bb.jpg
```

### 入力画像

<img width="134" alt="スクリーンショット 2019-12-07 20.09.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536396/fc654630-6f10-b3cb-4277-8a55459967c7.png">

### 出力画像

<img width="924" alt="スクリーンショット 2019-12-07 20.10.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536396/cb55bcd6-c046-5769-9ab2-2fb031133a65.png">

左から似ている順になっており、4番目と5番目は飲み物の色でギリギリ類似してるかな、、という感じ。
6番目、7番目はあまり類似しているうように見えません。
(個人的な感覚として、です)

返却結果の「d」で示される距離が確認できるので左から順に確認してみます。
0.0、32.25、65.86、97.33、112.90、132.70、133.21

入力画像はなんとなく海、空をイメージする画像のため、数値的には70以下くらいまでならそれなりかもしれませんね。

## おわりに

調べてみると、類似画像検索として「Apache alike」というプロジェクトもあるようです。
Apacheプロジェクトですので、そちらの方がメジャーかもしれません。

どちらにしても、実用には内部論理をある程度理解しておく必要があるなと感じました。