0

Ubuntu 14.04 LTS で VIPS(8.1.1) を使用して、複数のスレッドを使用して多くの小さなタイルを読み取り、それらをまとめて大きな画像にする Python(3.4.3) プログラムを作成しています。

非常に単純なテストでは:

from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Lock
from gi.repository import Vips

canvas = Vips.Image.black(8000,1000,bands=3)

def do_work(x):
    img = Vips.Image.new_from_file('part.tif')    # RGB tiff image
    with lock:
        canvas = canvas.insert(img, x*1000, 0)

with ThreadPoolExecutor(max_workers=8) as executor:
    for x in range(8):
        executor.submit(do_work, x)

canvas.write_to_file('complete.tif')

正しい結果が得られます。私の完全なプログラムでは、各スレッドの作業には、ソース ファイルからバイナリを読み取り、それらを tiff 形式に変換し、画像データを読み取り、キャンバスに挿入することが含まれます。動作しているように見えますが、結果を調べようとすると問題が発生しました。画像が非常に大きいため(〜50000 * 100000ピクセル)、画像全体を1つのファイルに保存できなかったので、試しました

canvas = canvas.resize(.5)
canvas.write_to_file('test.jpg')

これには非常に時間がかかり、結果の jpeg には黒いピクセルしかありません。サイズを 3 回変更すると、プログラムが強制終了されます。私も試しました

canvas.extract_area(20000,40000,2000,2000).write_to_file('test.tif')

これによりエラー メッセージsegmentation fault(core dumped)が表示されますが、画像は保存されます。その中に画像コンテンツがありますが、間違った場所にあるようです。

何が問題なのだろうか?

以下は、完全なプログラムのコードです。OpenCV + sharedmem (sharedmem は multiprocessing 部分を処理) を使用して同じロジックも実装され、問題なく動作しました。

import os
import subprocess
import pickle
from multiprocessing import Lock
from concurrent.futures import ThreadPoolExecutor
import threading
import numpy as np
from gi.repository import Vips

lock = Lock()

def read_image(x):
    with open(file_name, 'rb') as fin:
        fin.seek(sublist[x]['dataStartPos'])
        temp_array = np.fromfile(fin, dtype='int8', count=sublist[x]['dataSize'])

    name_base = os.path.join(rd_path, threading.current_thread().name + 'tempimg')
    with open(name_base + '.jxr', 'wb') as fout:
        temp_array.tofile(fout)
    subprocess.call(['./JxrDecApp', '-i', name_base + '.jxr', '-o', name_base + '.tif'])
    temp_img = Vips.Image.new_from_file(name_base + '.tif')
    with lock:
        global canvas
        canvas = canvas.insert(temp_img, sublist[x]['XStart'], sublist[x]['YStart'])

def assemble_all(filename, ramdisk_path, scene):
    global canvas, sublist, file_name, rd_path, tilesize_x, tilesize_y
    file_name = filename
    rd_path = ramdisk_path
    file_info = fetch_pickle(filename)   # A custom function
    # this info includes where to begin reading image data, image size and coordinates
    tilesize_x = file_info['sBlockList_P0'][0]['XSize']
    tilesize_y = file_info['sBlockList_P0'][0]['YSize']
    sublist = [item for item in file_info['sBlockList_P0'] if item['SStart'] == scene]
    max_x = max([item['XStart'] for item in file_info['sBlockList_P0']])
    max_y = max([item['YStart'] for item in file_info['sBlockList_P0']])
    canvas = Vips.Image.black((max_x+tilesize_x), (max_y+tilesize_y), bands=3)

    with ThreadPoolExecutor(max_workers=4) as executor:
        for x in range(len(sublist)):
            executor.submit(read_image, x)

    return canvas

上記のモジュール (mcv としてインポート) は、ドライバー スクリプトで呼び出されます。

canvas = mcv.assemble_all(filename, ramdisk_path, 0)

内容を調べるために、私は使用しました

canvas.extract_area(25000, 40000, 2000, 2000).write_to_file('test_vips1.jpg')
4

1 に答える 1

3

あなたの問題は、libvips がピクセルを計算する方法に関係していると思います。

OpenCV のようなシステムでは、イメージはメモリの巨大な領域です。一連の操作を実行すると、各操作によって何らかの方法でメモリ イメージが変更されます。

インターフェイスは似ていますが、libvips はこれとは異なります。libvips では、イメージに対して操作を実行するとき、実際には新しいセクションをパイプラインに追加しているだけです。libvips が実際に計算を行うのは、最終的に出力を何らかのシンク (ディスク上のファイル、または画像データで埋めたいメモリ領域、またはディスプレイの領域) に接続したときだけです。次に、libvips は再帰アルゴリズムを使用して、パイプライン全体にわたって多数のワーカー スレッドを上下に実行し、作成したすべての操作を同時に評価します。

プログラミング言語に例えると、OpenCV のようなシステムは不可欠であり、libvips は機能的です。

libvips の良いところは、パイプライン全体を一度に見ることができ、ほとんどのメモリ使用を最適化し、CPU を有効に活用できることです。悪い点は、操作の長いシーケンスを評価するために大量のスタックが必要になる可能性があることです (一方、OpenCV のようなシステムでは、画像サイズに制限される可能性が高くなります)。特に、libvips が評価に使用する再帰システムは、パイプラインの長さが C スタックによって制限され、多くのオペレーティング システムで約 2MB であることを意味します。

これは、あなたがやっていることを多かれ少なかれ行う簡単なテストプログラムです:

#!/usr/bin/python3

import sys
import pyvips

if len(sys.argv) < 4:
    print "usage: %s image-in image-out n" % sys.argv[0]
    print "   make an n x n grid of image-in"
    sys.exit(1)

tile = pyvips.Image.new_from_file(sys.argv[1])
outfile = sys.argv[2]
size = int(sys.argv[3])

img = pyvips.Image.black(size * tile.width, size * tile.height, bands=3)

for y in range(size):
    for x in range(size):
        img = img.insert(tile, x * size, y * size)

# we're not interested in huge files for this test, just write a small patch
img.crop(10, 10, 100, 100).write_to_file(outfile)

次のように実行します。

time ./bigjoin.py ~/pics/k2.jpg out.tif 2
real    0m0.176s
user    0m0.144s
sys 0m0.031s

(2k x 2k の JPG 画像)を読み込みk2.jpg、その画像を 2 x 2 グリッドに繰り返し、その一部を保存します。このプログラムは非常に大きな画像でもうまく機能します。 を削除して次のcropように実行してみてください。

./bigjoin.py huge.tif out.tif[bigtiff] 10

そして、巨大な tiff イメージを 100 回コピーして、非常に巨大な tiff ファイルにします。高速で、メモリをほとんど使用しません。

ただし、このプログラムは、小さな画像が何度もコピーされると非常に不満になります。たとえば、このマシン (Mac) では、次のコマンドを実行できます。

./bigjoin.py ~/pics/k2.jpg out.tif 26

しかし、これは失敗します:

./bigjoin.py ~/pics/k2.jpg out.tif 28
Bus error: 10

28 x 28 出力の場合、784 タイルです。単一のタイルを繰り返し挿入して画像を構築した方法は、784 操作の長さのパイプラインであり、スタック オーバーフローを引き起こすのに十分な長さです。私の Ubuntu ラップトップでは、パイプラインが失敗し始めるずっと前に、最大で約 2,900 の操作を行うことができます。

このプログラムを修正する簡単な方法があります。それは、深いパイプラインではなく、幅の広いパイプラインを構築することです。毎回 1 つの画像を挿入する代わりに、ストリップのセットを作成してから、ストリップを結合します。これで、パイプラインの深さはタイル数の平方根に比例します。例えば:

img = pyvips.Image.black(size * tile.width, size * tile.height, bands=3)

for y in range(size):
    strip = pyvips.Image.black(size * tile.width, tile.height, bands=3)
    for x in range(size):
        strip = strip.insert(tile, x * size, 0)
    img = img.insert(strip, 0, y * size)

今私は実行できます:

./bigjoin2.py ~/pics/k2.jpg out.tif 200

これは、40,000 枚の画像を結合したものです。

于 2015-11-11T22:00:19.950 に答える