Atraskite aukštos kokybės vaizdo transliacijas naršyklėje. Išmokite įdiegti pažangų laikinąjį filtravimą triukšmo mažinimui naudojant WebCodecs API ir VideoFrame.
WebCodecs įvaldymas: vaizdo kokybės gerinimas taikant laikinąjį triukšmo mažinimą
Internetu pagrįstų vaizdo komunikacijos, transliavimo ir realaus laiko programų pasaulyje kokybė yra svarbiausia. Vartotojai visame pasaulyje tikisi ryškaus, aiškaus vaizdo, nesvarbu, ar jie dalyvauja verslo susitikime, stebi tiesioginį renginį, ar naudojasi nuotoline paslauga. Tačiau vaizdo transliacijas dažnai vargina nuolatinis ir blaškantis artefaktas: triukšmas. Šis skaitmeninis triukšmas, dažnai matomas kaip grūdėta ar statiška tekstūra, gali pabloginti žiūrėjimo patirtį ir, kas stebina, padidinti pralaidumo suvartojimą. Laimei, galinga naršyklės API, WebCodecs, suteikia kūrėjams precedento neturintį žemo lygio valdymą, leidžiantį tiesiogiai spręsti šią problemą.
Šis išsamus gidas leis jums giliai pasinerti į WebCodecs naudojimą konkrečiai, didelio poveikio vaizdo apdorojimo technikai: laikinajam triukšmo mažinimui. Išnagrinėsime, kas yra vaizdo triukšmas, kodėl jis žalingas ir kaip galite pasinaudoti VideoFrame
objektu, kad sukurtumėte filtravimo konvejerį tiesiogiai naršyklėje. Aptarsime viską – nuo pagrindinės teorijos iki praktinio JavaScript įgyvendinimo, našumo aspektų su WebAssembly ir pažangių koncepcijų, skirtų profesionalaus lygio rezultatams pasiekti.
Kas yra vaizdo triukšmas ir kodėl jis svarbus?
Prieš sprendžiant problemą, pirmiausia turime ją suprasti. Skaitmeniniame vaizde triukšmas reiškia atsitiktinius ryškumo ar spalvų informacijos svyravimus vaizdo signale. Tai nepageidaujamas šalutinis vaizdo fiksavimo ir perdavimo proceso produktas.
Triukšmo šaltiniai ir tipai
- Jutiklio triukšmas: Pagrindinis kaltininkas. Esant prastam apšvietimui, kameros jutikliai sustiprina gaunamą signalą, kad sukurtų pakankamai ryškų vaizdą. Šis stiprinimo procesas taip pat padidina atsitiktinius elektroninius svyravimus, dėl kurių atsiranda matomas grūdėtumas.
- Šiluminis triukšmas: Dėl kameros elektronikos generuojamos šilumos elektronai gali judėti atsitiktinai, sukurdami triukšmą, kuris nepriklauso nuo šviesos lygio.
- Kvantavimo triukšmas: Atsiranda analoginio-skaitmeninio konvertavimo ir glaudinimo procesų metu, kai ištisinės vertės priskiriamos ribotam diskrečiųjų lygių rinkiniui.
Šis triukšmas paprastai pasireiškia kaip Gauso triukšmas, kai kiekvieno pikselio intensyvumas atsitiktinai svyruoja aplink savo tikrąją vertę, sukuriant smulkų, mirgantį grūdėtumą visame kadre.
Dvejopas triukšmo poveikis
Vaizdo triukšmas yra daugiau nei tik kosmetinė problema; jis turi reikšmingų techninių ir suvokimo pasekmių:
- Pablogėjusi vartotojo patirtis: Akivaizdžiausias poveikis – vaizdo kokybei. Triukšmingas vaizdas atrodo neprofesionaliai, blaško dėmesį ir gali apsunkinti svarbių detalių įžvelgimą. Programose, tokiose kaip telekonferencijos, dalyviai gali atrodyti grūdėti ir neryškūs, o tai mažina buvimo jausmą.
- Sumažėjęs glaudinimo efektyvumas: Tai mažiau intuityvi, bet ne mažiau svarbi problema. Šiuolaikiniai vaizdo kodekai (pvz., H.264, VP9, AV1) pasiekia aukštus glaudinimo laipsnius išnaudodami perteklinę informaciją. Jie ieško panašumų tarp kadrų (laikinasis perteklius) ir viename kadre (erdvinis perteklius). Triukšmas savo prigimtimi yra atsitiktinis ir nenuspėjamas. Jis pažeidžia šiuos perteklinės informacijos dėsningumus. Koduotuvas atsitiktinį triukšmą mato kaip aukšto dažnio detalę, kurią būtina išsaugoti, ir yra priverstas skirti daugiau bitų triukšmui, o ne tikrajam turiniui koduoti. Dėl to gaunamas arba didesnis failas esant tai pačiai suvokiamai kokybei, arba prastesnė kokybė esant tam pačiam bitų srautui.
Pašalinę triukšmą prieš kodavimą, galime padaryti vaizdo signalą labiau nuspėjamu, leisdami koduotuvui dirbti efektyviau. Tai lemia geresnę vaizdo kokybę, mažesnį pralaidumo naudojimą ir sklandesnę transliavimo patirtį vartotojams visame pasaulyje.
Pristatome WebCodecs: žemo lygio vaizdo valdymo galia
Daugelį metų tiesioginis vaizdo manipuliavimas naršyklėje buvo ribotas. Kūrėjai dažniausiai apsiribojo <video>
elemento ir Canvas API galimybėmis, kurios dažnai reikalavo našumą mažinančių duomenų nuskaitymų iš GPU. WebCodecs visiškai keičia žaidimo taisykles.
WebCodecs yra žemo lygio API, suteikianti tiesioginę prieigą prie naršyklėje integruotų medijos koduotuvų ir dekoderių. Ji skirta programoms, kurioms reikalingas tikslus medijos apdorojimo valdymas, pavyzdžiui, vaizdo redaktoriams, debesų žaidimų platformoms ir pažangiems realaus laiko komunikacijos klientams.
Pagrindinis komponentas, į kurį sutelksime dėmesį, yra VideoFrame
objektas. VideoFrame
vaizduoja vieną vaizdo kadrą kaip paveikslėlį, tačiau tai yra daug daugiau nei paprastas rastrinis atvaizdas. Tai labai efektyvus, perduodamas objektas, galintis saugoti vaizdo duomenis įvairiais pikselių formatais (pvz., RGBA, I420, NV12) ir turintis svarbius metaduomenis, tokius kaip:
timestamp
: Kadro pateikimo laikas mikrosekundėmis.duration
: Kadro trukmė mikrosekundėmis.codedWidth
ircodedHeight
: Kadro matmenys pikseliais.format
: Duomenų pikselių formatas (pvz., 'I420', 'RGBA').
Svarbiausia, kad VideoFrame
suteikia metodą pavadinimu copyTo()
, kuris leidžia mums nukopijuoti neapdorotus, nesuglaudintus pikselių duomenis į ArrayBuffer
. Tai yra mūsų pradinis taškas analizei ir manipuliavimui. Kai turime neapdorotus baitus, galime taikyti savo triukšmo mažinimo algoritmą ir tada sukurti naują VideoFrame
iš modifikuotų duomenų, kad perduotume jį toliau apdorojimo konvejeriu (pvz., vaizdo koduotuvui ar į „canvas“).
Laikinojo filtravimo supratimas
Triukšmo mažinimo technikas galima plačiai suskirstyti į du tipus: erdvinį ir laikinąjį.
- Erdvinis filtravimas: Ši technika veikia su vienu kadru atskirai. Ji analizuoja ryšius tarp kaimyninių pikselių, siekdama nustatyti ir išlyginti triukšmą. Paprastas pavyzdys yra suliejimo (blur) filtras. Nors erdviniai filtrai efektyviai mažina triukšmą, jie taip pat gali sušvelninti svarbias detales ir kraštus, todėl vaizdas tampa ne toks ryškus.
- Laikinasis filtravimas: Tai sudėtingesnis metodas, į kurį mes sutelkiame dėmesį. Jis veikia su keliais kadrais per tam tikrą laiką. Pagrindinis principas yra tas, kad tikrasis scenos turinys greičiausiai bus susijęs tarp kadrų, o triukšmas yra atsitiktinis ir nesusijęs. Lyginant pikselio vertę tam tikroje vietoje per kelis kadrus, galime atskirti nuoseklų signalą (tikrąjį vaizdą) nuo atsitiktinių svyravimų (triukšmo).
Paprasčiausia laikinojo filtravimo forma yra laikinasis vidurkinimas. Įsivaizduokite, kad turite dabartinį kadrą ir ankstesnį kadrą. Bet kuriam pikseliui jo „tikroji“ vertė greičiausiai yra kažkur tarp jo vertės dabartiniame kadre ir ankstesniame. Sumaišydami juos, galime išvidurkinti atsitiktinį triukšmą. Naują pikselio vertę galima apskaičiuoti naudojant paprastą svertinį vidurkį:
naujas_pikselis = (alfa * dabartinis_pikselis) + ((1 - alfa) * ankstesnis_pikselis)
Čia alfa
yra maišymo koeficientas tarp 0 ir 1. Didesnė alfa
reikšmė reiškia, kad labiau pasitikime dabartiniu kadru, todėl triukšmo mažinimas yra silpnesnis, bet judesio artefaktų yra mažiau. Mažesnė alfa
reikšmė užtikrina stipresnį triukšmo mažinimą, bet gali sukelti „vaiduoklių“ efektą (ghosting) ar pėdsakus judančiose srityse. Svarbiausia rasti tinkamą balansą.
Paprasto laikinojo vidurkinimo filtro įgyvendinimas
Sukurkime praktinį šios koncepcijos įgyvendinimą naudojant WebCodecs. Mūsų konvejerį sudarys trys pagrindiniai žingsniai:
- Gauti
VideoFrame
objektų srautą (pvz., iš interneto kameros). - Kiekvienam kadrui taikyti mūsų laikinąjį filtrą, naudojant ankstesnio kadro duomenis.
- Sukurti naują, išvalytą
VideoFrame
.
1 žingsnis: kadrų srauto nustatymas
Lengviausias būdas gauti tiesioginį VideoFrame
objektų srautą yra naudojant MediaStreamTrackProcessor
, kuris naudoja MediaStreamTrack
(pvz., iš getUserMedia
) ir pateikia jo kadrus kaip skaitomą srautą.
Konceptualus JavaScript nustatymas:
async function setupVideoStream() {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor({ track });
const reader = trackProcessor.readable.getReader();
let previousFrameBuffer = null;
let previousFrameTimestamp = -1;
while (true) {
const { value: frame, done } = await reader.read();
if (done) break;
// Here is where we will process each 'frame'
const processedFrame = await applyTemporalFilter(frame, previousFrameBuffer);
// For the next iteration, we need to store the data of the *original* current frame
// You would copy the original frame's data to 'previousFrameBuffer' here before closing it.
// Don't forget to close frames to release memory!
frame.close();
// Do something with processedFrame (e.g., render to canvas, encode)
// ... and then close it too!
processedFrame.close();
}
}
2 žingsnis: filtravimo algoritmas – darbas su pikselių duomenimis
Tai yra mūsų darbo esmė. Savo applyTemporalFilter
funkcijoje turime pasiekti gaunamo kadro pikselių duomenis. Dėl paprastumo tarkime, kad mūsų kadrai yra 'RGBA' formato. Kiekvienas pikselis yra vaizduojamas 4 baitais: raudona, žalia, mėlyna ir alfa (skaidrumas).
async function applyTemporalFilter(currentFrame, previousFrameBuffer) {
// Define our blending factor. 0.8 means 80% of the new frame and 20% of the old.
const alpha = 0.8;
// Get the dimensions
const width = currentFrame.codedWidth;
const height = currentFrame.codedHeight;
// Allocate an ArrayBuffer to hold the pixel data of the current frame.
const currentFrameSize = width * height * 4; // 4 bytes per pixel for RGBA
const currentFrameBuffer = new Uint8Array(currentFrameSize);
await currentFrame.copyTo(currentFrameBuffer);
// If this is the first frame, there's no previous frame to blend with.
// Just return it as is, but store its buffer for the next iteration.
if (!previousFrameBuffer) {
const newFrameBuffer = new Uint8Array(currentFrameBuffer);
// We'll update our global 'previousFrameBuffer' with this one outside this function.
return { buffer: newFrameBuffer, frame: currentFrame };
}
// Create a new buffer for our output frame.
const outputFrameBuffer = new Uint8Array(currentFrameSize);
// The main processing loop.
for (let i = 0; i < currentFrameSize; i++) {
const currentPixelValue = currentFrameBuffer[i];
const previousPixelValue = previousFrameBuffer[i];
// Apply the temporal averaging formula for each color channel.
// We skip the alpha channel (every 4th byte).
if ((i + 1) % 4 !== 0) {
outputFrameBuffer[i] = Math.round(alpha * currentPixelValue + (1 - alpha) * previousPixelValue);
} else {
// Keep the alpha channel as is.
outputFrameBuffer[i] = currentPixelValue;
}
}
return { buffer: outputFrameBuffer, frame: currentFrame };
}
Pastaba apie YUV formatus (I420, NV12): Nors RGBA yra lengvai suprantamas, dauguma vaizdo įrašų efektyvumo sumetimais yra apdorojami YUV spalvų erdvėse. Darbas su YUV yra sudėtingesnis, nes spalvų (U, V) ir ryškumo (Y) informacija saugoma atskirai („plokštumose“). Filtravimo logika išlieka ta pati, tačiau reikėtų atskirai iteruoti per kiekvieną plokštumą (Y, U ir V), atsižvelgiant į jų atitinkamus matmenis (spalvų plokštumos dažnai būna mažesnės raiškos – tai technika, vadinama „chroma subsampling“).
3 žingsnis: naujo filtruoto `VideoFrame` kūrimas
Kai mūsų ciklas baigiasi, outputFrameBuffer
talpina pikselių duomenis mūsų naujam, švaresniam kadrui. Dabar turime šiuos duomenis įvilkti į naują VideoFrame
objektą, nepamiršdami nukopijuoti metaduomenų iš originalaus kadro.
// Inside your main loop after calling applyTemporalFilter...
const { buffer: processedBuffer, frame: originalFrame } = await applyTemporalFilter(frame, previousFrameBuffer);
// Create a new VideoFrame from our processed buffer.
const newFrame = new VideoFrame(processedBuffer, {
format: 'RGBA',
codedWidth: originalFrame.codedWidth,
codedHeight: originalFrame.codedHeight,
timestamp: originalFrame.timestamp,
duration: originalFrame.duration
});
// IMPORTANT: Update the previous frame buffer for the next iteration.
// We need to copy the *original* frame's data, not the filtered data.
// A separate copy should be made before filtering.
previousFrameBuffer = new Uint8Array(originalFrameData);
// Now you can use 'newFrame'. Render it, encode it, etc.
// renderer.draw(newFrame);
// And critically, close it when you are done to prevent memory leaks.
newFrame.close();
Atminties valdymas yra kritiškai svarbus: VideoFrame
objektai gali talpinti didelius kiekius nesuglaudintų vaizdo duomenų ir gali būti paremti atmintimi, esančia už JavaScript „heap“ ribų. Jūs privalote iškviesti frame.close()
kiekvienam kadrui, su kuriuo baigėte darbą. Jei to nepadarysite, greitai išseks atmintis ir naršyklės skirtukas užstrigs.
Našumo aspektai: JavaScript prieš WebAssembly
Aukščiau pateiktas gryno JavaScript įgyvendinimas puikiai tinka mokymuisi ir demonstravimui. Tačiau 30 kadrų per sekundę, 1080p (1920x1080) raiškos vaizdo įrašui mūsų ciklas turi atlikti daugiau nei 248 milijonus skaičiavimų per sekundę! (1920 * 1080 * 4 baitai * 30 kadrų/s). Nors šiuolaikiniai JavaScript varikliai yra neįtikėtinai greiti, šis pikselių apdorojimas yra puikus pavyzdys, kur tinka labiau į našumą orientuota technologija: WebAssembly (Wasm).
WebAssembly metodas
WebAssembly leidžia naršyklėje vykdyti kodą, parašytą tokiomis kalbomis kaip C++, Rust ar Go, beveik natūraliu greičiu. Mūsų laikinojo filtro logiką šiomis kalbomis įgyvendinti paprasta. Jūs parašytumėte funkciją, kuri priima rodykles į įvesties ir išvesties buferius ir atlieka tą pačią iteracinę maišymo operaciją.
Konceptuali C++ funkcija Wasm:
extern "C" {
void apply_temporal_filter(unsigned char* current_frame, unsigned char* previous_frame, unsigned char* output_frame, int buffer_size, float alpha) {
for (int i = 0; i < buffer_size; ++i) {
if ((i + 1) % 4 != 0) { // Skip alpha channel
output_frame[i] = (unsigned char)(alpha * current_frame[i] + (1.0 - alpha) * previous_frame[i]);
} else {
output_frame[i] = current_frame[i];
}
}
}
}
Iš JavaScript pusės jūs įkeltumėte šį sukompiliuotą Wasm modulį. Pagrindinis našumo pranašumas atsiranda dėl bendros atminties naudojimo. Galite sukurti ArrayBuffer
JavaScript'e, kurie yra paremti Wasm modulio linijine atmintimi. Tai leidžia perduoti kadrų duomenis į Wasm be jokių brangių kopijavimo operacijų. Visas pikselių apdorojimo ciklas tada veikia kaip vienas, labai optimizuotas Wasm funkcijos iškvietimas, kuris yra žymiai greitesnis nei JavaScript `for` ciklas.
Pažangios laikinojo filtravimo technikos
Paprastas laikinasis vidurkinimas yra puikus atspirties taškas, tačiau jis turi didelį trūkumą: sukelia judesio suliejimą arba „vaiduoklių“ efektą (ghosting). Kai objektas juda, jo pikseliai dabartiniame kadre yra sumaišomi su fono pikseliais iš ankstesnio kadro, sukuriant pėdsaką. Norint sukurti tikrai profesionalaus lygio filtrą, turime atsižvelgti į judesį.
Judesiu kompensuotas laikinasis filtravimas (MCTF)
Auksinis standartas laikinojo triukšmo mažinimui yra judesiu kompensuotas laikinasis filtravimas. Užuot aklai maišius pikselį su tuo, kuris yra tose pačiose (x, y) koordinatėse ankstesniame kadre, MCTF pirmiausia bando išsiaiškinti, iš kur tas pikselis atėjo.
Procesą sudaro:
- Judesio įvertinimas: Algoritmas padalija dabartinį kadrą į blokus (pvz., 16x16 pikselių). Kiekvienam blokui jis ieško ankstesniame kadre bloko, kuris yra panašiausias (pvz., turi mažiausią absoliučių skirtumų sumą). Poslinkis tarp šių dviejų blokų vadinamas „judesio vektoriumi“.
- Judesio kompensavimas: Tada jis sukuria „judesiu kompensuotą“ ankstesnio kadro versiją, paslinkdamas blokus pagal jų judesio vektorius.
- Filtravimas: Galiausiai, jis atlieka laikinąjį vidurkinimą tarp dabartinio kadro ir šio naujo, judesiu kompensuoto ankstesnio kadro.
Tokiu būdu judantis objektas yra maišomas su savimi iš ankstesnio kadro, o ne su fonu, kurį jis ką tik atidengė. Tai drastiškai sumažina „vaiduoklių“ artefaktus. Judesio įvertinimo įgyvendinimas yra skaičiavimams imlus ir sudėtingas, dažnai reikalaujantis pažangių algoritmų, ir beveik visada yra užduotis WebAssembly ar net WebGPU skaičiavimo šešėliams (compute shaders).
Adaptyvusis filtravimas
Kitas patobulinimas – padaryti filtrą adaptyvų. Užuot naudojus fiksuotą alfa
vertę visam kadrui, ją galima keisti atsižvelgiant į vietines sąlygas.
- Judesio adaptyvumas: Srityse, kuriose aptinkamas didelis judesys, galite padidinti
alfa
(pvz., iki 0,95 ar 1,0), kad beveik visiškai pasikliautumėte dabartiniu kadru, išvengiant judesio suliejimo. Statinėse srityse (pvz., siena fone), galite sumažintialfa
(pvz., iki 0,5), kad triukšmo mažinimas būtų daug stipresnis. - Šviesumo adaptyvumas: Triukšmas dažnai labiau matomas tamsesnėse vaizdo srityse. Filtras galėtų būti agresyvesnis šešėliuose ir mažiau agresyvus šviesiose srityse, siekiant išsaugoti detales.
Praktiniai naudojimo atvejai ir taikymas
Galimybė atlikti aukštos kokybės triukšmo mažinimą naršyklėje atveria daugybę galimybių:
- Realaus laiko komunikacija (WebRTC): Iš anksto apdoroti vartotojo interneto kameros srautą prieš jį siunčiant į vaizdo koduotuvą. Tai didžiulis laimėjimas vaizdo skambučiams prasto apšvietimo sąlygomis, gerinantis vaizdo kokybę ir mažinantis reikalingą pralaidumą.
- Internetu pagrįstas vaizdo redagavimas: Pasiūlyti „Denoise“ (triukšmo šalinimo) filtrą kaip funkciją naršyklės vaizdo redaktoriuje, leidžiant vartotojams išvalyti įkeltą filmuotą medžiagą be serverio apdorojimo.
- Debesų žaidimai ir nuotolinis darbalaukis: Išvalyti gaunamus vaizdo srautus, siekiant sumažinti glaudinimo artefaktus ir pateikti aiškesnį, stabilesnį vaizdą.
- Kompiuterinės regos išankstinis apdorojimas: Internetu pagrįstoms AI/ML programoms (pvz., objektų sekimui ar veido atpažinimui), įvesties vaizdo triukšmo pašalinimas gali stabilizuoti duomenis ir lemti tikslesnius bei patikimesnius rezultatus.
Iššūkiai ir ateities kryptys
Nors šis metodas yra galingas, jis turi ir iššūkių. Kūrėjai turi atsižvelgti į:
- Našumas: Realaus laiko HD ar 4K vaizdo apdorojimas yra reiklus. Būtinas efektyvus įgyvendinimas, paprastai su WebAssembly.
- Atmintis: Vieno ar kelių ankstesnių kadrų saugojimas nesuglaudintuose buferiuose sunaudoja didelį kiekį RAM. Būtinas kruopštus valdymas.
- Uždelsimas: Kiekvienas apdorojimo žingsnis prideda uždelsimą. Realaus laiko komunikacijai šis konvejeris turi būti labai optimizuotas, kad būtų išvengta pastebimų vėlavimų.
- Ateitis su WebGPU: Būsimoji WebGPU API atvers naują horizontą tokio tipo darbams. Ji leis šiuos pikselių algoritmus vykdyti kaip labai lygiagrečius skaičiavimo šešėlius (compute shaders) sistemos GPU, siūlydama dar vieną didžiulį našumo šuolį, pranokstantį net WebAssembly CPU.
Išvada
WebCodecs API žymi naują erą pažangiam medijos apdorojimui internete. Ji griauna tradicinio „juodosios dėžės“ <video>
elemento barjerus ir suteikia kūrėjams smulkaus lygio valdymą, reikalingą kurti tikrai profesionalias vaizdo programas. Laikinasis triukšmo mažinimas yra puikus jos galios pavyzdys: sudėtinga technika, kuri tiesiogiai sprendžia tiek vartotojo suvokiamą kokybę, tiek pagrindinį techninį efektyvumą.
Matėme, kad perimdami atskirus VideoFrame
objektus, galime įgyvendinti galingą filtravimo logiką, siekiant sumažinti triukšmą, pagerinti glaudinimą ir suteikti aukštesnės kokybės vaizdo patirtį. Nors paprastas JavaScript įgyvendinimas yra puikus atspirties taškas, kelias į gamybai paruoštą, realaus laiko sprendimą veda per WebAssembly našumą ir, ateityje, per lygiagrečią WebGPU apdorojimo galią.
Kitą kartą, kai pamatysite grūdėtą vaizdo įrašą interneto programoje, prisiminkite, kad įrankiai tai ištaisyti dabar, pirmą kartą, yra tiesiogiai interneto kūrėjų rankose. Tai jaudinantis laikas kurti su vaizdo įrašais internete.