Reputation: 2607
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)
Without shadows
With shadows (Problem)
When there are car shadows, they are also in the minimum area rectangle. How can I exclude shadows from the rectangle?
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()
Upvotes: 3
Views: 5809
Reputation: 8008
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:
all image:
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)
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()
Upvotes: 1