Introducere în procesarea imaginilor Python în fotografia computațională
Publicat: 2022-03-11Fotografia computațională se referă la îmbunătățirea procesului fotografic prin calcul. Deși, în mod normal, avem tendința de a crede că acest lucru se aplică numai post-procesării rezultatului final (similar cu editarea foto), posibilitățile sunt mult mai bogate, deoarece calculul poate fi activat la fiecare pas al procesului fotografic - începând cu iluminarea scenei, continuând cu obiectivul și, eventual, chiar și la afișarea imaginii capturate.
Acest lucru este important deoarece permite să faceți mult mai mult și în moduri diferite decât ceea ce se poate realiza cu o cameră obișnuită. De asemenea, este important pentru că cel mai răspândit tip de cameră în zilele noastre - camera mobilă - nu este deosebit de puternică în comparație cu fratele său mai mare (DSLR-ul), dar reușește să facă o treabă bună valorificând puterea de calcul disponibilă pe dispozitiv. .
Vom arunca o privire la două exemple în care calculul poate îmbunătăți fotografia — mai precis, vom vedea cum pur și simplu luarea mai multor fotografii și utilizarea unui pic de Python pentru a le combina poate crea rezultate frumoase în două situații în care hardware-ul camerei mobile nu funcționează. strălucește cu adevărat — lumină scăzută și gamă dinamică ridicată.
Fotografie cu lumină scăzută
Să presupunem că vrem să facem o fotografie cu lumină scăzută a unei scene, dar camera are o deschidere mică (obiectiv) și un timp de expunere limitat. Aceasta este o situație tipică pentru camerele telefoanelor mobile care, având în vedere o scenă de lumină scăzută, ar putea produce o imagine ca aceasta (făcută cu o cameră iPhone 6):
Dacă încercăm să îmbunătățim contrastul, rezultatul este următorul, care este și destul de rău:
Ce se întâmplă? De unde vine tot acest zgomot?
Răspunsul este că zgomotul vine de la senzor - dispozitivul care încearcă să determine când lumina îl lovește și cât de intensă este acea lumină. În condiții de lumină slabă, totuși, trebuie să-și crească sensibilitatea cu mult pentru a înregistra orice, iar această sensibilitate ridicată înseamnă că începe și să detecteze false pozitive - fotoni care pur și simplu nu sunt acolo. (Ca o notă secundară, această problemă nu ne afectează doar dispozitivele, ci și pe noi oamenii: data viitoare când vă aflați într-o cameră întunecată, luați un moment pentru a observa zgomotul prezent în câmpul vizual.)
O anumită cantitate de zgomot va fi întotdeauna prezentă într-un dispozitiv de imagistică; totuși, dacă semnalul (informații utile) are intensitate mare, zgomotul va fi neglijabil (raport semnal/zgomot ridicat). Când semnalul este scăzut, cum ar fi în lumină slabă, zgomotul va ieși în evidență (semnal scăzut la zgomot).
Totuși, putem depăși problema zgomotului, chiar și cu toate limitările camerei, pentru a obține fotografii mai bune decât cele de mai sus.
Pentru a face asta, trebuie să luăm în considerare ce se întâmplă în timp: semnalul va rămâne același (aceeași scenă și presupunem că este static), în timp ce zgomotul va fi complet aleatoriu. Aceasta înseamnă că, dacă facem multe fotografii ale scenei, acestea vor avea versiuni diferite ale zgomotului, dar aceleași informații utile.
Deci, dacă facem o medie a multor imagini luate de-a lungul timpului, zgomotul se va anula, în timp ce semnalul nu va fi afectat.
Următoarea ilustrație prezintă un exemplu simplificat: Avem un semnal (triunghi) afectat de zgomot și încercăm să recuperăm semnalul făcând o medie a mai multor instanțe ale aceluiași semnal afectat de zgomot diferit.
Vedem că, deși zgomotul este suficient de puternic pentru a distorsiona complet semnalul în orice caz, medierea reduce progresiv zgomotul și recuperăm semnalul inițial.
Să vedem cum se aplică acest principiu imaginilor: În primul rând, trebuie să facem mai multe fotografii ale subiectului cu expunerea maximă pe care o permite camera. Pentru cele mai bune rezultate, utilizați o aplicație care permite fotografierea manuală. Este important ca fotografiile să fie făcute din aceeași locație, așa că un trepied (improvizat) va ajuta.
Mai multe fotografii vor însemna, în general, o calitate mai bună, dar numărul exact depinde de situație: cât de multă lumină există, cât de sensibilă este camera, etc. O rază bună poate fi între 10 și 100.
Odată ce avem aceste imagini (în format brut dacă este posibil), le putem citi și procesa în Python.
Pentru cei care nu sunt familiarizați cu procesarea imaginilor în Python, ar trebui să menționăm că o imagine este reprezentată ca o matrice 2D de valori de octeți (0-255) - adică pentru o imagine monocromă sau în tonuri de gri. O imagine color poate fi gândită ca un set de trei astfel de imagini, una pentru fiecare canal de culoare (R, G, B), sau efectiv o matrice 3D indexată după poziție verticală, poziție orizontală și canal de culoare (0, 1, 2) .
Vom folosi două biblioteci: NumPy (http://www.numpy.org/) și OpenCV (https://opencv.org/). Primul ne permite să efectuăm calcule pe matrice foarte eficient (cu cod surprinzător de scurt), în timp ce OpenCV se ocupă de citirea/scrierea fișierelor de imagine în acest caz, dar este mult mai capabil, oferind multe proceduri grafice avansate - dintre care unele vom utilizați mai târziu în articol.
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)
Rezultatul (cu auto-contrast aplicat) arată că zgomotul a dispărut, o îmbunătățire foarte mare față de imaginea originală.
Cu toate acestea, observăm în continuare câteva artefacte ciudate, cum ar fi cadrul verzui și modelul de tip grilă. De data aceasta, nu este un zgomot aleatoriu, ci un zgomot de tip fix. Ce s-a întâmplat?
Din nou, putem da vina pe senzor. În acest caz, vedem că diferite părți ale senzorului reacționează diferit la lumină, rezultând un model vizibil. Unele elemente ale acestor modele sunt regulate și sunt cel mai probabil legate de substratul senzorului (metal/siliciu) și de modul în care acesta reflectă/absoarbe fotonii care intră. Alte elemente, cum ar fi pixelul alb, sunt pur și simplu pixeli senzori defecte, care pot fi prea sensibili sau prea insensibili la lumină.
Din fericire, există o modalitate de a scăpa și de acest tip de zgomot. Se numește scădere în cadrul întunecat.
Pentru a face acest lucru, avem nevoie de o imagine a zgomotului modelului în sine, iar aceasta poate fi obținută dacă fotografiem întunericul. Da, așa este, doar acoperiți orificiul camerei și faceți o mulțime de fotografii (să zicem 100) cu timpul maxim de expunere și valoarea ISO și procesați-le așa cum este descris mai sus.
Când facem o medie pe multe cadre negre (care nu sunt de fapt negre, din cauza zgomotului aleatoriu) vom ajunge cu zgomotul de tip fix. Putem presupune că acest zgomot fix va rămâne constant, așa că acest pas este necesar o singură dată: imaginea rezultată poate fi reutilizată pentru toate fotografiile viitoare cu lumină scăzută.
Iată cum arată partea din dreapta sus a zgomotului modelului (ajustat de contrast) pentru un iPhone 6:
Din nou, observăm textura asemănătoare grilei și chiar ceea ce pare a fi un pixel alb blocat.

Odată ce avem valoarea acestui zgomot de cadru întunecat (în variabila average_noise
), o putem scădea pur și simplu din fotografia noastră până acum, înainte de a normaliza:
average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)
Iată fotografia noastră finală:
Interval dinamic ridicat
O altă limitare pe care o are o cameră mică (mobilă) este intervalul său dinamic mic, adică intervalul de intensități luminoase la care poate capta detalii este destul de mic.
Cu alte cuvinte, camera este capabilă să surprindă doar o bandă îngustă a intensităților luminii dintr-o scenă; intensitățile de sub acea bandă apar ca negru pur, în timp ce intensitățile de deasupra acesteia apar ca alb pur și orice detalii se pierd din acele regiuni.
Cu toate acestea, există un truc pe care camera (sau fotograful) îl poate folosi - și anume ajustarea timpului de expunere (timpul în care senzorul este expus la lumină) pentru a controla cantitatea totală de lumină care ajunge la senzor, în mod eficient. deplasarea intervalului în sus sau în jos pentru a capta intervalul cel mai potrivit pentru o anumită scenă.
Dar acesta este un compromis. Multe detalii nu reușesc să ajungă în fotografia finală. În cele două imagini de mai jos, vedem aceeași scenă surprinsă cu timpi de expunere diferiți: o expunere foarte scurtă (1/1000 sec), o expunere medie (1/50 sec) și o expunere lungă (1/4 sec).
După cum puteți vedea, niciuna dintre cele trei imagini nu este capabilă să surprindă toate detaliile disponibile: filamentul lămpii este vizibil doar în prima fotografie, iar unele dintre detaliile florilor sunt vizibile fie în mijloc, fie în ultima fotografie, dar nu. ambii.
Vestea bună este că putem face ceva în privința asta și, din nou, implică construirea pe mai multe fotografii cu un pic de cod Python.
Abordarea pe care o vom lua se bazează pe munca lui Paul Debevec și colab., care descrie metoda în lucrarea sa aici. Metoda funcționează astfel:
În primul rând, necesită mai multe fotografii ale aceleiași scene (staționare), dar cu timpi de expunere diferiți. Din nou, ca și în cazul precedent, avem nevoie de un trepied sau un suport pentru a ne asigura că camera nu se mișcă deloc. Avem nevoie și de o aplicație de fotografiere manuală (dacă folosim un telefon) pentru a putea controla timpul de expunere și a preveni ajustările automate ale camerei. Numărul de fotografii necesare depinde de intervalul de intensități prezente în imagine (de la trei în sus), iar timpii de expunere ar trebui să fie distanțați în intervalul respectiv, astfel încât detaliile pe care ne interesează să le păstrăm să apară clar în cel puțin o fotografie.
Apoi, se folosește un algoritm pentru a reconstrui curba de răspuns a camerei pe baza culorii acelorași pixeli de-a lungul diferiților timpi de expunere. Acest lucru ne permite practic să stabilim o hartă între luminozitatea reală a scenei a unui punct, timpul de expunere și valoarea pe care pixelul corespunzător o va avea în imaginea capturată. Vom folosi implementarea metodei Debevec din biblioteca 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)
Curba de răspuns arată cam așa:
Pe axa verticală, avem efectul cumulativ al luminozității scenei a unui punct și al timpului de expunere, în timp ce pe axa orizontală avem valoarea (de la 0 la 255 pe canal) pe care o va avea pixelul corespunzător.
Această curbă ne permite să efectuăm operația inversă (care este următorul pas al procesului) - având în vedere valoarea pixelilor și timpul de expunere, putem calcula luminozitatea reală a fiecărui punct din scenă. Această valoare a luminozității se numește iradiere și măsoară cantitatea de energie luminoasă care cade pe o unitate de suprafață a senzorului. Spre deosebire de datele imaginii, acestea sunt reprezentate folosind numere în virgulă mobilă deoarece reflectă o gamă mult mai largă de valori (deci, interval dinamic ridicat). Odată ce avem imaginea de iradiere (imaginea HDR), o putem salva pur și simplu:
# Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)
Pentru aceia dintre noi suficient de norocoși să aibă un afișaj HDR (care devine din ce în ce mai obișnuit), poate fi posibil să vizualizați această imagine direct în toată splendoarea ei. Din păcate, standardele HDR sunt încă la început, așa că procesul de a face asta poate fi oarecum diferit pentru diferite afișaje.
Pentru noi ceilalți, vestea bună este că încă putem profita de aceste date, deși un afișaj normal necesită ca imaginea să aibă canale cu valoare de octet (0-255). Deși trebuie să renunțăm la o parte din bogăția hărții de iradiere, cel puțin avem controlul asupra modului în care o facem.
Acest proces se numește tone-mapping și implică conversia hărții de iradiere în virgulă mobilă (cu o gamă mare de valori) într-o imagine cu valoare standard de octet. Există tehnici pentru a face asta, astfel încât multe dintre detaliile suplimentare să fie păstrate. Doar pentru a vă oferi un exemplu despre cum poate funcționa acest lucru, imaginați-vă că înainte de a strânge intervalul cu virgulă mobilă în valori de octeți, îmbunătățim (ascuțim) marginile care sunt prezente în imaginea HDR. Îmbunătățirea acestor margini va ajuta la păstrarea lor (și implicit a detaliilor pe care le oferă) și în imaginea cu interval dinamic scăzut.
OpenCV oferă un set de acești operatori de cartografiere a tonurilor, cum ar fi Drago, Durand, Mantiuk sau Reinhardt. Iată un exemplu despre cum poate fi folosit unul dintre acești operatori (Durand) și despre rezultatul pe care îl produce.
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)
Folosind Python, vă puteți crea și proprii operatori dacă aveți nevoie de mai mult control asupra procesului. De exemplu, acesta este rezultatul obținut cu un operator personalizat care elimină intensitățile care sunt reprezentate în foarte puțini pixeli înainte de a micșora intervalul de valori la 8 biți (urmat de un pas de auto-contrast):
Și iată codul pentru operatorul de mai sus:
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)
Concluzie
Am văzut cum, cu un pic de Python și câteva biblioteci de sprijin, putem depăși limitele camerei fizice pentru a îmbunătăți rezultatul final. Ambele exemple pe care le-am discutat folosesc mai multe fotografii de calitate scăzută pentru a crea ceva mai bun, dar există multe alte abordări pentru diferite probleme și limitări.
În timp ce multe telefoane cu cameră au aplicații stocate sau încorporate care abordează aceste exemple particulare, în mod clar nu este deloc dificil să le programați manual și să vă bucurați de nivelul mai înalt de control și înțelegere care poate fi obținut.
Dacă sunteți interesat de calculele de imagini pe un dispozitiv mobil, consultați Tutorialul OpenCV: Detectarea obiectelor în timp real utilizând MSER în iOS de către colegul Toptaler și dezvoltator de elită OpenCV Altaibayar Tseveenbayar.