使用VIPS处理非常大的图像很困难

时间:2015-11-11 19:56:52

标签: python multithreading image-processing vips

我正在编写一个Python(3.4.3)程序,该程序在Ubuntu 14.04 LTS上使用VIPS(8.1.1)来读取使用多个线程的许多小块并将它们组合成一个大图像。

在一个非常简单的测试中:

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像素),我无法将整个图像保存在一个文件中,所以我试过了

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

这需要很长时间,并且生成的jpeg只有黑色像素。如果我调整三次,程序就会被杀死。我也试过

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

这会导致错误消息segmentation fault(core dumped),但会保存图像。其中有图像内容,但它们似乎位于错误的位置。

我想知道问题是什么?

以下是完整计划的代码。使用OpenCV + sharedmem(sharedmem处理多处理部分)也实现了相同的逻辑,并且它没有问题。

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')

1 个答案:

答案 0 :(得分:2)

我认为你的问题与vips计算像素的方式有关。

在像OpenCV这样的系统中,您可以对图像执行一系列操作。每个操作都会进行大量计算并以某种方式修改内存。

Vips不是这样的,虽然界面看起来很相似。在vips中,当您对图像执行操作时,实际上只是在管道中添加了新的部分。只有当你最终将输出连接到某个接收器(磁盘上的文件,或者你想要填充图像数据的内存区域或显示区域)时,vips才会实际进行任何计算。 vips使用递归算法在管道的整个长度上下运行大量工作者,同时评估您创建的所有操作。

为了与编程语言类比,像OpenCV这样的系统是必不可少的,vips是功能性的。

vips的优点在于它可以立即看到整个流水线,它可以优化大部分内存使用并充分利用你的CPU。糟糕的是,长序列操作可能需要大量内存来评估(而对于像OpenCV这样的系统,您更有可能受到图像大小的限制)。特别是,vips用于评估的递归系统意味着管道长度受到C堆栈的限制,在许多操作系统上约为2MB。

这是一个简单的测试程序,可以或多或少地执行您正在做的事情:

#!/usr/bin/python

import sys

from gi.repository import Vips

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 = Vips.Image.new_from_file(sys.argv[1])
outfile = sys.argv[2]
size = int(sys.argv[3])

img = Vips.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

加载k2.jpg(2k x 2k 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笔记本电脑上,我可以在开始失败之前获得长达约1,400次操作的管道。

有一个简单的方法来修复这个程序:构建一个广泛而不是深层的管道。不是每次都插入一个图像,而是制作一组条带,然后连接条带。现在,管道深度将与瓦片数量的平方根成比例。例如:

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

for y in range(size):
    strip = Vips.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张图片。