如何在 python 中绘制 low-contrast 个对象的轮廓?

How can I contour low-contrast objects in python?

我在绘制此类 low-contrast 对象的轮廓时遇到困难:

我的目标是这样的输出:

在上面的示例中,我使用了 cv2.findContours 和下面的代码,但使用的阈值为 105 ret,thresh = cv.threshold(blur, 105, 255, 0)。但是,如果我为 low-contrast 图像重现它,我找不到最佳阈值:

import numpy as np
from PIL import Image
import requests
from io import BytesIO
import cv2 as cv

url = 'https://i.stack.imgur.com/OeZJ9.jpg'
response = requests.get(url)

img = Image.open(BytesIO(response.content)).convert('RGB')
img = np.array(img) 

imgray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

blur = cv.GaussianBlur(imgray, (105, 105), 0)
        
ret,thresh = cv.threshold(blur, 205, 255, 0)
im2, cnts, hierarchy = cv.findContours(thresh,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE)
cv.drawContours(img, cnts, -1, (0,0,255), 5)
plt.imshow(img, cmap = 'gray')

输出:

我明白是背景的强度和物体重叠的问题,但我找不到其他成功的方法。我尝试过的其他事情包括:

  1. 阈值化,in skimageskimage.measure.find_contours
  2. 分水岭算法,in opencv
  3. 腐蚀和扩张in opencv,这会降低太多轮廓分辨率。

我希望能帮助您以尽可能高的分辨率勾勒出与背景对比度较低的对象。

再见,

为了解决你的问题,我会使用这个片段来检测轮廓并在它们的区域过滤它们,只留下大于给定尺寸的那些。在你的情况下,我假设你只是在搜索一个对象,但我让代码准备好扩展到多张图片

import cv2
import numpy as np


# input image
path = "16.jpg"

# finding contours
def getContours(img, imgContour):    

    contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    finalContours = []
    
    # for each contour found
    for cnt in contours:
        # find its area in pixel^2
        area = cv2.contourArea(cnt)
        print("Contour area: ", area)

        # fixed assuming you are searching for the biggest object
        # value can be found via previous print
        minArea = 18000
        
        if (area > minArea):

            perimeter = cv2.arcLength(cnt, False)
            
            # smaller epsilon -> more vertices detected [= more precision]
            # improving bounding box precision - original value 0.02 * perimeter
            epsilon = 0.002*perimeter
            # check how many vertices         
            approx = cv2.approxPolyDP(cnt, epsilon, True)
            print(len(approx))
            
            finalContours.append([len(approx), area, approx, cnt])

    # leaving this part if you have more objects to detect
    # not needed when minArea has been chosen to detect only one object
    # sorting the final results in descending order depending on the area
    finalContours = sorted(finalContours, key = lambda x:x[1], reverse=True)
    print("Final Contours number: ", len(finalContours))
    
    for con in finalContours:
        cv2.drawContours(imgContour, con[3], -1, (0, 0, 255), 3)

    return imgContour, finalContours

 
# sourcing the input image
img = cv2.imread(path)
# img.shape gives back height, width, color in this order
original_height, original_width, color = img.shape 
print('Original Dimensions : ', original_width, original_height)

# resizing to see the entire image
scale_percent = 30
width = int(original_width * scale_percent / 100)
height = int(original_height * scale_percent / 100)
print('Resized Dimensions : ', width, height)

dim = (width, height)
# resize image
resized = cv2.resize(img, dim, interpolation = cv2.INTER_AREA)
cv2.imshow("Starting image", resized)
cv2.waitKey()

# blurring
imgBlur = cv2.GaussianBlur(resized, (7, 7), 1)
# graying
imgGray = cv2.cvtColor(imgBlur, cv2.COLOR_BGR2GRAY)

# inizialing thresholds
threshold1 = 14
threshold2 = 17

# canny
imgCanny = cv2.Canny(imgGray, threshold1, threshold2)
# showing the last produced result
cv2.imshow("Canny", imgCanny)
cv2.waitKey()

kernel = np.ones((2, 2))
imgDil = cv2.dilate(imgCanny, kernel, iterations = 3)
imgThre = cv2.erode(imgDil, kernel, iterations = 3)

imgFinalContours, finalContours = getContours(imgThre, resized)

# show the contours on the unfiltered resized image
cv2.imshow("Final Contours", imgFinalContours)
cv2.waitKey()
cv2.destroyAllWindows()

您使用所选值得到的运行最终输出如下:


祝你有美好的一天,
安东尼诺

这里提出了什么

通过颜色渐变变化检测轮廓(见Antonino的回复)

绘制相对于背景具有低对比度的对象的轮廓并不是一项微不足道的任务。尽管 Antonino 的代码片段接近轮廓,但对于轮廓检测还不够:

  • finalContours不是一条单一的等高线,而是一列不清楚的线,即使使用了最好的可能参数(见下文):

  • 为了找到最佳参数,我使用了下面的伪代码,它输出了数千张视觉分类的图像(参见输出图像)。然而,none 的可能参数组合是成功的,即输出了想要的轮廓:

     for scale_percent in range(30,51,5):
         for threshold1 in range(5, 21):
             for threshold2 in range(10,31):
                 for gauss_kernel in range(1,11,2):
                     for std in [0,1,2]:
                         for kernel_size in range(2,6):
                             for iterations_dialation in [2,3]:
                                 for iterations_erosion in [2,3]:
                                     for img in images:
                                         name = img[3:]
                                         img = cv2.imread('my/img/dir'+img)
    
                                         original_height, original_width, color = img.shape 
                                         width = int(original_width * scale_percent / 100)
                                         height = int(original_height * scale_percent / 100)
    
                                         dim = (width, height)
                                         resized = cv2.resize(img, dim, interpolation = cv2.INTER_AREA)
    
                                         imgBlur = cv2.GaussianBlur(resized, (gauss_kernel, gauss_kernel), std)
    
                                         imgGray = cv2.cvtColor(imgBlur, cv2.COLOR_BGR2GRAY)
    
                                         imgCanny = cv2.Canny(imgGray, threshold1, threshold2)
    
                                         plt.subplot(231),plt.imshow(resized), plt.axis('off')
                                         plt.title('Original '+ str(name))    
    
                                         plt.subplot(232),plt.imshow(imgCanny,cmap = 'gray')
                                         plt.title('Canny Edge-detector\n thr1 = {}, thr2 = {}'.format(threshold1, threshold2)), plt.axis('off')
    
                                         kernel_s = (kernel_size, kernel_size)
                                         kernel = np.ones(kernel_s)
    
                                         imgDil = cv2.dilate(imgCanny, kernel, iterations = iterations_dialation)
                                         plt.subplot(233),plt.imshow(imgDil, cmap = 'gray'), plt.axis('off')
                                         plt.title("Dilated\n({},{}) iterations = {}".format(kernel_size, kernel_size,
                                                                                             iterations_dialation))
    
                                         kernel_erosion = np.ones(())
                                         imgThre = cv2.erode(imgDil, kernel, iterations = iterations_erosion)
                                         plt.subplot(234),plt.imshow(imgThre, cmap = 'gray'), plt.axis('off')
                                         plt.title('Eroded\n({},{}) iterations = {}'.format(kernel_size, kernel_size, 
                                                                                            iterations_erosion))
    
                                         imgFinalContours, finalContours = getContours(imgThre, resized)
    
                                         plt.subplot(235), plt.axis('off')
                                         plt.title("Contours")
    
                                         plt.subplot(236), plt.axis('off')
                                         plt.title('Contours')
    
                                         plt.tight_layout(pad = 0.1)
    
                                         plt.imshow(imgFinalContours) 
    
                                         plt.savefig("my/results/"
                                                     +name[:6]+"_scale_percent({})".format(scale_percent)+
                                                     "_threshold1({})".format(threshold1)
                                                    +"_threshold2({})".format(threshold2)
                                                    +"_gauss_kernel({})".format(gauss_kernel)
                                                    +"_std({})".format(std)
                                                    +"_kernel_size({})".format(kernel_size)
                                                    +"_iterations_dialation({})".format(iterations_dialation)
                                                    +"_iterations_erosion({})".format(iterations_erosion)
                                                    +".jpg")
                                         plt.title(name)
    
     images = ["b_36_2.jpg", "b_78_2.jpg", "b_51_2.jpg","b_72_2.jpg", "a_78_2.jpg", "a_70_2.jpg"]
     process_images_1(images)
    

输出:

解决方案

使用预训练的深度学习模型

初步的想法是使用grabcut来训练模型,但是这会非常耗时。因此,预训练的深度学习模型是第一枪。虽然有些工具 failed, this other tool outperformed any other method tried before (see image below). Hence, all the credit to the creator of the GitHub repository, extended to the creators of operating models (U^2-NET, BASNet). The https://github.com/OPHoperHPO/image-background-remove-tool 不需要任何图像预处理,但包含关于如何部署它的非常简单的文档,甚至还有一个可执行文件 google colab notebook。输出图像是具有透明背景的png图像: 因此,找到轮廓所需要做的就是隔离 alpha 通道:

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

filename = '/a_58_2_pg_0.png'
image_4channel = cv2.imread(filename, cv2.IMREAD_UNCHANGED)
alpha_channel = image_4channel[...,-1]
contours, hier = cv2.findContours(alpha_channel, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

for idx,contour in enumerate(contours):

        # create mask
        # zeros with same shape
        mask = np.zeros(alpha_channel.shape,np.uint8)
        
        # draw contour
        mask = cv2.drawContours(mask,[contour],-1,(255,255,255),-1) # -1 to fill the mask
        cv2.imwrite('/contImage.jpg', mask)
        plt.imshow(mask)