Search code examples
python-3.xopencvimage-processingobject-detection

OpenCV- Detecting objects excluding shadows


I'm developing a way to detect cars from an aerial view image. I'm using scikit package to calculate the difference of an empty parking lot image and a parking lot with cars to detect the foreign objects. Then I draw the minimum area rectangle around cars.

This works well when there are no shadows.

Empty parking lot image (Please ignore the maroon color car)

Empty parking lot

Without shadows

Without shadows original

Without shadows threshold

Without shadows result

With shadows (Problem)

When there are car shadows, they are also in the minimum area rectangle. How can I exclude shadows from the rectangle?

With shadows original

With shadows threshold

With shadows result

Here is my source

import numpy as np
from skimage.measure import compare_ssim
import imutils
from cv2 import cv2

# construct the argument parse and parse the arguments
# load the two input images
imageA = cv2.imread('empty-lot.png')
imageB = cv2.imread('two-car-lot-shadow.png')

# convert the images to grayscale
grayA = cv2.cvtColor(imageA, cv2.COLOR_BGR2GRAY)
grayB = cv2.cvtColor(imageB, cv2.COLOR_BGR2GRAY)

# compute the Structural Similarity Index (SSIM) between the two
# images, ensuring that the difference image is returned
(score, diff) = compare_ssim(grayA, grayB, full=True, gaussian_weights=True, sigma=4)
diff = (diff * 255).astype("uint8")
print("SSIM: {}".format(score))

# threshold the difference image, followed by finding contours to
# obtain the regions of the two input images that differ
thresh = cv2.threshold(diff, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if imutils.is_cv2() else cnts[1]

# loop over the contours
for c in cnts:
    # compute the min area rect of the contour and area
    rect = cv2.minAreaRect(c)
    box = cv2.boxPoints(rect)
    area = cv2.contourArea(c)
    # remove small contour areas
    if area < 10000: continue
    # convert all coordinates floating point values to int
    box = np.int0(box)
    # draw a green rectangle
    cv2.drawContours(imageB, [box], 0, (0, 255, 0), 2)

# show the output images
cv2.imshow("Modified", imageB)
cv2.imshow("Thresh", thresh)
k = cv2.waitKey() & 0xFF
if k == 27:
    exit()

Solution

  • You can convert the image format to HSV, it's easy to remove the shadow.

    import ctypes
    import numpy as np
    import cv2
    from pathlib import Path
    from typing import List, Union, Callable
    
    
    def main():
        img_bgr: np.ndarray = cv2.imread(str(Path('parking.jpg')))
        img_hsv: np.ndarray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
        mask = cv2.inRange(src=img_hsv, lowerb=np.array([0, 64, 153]), upperb=np.array([179, 255, 255]))
        img_hsv_modify: np.ndarray = cv2.bitwise_and(img_bgr, img_bgr, mask=mask)
        # show_img(img_hsv_modify)
    
        img_mask_gray = cv2.cvtColor(img_hsv_modify, cv2.COLOR_BGR2GRAY)
        threshold_val, img_bit = cv2.threshold(img_mask_gray, 0, 255, cv2.THRESH_BINARY)  # type: float, np.ndarray
        contours, hierarchy = cv2.findContours(img_bit, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
        img_bgr_copy = np.copy(img_bgr)
        for cnt in filter(lambda c: cv2.contourArea(c) > 10000, contours):
            box = cv2.boxPoints(cv2.minAreaRect(cnt))
            cv2.drawContours(img_bgr_copy, [np.int0(box)], -1, color=(0, 255, 0), thickness=2)
    
        show_img([img_bgr, img_hsv_modify, cv2.cvtColor(img_bit, cv2.COLOR_GRAY2BGR), img_bgr_copy],
                 note=['original', 'hsv modify', 'bit', 'result'])
        show_img(img_bgr_copy)
    
    
    if __name__ == '__main__':
        main()
    
    

    And you may ask how do I know the lower bound and upper bound. (Please see the extension code: control_bar_hsv.py)

    mask = cv2.inRange(src=img_hsv, lowerb=np.array([0, 64, 153]), upperb=np.array([179, 255, 255]))
    

    result:

    enter image description here

    all image:

    enter image description here

    Extension code

    show_img:

    def show_img(img_list: Union[np.ndarray, List[np.ndarray]], combine_fun: Callable = np.vstack,
                 window_name='demo', window_size=(ctypes.windll.user32.GetSystemMetrics(0) // 2, ctypes.windll.user32.GetSystemMetrics(1) // 2),
                 delay_time=0, note: Union[str, List[str]] = None, **options):
        if isinstance(img_list, np.ndarray):
            img_list = [img_list]
    
        if isinstance(note, str):
            print(note)
    
        cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
        if window_size:
            w, h = window_size
            cv2.resizeWindow(window_name, w, h)
    
        result_list = []
        for idx, img in enumerate(img_list):
            img = np.copy(img)
            if note and isinstance(note, list) and idx < len(note):
                cv2.putText(img, note[idx], org=options.get('org', (50, 50)),
                            fontFace=options.get('fontFace', cv2.FONT_HERSHEY_SIMPLEX),
                            fontScale=options.get('fontScale', 2), color=(0, 255, 255), thickness=4)
            result_list.append(img)
        cv2.imshow(window_name, combine_fun(result_list))
        cv2.waitKey(delay_time)
    
    

    control_bar_hsv.py

    It's easy to use, just copy and paste and then tell it where is your image path, done.

    and if you want to create other control_bar, maybe ControlBarBase will help you write to write less code.

    """
    control_bar_hsv.py
    """
    
    import cv2
    from pathlib import Path
    import numpy as np
    import ctypes
    import functools
    from typing import Tuple, Callable
    
    
    class ControlBarBase:
        __slots__ = ('img_bgr',)
        WAIT_TIME = 500  # milliseconds
        CONTROL_PANEL_NAME = 'control_panel'
        IMAGE_PANEL_NAME = 'image'
        SCREEN_SIZE: Tuple[int, int] = None
    
        def __init__(self, img_path: Path):
            self.img_bgr: np.ndarray = cv2.imread(str(img_path))
            self.init_window()
            self.init_track_bar()
    
        def init_window(self):
            for name in (self.CONTROL_PANEL_NAME, self.IMAGE_PANEL_NAME):
                cv2.namedWindow(name, cv2.WINDOW_NORMAL)
                if self.SCREEN_SIZE:
                    screen_width, screen_height = self.SCREEN_SIZE
                    cv2.resizeWindow(name, int(screen_width), int(screen_height))
    
        def init_track_bar(self):
            """
            self.build_track_bar(label_name, range_lim=(0, 255), default_value=0, callback)
            """
            raise NotImplementedError('subclasses of _ControlBarBase must provide a init_track_bar() method')
    
        def render(self):
            raise NotImplementedError('subclasses of _ControlBarBase must provide a render() method.')
    
        def build_track_bar(self, label_name: str,
                            range_lim: Tuple[int, int], default_value: int, callback: Callable = lambda x: ...):
            min_val, max_val = range_lim
            cv2.createTrackbar(label_name, self.CONTROL_PANEL_NAME, min_val, max_val, callback)
            cv2.setTrackbarPos(label_name, self.CONTROL_PANEL_NAME, default_value)
    
        def get_trackbar_pos(self, widget_name: str):
            return cv2.getTrackbarPos(widget_name, self.CONTROL_PANEL_NAME)
    
        def run(self):
            while 1:
                img, callback_func = self.render()
                cv2.imshow(self.IMAGE_PANEL_NAME, img)
                if (cv2.waitKey(self.WAIT_TIME) & 0xFF == ord('q') or
                        cv2.getWindowProperty(self.IMAGE_PANEL_NAME, 0) == -1 or
                        cv2.getWindowProperty(self.CONTROL_PANEL_NAME, 0) == -1):
                    callback_func()
                    break
            cv2.destroyAllWindows()
    
    
    class ControlBarHSV(ControlBarBase):
        __slots__ = ()
        WAIT_TIME = 500
        SCREEN_SIZE = ctypes.windll.user32.GetSystemMetrics(0) / 2, ctypes.windll.user32.GetSystemMetrics(1) / 2
    
        def init_track_bar(self):
            self.build_track_bar('HMin', range_lim=(0, 179), default_value=0)
            self.build_track_bar('SMin', (0, 255), 0)
            self.build_track_bar('VMin', (0, 255), 0)
    
            self.build_track_bar('HMax', (0, 179), 179)
            self.build_track_bar('SMax', (0, 255), 255)
            self.build_track_bar('VMax', (0, 255), 255)
    
        def render(self):
            # get current positions of all trackbars
            h_min = self.get_trackbar_pos('HMin')
            s_min = self.get_trackbar_pos('SMin')
            v_min = self.get_trackbar_pos('VMin')
    
            h_max = self.get_trackbar_pos('HMax')
            s_max = self.get_trackbar_pos('SMax')
            v_max = self.get_trackbar_pos('VMax')
    
            # Set minimum and max HSV values to display
            lower = np.array([h_min, s_min, v_min])
            upper = np.array([h_max, s_max, v_max])
    
            # Create HSV Image and threshold into a range.
            hsv = cv2.cvtColor(self.img_bgr, cv2.COLOR_BGR2HSV)
            mask = cv2.inRange(hsv, lower, upper)
            img_output: np.ndarray = cv2.bitwise_and(self.img_bgr, self.img_bgr, mask=mask)
    
            @functools.wraps(self.render)
            def cb_func():
                return print(f'cv2.inRange(src=hsv, lowerb=np.array([{h_min}, {s_min}, {v_min}]), upperb=np.array([{h_max}, {s_max}, {v_max}]))')
    
            return img_output, cb_func
    
    
    if __name__ == '__main__':
        from argparse import ArgumentParser
    
        arg_parser = ArgumentParser()
        arg_parser.add_argument("src_path", type=Path, help="source image path")
        args = arg_parser.parse_args()
        obj = ControlBarHSV(args.src_path)  # ControlBarHSV(Path('parking.jpg'))
        obj.run()
    
    

    enter image description here