在Python中从二维数组中随机抽取子数组

3

问题:

假设我有一个二维数组,我想要从中随机抽取(使用蒙特卡罗方法)较小的二维子数组,如下图中示黑色斑块所示。我正在寻找一种有效的方法来实现这一点。

enter image description here

预期(但是部分)解决方案:

在经过数小时的搜索后,我找到了一个函数,它部分地实现了我的需求,但缺少随机位置采样的能力。虽然它有一个random_state参数,但我不理解它是否可以根据其参数从随机位置取样。

sklearn.feature_extraction.image.extract_patches_2d(image, patch_size, max_patches=None, random_state=None)

问题:

随机选择补丁坐标(2D子数组),并使用它们从较大的数组中切割一个补丁,如上图所示。允许随机抽样的补丁重叠。


请在问题下方的解决方案中查看,其中写着“...缺乏在随机位置采样补丁的能力”。 - user11
1
问题似乎很清楚,我认为人们对于给予负评的态度过于自由了。 - Brad Solomon
生成随机的补丁坐标/参数并将它们用于切片numpy ndarray - 这样做是否高效? - wwii
@wwii:是的,那就是我想要的。我使用“高效”这个术语来指代能够实现这一点的方法/函数/算法,而不是采用循环这种蛮力方法。 - user11
1
有些相关:在平面上生成不重叠的随机正方形 - Brad Solomon
显示剩余5条评论
2个回答

2
这是一个样本切割器,可以从任意维度的数组中创建样本切片。它使用函数来控制在任何轴上切割的起始位置和切割的宽度。
以下是参数的解释:
- `arr` - 输入的numpy数组。 - `loc_sampler_fn` - 这是您想要用来设置盒子角落的函数。如果您想要从轴的任何位置均匀采样盒子的角落,请使用`np.random.uniform`。如果您希望角落靠近数组的中心,请使用`np.random.normal`。但是,我们需要告诉函数要在哪个范围内进行采样。这就带我们到下一个参数。 - `loc_dim_param` - 这将每个轴的大小传递给`loc_sampler_fn`。如果我们在位置采样器中使用`np.random.uniform`,我们希望从整个轴的范围内采样。`np.random.uniform`有两个参数:`low`和`high`,因此通过将轴的长度传递给`high`,它会在整个轴上均匀采样。换句话说,如果轴的长度为`120`,我们希望`np.random.uniform(low=0, high=120)`,因此我们设置`loc_dim_param='high'`。 - `loc_params` - 这将任何其他参数传递给`loc_sampler_fn`。根据示例,我们需要向`np.random.uniform`传递`low=0`,因此我们传递字典`loc_params={'low':0}`。
从这里开始,对于盒子的形状基本上是相同的。如果您希望从3到10均匀采样盒子的高度和宽度,请传入`shape_sampler_fn=np.random.uniform`,并将`shape_dim_param=None`传递给它,因为我们没有使用轴的大小做任何事情,以及`shape_params={'low':3, 'high':11}`。
def box_sampler(arr, 
                loc_sampler_fn, 
                loc_dim_param, 
                loc_params, 
                shape_sampler_fn, 
                shape_dim_param,
                shape_params):
    '''
    Extracts a sample cut from `arr`.

    Parameters:
    -----------
    loc_sampler_fn : function
        The function to determine the where the minimum coordinate
        for each axis should be placed.
    loc_dim_param : string or None
        The parameter in `loc_sampler_fn` that should use the axes
        dimension size
    loc_params : dict
        Parameters to pass to `loc_sampler_fn`.
    shape_sampler_fn : function
        The function to determine the width of the sample cut 
        along each axis.
    shape_dim_param : string or None
        The parameter in `shape_sampler_fn` that should use the
        axes dimension size.
    shape_params : dict
        Parameters to pass to `shape_sampler_fn`.

    Returns:
    --------
    (slices, x) : A tuple of the slices used to cut the sample as well as
    the sampled subsection with the same dimensionality of arr.
        slice :: list of slice objects
        x :: array object with the same ndims as arr
    '''
    slices = []
    for dim in arr.shape:
        if loc_dim_param:
            loc_params.update({loc_dim_param: dim})
        if shape_dim_param:
            shape_params.update({shape_dim_param: dim})
        start = int(loc_sampler_fn(**loc_params))
        stop = start + int(shape_sampler_fn(**shape_params))
        slices.append(slice(start, stop))
    return slices, arr[slices]

一个在宽度为3到9之间的二维数组上进行均匀切割的例子:

a = np.random.randint(0, 1+1, size=(100,150))
box_sampler(a, 
            np.random.uniform, 'high', {'low':0}, 
            np.random.uniform, None, {'low':3, 'high':10})
# returns:
([slice(49, 55, None), slice(86, 89, None)], 
 array([[0, 0, 1],
        [0, 1, 1],
        [0, 0, 0],
        [0, 0, 1],
        [1, 1, 1],
        [1, 1, 0]]))

从一个10x20x30的三维数组中提取2x2x2块的示例:

a = np.random.randint(0,2,size=(10,20,30))
box_sampler(a, np.random.uniform, 'high', {'low':0}, 
               np.random.uniform, None, {'low':2, 'high':2})
# returns:
([slice(7, 9, None), slice(9, 11, None), slice(19, 21, None)], 
 array([[[0, 1],
         [1, 0]],
        [[0, 1],
         [1, 1]]]))

根据评论更新。

针对您的具体目的,看起来您需要一个矩形样本,其中起始角落从数组中任何位置均匀采样,并且沿每个轴的样本宽度是均匀采样的,但可以受到限制。

这里有一个生成这些样本的函数。min_widthmax_width可以接受整数的可迭代对象(例如元组)或单个整数。

def uniform_box_sampler(arr, min_width, max_width):
    '''
    Extracts a sample cut from `arr`.

    Parameters:
    -----------
    arr : array
        The numpy array to sample a box from
    min_width : int or tuple
        The minimum width of the box along a given axis.
        If a tuple of integers is supplied, it my have the
        same length as the number of dimensions of `arr`
    max_width : int or tuple
        The maximum width of the box along a given axis.
        If a tuple of integers is supplied, it my have the
        same length as the number of dimensions of `arr`

    Returns:
    --------
    (slices, x) : A tuple of the slices used to cut the sample as well as
    the sampled subsection with the same dimensionality of arr.
        slice :: list of slice objects
        x :: array object with the same ndims as arr
    '''
    if isinstance(min_width, (tuple, list)):
        assert len(min_width)==arr.ndim, 'Dimensions of `min_width` and `arr` must match'
    else:
        min_width = (min_width,)*arr.ndim
    if isinstance(max_width, (tuple, list)):
        assert len(max_width)==arr.ndim, 'Dimensions of `max_width` and `arr` must match'
    else:
        max_width = (max_width,)*arr.ndim

    slices = []
    for dim, mn, mx in zip(arr.shape, min_width, max_width):
        fn = np.random.uniform
        start = int(np.random.uniform(0,dim))
        stop = start + int(np.random.uniform(mn, mx+1))
        slices.append(slice(start, stop))
    return slices, arr[slices]

生成一个盒子的示例,该盒子从数组中任意位置开始均匀地切割,高度是从1到4的随机均匀抽取,宽度是从2到6的随机均匀抽取(仅供展示)。在这种情况下,盒子的大小为3乘4,从第66行和第19列开始。

x = np.random.randint(0,2,size=(100,100))
uniform_box_sampler(x, (1,2), (4,6))
# returns:
([slice(65, 68, None), slice(18, 22, None)], 
 array([[1, 0, 0, 0],
        [0, 0, 1, 1],
        [0, 1, 1, 0]]))

它似乎正在做我想要的事情,但是在arr之后的函数参数方面我不太清楚。我尝试查看示例并将其与参数定义连接起来,但我无法理解。您能否帮助澄清这些参数或尝试将它们与您的示例连接起来? - user11
非常好。我现在能够理解它了。那么 shape_* 参数呢?您介意也加上它们的说明吗?谢谢。 - user11
你能描述一下如何根据宽度/高度对盒子进行采样吗? - user11
1
对于您的特定情况,该函数要简单得多,因为您只需要对数组进行均匀采样,并确定盒子的宽度/高度。请参见答案中的附加信息。 - James
谢谢。非均匀采样函数的参数不易理解。 - user11
显示剩余2条评论

1
看起来你对sklearn.feature_extraction.image.extract_patches_2d的问题是它强制你指定一个单一的补丁大小,而你正在寻找不同大小的随机补丁。
这里需要注意的一件事是你的结果不能是NumPy数组(与sklearn函数的结果不同),因为数组必须具有统一长度的行/列。因此,你的输出需要是包含不同形状数组的其他数据结构。
以下是解决方法:
from itertools import product

def random_patches_2d(arr, n_patches):
    # The all possible row and column slices from `arr` given its shape
    row, col = arr.shape
    row_comb = [(i, j) for i, j in product(range(row), range(row)) if i < j]
    col_comb = [(i, j) for i, j in product(range(col), range(col)) if i < j]

    # Pick randomly from the possible slices.  The distribution will be
    #     random uniform from the given slices.  We can't use
    #     np.random.choice because it only samples from a 1d array.
    a = np.random.choice(np.arange(len(row_comb)), size=n_patches)
    b = np.random.choice(np.arange(len(col_comb)), size=n_patches)
    for i, j in zip(a, b):
        yield arr[row_comb[i][0]:row_comb[i][1], 
                  col_comb[i][0]:col_comb[i][1]]

例子:

np.random.seed(99)
arr = np.arange(49).reshape(7, 7)
res = list(random_patches_2d(arr, 5))
print(res[0])
print()
print(res[3])
[[0 1]
 [7 8]]

[[ 8  9 10 11]
 [15 16 17 18]
 [22 23 24 25]
 [29 30 31 32]]

简明:

def random_patches_2d(arr, n_patches):
    row, col = arr.shape
    row_comb = [(i, j) for i, j in product(range(row), range(row)) if i < j]
    col_comb = [(i, j) for i, j in product(range(col), range(col)) if i < j]
    a = np.random.choice(np.arange(len(row_comb)), size=n_patches)
    b = np.random.choice(np.arange(len(col_comb)), size=n_patches)
    for i, j in zip(a, b):
        yield arr[row_comb[i][0]:row_comb[i][1], 
                  col_comb[i][0]:col_comb[i][1]]

回应您的评论:您可以逐步添加1个补丁,并在每个补丁后检查该区域。
# `size` is just row x col
area = arr.size
patch_area = 0
while patch_area <= area:  # or while patch_area <= 0.1 * area:
    patch = random_patches_2d(arr, n_patches=1)
    patch_area += patch

感谢您提供的解决方案。我认为它很有帮助,但是我想一次只取一个框(这是我在问题中应该解释的..抱歉没有说明),因为在取样一定数量的补丁后,我想检查这些补丁的面积是否超过了某个值,比如说主全局数组总面积的10%。此外,感谢您提供另一个答案的链接,虽然那是针对不同但相关的问题。 - user11
1
如果您感兴趣,我更新了答案并添加了一个代码片段以反映您试图做的事情。 - Brad Solomon
1
此外,如果您有一个非常大的图像,您应该考虑使用 numpy.as_strided。它非常有效地形成数组上不同的“窗口”,但比基本索引要复杂一些。 - Brad Solomon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接