論文のPDFから図を自動で抽出する

この記事は、CAMPHOR- Advent Calendar 2020 の2日目の記事です。

論文のPDFから図を自動で抽出する

こんにちは、シバニャンです。最近やっていることと言えば、修士論文の執筆です。修士論文を書くためには、普段から大量の論文を読んでおく必要があります。僕の研究分野はコンピュータビジョンという画像を中心に扱う分野で、年数回、国際会議があるたびに1000本程の論文が発表されます。その中から自分の研究に関係があったり、直接関係はなくても役立ちそうなアイディアがある論文を見つける必要があります。

論文ギャラリーを作りたい

論文が発表されると、このようにWebサイト上に論文リストが出ます。普通だと、自分に関係するキーワードを検索して、ざっくりと目を通して読みたい論文を見つけます。ですが、もしこれが文だけではなく論文の図がギャラリーのように一覧できたら、論文探しがとても捗ると思いませんか?そこで、論文ギャラリーを作るために、論文から図を抽出することにしました。

論文のPDFから論文の図を抽出するのは素直にはいかず、PDFが元データとして持っている図を書き出せば出来るわけではありません。なぜなら、論文はLaTeXで複数の図をまとめてFigure 1に載せたりするため、元データの図を抽出するとそれぞれの図がバラバラになって出てきてしまうからです。

例えば、 上のような論文PDFから元データの画像を抽出すると、 上のように,論文のFigの一部がバラバラに抽出されてしまいます。 これは、論文のギャラリーのためは使えません。(ネット上にあるPDFから画像を抽出する方法はだいたいこの方法です。)

ギャラリーのためには、 上のようにFig全体が一枚の画像として欲しいです。

PDFの構造を使う

そこで、論文からFigure 1、Figure 2のようにひとかたまりの図を取り出す方法を紹介します。この記事では、画像から論文の図を認識して検出するのではなく(こっちの方向を期待していた方はごめんなさい。)、PDFの構造を解析して、図の「ひとかたまり」を抽出します。 そのためには、PDFについてある程度知っておく必要があります。実際の実装とは異なりますが、PDFは概念的には下図のようなグラフ構造として理解できます。 (引用 https://www.oreilly.com/library/view/pdf-explained/9781449321581/ch04.html)

今回興味がある論文の図は、各ページのリソースオブジェクトにあります。リソースオブジェクトは、PDFに画像やフォントなどのデータを入れるためのオブジェクトです。PythonのPDFを扱うライブラリ(https://github.com/pmaupin/pdfrw) を使うと、1ページ目のリソースオブジェクトはこのように取得できます。

from pdfrw import PdfReader
from pdfrw.findobjs import find_objects

pdf = PdfReader(file_path)
print(list(find_objects(pdf.pages)))

リソースオブジェクトにはいくつか種類がありますが、画像はXObjectというタイプのオブジェクトです。 XObjectはサブタイプを持っていて、サブタイプにはImageとFormがあります。

Imageは元画像を表すXObjectで、Formは画像の配置や線画を表すXObjectです。

PDFをVSCodeで開いてXObjectで検索してみると、Imageは

このようにタイプや圧縮形式などを書いた後に圧縮されたバイト列が続きます。このバイト列は画像のバイナリで、PDFはこのようにして画像をファイルの中に持っていることがわかります。

Formは、

と、Imageと同様に、タイプや圧縮形式などの情報を書いた後にstreamからendstreamまで圧縮されたバイト列が続きます。

このバイト列は、デコードすると

q
0 0 637 128 re
W n /Fm1 Do
Q

のように独自の命令列になっています。

元画像と、この命令列を使ってレンダリングをすれば、PDFのFormオブジェクト(=論文の図など)を描画できるということになります。 これを一から実装するのはさすがに大変なので、ライブラリの力に頼って論文の図を取り出します。

import sys
import os

from pdfrw import PdfReader, PdfWriter
from pdfrw.findobjs import trivial_xobjs, wrap_object, find_objects
from pdfrw.objects import PdfDict, PdfArray, PdfName

arxiv_id = "1506.02640"
file_path = f"data/paper_pdf/{arxiv_id}.pdf"
out_path = f"data/paper_summary/{arxiv_id}.pdf"

WIDTH = 8.5 * 72
MARGIN = 0.5*72

pdf = PdfReader(file_path)
objects = []
for xobj in list(find_objects(pdf.pages)):
    if xobj.Type==PdfName.XObject and xobj.Subtype == PdfName.Form:
        if '/PTEX.FileName' in xobj:
            wrapped = wrap_object(xobj, WIDTH, MARGIN)
            objects.append(wrapped)

if not objects:
    raise IndexError("No XObjects found")
writer = PdfWriter(out_path)
writer.addpages(objects)
writer.write()

find_objects関数で見つかったすべてのリソースに対して、forループでXObjectのFormを取り出しています.取り出したXObjectはwrap_object関数でPDFのページオブジェクトに変換されます。 最後に書き出せば、PDFとして論文の画像を得ることができます。

論文のFigから上部分を取り出すことができました。 PNGにしたい場合は、PDFをPNGに変換するツール(https://github.com/Belval/pdf2image)を使えばできます。

ものによっては、Formではなく元画像のImageとして論文の図を貼り付けている論文もあるので、必ずしもこの手法がうまくいくとは限りません。 ですが、画像がたくさん集まって論文のFigになっているような論文の場合は、元画像(XObjectのImage)をそのまま取ってくるよりも、画像の配置(XObjectのForm)を取ってくるほうが良く取れたので、ギャラリーのためにはこちらの方が便利だと思います。

著作権的にどうなのかはわからないですが、ギャラリー以外にも、新着論文情報をつぶやくTwitter botやSlack botを作ったり、 この取り出し方を学習データにして、画像認識的に論文の図を取り出しても面白いかもしれないですね。

CAMPHOR- Advent Calendar 2020 の次の記事は、ぼのさんによる「Unityによるアイワナ風ゲーム制作」です。お楽しみに!

2023/06/27 追記

はまなすなぎさ (@RosaRugosaBeach) / Twitter さんにコメント頂いて、同じ画像が複数出てしまう問題が解決できました!ありがとうございます。