如何将二进制网格图像转换为二维数组?

时间:2021-05-04 03:32:50

标签: python numpy computer-vision python-imaging-library edge-detection

我有一些二进制(黑白)网格的图像,如下所示:

enter image description here

现在,我想将这些图像转换为常规的 2D NumPy 数组,其中每个单元格必须对应于 0,如果源单元格是白色(或未着色),而 1 如果单元格是黑色.也就是说,预期的输出是:

[[0,1,0,0,1],
 [0,0,0,0,1],
 [0,1,0,0,0],
 [0,0,0,0,0],
 [0,0,0,0,0],
 [0,0,0,1,0],
 [0,0,1,0,0]]

我查看了许多建议,包括 this one,但它们没有说明我必须如何将原始像素减少到常规网格。

我当前的代码:

import numpy as np
from PIL import Image

def from_img(imgfile, size, keep_ratio=True, reverse=False):
    def resample(img_, size):
        return img.resize(size, resample=Image.BILINEAR)            
    def makebw(img, threshold=200):
        edges = (255 if reverse else 0, 0 if reverse else 255)
        return img.convert('L').point(lambda x: edges[1] if x > threshold else edges[0], mode='1')
    img = Image.open(imgfile)
    if keep_ratio:
        ratio = max(size) / max(img.size)
        size = tuple(int(sz * ratio) for sz in img.size)
    return np.array(makebw(resample(img, size)), dtype=int)

此代码可能适用于单元格之间不包含边框的图像,并且仅适用于手动指定行数和列数的情况。但我相信一定有一种方法可以通过边缘检测/重采样技术来自动化这个例程......

更新

虽然对于如上所示的均匀、规则的黑白网格有很好的解决方案(见下文建议),但对于具有多种非黑白颜色的不均匀、嘈杂的图像,这项任务更加困难:

enter image description here

我现在正在研究一个 opencv 实现,它可以检测轮廓并尝试挑出像元大小来重建网格矩阵。我当前的代码:

import matplotlib.pyplot as plt
import numpy as np
import cv2

def find_contours(fpath, gray_thresh=150, extent_param=0.85, area_param=(0.0003, 0.3), ratio_param=(0.75, 1.33)):
    """
    Finds contours (shapes) in an image (loading it from a file) and filters the contours
    according to a number of parameters.
    gray_thresh: grayscale threshold
    extent_param: minimum extent of contour (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html#extent)
    area_param: min and max ratio of contour area to image area
    ratio_param: min and max ratio of contour (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html#aspect-ratio)
    """
    
    image = cv2.imread(fpath)
    # grayscale image
    imgray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, thresh = cv2.threshold(imgray, gray_thresh, 255, 0)
    # get all contours (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html)
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)    
    
    # get min and max contour area in pixels (from given ratios)
    if area_param:
        area = imgray.shape[0] * imgray.shape[1]
        min_area = float(area) * area_param[0]
        max_area = float(area) * area_param[1]
    # filtered contours
    contours2 = []
    # contour sizes
    sizes = []
    # contour coords
    pos = []
    # iterate by found contours
    for c in contours:
        # get contour area
        c_area = cv2.contourArea(c)
        # get bounding rect
        rect = cv2.boundingRect(c)
        # get extent (ratio of contour area to bounding rect area)
        extent = float(c_area) / (rect[2] * rect[3])  
        # get aspect ratio of bounding rect
        ratio = float(rect[2]) / rect[3]
        # perform filtering (leave rect-shaped contours or filter by extent)
        if (len(c) == 4 or not extent_param or extent >= extent_param) and \
           (not area_param or (c_area >= min_area and c_area <= max_area)) and \
           (not ratio_param or (ratio >= ratio_param[0] and ratio <= ratio_param[1])):
            # add filtered contour to list, as well as its size and pos
            contours2.append(c)
            sizes.append(rect[-2:])
            pos.append(rect[:2])
            
    # get most frequent block size (w, h), first and last block
    size_mode = max(set(sizes), key=sizes.count) 
    first_pos = min(pos)
    last_pos = max(pos)

    # return original image, grayscale image, most frequent contour size, first and last contour coords
    return image, imgray, contours2, size_mode, first_pos, last_pos

def get_mean_colors_of_contours(img, imgray, contours):
    """
    Returns the mean colors of given contours and one common mean.
    """
    l_means = []
    for c in contours:
        mask = np.zeros(imgray.shape, np.uint8)
        cv2.drawContours(mask, [c], 0, 255, -1)
        l_means.append(cv2.mean(img, mask=mask)[0])
    return np.mean(l_means), l_means

def get_color(x):
    if x == 'r':
        return (255, 0, 0)
    elif x == 'g':
        return (0, 255, 0)
    elif x == 'b':
        return (0, 0, 255)
    return x

def text_in_contours(img, contours, values, val_format=None, text_color='b', text_scale=1.0):
    """
    Prints stuff inside given contours.
    img: original image (array)
    contours: identified contours
    values: stuff to print (iterable of same length as contours)
    val_format: optional callback function to format a single value before printing
    text_color: color of output text (default = blue)
    text_scale: initial font scale (font will be auto adjusted)
    """
    text_color = get_color(text_color)
    if not text_color: return
    for c, val in zip(contours, values):
        rect = cv2.boundingRect(c)
        center = (rect[0] + rect[2] // 2, rect[1] + rect[3] // 2)
        txt = val_format(val) if val_format else str(val)
        if not txt: continue
        font = cv2.FONT_HERSHEY_DUPLEX
        fontScale = min(rect[2:]) * text_scale / 100
        lineType = 1
        text_size, _ = cv2.getTextSize(txt, font, fontScale, lineType)
        text_origin = (center[0] - text_size[0] // 2, center[1] + text_size[1] // 2)
        cv2.putText(img, txt, text_origin, font, fontScale, text_color, lineType, cv2.LINE_AA)
    return img

def draw_contours(fpath, contour_color='r', contour_width=1, **kwargs):
    """
    Finds contours in image and draws their outlines.
    fpath: path to image file
    contour_color: color used to outline contours (r,g,b, tuple or None)
    contour_width: outline width
    kwargs: args passed to find_contours()
    """
    if not contour_color: return
    contour_color = get_color(contour_color)     
    img, imgray, contours, size_mode, first_pos, last_pos = find_contours(fpath, **kwargs)    
    cv2.drawContours(img, contours, -1, contour_color, contour_width)                
    return img, imgray, contours, size_mode, first_pos, last_pos
    
def show_image(img, fig_height_inches=8):
    """
    Shows an image in iPython notebook.
    """
    height, width = img.shape[:2]
    aspect = width / height
    fig = plt.figure(figsize=(fig_height_inches * aspect, fig_height_inches))
    ax = plt.Axes(fig, [0., 0., 1., 1.])
    ax.set_axis_off()
    fig.add_axes(ax)
    ax.imshow(img, interpolation='nearest', aspect='equal')
    plt.show()

现在这可以帮助我在大多数情况下识别白细胞,例如

img, imgray, contours, size_mode, first_pos, last_pos = draw_contours('sss4.jpg')
mean_col, cols = get_mean_colors_of_contours(img, imgray, contours)
print(f'mean color = {mean_col}')
on_contour = lambda val: str(int(val)) if (val / mean_col) >= 0.9 else None
img = text_in_contours(img, contours, cols, on_contour)
show_image(img, 15)

输出

mean color = 252.54154936140293

enter image description here

所以,我现在只需要一些方法来重建带有 1 和 0 的网格,在缺失的点(没有识别出白细胞的地方)添加 1。

2 个答案:

答案 0 :(得分:1)

鉴于您有一个非常漂亮且形状规则的网格,我们可以通过随机抽样并检查洪水填充区域的大小来确定每个图块的大小。

我使用了从样本中收到的计数模式,但是如果您知道某些网格有很多黑色瓷砖,那么您可能应该采用 stipple() 返回的最小尺寸,因为我们每次遇到黑色瓷砖,它将包括图像的整个背景,这可能会压倒白色瓷砖的数量。

一旦我们获得了图块的大小,我们就可以使用它来索引每个图块中的像素并检查它是白色还是黑色。

import cv2
import numpy as np
import random
import math

# stipple search
def stipple(mask, iters):
    # get resolution
    height, width = mask.shape[:2];

    # do random checks
    counts = [];
    for a in range(iters):
        # get random position
        copy = np.copy(mask);
        x = random.randint(0, width-1);
        y = random.randint(0, height-1);

        # fill
        cv2.floodFill(copy, None, (x, y), 100);

        # count
        count = np.count_nonzero(copy == 100);
        counts.append(count);
    return counts;

# load image
gray = cv2.imread("tiles.jpg", cv2.IMREAD_GRAYSCALE);

# mask
mask = cv2.inRange(gray, 100, 255);
height, width = mask.shape[:2];

# check
sizes = stipple(mask, 10);
print(sizes);

# get most common size // or search for the smallest size
size = max(set(sizes), key=sizes.count);

# get side size
side = math.sqrt(size);

# get grid dimensions
grid_width = int(round(width / side));
grid_height = int(round(height / side));
print([grid_width, grid_height]);

# recalculate size to nearest rounded whole number
side = int(width / grid_width);
print(side);

# make grid
grid = [];
start_index = int(side / 2.0);
for y in range(start_index, height, side):
    row = [];
    for x in range(start_index, width, side):
        row.append(mask[y,x] == 255);
    grid.append(row[:]);

# print
out_str = "";
for row in grid:
    for elem in row:
        out_str += str(int(elem));
    out_str += "\n";
print(out_str);

# show
cv2.imshow("Mask", mask);
cv2.waitKey(0);

答案 1 :(得分:1)

我的想法是将输入图像转换为 mode '1',以某种方式检测瓷砖的宽度和高度,调整输入图像的大小 w.r.t.这些,并简单地转换为一些 NumPy 数组。

检测瓷砖的宽度和高度可能是这样的:

  • 使用 np.diff 检测相邻像素之间的变化,并根据这些信息创建联合图像:

    Union image

  • 再次使用 np.diffnp.sumnp.nonzero 计算这些检测到的变化之间的距离。

  • 最后,使用 np.median 获得这些距离的中值,并从中确定网格的行数和列数,并相应地调整输入图像的大小。

完整代码如下:

import numpy as np
from PIL import Image

# Open image, convert to black and white mode
image = Image.open('grid.png').convert('1')
w, h = image.size

# Temporary NumPy array of type bool to work on
temp = np.array(image)

# Detect changes between neighbouring pixels
diff_y = np.diff(temp, axis=0)
diff_x = np.diff(temp, axis=1)

# Create union image of detected changes
temp = np.zeros_like(temp)
temp[:h-1, :] |= diff_y
temp[:, :w-1] |= diff_x

# Calculate distances between detected changes
diff_y = np.diff(np.nonzero(np.diff(np.sum(temp, axis=0))))
diff_x = np.diff(np.nonzero(np.diff(np.sum(temp, axis=1))))

# Calculate tile height and width
ht = np.median(diff_y[diff_y > 1]) + 2
wt = np.median(diff_x[diff_x > 1]) + 2

# Resize image w.r.t. tile height and width
array = (~np.array(image.resize((int(w/wt), int(h/ht))))).astype(int)
print(array)

对于给定的输入图像,我们得到所需/预期的输出:

[[0 1 0 0 1]
 [0 0 0 0 1]
 [0 1 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 1 0]
 [0 0 1 0 0]]

全黑的列或行无关紧要:

Grid 2

[[0 1 0 0 1]
 [0 0 0 0 1]
 [0 1 0 0 1]
 [0 0 0 0 1]
 [0 0 0 0 1]
 [0 0 0 1 1]
 [0 0 1 0 1]]

而且,即使是单个白色瓷砖也足够了:

Grid 3

[[1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 0 1 1 1]
 [1 1 1 1 1]]

为了测试,我对您的输入图像进行了阈值处理,并将其保存为单通道 PNG。对于任意 JPG 输入图像,您可能需要在转换为模式 '1' 之前进行一些阈值处理以避免伪影。

----------------------------------------
System information
----------------------------------------
Platform:      Windows-10-10.0.16299-SP0
Python:        3.9.1
PyCharm:       2021.1.1
NumPy:         1.20.2
Pillow:        8.2.0
----------------------------------------