Wprowadzenie do przetwarzania obrazów w Pythonie w fotografii obliczeniowej
Opublikowany: 2022-03-11Fotografia obliczeniowa polega na wzbogaceniu procesu fotograficznego o obliczenia. Chociaż zwykle myślimy, że dotyczy to tylko przetwarzania końcowego wyniku końcowego (podobnie jak w przypadku edycji zdjęć), możliwości są znacznie bogatsze, ponieważ obliczenia można włączyć na każdym etapie procesu fotograficznego — zaczynając od oświetlenia sceny, kontynuując obiektyw, a ostatecznie nawet przy wyświetlaniu przechwyconego obrazu.
To ważne, bo pozwala na zrobienie znacznie więcej i na inne sposoby niż to, co można osiągnąć zwykłym aparatem. Jest to również ważne, ponieważ najpopularniejszy obecnie rodzaj aparatu — aparat mobilny — nie jest szczególnie wydajny w porównaniu z jego większym bratem (DSLR), ale udaje mu się wykonać dobrą robotę, wykorzystując moc obliczeniową, jaką ma do dyspozycji w urządzeniu .
Przyjrzymy się dwóm przykładom, w których obliczenia mogą ulepszyć fotografię — dokładniej, zobaczymy, jak proste zrobienie większej liczby zdjęć i użycie odrobiny Pythona do ich połączenia może dać dobre wyniki w dwóch sytuacjach, w których sprzęt mobilny nie jest naprawdę błyszczą — słabe oświetlenie i wysoki zakres dynamiki.
Fotografia w słabym świetle
Powiedzmy, że chcemy zrobić zdjęcie sceny w słabym świetle, ale aparat ma małą przysłonę (obiektyw) i ograniczony czas naświetlania. Jest to typowa sytuacja dla aparatów w telefonach komórkowych, które przy słabym oświetleniu mogą wytworzyć taki obraz (zrobiony aparatem iPhone'a 6):
Jeśli spróbujemy poprawić kontrast, wynik jest następujący, co również jest dość złe:
Co się dzieje? Skąd bierze się cały ten hałas?
Odpowiedź brzmi: szum pochodzi z czujnika — urządzenia, które próbuje określić, kiedy pada na niego światło i jak intensywne jest to światło. Jednak w słabym świetle musi znacznie zwiększyć swoją czułość, aby cokolwiek zarejestrować, a ta wysoka czułość oznacza, że zaczyna również wykrywać fałszywe alarmy — fotony, których po prostu nie ma. (Na marginesie, ten problem dotyczy nie tylko urządzeń, ale także nas, ludzi: następnym razem, gdy będziesz w ciemnym pokoju, poświęć chwilę, aby zauważyć hałas w polu widzenia.)
Pewna ilość szumu zawsze będzie obecna w urządzeniu do obrazowania; jeśli jednak sygnał (użyteczna informacja) ma duże natężenie, szum będzie znikomy (wysoki stosunek sygnału do szumu). Gdy sygnał jest niski — na przykład przy słabym oświetleniu — szum będzie się wyróżniał (niski sygnał do szumu).
Mimo to możemy przezwyciężyć problem z hałasem, nawet przy wszystkich ograniczeniach aparatu, aby uzyskać lepsze zdjęcia niż to powyżej.
Aby to zrobić, musimy wziąć pod uwagę to, co dzieje się w czasie: sygnał pozostanie ten sam (ta sama scena i zakładamy, że jest statyczna), a szum będzie całkowicie losowy. Oznacza to, że jeśli wykonamy wiele ujęć sceny, będą miały różne wersje szumu, ale te same przydatne informacje.
Tak więc, jeśli uśrednimy wiele zdjęć zrobionych w czasie, szumy znikną, a sygnał pozostanie nienaruszony.
Poniższa ilustracja przedstawia uproszczony przykład: Mamy sygnał (trójkąt) dotknięty szumem i próbujemy go odzyskać, uśredniając wiele wystąpień tego samego sygnału z różnymi szumami.
Widzimy, że chociaż szum jest wystarczająco silny, aby całkowicie zniekształcić sygnał w dowolnym pojedynczym przypadku, uśrednianie stopniowo zmniejsza szum i odzyskujemy oryginalny sygnał.
Zobaczmy, jak ta zasada odnosi się do zdjęć: Po pierwsze, musimy wykonać wiele zdjęć obiektu z maksymalną ekspozycją, na jaką pozwala aparat. Aby uzyskać najlepsze wyniki, użyj aplikacji, która umożliwia ręczne fotografowanie. Ważne jest, aby zdjęcia były robione z tego samego miejsca, więc pomoże (improwizowany) statyw.
Więcej zdjęć ogólnie oznacza lepszą jakość, ale dokładna liczba zależy od sytuacji: ile światła, jak czuły jest aparat itp. Dobry zasięg może wynosić od 10 do 100.
Gdy już mamy te obrazy (jeśli to możliwe, w formacie surowym), możemy je czytać i przetwarzać w Pythonie.
Dla tych, którzy nie są zaznajomieni z przetwarzaniem obrazów w Pythonie, powinniśmy wspomnieć, że obraz jest reprezentowany jako tablica 2D wartości bajtów (0-255) — to znaczy dla obrazu monochromatycznego lub w skali szarości. Obraz kolorowy można traktować jako zestaw trzech takich obrazów, po jednym dla każdego kanału koloru (R, G, B) lub w rzeczywistości tablicę 3D indeksowaną według położenia pionowego, położenia poziomego i kanału kolorów (0, 1, 2) .
Wykorzystamy dwie biblioteki: NumPy (http://www.numpy.org/) i OpenCV (https://opencv.org/). Pierwszy pozwala nam na bardzo efektywne wykonywanie obliczeń na tablicach (z zaskakująco krótkim kodem), podczas gdy OpenCV obsługuje w tym przypadku odczyt/zapis plików graficznych, ale jest o wiele bardziej wydajny, zapewniając wiele zaawansowanych procedur graficznych — niektóre z nich będziemy użyj w dalszej części artykułu.
import os import numpy as np import cv2 folder = 'source_folder' # We get all the image files from the source folder files = list([os.path.join(folder, f) for f in os.listdir(folder)]) # We compute the average by adding up the images # Start from an explicitly set as floating point, in order to force the # conversion of the 8-bit values from the images, which would otherwise overflow average = cv2.imread(files[0]).astype(np.float) for file in files[1:]: image = cv2.imread(file) # NumPy adds two images element wise, so pixel by pixel / channel by channel average += image # Divide by count (again each pixel/channel is divided) average /= len(files) # Normalize the image, to spread the pixel intensities across 0..255 # This will brighten the image without losing information output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) # Save the output cv2.imwrite('output.png', output)
Wynik (z zastosowanym automatycznym kontrastem) pokazuje, że szum zniknął, bardzo duża poprawa w stosunku do oryginalnego obrazu.
Jednak nadal zauważamy dziwne artefakty, takie jak zielonkawa ramka i wzór przypominający siatkę. Tym razem nie jest to przypadkowy szum, ale stały wzór. Co się stało?
Znowu możemy winić za to czujnik. W tym przypadku widzimy, że różne części czujnika inaczej reagują na światło, co daje widoczny wzór. Niektóre elementy tych wzorów są regularne i najprawdopodobniej są związane z podłożem czujnika (metal/krzem) i tym, jak odbija/pochłania nadchodzące fotony. Inne elementy, takie jak biały piksel, to po prostu uszkodzone piksele czujnika, które mogą być nadmiernie wrażliwe lub nadmiernie niewrażliwe na światło.
Na szczęście jest też sposób na pozbycie się tego typu hałasu. Nazywa się to odejmowaniem ciemnej klatki.
Aby to zrobić, potrzebujemy obrazu samego szumu wzoru, który można uzyskać, fotografując ciemność. Tak, zgadza się — po prostu zakryj otwór aparatu i zrób dużo zdjęć (powiedzmy 100) z maksymalnym czasem naświetlania i wartością ISO, a następnie przetwórz je tak, jak opisano powyżej.
Podczas uśredniania wielu czarnych ramek (które w rzeczywistości nie są czarne z powodu losowego szumu) otrzymamy stały szum wzorcowy. Możemy założyć, że ten stały szum pozostanie stały, więc ten krok jest potrzebny tylko raz: wynikowy obraz można ponownie wykorzystać do wszystkich przyszłych ujęć przy słabym oświetleniu.
Oto jak wygląda prawa górna część szumu wzorca (dostosowany kontrast) dla iPhone'a 6:
Ponownie zauważamy teksturę przypominającą siatkę, a nawet coś, co wydaje się być zablokowanym białym pikselem.
Gdy mamy już wartość tego szumu ciemnej klatki (w zmiennej average_noise
), możemy po prostu odjąć ją od naszego dotychczasowego ujęcia, przed normalizacją:

average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)
Oto nasze ostatnie zdjęcie:
Wysoki zakres dynamiki
Innym ograniczeniem małego (mobilnego) aparatu jest jego mały zakres dynamiczny, co oznacza, że zakres natężenia światła, przy którym może uchwycić szczegóły, jest raczej niewielki.
Innymi słowy, kamera jest w stanie uchwycić tylko wąski zakres natężenia światła ze sceny; intensywności poniżej tego pasma pojawiają się jako czysta czerń, podczas gdy intensywności powyżej tego pasma pojawiają się jako czysta biel, a wszelkie szczegóły są tracone z tych obszarów.
Istnieje jednak sztuczka, z której może skorzystać aparat (lub fotograf), a mianowicie dostosowanie czasu ekspozycji (czasu, w którym czujnik jest wystawiony na działanie światła), aby skutecznie kontrolować całkowitą ilość światła docierającego do czujnika przesunięcie zakresu w górę lub w dół w celu uchwycenia najbardziej odpowiedniego zakresu dla danej sceny.
Ale to jest kompromis. Wiele szczegółów nie mieści się w finalnym zdjęciu. Na dwóch poniższych obrazach widzimy tę samą scenę uchwyconą z różnymi czasami naświetlania: bardzo krótką ekspozycję (1/1000 s), średnią ekspozycję (1/50 s) i długą ekspozycję (1/4 s).
Jak widać, żaden z trzech obrazów nie jest w stanie uchwycić wszystkich dostępnych szczegółów: żarnik lampy jest widoczny tylko w pierwszym ujęciu, a niektóre szczegóły kwiatu są widoczne w środku lub w ostatnim ujęciu, ale nie Zarówno.
Dobrą wiadomością jest to, że jest coś, co możemy z tym zrobić, i znowu wiąże się to z tworzeniem wielu ujęć z odrobiną kodu Pythona.
Podejście, które przyjmiemy, opiera się na pracy Paula Debevec i in., który opisuje metodę w swoim artykule tutaj. Metoda działa tak:
Po pierwsze, wymaga wielu ujęć tej samej sceny (stacjonarnej), ale z różnymi czasami naświetlania. Ponownie, tak jak w poprzednim przypadku, potrzebujemy statywu lub podpórki, aby aparat w ogóle się nie poruszył. Potrzebujemy również aplikacji do ręcznego robienia zdjęć (w przypadku korzystania z telefonu), abyśmy mogli kontrolować czas naświetlania i zapobiegać autoregulacji aparatu. Liczba wymaganych ujęć zależy od zakresu intensywności występujących na zdjęciu (od trzech wzwyż), a czasy ekspozycji powinny być rozłożone w tym zakresie, aby szczegóły, które chcemy zachować, były wyraźnie widoczne przynajmniej na jednym zdjęciu.
Następnie algorytm jest używany do rekonstrukcji krzywej reakcji kamery na podstawie koloru tych samych pikseli w różnych czasach ekspozycji. To w zasadzie pozwala nam ustalić mapę między rzeczywistą jasnością sceny punktu, czasem ekspozycji i wartością, jaką odpowiedni piksel będzie miał na przechwyconym obrazie. Wykorzystamy implementację metody Debevec z biblioteki OpenCV.
# Read all the files with OpenCV files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] images = list([cv2.imread(f) for f in files]) # Compute the exposure times in seconds exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]]) # Compute the response curve calibration = cv2.createCalibrateDebevec() response = calibration.process(images, exposures)
Krzywa odpowiedzi wygląda mniej więcej tak:
Na osi pionowej mamy skumulowany efekt jasności sceny punktu i czasu naświetlania, natomiast na osi poziomej mamy wartość (0 do 255 na kanał), którą będzie miał odpowiedni piksel.
Ta krzywa pozwala nam wykonać operację odwrotną (która jest kolejnym krokiem w procesie) — mając wartość piksela i czas ekspozycji, możemy obliczyć rzeczywistą jasność każdego punktu na scenie. Ta wartość jasności nazywana jest irradiancją i mierzy ilość energii świetlnej, która pada na jednostkę powierzchni czujnika. W przeciwieństwie do danych obrazu jest reprezentowany za pomocą liczb zmiennoprzecinkowych, ponieważ odzwierciedla znacznie szerszy zakres wartości (stąd wysoki zakres dynamiczny). Gdy mamy już obraz natężenia promieniowania (obraz HDR), możemy go po prostu zapisać:
# Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)
Dla tych z nas, którzy mają szczęście, że mają wyświetlacz HDR (co jest coraz bardziej powszechne), możliwe będzie zwizualizowanie tego obrazu bezpośrednio w całej okazałości. Niestety, standardy HDR wciąż są w powijakach, więc proces ich wykonywania może być nieco inny dla różnych wyświetlaczy.
Dla reszty z nas dobrą wiadomością jest to, że nadal możemy korzystać z tych danych, chociaż normalne wyświetlanie wymaga, aby obraz miał wartość bajtów (0-255) kanałów. Chociaż musimy zrezygnować z części bogactwa mapy irradiancji, przynajmniej mamy kontrolę nad tym, jak to zrobić.
Proces ten nazywa się mapowaniem tonów i polega na przekształceniu zmiennoprzecinkowej mapy natężenia promieniowania (z dużym zakresem wartości) na obraz o standardowej wartości bajtowej. Istnieją techniki, które pozwalają to zrobić, aby zachować wiele dodatkowych szczegółów. Aby dać ci przykład, jak to może działać, wyobraź sobie, że zanim skompresujemy zakres zmiennoprzecinkowy do wartości bajtów, uwydatniamy (wyostrzamy) krawędzie obecne na obrazie HDR. Wzmocnienie tych krawędzi pomoże je zachować (i pośrednio zapewniane przez nie szczegóły) również na obrazie o niskim zakresie dynamicznym.
OpenCV udostępnia zestaw tych operatorów mapowania tonów, takich jak Drago, Durand, Mantiuk czy Reinhardt. Oto przykład wykorzystania jednego z tych operatorów (Durand) i wyniku, jaki daje.
durand = cv2.createTonemapDurand(gamma=2.5) ldr = durand.process(hdr) # Tonemap operators create floating point images with values in the 0..1 range # This is why we multiply the image with 255 before saving cv2.imwrite('durand_image.png', ldr * 255)
Używając Pythona, możesz również tworzyć własne operatory, jeśli potrzebujesz większej kontroli nad procesem. Na przykład jest to wynik uzyskany za pomocą operatora niestandardowego, który usuwa intensywności reprezentowane w bardzo niewielu pikselach przed zmniejszeniem zakresu wartości do 8 bitów (po którym następuje krok automatycznego kontrastu):
A oto kod dla powyższego operatora:
def countTonemap(hdr, min_fraction=0.0005): counts, ranges = np.histogram(hdr, 256) min_count = min_fraction * hdr.size delta_range = ranges[1] - ranges[0] image = hdr.copy() for i in range(len(counts)): if counts[i] < min_count: image[image >= ranges[i + 1]] -= delta_range ranges -= delta_range return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)
Wniosek
Widzieliśmy, jak z odrobiną Pythona i kilkoma bibliotekami obsługującymi, możemy przesuwać granice fizycznej kamery, aby poprawić efekt końcowy. Oba omówione przez nas przykłady wykorzystują wiele ujęć niskiej jakości, aby stworzyć coś lepszego, ale istnieje wiele innych podejść do różnych problemów i ograniczeń.
Podczas gdy wiele telefonów z aparatami ma wbudowane lub sklepowe aplikacje, które odnoszą się do tych konkretnych przykładów, wyraźnie nie jest trudno je zaprogramować ręcznie i cieszyć się wyższym poziomem kontroli i zrozumienia, który można uzyskać.
Jeśli interesują Cię obliczenia obrazów na urządzeniu mobilnym, zapoznaj się z samouczkiem OpenCV: Wykrywanie obiektów w czasie rzeczywistym za pomocą MSER w iOS autorstwa innego Toptalera i elitarnego programisty OpenCV, Altaibayara Tseveenbayara.