shimada-kの日記

ソフトウェア・エンジニアのブログです

ImageMagickとマルチスレッドは混ぜるなキケン!?

画像処理をする要件が出たので手軽に扱えるライブラリは無いものかと探した結果、ImageMagickに行き着きました。

ライブラリを利用して、油彩化とシャープネスを組み合わせた加工を施すプログラムです。

f:id:shimada-k:20110802023826p:image:w640

この処理をスレッドに割り当て、マルチスレッドで画像処理を行うロジックを実装してみました。

#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <pthread.h>

#include <ImageMagick/wand/MagickWand.h>

#define SRC_BASE_PATH	"src"

/*
	#apt-get install libmagickwand-dev
	gcc -o img_process img_process.c `pkg-config Wand --cflags --libs` -lpthread
	./img_process -N %thread_num
*/

#define QUALITY		90
#define STR_PATH_MAX		32
#define STR_NUM_MAX		5

char *progname;

void usage(void)
{
	fprintf(stderr, "%s: [-N num_threads]\n", progname);
}

void *thread_main(void *arg)
{
	MagickWand *m_wand;
	char rpath[STR_PATH_MAX], wpath[STR_PATH_MAX];
	int w, h, i;

	sprintf(rpath, "%s/nature%d.png", SRC_BASE_PATH, (long)arg);
	printf("[input] %s\n", rpath);	/* debug */

	sprintf(wpath, "nature%d.png", (long)arg);	/* 加工後のファイルは実行ファイルと同じディレクトリに出力 */
	printf("[output] %s\n", wpath);	/* debug */

	/* 初期化処理 */
	MagickWandGenesis();

	m_wand = NewMagickWand();

	/* 画像ファイルの読み込み */
	if(MagickReadImage(m_wand, rpath) == MagickFalse){
		puts("ファイルを読みめませんでした");
		exit(EXIT_FAILURE);
	}

	/* サイズ変換処理 -小さくすると高速化になる- */
#if 0
	w = MagickGetImageWidth(m_wand);
	h = MagickGetImageHeight(m_wand);
	MagickResizeImage(m_wand, w / 2, h / 2, LanczosFilter, 1.0);	/*  */
#endif

	/* 油彩画加工+シャープネス */
	MagickOilPaintImage(m_wand, 2);
	MagickSharpenImage(m_wand, 0, 50);

	MagickSetImageCompressionQuality(m_wand, QUALITY);

	/* 画像ファイル書き出し */
	if(MagickWriteImage(m_wand, wpath) == MagickFalse){
		exit(EXIT_FAILURE);
	}

	/* 後片付け */
	if(m_wand){
		m_wand = DestroyMagickWand(m_wand);
	}

	MagickWandTerminus();

	printf("%s was wrote.\n", wpath);
	pthread_exit(NULL);

}

int main(int argc, char *argv[])
{
	MagickWand *m_wand;
	int w, h, thread_num = -1, target_cpu;
	long i;
	pthread_t *threads;
	progname = argv[0];

	if(argc != 3){	/* 引数違反 */
		usage();
		exit(EXIT_FAILURE);
	}
	else{
		thread_num = atoi(argv[2]);	/* スレッド数を指定 */
	}

	threads = calloc(thread_num, sizeof(pthread_t));

	for(i = 0; i < thread_num; i++){
		if((pthread_create(threads + i, NULL, thread_main, (void *)i)) == -1){
			puts("pthread_create(3) error");
			exit(EXIT_FAILURE);
		}
	}

	printf("thread_num:%d\n", thread_num);

	/* JOIN */
	for(i = 0; i < thread_num; i++){
		pthread_join(threads[i], NULL);
	}

	free(threads);

	return 0;
}

「-N」に続く引数に対応する画像をカレントディレクトリにあるsrcディレクトリから読み込み、画像処理を施すプログラムです。「-N 2」という引数を与えるとsrc/nature0.pngとsrc/nature1.pngというファイルを読み込んでそれぞれマルチスレッドで処理します。

しかし、コンパイルし、実行すると以下のようなエラーが出て終了してしまいます。

thread_num:2
[input] src/nature3.png
[output] nature0.png
[input] src/nature4.png
[output] nature1.png
nature1.png was wrote.
imgproc: magick/semaphore.c:262: LockSemaphoreInfo: Assertion `semaphore_info != (SemaphoreInfo *) ((void *)0)' failed.

これをgoogleで検索した結果、よく報告される現象なようです。最新版なら問題ないだろうと思い、ソースからビルドすることを考えてみます。そこで、最新版のImageMagickをダウンロードします。

[本家JPミラー] ftp://ftp.kddlabs.co.jp/graphics/ImageMagick/

私はこの中のbeta版のImageMagick-6.7.1-1.tar.bz2をダウンロードしました。

ビルドする前に依存関係にあるパッケージが山ほどあるので、インストールしておきます。

環境:Linux debian 2.6.39-2-amd64 [debian wheezy]

#apt-get install libfreetype6-dev	libfreetype.la
#apt-get install xorg-dev		Xos.h
#apt-get install libbz2-dev		bzlib.h
#apt-get install libtiff4-dev		tiffconf.h
#apt-get install liblcms1-dev		lcms.h	(libcms2-devだとlcms2.hがインストールされる)
#apt-get install liblqr-1-0-dev		lqr.h
#apt-get install libglib2.0-dev		glib.h(lqr.hで内部依存)
#apt-get install graphviz-dev		gcv.h
#apt-get install libxml2-dev		libxml/parser.h
#apt-get install librsvg2-dev		librsvg/rsvg.h
#apt-get install libdjvulibre-dev	libdjvu/ddjvuapi.h
#apt-get install libopenexr-dev		ImfCRgbaFile.h
#apt-get install libjasper-dev		jasper/jasper.h
#apt-get install libwmf-dev		libwmf/api.h
#apt-get install libjpeg62-dev		libjpeg.la

libjpeg62-devをインストールするとlibjpeg.laが/usr/lib/以下にインストールされます。私の環境では他のlibjpegではlibjpeg.laはインストールされませんでした。

さらにImageMagickをコンパイルする際に必要になるパスにリンクをはっておきます。

#ln -s /usr/lib/x86_64-linux-gnu/libfreetype.la /usr/lib/libfreetype.la

これでコンパイルは通るのでconfigure && make && make installでOKです。

コンパイルとインストールは

./configure --disable-openmp --disable-opencl && make && make install

でOKです。

~~追記~~
ImageMagickOpenMPが使われているので、pthreadを使ったマルチスレッドアプリケーションではOpenMPを無効にしないといけません(pthreadとOpenMPを両方使用することは多くのプラットフォームでサポート外なため)。


インストールするとライブラリは/usr/local/lib/以下に、ヘッダファイルは/usr/local/include/以下にインストールされます。なので、通常のライブラリ、インクルードパスにリンクをはっておきます。

#ln -s /usr/local/lib/libMagickWand.so.4.0.1 /usr/lib/libMagickWand.so.4
#ln -s /usr/local/lib/libMagickCore.so.4.0.1 /usr/lib/libMagickCore.so.4
#ln -s /usr/local/include/ImageMagick /usr/include/ImageMagick

これでコンパイルと実行が可能になっているはずです。しかし、私の環境では最新のImageMagickでも、configureでOpenMPとOpenCLを無効にしても先述のエラーは消えませんでした。

そこでImageMagickのソースを調査し、デバッグを試みてみました。エラー箇所は

imgproc: magick/semaphore.c:262: LockSemaphoreInfo: Assertion `semaphore_info != (SemaphoreInfo *) ((void *)0)' failed.

のようにmagick/semaphore.cの262行目です。ここではassert(3)が呼び出されていました。どうやらassert(3)の引数が偽なためにabortしているようです。そこで、assert(3)の引数をみると

assert(semaphore_info != (SemaphoreInfo *) NULL);

となっています。つまりsemaphore_infoがNULLなために起こっているエラーなのです。semaphore_infoはLockSemaphoreInfo()の引数です。LockSemaphoreInfo()を呼び出している箇所でNULLならLockSemaphoreInfo()を呼び出さないように改変すれば解決するかもしれません。とってつけたような話ですが、試してみる価値はありそうです。

LockSemaphoreInfo()を呼び出している箇所はwand/wand.cにあります。このソースにあるLockSemaphoreInfo()の呼び出しをif文でかこってやります。

if (wand_semaphore != (SemaphoreInfo *)NULL)
	(void) LockSemaphoreInfo(wand_semaphore);

さらに、DestroySemaphoreInfo(), UnlockSemaphoreInfo()の呼び出しもif文でかこって、NULLを引数にして呼び出さないように改変しました。

改変後のソースとオリジナルのソースをdiff(1)した結果です。

--- wand.org.c	2010-12-05 08:12:08.000000000 +0900
+++ wand.c	2011-08-01 21:03:28.696000048 +0900
@@ -78,7 +78,10 @@
 
   if (wand_semaphore == (SemaphoreInfo *) NULL)
     AcquireSemaphoreInfo(&wand_semaphore);
-  LockSemaphoreInfo(wand_semaphore);
+
+  if (wand_semaphore != (SemaphoreInfo *)NULL)	/* 追加 */
+    (void) LockSemaphoreInfo(wand_semaphore);	/* 追加 */
+
   if ((wand_ids == (SplayTreeInfo *) NULL) && (instantiate_wand == MagickFalse))
     {
       wand_ids=NewSplayTree((int (*)(const void *,const void *)) NULL,
@@ -87,7 +90,10 @@
     }
   id++;
   (void) AddValueToSplayTree(wand_ids,(const void *) id,(const void *) id);
-  UnlockSemaphoreInfo(wand_semaphore);
+
+  if(wand_semaphore != (SemaphoreInfo *)NULL)	/* 条件追加 */
+    UnlockSemaphoreInfo(wand_semaphore);
+
   return(id);
 }
 

@@ -115,12 +121,18 @@
 {
   if (wand_semaphore == (SemaphoreInfo *) NULL)
     AcquireSemaphoreInfo(&wand_semaphore);
-  LockSemaphoreInfo(wand_semaphore);
+
+  if(wand_semaphore != (SemaphoreInfo *)NULL)	/* 条件追加 */
+    LockSemaphoreInfo(wand_semaphore);
+
   if (wand_ids != (SplayTreeInfo *) NULL)
     wand_ids=DestroySplayTree(wand_ids);
   instantiate_wand=MagickFalse;
-  UnlockSemaphoreInfo(wand_semaphore);
-  DestroySemaphoreInfo(&wand_semaphore);
+
+  if(wand_semaphore != (SemaphoreInfo *)NULL){	/* 条件追加 */
+    UnlockSemaphoreInfo(wand_semaphore);
+    DestroySemaphoreInfo(&wand_semaphore);
+  }
 }
 

 /*
@@ -147,8 +159,12 @@
 */
 WandExport void RelinquishWandId(const size_t id)
 {
-  LockSemaphoreInfo(wand_semaphore);
+  if(wand_semaphore != (SemaphoreInfo *)NULL)	/* 条件追加 */
+    LockSemaphoreInfo(wand_semaphore);
+
   if (wand_ids != (SplayTreeInfo *) NULL)
     (void) DeleteNodeByValueFromSplayTree(wand_ids,(const void *) id);
-  UnlockSemaphoreInfo(wand_semaphore);
+
+  if(wand_semaphore != (SemaphoreInfo *)NULL)	/* 条件追加 */
+    UnlockSemaphoreInfo(wand_semaphore);
 }

これでプログラムを再度実行してみます。

thread_num:2
[input] src/nature0.png
[output] nature0.png
[input] src/nature1.png
[output] nature1.png
nature0.png was wrote.
nature1.png was wrote.

できてます。ためしにスレッド数を増やしてみましょう。スレッド数を10にしてやってみます(srcディレクトリに画像ファイルを10枚用意します)。

N 10
[input] src/nature3.png
[input] src/nature6.png
[output] nature6.png
[input] src/nature8.png
[output] nature8.png
thread_num:10
[input] src/nature9.png
[output] nature9.png
[input] src/nature1.png
[output] nature1.png
[input] src/nature7.png
[output] nature7.png
[input] src/nature4.png
[output] nature4.png
[input] src/nature0.png
[output] nature0.png
[output] nature3.png
[input] src/nature2.png
[output] nature2.png
[input] src/nature5.png
[output] nature5.png
nature4.png was wrote.
nature6.png was wrote.
nature0.png was wrote.
nature3.png was wrote.
nature2.png was wrote.
nature7.png was wrote.
nature1.png was wrote.
nature8.png was wrote.
nature5.png was wrote.
nature9.png was wrote.

いいですね。10スレッドでも正常に終了しています。