Viděli jste ducha?
Já jsem vždycky tak nějak podvědomě cítil, že zpracování videa je moc složité a moc pomalé. A že tím pádem nemá smysl snažit se dělat jej v Pythonu. Jenže když použijeme numpy a OpenCV, tak vlastně používáme C-čkové knihovny, a Python zde funguje jenom jako jakési lepidlo. Tak směle do toho.
Abych Vás nalákal, podívejte se na tohle video:
Kde vzít OpenCV
Instalace OpenCV (a numpy) není vždy úplně jednoduchá. Radím Vám
postupovat podle oficiálního návodu pro Váš operační systém. Pro Linuxy
bude pravděpodobně v repozitářích, a stačí nainstalovat. Např. pro debian
je to balíček python-opencv
. Pro Windows je návod na
stránkách OpenCV.
Hodně štěstí.
Kde vzít video
Kde vzít video na hraní? Jedna možnost (a tu Vám rozhodně pro začátek doporučuji) je nahrát si vlastní. Postavte si foťák na stativ, spusťte nahrávání, a projděte se po místnosti. Víc není potřeba.
Jestli ale hledáte nepřebernou studnici nápadů, zkuste YouTube spolu s balíčkem pafy. Pafy Vám umožní stáhnout video, které pak OpenCV použije. Např. takto:
import os.path
import cv2
import pafy
def main(url):
video = pafy.new(url)
best = video.getbest()
fn = best.generate_filename()
if not os.path.exists(fn):
print("Downloading %s" % fn)
best.download(filepath=fn)
else:
print("Using %s" % fn)
camera = cv2.VideoCapture(fn)
while True:
(grabbed, frame) = camera.read()
if not grabbed:
break
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"): # quit
break
if key == ord("p"): # pause
cv2.waitKey(0)
camera.release()
if "__main__" == __name__:
main("https://www.youtube.com/watch?v=rBDiIYaFEF0")
Teoreticky by měl OpenCV umět přehrát video rovnou z URL, takto:
url = best.url
camera = cv2.VideoCapture(url)
ale to mi pro YouTube videa nefunguje.
Laciné triky
Představme si situaci: Máme video natáčené statickou kamerou s poměrně stabilními světelnými podmínkami. Chceme zjistit, co se na videu pohybuje. Vezmeme tedy 2 po sobě jdoucí snímky, a odečteme. Místa, na kterých je nula, jsou na obou obrázcích stejná, tedy nehybná. Tedy možná pozadí. Místa, na kterých není nula, nejsou stejná, tudíž se možná pohybovala, takže to není pozadí. Zkusíme si to takhle zobrazit:
camera = cv2.VideoCapture(fn)
prev = None
while True:
(grabbed, frame) = camera.read()
if not grabbed:
break
if prev is None:
prev = frame
continue
gray0 = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
gray1 = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
diff = gray0 - gray1
cv2.imshow("Frame", diff)
if ord("q") == cv2.waitKey(1) & 0xFF:
break
prev = frame
camera.release()
A výsledek:
Což samozřejmně není ono z jednoho prostého důvodu. gray0
a gray1
jsou
numpy array
a jejich hodnoty jsou neznaménkové byte. Budou se normálně odčítat
s přetečením, takže 127 - 128 je 255. Nejlepší je použít místo takového odečtení
funkci cv2.absdiff
, které provede odečtení se znaménkem, a do výsledku uloží
absolutní hodnotu.
# ...
gray0 = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
gray1 = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
diff = cv2.absdiff(gray1, gray0)
cv2.imshow("Frame", diff)
# ...
Tohle už by šlo:
Až na to, že tam vidíme bílé plošky, na kterých byla absolutní hodnota rozdílu nízká. Zbavíme se jich takto:
# ...
diff = cv2.absdiff(gray1, gray0)
diff = cv2.threshold(diff, 35, 0, cv2.THRESH_TOZERO)[1]
cv2.imshow("Frame", diff)
# ...
A teď konečně duch
To, co vidíme na předchozím obrázku je pouhý obrys pohybující se části. Aby to aspoň vzdáleně připomínalo ducha, musíme to přidat do původního obrázku. Bude to ale pořád jen obrys. Takže si schováme několik těch obrysů, a do původního obrázku přidáme ne ten nejnovější, ale několik starších. Navíc, ty starší trošku budeme postupně víc a víc ztmavovat. Asi takto:
import os.path
import time
import cv2
import pafy
class AddressableQueue(object):
"""
Creates a data structure that will hold some number of records.
A newly added record is always put on the position 0, the previusly
first record will become second, second will become third, and so
on. The structure only holds some defined number of records.
"""
def __init__(self, max_records=20):
self.max_records = max_records
self.data = []
def add(self, i):
self.data.append(i)
if len(self.data) > 2*self.max_records:
del self.data[0:self.max_records]
def get_at_or_first(self, idx):
"""
Returns the idx-th record, or the first record if there is
nothing on that index.
"""
if idx >= len(self.data):
idx = 0
return self.data[-idx-1]
def main(url):
video = pafy.new(url)
best = video.getbest()
fn = best.generate_filename()
if not os.path.exists(fn):
print("Downloading %s" % fn)
best.download(filepath=fn)
else:
print("Using %s" % fn)
camera = cv2.VideoCapture(fn)
prev = None
BASE = 10
FRAME_COUNT = 10
pic_queue = AddressableQueue(max_records=FRAME_COUNT+BASE)
(grabbed, frame) = camera.read()
prev = frame
while grabbed:
gray0 = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
gray1 = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
diff = cv2.absdiff(gray1, gray0)
diff = cv2.threshold(diff, 35, 0, cv2.THRESH_TOZERO)[1]
pic_queue.add(diff)
out = frame
for i in range(0, FRAME_COUNT):
white = pic_queue.get_at_or_first(BASE + i)
white = cv2.cvtColor(white, cv2.COLOR_GRAY2RGB)
out = cv2.addWeighted(out, 1.0, white, 1.0/(1 + i), 0)
cv2.imshow("Frame", out)
if ord("q") == cv2.waitKey(1) & 0xFF:
break
prev = frame
(grabbed, frame) = camera.read()
time.sleep(0.01)
camera.release()
if "__main__" == __name__:
main("https://www.youtube.com/watch?v=rBDiIYaFEF0")
AddressableQueue
je taková jednoduchá fronta, která uchovává
maximálně předem určený počet prvků. V případě, že nově přidaný by
překročil ten maximální počet, tak zahodí nejstarší. Do ní (do
instance pic_queue
) si budeme ukládat spočtené diff
y.
Začátek cyklu pak probíhá opět stejně. Načteme obrázek z videa,
převedeme na odstíny šedi, odečteme od něj předchozí, uděláme
threshold
. Výsledek si přidáme do fronty pic_queue
.
Pak vezmeme snímky 10-20 snímků zpět, a z nich uděláme ducha.
Pomocí addWeighted
smícháme 2 obrázky. Druhý a čtvrtý parametr
je váha jednotlivých obrázků, pátý parametr je číslo, které se
ještě přičte k výsledku.
Co dál? Zkuste si pohrát. Třeba můžete změnit BASE = 30
,
nebo měnit váhu obrázků v addWeighted
.
Závěr
Všiměte si, že tady vlastně provedeme 24 operací s rastrem obrázku. Jednoduchých operací, ale stejně, jsou to operace, které zpracují celou matici, v tomto případě o velikosti 310x240px, a video je rychlejší, než v reálu.
Zde je skript, který vygeneruje přesně to video ze začátku.
Jako zdroj jsem použil video ITF Taekwon Do Patterns | Do San od Riverina Traditional Taekwondo zveřejněné pod licencí Creative Commons.
Komentáře byly zrušeny
V EU teď máme složitou situaci s Cookies. Na komentáře jsem používal jistou službu třetí strany. Ta však používá Cookies poměrně, ehm, benevolentně. Tak jsem se rozhodl komentáře zrušit. Pokud chcete, můžete mi napsat přímo