0

我正在编写一个 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 只有黑色像素。如果我确实调整了 3 次,程序就会被杀死。我也试过

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

它加载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 笔记本电脑上,我可以在管道开始失败之前进行多达 2,900 次操作。

有一个简单的方法可以修复这个程序:构建一个宽而不是深的管道。不要每次都插入一个图像,而是制作一组条带,然后连接这些条带。现在管道深度将与瓷砖数量的平方根成正比。例如:

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 回答