OpenCV运动检测不受随机噪声触发

1 投票
3 回答
113 浏览
提问于 2025-04-14 17:36

我刚开始接触图像处理,遇到了一些困难。我正在制作自己的安防软件,写了一个函数来检测运动,以便开始录制并通知我。

这个函数的想法是比较两张图片,找出其中的运动。但我遇到的问题是:

  1. 检测效果很好,但在晚上图像会有一些噪声,甚至在白天的阴影中也会错误触发检测。
  2. 函数没有错误触发,但有些运动却没能被检测到。

我尝试解决第二个问题的方法是通过注释掉的代码,主要思路是:

  • 对灰度图像进行分割和模糊处理。
  • 用自适应阈值替代基本的阈值(高斯和均值)。

这是我的代码:

import cv2
import numpy as np
from skimage.metrics import structural_similarity as ssim

def count_diff_nb(img_1, img_2):

    # resize images
    img_1_height, img_1_width = img_1.shape[:2]
    new_height = int((600 / img_1_width) * img_1_height)

    img_1 = cv2.resize(img_1, (600,new_height))
    img_2 = cv2.resize(img_2, (600,new_height))

    # convert to gray scale
    gray_image1 = cv2.cvtColor(img_1, cv2.COLOR_BGR2GRAY)
    gray_image2 = cv2.cvtColor(img_2, cv2.COLOR_BGR2GRAY)

    # Gaussian blur in order to remove some noise
    blur1 = cv2.GaussianBlur(gray_image1, (5,5), 0)
    blur2 = cv2.GaussianBlur(gray_image2, (5,5), 0)

    # divide (bad idea)
    #divide1 = cv2.divide(gray_image1, blur1, scale=255)
    #divide2 = cv2.divide(gray_image2, blur2, scale=255)

    # Compute SSIM between two images
    #ssim_value, diff = ssim(gray_image1, gray_image2, full=True)
    ssim_value, diff = ssim(blur1, blur2, full=True)
    #ssim_value, diff = ssim(divide1, divide2, full=True)
    diff_percent = (1 - ssim_value) * 100

    # The diff image contains the actual image differences between the two images
    # and is represented as a floating point data type so we must convert the array 
    # to 8-bit unsigned integers in the range [0,255] before we can use it with OpenCV
    diff = (diff * 255).astype("uint8")

    # Adaptative threshold (bad idea too)
    #thresh = cv2.adaptiveThreshold(diff, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)
    #thresh = cv2.adaptiveThreshold(diff, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 3, 10)

    # Threshold the difference image
    thresh = cv2.threshold(diff, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    # followed by finding contours to
    # obtain the regions that differ between the two images
    contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours = contours[0] if len(contours) == 2 else contours[1]
    
    # Highlight differences
    mask = np.zeros(img_1.shape, dtype='uint8')
    filled = img_2.copy()


    contours_nb = 0
    for c in contours:
        # limit is an area so sqrt of size
        area = cv2.contourArea(c)
        # 72000 is 1/3 of global img area
        if area > 2000 and area < 72000:
            contours_nb = contours_nb + 1
            x,y,w,h = cv2.boundingRect(c)
            cv2.rectangle(img_1, (x, y), (x + w, y + h), (36,255,12), 2)
            cv2.rectangle(img_2, (x, y), (x + w, y + h), (36,255,12), 2)
            cv2.drawContours(mask, [c], 0, (0,255,0), -1)
            cv2.drawContours(filled, [c], 0, (0,255,0), -1)

    return contours_nb, diff_percent, img_2, filled

你们有没有什么建议或者我遗漏了什么,以便找到灵敏度(不漏掉检测)和忽略黑暗中随机噪声之间的平衡点?

我考虑在转换为灰度之前忽略暗色,但如果移动的物体是黑色的,那我觉得这不是个好主意。

非常感谢!

编辑:

我通过实现这个解决方案,完全改变了我的方法,感谢@pippo1980的建议。我使用了BackgroundSubtractorMOG2,这在我的情况下效果最好。(我测试了不同的选项)。

现在几乎完美,最后一个问题是在日出和日落时,我的便宜摄像头在噪声和图像模糊/随机噪声方面有些挣扎。

我在寻找解决办法,但不太确定。

这是正常工作时的效果,你可以看到掩膜非常清晰:

enter image description here

enter image description here

而在日落时,图像模糊/有噪声:

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

3 个回答

0

这是我目前的完整解决方案,运行得很好,除了当太阳快速出来时,遇到明亮的表面会有点问题。

图像预处理:

import cv2

def erode_and_contours(fg_mask, frame):
    #thresholding
    retval, mask_thresh = cv2.threshold(fg_mask, 180, 255, cv2.THRESH_BINARY)

    # erosion and dilation
    # set the kernel
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3 ,3))
    # apply erosion
    mask_eroded = cv2.morphologyEx(mask_thresh, cv2.MORPH_OPEN, kernel)

    # Find contours
    contours, hierarchy = cv2.findContours(mask_eroded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # Filtering contours
    min_contour_area = 4000
    large_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > min_contour_area]
                
    # Draw bounding boxes
    frame_out = frame.copy()
    for cnt in large_contours:
        x, y, w, h = cv2.boundingRect(cnt)
        frame_out = cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 200, 0), 3)

    return large_contours, mask_eroded, frame_out


def extract_contours(contours):
    # sort contours by x and y 
    sorted_contours = sorted(contours, key=lambda c: cv2.boundingRect(c)[:2])
    # extract rectangles under form [x, y, width, height]
    rectangles = [[cv2.boundingRect(c)[0], cv2.boundingRect(c)[1], cv2.boundingRect(c)[2], cv2.boundingRect(c)[3]] for c in sorted_contours]

    return rectangles

def check_contours_movement(stored_positions_list, movement_threshold_percent):
    # return True if contours movement is > to movement_threshold_percent 

    if len(stored_positions_list) < 2:
        return False, 0  # Not enough positions to compare

    # search the min len
    min_len = None
    for positions_list in stored_positions_list:
        if min_len is None:
            min_len = len(positions_list)

        if min_len > len(positions_list):
            min_len = len(positions_list)


    first_positions = stored_positions_list[0]
    for positions_list in stored_positions_list[1:]:
        for i in range(min_len):
            x_diff = calc_diff_percent(first_positions[i][0], positions_list[i][0])
            y_diff = calc_diff_percent(first_positions[i][1], positions_list[i][1])
            w_diff = calc_diff_percent(first_positions[i][2], positions_list[i][2])
            h_diff = calc_diff_percent(first_positions[i][3], positions_list[i][3])

            mean_diff = (x_diff + y_diff + w_diff + h_diff) / 4
            if mean_diff > movement_threshold_percent:
                return True, mean_diff

    return False, 0

def calc_diff_percent(nb1, nb2):
    nb1 = max(nb1, 1)
    nb2 = max(nb2, 1)
    if nb2 < nb1:
        nb1, nb2 = nb2, nb1
    res = (nb2 - nb1) / nb1 * 100
    return res

主循环:

import cv2
import time
from image_pre_processing import erode_and_contours, extract_contours, check_contours_movement


def main(camera_index):
        camera = cv2.VideoCapture(camera_index)
        if not camera.isOpened():
            print("Error : impossible to open camera feed.")
            return

        init_phase = 0
        is_registering = False
        last_send_time = time.time()

        movement_start_time = None
        movement_duration_threshold = 2
        update_positions_interval = 0.5
        update_positions_time = time.time()

        backSub = cv2.createBackgroundSubtractorMOG2()
        backSub.setHistory(300)
        backSub.setDetectShadows(True)

        stored_contours_positions = []

        while True:
            ret, frame = camera.read()

            denoised = cv2.fastNlMeansDenoisingColored(frame, None, 5, 5, 3, 9)
            gray = cv2.cvtColor(denoised, cv2.COLOR_BGR2GRAY)
            fg_mask = backSub.apply(gray)
            cv2.imshow('gray', gray)
            
            if init_phase > 10:
                large_contours, mask_eroded, frame_out = erode_and_contours(fg_mask, frame)

                # movement identified
                nb_diff = len(large_contours)
                if nb_diff > 0:
                    # If begin of a movement we start to count
                    if movement_start_time is None:
                        movement_start_time = time.time()

                    # temporize and register contours position
                    if (time.time() - update_positions_time) >= update_positions_interval:
                        stored_contours_positions.append(extract_contours(large_contours))
                        update_positions_time = time.time()

                    contours_are_moving, mean_position_diff = check_contours_movement(stored_contours_positions, 30)

                    # if movement duration is > to threshold
                    if (time.time() - movement_start_time) >= movement_duration_threshold and contours_are_moving is True:
                        timing = time.time() - movement_start_time
                
                        # Send only 1 image per second
                        if  (time.time() - last_send_time) >= 1:
                            if is_registering is False:
                                is_registering = True
                                    print(f"There's some activity on camera. nb_diff : {nb_diff}, for {timing:.2f}s and {mean_position_diff:.2f}% moving")
                            send_image(frame_out)
                            last_send_time = time.time()
                else:
                    # if no movement we re-init
                    is_registering = False
                    movement_start_time = None
                    stored_contours_positions = []
            else:
                init_phase = init_phase + 1

            # temporize to not overload
            time.sleep(0.1)

            

由于我在算法检测运动时遇到问题,特别是在太阳突然出现和有光亮的地方,我在考虑两个选项:

  1. 研究一下直方图处理
  2. 使用tensorflow和yolo来检查我是否能检测到一些人或其他物体(不过这可能有点过于复杂,而且在树莓派上运行会消耗太多CPU资源)
0

我没有你的输入,所以我尝试用这个作为输入:"inp_short_2.mp4"

这是一个图片预览:

这里输入图片描述

使用的代码是:

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

def ResizeWithAspectRatio(image, width=None, height=None, inter=cv2.INTER_AREA):
    dim = None
    (h, w) = image.shape[:2]

    if width is None and height is None:
        return image
    if width is None:
        r = height / float(h)
        dim = (int(w * r), height)
    else:
        r = width / float(w)
        dim = (width, int(h * r))

    return cv2.resize(image, dim, interpolation=inter)


draw_windows = True  ## change fo False for no windows only calc


def drawWindow(window_name, image):
    if draw_windows:
        
        resize = ResizeWithAspectRatio(image, width= 1000)
        
        cv2.imshow(window_name, resize)
        
        cv2.moveWindow(window_name, 600, 200)
        
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    
    pass
        
        
# vid_path = ('inp.mp4')
# vid_path = ('inp_short.mp4')
vid_path = ('inp_short_2.mp4')


cap = cv2.VideoCapture(vid_path)

backSub = cv2.createBackgroundSubtractorMOG2()

backSub.setDetectShadows(0)

backSub.setHistory(30)


# cv2.imshow('backSub_1st' , backSub)

# cv2.waitKey(0)


if not cap.isOpened():
    print("Error opening video file")
    
    
    
total_contours = 0

total_frames = 0

frames_out_list = []
    
while cap.isOpened():
    
    # print('cap.isOpened()' , cap.isOpened())
    
    # Capture frame-by-frame
    ret, frame = cap.read()
    
    total_frames += 1
    
    
    if ret:
        
        
        frame_copy = frame.copy()
    
        # print('ret : ' , ret)
        # Apply background subtraction
        
        fg_mask = backSub.apply(cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY))
        
        # print('fg_mask : ',  np.sum(fg_mask))
        
        # drawWindow('fg_mask', fg_mask)

        # apply global threshold to remove shadows
        retval, mask_thresh = cv2.threshold( fg_mask, 180, 255, cv2.THRESH_BINARY)

        # mask_thresh = fg_mask

        # set the kernal
        # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
        # Apply erosion
        # mask_eroded = cv2.morphologyEx(mask_thresh, cv2.MORPH_OPEN, kernel)
        
        # Apply morphological operations to reduce noise and fill gaps
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
        
        # mask_eroded = mask_thresh
        
        mask_eroded  = cv2.erode(mask_thresh, kernel, iterations=1)
        mask_eroded  = cv2.dilate(mask_thresh, kernel, iterations=1)


        min_contour_area = 500  # Define your minimum area threshold
        
        # Find contours
        contours, hierarchy = cv2.findContours(mask_eroded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        # print(contours)
        frame_ct = cv2.drawContours(frame, contours, -1, (0, 255, 0), 2)
        # Display the resulting frame
        # cv2.imshow('Frame_final', frame_ct)
        
        # cv2.waitKey(0)
        
        
        large_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > min_contour_area ] #and cv2.contourArea(cnt) < 500000]

        #large_contours = [cnt for cnt in contours if cv2.contourArea(contours) > min_contour_area]

        frame_out = frame.copy()

        for cnt in large_contours:
            
            
            frame_ct = cv2.drawContours(frame, cnt, -1, (0, 255, 0), thickness = cv2.FILLED)
            
            total_contours += 1
            
            x, y, w, h = cv2.boundingRect(cnt)
            frame_out = cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 200), 3)
 
            # Display the resulting frame
            # drawWindow('Frame_final', frame_out)
            
            print('fg_mask : ',  np.sum(fg_mask))
            
            # drawWindow('fg_mask', fg_mask)

            
            frames_out_list.append(cnt)
            

            
    else:
        
        break
    
    if total_contours > 100:
        
        break
    
    else:
        
        pass
    
print('total_contours :' ,total_contours,'/', total_frames)

print('mask size : ', fg_mask.shape, fg_mask.size)

for contour in frames_out_list :
    
    x, y, w, h = cv2.boundingRect(contour)
    
    frame_out = cv2.rectangle(frame_copy, (x, y), (x+w, y+h), (0, 0, 200), 3)
    

drawWindow('Frame_final', frame_copy)


在运行这个视频时,我得到了这些检测到的框:

这里输入图片描述

而使用另一个输入:

"inp_short.mp4"

示例图片:

这里输入图片描述

使用的代码是:

# set the kernal
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
# Apply erosion
mask_eroded = cv2.morphologyEx(mask_thresh, cv2.MORPH_OPEN, kernel)

与上面的代码中使用的掩膜操作不同,我得到了:

这里输入图片描述

这是一种噪声,但对于一张充满光线和大量移动的树枝的图片来说,似乎还不错。

这是我第一次尝试使用OpenCV的功能。我不是专家,但我觉得用一个不错的库和几个循环能得到这样的结果还不错。没有输入和完整的代码,我们其实很难对你的结果或问题做出评论。

1

我不知道你哪里做错了,但如果你稍微在网上查查,应该能找到很多解决方法。比如,从使用OpenCV进行移动物体检测的轮廓检测和背景减法中,你可以找到一个很不错的物体检测流程图,用OpenCV来实现:

这里输入图片描述

这个流程图提到了背景减法,而你的算法中没有描述这一点,不过我可能错了,我对OpenCV不是很了解。在文档中,他们把这种方法描述为:

每一帧都用来计算前景掩码,同时也用来更新背景。如果你想改变更新背景模型时使用的学习率,可以通过给apply方法传递一个参数来设置特定的学习率……

你实际上可以在OpenCV文档中找到关于这种方法的信息:

这里输入图片描述

显然,这里有两种方法,BackgroundSubtractorMOGBackgroundSubtractorMOG2

其实在这里也描述了三种方法:

MOG、MOG2和GMG之间的区别

撰写回答