widzenie-komputerowe-MP/wko-04.ipynb

3.0 MiB

Logo 1

Widzenie komputerowe

04. Zaawansowane przetwarzanie obrazów i fotografia obliczeniowa [laboratoria]

Andrzej Wójtowicz (2021)

Logo 2

W poniższych materiałach krótko omówimy zagadnienia związane z fotografią obliczeniową, a większą część czasu pozostawimy na realizację zadania dotyczącego wykrywania linii.

Na początku załadujmy niezbędne biblioteki.

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

Obrazy HDR

Na poprzednich zajęciach widzieliśmy w jaki sposób można poprawić kontrast np. przy pomocy wyrównania histogramu. W niektórych przypadkach jest to wystarczająca operacja pozwalająca uzyskać zadowalający efekt. Niestety, jest też wiele przypadków gdy na potrzebę uchwycenia głównych elementów na zdjęciu obraz musi mieć mniejszą lub większą ekspozycję, co kończy się niedoświetleniem lub prześwietleniem obiektów znajdujących się np. na drugim planie. Zasadniczo w takiej sytuacji chcielibyśmy wyjść poza standardowy ograniczony zakres wartości 0-255 do jakiegoś szerszego zakresu, który z jednej strony pozwoliłby nam uchwycić więcej informacji znajdujących się na scenie, ale z drugiej strony musimy też pamiętać o ponownej konwersji do standardowego zakresu.

Pokażemy tutaj w jaki sposób można wykorzystać technikę _High dynamic range (HDR) imaging w OpenCV do uzyskania obrazu o dużej rozpiętości tonalnej. Poniżej mamy kilka zdjęć tego samego obiektu (źródło: Wikipedia, CC BY-SA 3.0), wykonanych z różnym czasem naświetlania, przez co możemy zauważyć, że część elementów jest niedoświetlona, a część prześwietlona:

exposure_times = np.array([ 1/30.0, 0.25, 2.5, 15.0 ], dtype=np.float32)
   
f_names = ["st-louis-arch-0.033.jpg", 
           "st-louis-arch-0.25.jpg", 
           "st-louis-arch-2.5.jpg", 
           "st-louis-arch-15.jpg"]

plt.figure(figsize=(20,5))
images = []
for i, f_name in enumerate(f_names):
    image = cv.imread(f"img/{f_name}", cv.IMREAD_COLOR)
    images.append(image)
    
    plt.subplot(141 + i)
    plt.imshow(image[:,:,::-1])
    plt.title(f"{str(round(exposure_times[i], 3))} sec.")

Szczegółowy opis dalszych kroków można znaleźć np. w rozdziale _10.1 Photometric calibration i 10.2 High dynamic range imaging książki R. Szeliski Computer Vision (2021) - tutaj ograniczymy się do technicznego rozwiązania kolejnych problemów.

Pierwszym problemem, który musimy rozwiązać, jest wyrównanie/dopasowanie obrazów. Nawet jeśli zdjęcie jest robione ze statywu (lub gorzej - z ręki), to po nałożeniu zdjęć na siebie będą widoczne artefakty wynikające z wibracji i przesunięć. W OpenCV możemy to rozwiązać poprzez funkcję cv.createAlignMTB(), która tworzy tzw. bitmapy z progiem mediany (ang. _median threshold bitmaps), w których zawartość obrazu jest obliczana przez przypisanie wartości 1 pikselom jaśniejszym niż mediana luminancji i 0 w przeciwnym wypadku.

align_mtb = cv.createAlignMTB()
align_mtb.process(images, images)

W dalszych krokach musimy:

  • oszacować radiometryczną funkcję odpowiedzi na podstawie wyrównanych obrazów,
  • oszacować mapę radiacyjną, wybierając lub mieszając piksele z różnych ekspozycji,
  • wykonać mapowanie tonalne wynikowego obrazu HDR z powrotem do postaci umożliwiającej jego normalne wyświetlanie.
calibrate_debevec = cv.createCalibrateDebevec()
response_debevec = calibrate_debevec.process(images, exposure_times)
merge_debevec = cv.createMergeDebevec()
hdr_debevec = merge_debevec.process(images, exposure_times, response_debevec)

Teoretycznie moglibyśmy zapisać teraz wynikowy plik .hdr np. przy pomocy cv.imwrite("hdr_debevec.hdr", hdr_debevec) i edytować dalej go w programie graficznym. W tym miejscu jednak wykonamy dalszą obróbkę przy pomocy OpenCV, tj. wykonamy mapowanie tonalne. Poniżej mamy zaprezentowane wyniki uzyskane metodą Reinharda i Mantiuka:

tonemap_rReinhard = cv.createTonemapReinhard(1.5, 0,0,0)
ldr_reinhard = tonemap_rReinhard.process(hdr_debevec)
plt.imshow(ldr_reinhard[:,:,::-1]);
[ WARN:0@261.938] global /io/opencv/modules/core/src/matrix_expressions.cpp (1333) assign OpenCV/MatExpr: processing of multi-channel arrays might be changed in the future: https://github.com/opencv/opencv/issues/16739
tonemap_mantiuk = cv.createTonemapMantiuk(2.2, 0.85, 1.2)
ldr_mantiuk = tonemap_mantiuk.process(hdr_debevec)
ldr_mantiuk = np.clip(3 * ldr_mantiuk, 0, 1)
plt.imshow(ldr_mantiuk[:,:,::-1]);

Gładkie klonowanie

Na wcześniejszych zajęciach widzieliśmy, że możemy umieszczać elementy jednego obrazu w drugim obrazie np. poprzez wykorzystanie maski i kanału dotyczącego przezroczystości. Innym ciekawym podejściem jest praca na gradientach obrazu (zamiast na jego intensywności), co może dać równie interesujące, a czasem i bardziej realistyczne wyniki. W tzw. gładkim klonowaniu (ang. _seamless cloning) intensywność obiektu docelowego będzie różna od intensywności obiektu źródłowego, natomiast gradienty będą podobne. Szczegóły omawianej metody znajdują się w artykule P. Perez et al. (2003) Poisson Image Editing.

Poniżej mamy obraz basenu i kąpiących się w nim ludzi oraz niedźwiedzia, którego chcemy umieścić w basenie:

swimmingpool = cv.imread("img/swimmingpool.jpg", cv.IMREAD_COLOR)
bear = cv.imread("img/bear.jpg", cv.IMREAD_COLOR)
bear_mask = cv.imread("img/bear-mask.jpg", cv.IMREAD_GRAYSCALE)

plt.figure(figsize=(20,5))
plt.subplot(131)
plt.imshow(swimmingpool[:,:,::-1])
plt.title("Swimming pool")
plt.subplot(132)
plt.imshow(bear[:,:,::-1])
plt.title("Bear")
plt.subplot(133)
plt.imshow(bear_mask[:,:], cmap='gray')
plt.title("Bear mask");

Przy pomocy metody cv.seamlessClone() wykonujemy gładkie klonowanie:

center = (250,700)

swimmingpool_with_bear = cv.seamlessClone(bear, swimmingpool, bear_mask, center, cv.NORMAL_CLONE)

plt.figure(figsize=(10,10))
plt.imshow(swimmingpool_with_bear[:,:,::-1])
plt.title("I think this might be photoshopped");

Spróbujmy teraz umieścić tekst na teksturze:

texture = cv.imread("img/texture.jpg", cv.IMREAD_COLOR)
bologna = cv.imread("img/bologna-on-wall.jpg", cv.IMREAD_COLOR)

plt.figure(figsize=(20,5))
plt.subplot(131)
plt.imshow(texture[:,:,::-1])
plt.title("Texture")
plt.subplot(132)
plt.imshow(bologna[:,:,::-1])
plt.title("Bologna to clone");

W tym wypadku stworzenie maski może być problematyczne, zatem możemy pójść na skróty i przyjąć, że cały obraz stanowi maskę. Przy takim podejściu zobaczymy jednak, że opcja cv.NORMAL_CLONE spowoduje mocne rozmazanie wokół wklejanej części, stąd też lepszym rozwiązaniem będzie opcja cv.MIXED_CLONE, co wynika z kombinacji gradientów z jednego i drugiego obrazu:

mask = 255 * np.ones(bologna.shape, bologna.dtype)

width, height, channels = texture.shape
center = (height//2, width//2)

normal_clone = cv.seamlessClone(bologna, texture, mask, center, cv.NORMAL_CLONE)
mixed_clone  = cv.seamlessClone(bologna, texture, mask, center, cv.MIXED_CLONE)

plt.figure(figsize=[20,10])
plt.subplot(121)
plt.title("Normal clone")
plt.imshow(normal_clone[:,:,::-1])
plt.subplot(122)
plt.title("Mixed clone")
plt.imshow(mixed_clone[:,:,::-1])
plt.show()

Usuwanie niechcianych obiektów

Spróbujemy teraz usunąć ze zdjęcia defekty, które często są domeną starych fotografii. Poniżej mamy zdjęcie prezydenta Lincolna z rysą na poziomie włosów; dodatkowo ręcznie ustaliliśmy maskę wskazującą miejsce występowania tej rysy:

lincoln = cv.imread("img/lincoln.jpg", cv.IMREAD_COLOR)
lincoln_mask = cv.imread("img/lincoln-mask.jpg", cv.IMREAD_GRAYSCALE)

plt.figure(figsize=(15,10))
plt.subplot(121)
plt.imshow(lincoln[:,:,::-1])
plt.title("Lincoln last photo")
plt.subplot(122)
plt.imshow(lincoln_mask[:,:], cmap='gray')
plt.title("Mask");

Używając metody cv.inpaint() możemy przywrócić wybrany region w obrazie przy pomocy sąsiedztwa zadanego regionu. Metoda cv.INPAINT_NS implementuje podejście opisane w M. Bertalmio et al. (2001) _Navier-Stokes, Fluid Dynamics, and Image and Video Inpainting, która ogólnie mówiąc ma za zadanie zachować gradienty (np. krawędzie) i rozprowadzać informacje o kolorach na płaskich przestrzeniach. Z kolei cv.INPAINT_TELEA implementuje metodę opisaną w A. Telea (2004) An image inpainting technique based on the fast marching method. W obu przypadkach możemy zauważyć oczekiwane wypełnienie na poziomie włosów, aczkolwiek po prawej stronie jest widoczne pewne rozmazanie - być może należałoby poprawić/zawęzić wejściową maskę.

lincoln_inpainted_ns = cv.inpaint(src=lincoln, inpaintMask=lincoln_mask, inpaintRadius=5, flags=cv.INPAINT_NS)
lincoln_inpainted_t  = cv.inpaint(src=lincoln, inpaintMask=lincoln_mask, inpaintRadius=5, flags=cv.INPAINT_TELEA)

plt.figure(figsize=(15,10))
plt.subplot(131)
plt.imshow(lincoln[:,:,::-1])
plt.title("Lincoln last photo")
plt.subplot(132)
plt.imshow(lincoln_inpainted_ns[:,:,::-1])
plt.title("Inpainted: Navier-Stokes")
plt.subplot(133)
plt.imshow(lincoln_inpainted_t[:,:,::-1])
plt.title("Inpainted: Telea");

Transformata Hougha

Zadanie 1

W poniższym zadaniu należy przygotować serię operacji, która pozwoli wykryć proste linie na obrazie img/road-lanes.jpg przy pomocy transformaty Hougha (wym. _hafa), zaimplementowanej w funkcji cv.HoughLines() lub cv.HoughLinesP(). Możemy przyjąć, że przetwarzane zdjęcie jest pojedynczą klatką uzyskaną w kamery znajdującej się na jadącym samochodzie.

Kolejne kroki mogą wyglądać następująco:

  1. progowanie obrazu w celu uzyskania pasów na drodze,
  2. ograniczenie przetwarzanego obrazu do interesującego nas fragmentu (np. może to być traprez osadzony na dole ekranu zwężający się do środka - góra i boki nas za bardzo nie interesują),
  3. wykrycie krawędzi,
  4. wykrycie linii transformatą Hougha,
  5. ekstrapolacja znalezionych wyżej linii,
  6. naniesienie linii na obraz wejściowy.
# _, img_bin = cv.threshold(img, 100, 255, cv.THRESH_BINARY)
# _, img_bin_inv = cv.threshold(img, 100, 255, cv.THRESH_BINARY_INV)
# _, img_trunc = cv.threshold(img, 100, 255, cv.THRESH_TRUNC)
# _, img_to_zero = cv.threshold(img, 100, 255, cv.THRESH_TOZERO)
# _, img_to_zero_inv = cv.threshold(img, 100, 255, cv.THRESH_TOZERO_INV)

image = cv.imread("img/road-lanes.jpg", cv.IMREAD_GRAYSCALE)
_, image = cv.threshold(image, 210, 255, cv.THRESH_BINARY)

plt.figure(figsize=[10,8])
plt.subplot(221)
plt.title("clean")
plt.imshow(image, cmap='gray')

rows, cols = image.shape
mask = np.zeros((rows, cols))
points = np.array([[400, 250], [650, 250], [900, 560], [100, 560]])
mask = cv.fillPoly(mask, pts=[points], color=(255, 255, 255))
rect = cv.boundingRect(points)
x,y,w,h = rect
croped = mask[y:y+h, x:x+w].copy()

plt.subplot(222)
plt.title("mask")
plt.imshow(mask, cmap='gray')



# edges = cv.Canny(croped, threshold1=50, threshold2=200)
# plt.subplot(223)
# plt.title("edges")
# plt.imshow(edges, cmap='gray')

<matplotlib.image.AxesImage at 0x7f897e801250>
import math
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
road_rgb = cv.cvtColor(cv.imread("img/road-lanes.jpg", cv.IMREAD_COLOR), cv.COLOR_BGR2RGB)

road_gray = cv.imread("img/road-lanes.jpg", cv.IMREAD_GRAYSCALE)

_, road_bin = cv.threshold(road_gray, 210, 255, cv.THRESH_BINARY)

rows,cols = road_bin.shape

mask = np.zeros((rows,cols), np.uint8)
pts = np.array([[400,350],[cols-400,350],[cols,rows],[0,rows]], np.int32)
cv.fillPoly(mask, [pts], (255,255,255))

masked_road = cv.bitwise_and(road_bin, road_bin, mask=mask)

edges = cv.Canny(masked_road, 50, 200)

lines = cv.HoughLinesP(masked_road, 1, np.pi / 180, 50, None, 50, 300)

# Draw the lines
if lines is not None:
    for i in range(0, len(lines)):
        l = lines[i][0]
        cv.line(road_rgb, (l[0], l[1]), (l[2], l[3]), (255,0,0), 3, cv.LINE_AA)

plt.imshow(road_rgb, cmap="gray")
<matplotlib.image.AxesImage at 0x7f07f864d130>

Pasy wykryte na drodze