360-videon uudelleenkehystys Flutterissa: Pallomainen projektio, Quaternion-avainruudut ja GPU-vienti
Kuinka muutimme raa'an tasasuorakulmaisen 360°-materiaalin tasaiseksi, kehystetyksi, litteĂ€ksi videoksi â kokonaan Flutter-sovelluksen sisĂ€llĂ€.
Ongelma, josta kukaan ei varoita sinua
Normaalissa videossa on yksi ilmeinen asia nĂ€ytettĂ€vĂ€nĂ€: ruutu, johon kamera oli suunnattu. 360°-videossa sellaista ei ole. Kamera tallensi kaiken â tĂ€yden pikselipallon, joka on tallennettu tasaisena tasasuorakulmaisena suorakaiteena (2:1-kuva, jossa X-akseli on pituusaste ja Y-akseli leveysaste). Ennen kuin voit laittaa tĂ€mĂ€n materiaalin Instagramiin, YouTubeen tai puhelimen nĂ€ytölle, jonkun on vastattava kysymykseen, johon kamera tarkoituksellisesti kieltĂ€ytyi vastaamasta:
Minne katsojan tulisi katsoa ja milloin?
"Uudelleenkehystys" on virtuaalikameran lennĂ€ttĂ€mistĂ€ pallon lĂ€pi ajan myötĂ€ â panorointia, kallistusta, zoomausta â ja normaalin litteĂ€n 16:9 (tai 9:16 tai 2.35:1) videon renderöintiĂ€. TĂ€mĂ€ on ominaisuus, jonka ympĂ€rille GoPro Player, Insta360 Studio ja Adobe PremiĂšren GoPro VR Reframe -laajennus on rakennettu.
Rakensimme sen kokonaan Flutterissa. TÀmÀ kirjoitus on katsaus osiin, jotka olivat aidosti vaikeita: projektion matematiikka, gimbal-lock-vapaa interpolointi, ±180° sauma ja GPU-vientiputki, jonka on kestettÀvÀ oikeita Android-dekoodereita.
PÀÀstÀ pÀÀhÀn -työnkulku:
360-videon tallennus/tuonti
â kevyen esikatselu-proksin transkoodaus
â kĂ€yttĂ€jĂ€ napauttaa / piirtÀÀ / tunnistaa kohteen automaattisesti
â kohteen seuranta tuottaa polun
â avainruutujen generointi + tasoitus
â SLERP-interpolointi ruutukohtaiselle kameran suunnalle (reaaliaikainen esikatselu)
â natiivi GPU-vienti litteĂ€ksi videoksi
â FFmpeg-jĂ€lkikĂ€sittely (moov atom + A/V-synkronoinnin korjaus)
Arkkitehtuuri lyhyesti
Pidimme jÀrjestelmÀn kolmessa selkeÀssÀ kerroksessa, jotta matematiikka on testattavissa erillÀÀn Flutterista:
| Kerros | Sijainti | Vastuu |
|---|---|---|
| Mallit | lib/data/models/reframe/ | SphericalCoordinates/Quaternion, CameraOrientation/MotionConstraints, ReframeKeyframe, ReframeProject |
| Palvelut | lib/services/reframe/ | SphericalProjection (sÀteenseuranta), CoordinateConverter, KeyframeInterpolator/Generator/Smoother, ReframeExportService |
| Esityskerros | lib/presentation/.../reframe/ | Riverpod ReframeNotifier, natiivi OpenGL Spherical360Player, bbox-piirustuspeite, avainruutuaikajana |
Kultainen sÀÀntö: koko trigonometria on puhdasta Dartia ilman Flutter-tuonteja. TÀmÀ tarkoittaa, ettÀ jokainen projektio- ja interpolointifunktio on yksikkötestattavissa ilman widget-puuta, ja sama matematiikka ohjaa sekÀ reaaliaikaista esikatselua ettÀ vientiÀ.
1. Kaiken ydin: tasasuorakulmainen â· pallomainen
Kaikki alkaa yhdestÀ kuvauksesta. Tasasuorakulmainen kehys on vain suorakulmio, jossa:
- X (0 â leveys) pyyhkĂ€isee pituusastetta (yaw) â180°:sta +180°:een
- Y (0 â korkeus) pyyhkĂ€isee leveysastetta (pitch) +90°:sta (ylĂ€/zeniitti) â90°:een (ala/nadiiri)
SphericalCoordinates on tarkoituksella pieni â vain yaw ja pitch. Roll, FOV ja zoom kuuluvat kameraan, eivĂ€t suuntaan. TĂ€ssĂ€ on muunnos, johon koko ominaisuus perustuu:
factory SphericalCoordinates.fromEquirectangular({
required double x, required double y,
required double videoWidth, required double videoHeight,
}) {
final normalizedX = x / videoWidth;
final normalizedY = y / videoHeight;
// X maps to yaw: 0->-180°, 0.5->0°, 1.0->180°
final yaw = (normalizedX - 0.5) * 360.0;
// Y maps to pitch: 0->90° (top), 0.5->0° (horizon), 1.0->-90° (bottom)
final pitch = (0.5 - normalizedY) * 180.0;
return SphericalCoordinates(
yaw: yaw.clamp(-180.0, 180.0),
pitch: pitch.clamp(-90.0, 90.0),
);
}
KÀÀnteinen on peilikuva (normalizedX = yaw/360 + 0.5), ja kÀytÀmme sitÀ jatkuvasti piirtÀÀksemme seurattavan rajauslaatikon takaisin soittimeen.
"Napauta sitĂ€, mitĂ€ nĂ€et" â perspektiivi-sĂ€teenseuranta
KÀyttÀjÀ ei vuorovaikuta tasasuorakulmaisen suorakulmion kanssa. He vuorovaikuttavat renderöidyn perspektiivinÀkymÀn kanssa. Joten kun he napauttavat kohtaa soittimessa, meidÀn on lÀhetettÀvÀ sÀde virtuaalisen neulanreikÀkameran lÀpi ja löydettÀvÀ, minkÀ suunnan pallosta he osuvat. Se on SphericalProjection.screenToSpherical:
final ndcX = (2.0 * screenPoint.dx / playerSize.width) - 1.0;
final ndcY = 1.0 - (2.0 * screenPoint.dy / playerSize.height);
final aspectRatio = playerSize.width / playerSize.height;
final tanHalfFov = math.tan(cameraFov * math.pi / 360.0); // half-angle
double rayX = ndcX * aspectRatio * tanHalfFov;
double rayY = ndcY * tanHalfFov;
double rayZ = 1.0;
// ...normalize, rotate the ray by pitch (around X) then yaw (around Y)...
final sphericalYaw = math.atan2(finalRayX, finalRayZ) * 180.0 / math.pi;
final sphericalPitch = math.asin(finalRayY.clamp(-1.0, 1.0)) * 180.0 / math.pi;
.clamp(-1.0, 1.0) ennen asin-funktiota saattaa vaikuttaa vainoharhaiselta, mutta liukulukuvirhe voi antaa arvon 1.0000001 ja asin palauttaa NaN:n â ja yksi NaN pilaa jokaisen seuraavan ruudun. Puolustava rajaaminen kÀÀnteisten trigonometristen funktioiden ympĂ€rillĂ€ on toistuva teema tĂ€ssĂ€ koodikannassa.
2. Tasainen kameran liike: Quaternionit, ei Euler-kulmat
Naivi lÀhestymistapa on tallentaa yaw/pitch jokaiseen avainruutuun ja interpoloida luvut lineaarisesti. Se nÀyttÀÀ kauhealta. Euler-kulmien interpolointi aiheuttaa gimbal lockin, epÀtasaisen kulmanopeuden ja rumia nykÀyksiÀ napojen lÀhellÀ.
Interpoloimme suuntaa Quaternionina kÀyttÀen SLERPiÀ (pallomaista lineaarista interpolointia). Jokainen suunta muunnetaan Quaternioniksi ZYX Euler -konvention avulla:
Quaternion toQuaternion() {
final yawRad = yaw * math.pi / 180.0;
final pitchRad = pitch * math.pi / 180.0;
final cy = math.cos(yawRad * 0.5); final sy = math.sin(yawRad * 0.5);
final cp = math.cos(pitchRad * 0.5); final sp = math.sin(pitchRad * 0.5);
return Quaternion(w: cy * cp, x: cy * sp, y: sy * cp, z: -sy * sp);
}
Ja SLERP-funktiossa itsessÀÀn on kaksi tuotantotasoisen toteutuksen tarvitsemaa turvaventtiiliÀ:
static Quaternion slerp(Quaternion a, Quaternion b, double t) {
a = a.normalize(); b = b.normalize();
var dotProduct = a.dot(b);
if (dotProduct < 0.0) { b = -b; dotProduct = -dotProduct; } // take the shortest hemisphere
if (dotProduct > 0.9995) { // near-parallel -> fall back to LERP
return Quaternion(w: a.w + t*(b.w-a.w), x: a.x + t*(b.x-a.x),
y: a.y + t*(b.y-a.y), z: a.z + t*(b.z-a.z)).normalize();
}
final theta0 = math.acos(dotProduct);
final theta = theta0 * t;
final sinTheta = math.sin(theta);
final sinTheta0 = math.sin(theta0);
final s0 = math.cos(theta) - dotProduct * sinTheta / sinTheta0;
final s1 = sinTheta / sinTheta0;
return Quaternion(w: s0*a.w + s1*b.w, /* ...*/).normalize();
}
Kaksi ehdotonta yksityiskohtaa:
- Lyhyin pallonpuolisko â q ja âq edustavat samaa suuntaa. Jos pistetulo on negatiivinen, negatoit toisen syötteen tai kamerasi kulkee pitkĂ€n reitin pallon ympĂ€ri.
- LĂ€hes yhdensuuntainen LERP-palautus â kun kaksi avainruutua ovat lĂ€hes identtiset, sinTheta0 â 0 ja jaat lĂ€hes nollalla. Paluu tavalliseen LERP-interpolointiin, kun pistetulo > 0.9995, vĂ€lttÀÀ ongelman.
Hienovarainen mutta tÀrkeÀ suunnittelupÀÀtös: me SLERP-interpoloimme suuntaa, mutta LERP-interpoloimme FOV/zoomia ja kulma-LERP-interpoloimme rollia. Pallomainen interpolointi on oikea työkalu suunnalle; zoom on vain skalaari ja sen tulisi liikkua lineaarisesti:
final qInterp = Quaternion.slerp(from.direction.toQuaternion(), to.direction.toQuaternion(), t);
final interpDirection = SphericalCoordinates.fromQuaternion(qInterp).normalize();
final interpFov = _lerp(from.fov, to.fov, t);
final interpRoll = _lerpAngle(from.roll, to.roll, t); // wraps across ±180
final interpZoom = _lerp(from.zoom, to.zoom, t);
đĄ Kun muunnamme takaisin Quaternionista yaw/pitch-arvoihin, kĂ€ytĂ€mme atan2-funktiota asin-funktion sijaan "numeerisen vakauden vuoksi (vĂ€lttÀÀ asin-funktion mÀÀrittelyalueen ongelmat)." SLERP-interpoloinnin jĂ€lkeen Quaternion voi olla hyvin lievĂ€sti denormalisoitu, ja atan2 toimii sujuvammin, kun taas asin heittÀÀ NaN:n.
Pehmennys: kÀsin toteutettu, saapuvan siirtymÀn yhteydessÀ
Jokainen avainruutu omistaa pehmennyskÀyrÀn siirtymÀlle siihen. KÀyrÀt on kirjoitettu kÀsin, jotta ne ovat identtiset kaikilla alustoilla:
case KeyframeEasing.linear: return t;
case KeyframeEasing.easeInOut: return t * t * (3 - 2 * t); // smoothstep S-curve
case KeyframeEasing.easeIn: return t * t; // quadratic
case KeyframeEasing.easeOut: return 1 - (1 - t) * (1 - t);
3. ±180° sauma: virhe, joka kummittelee jokaisessa 360-ominaisuudessa
TĂ€ssĂ€ on ansa. Yaw-arvo elÀÀ ympyrĂ€n kehĂ€llĂ€: +179° ja â179° ovat 2°:n pÀÀssĂ€ toisistaan, eivĂ€t 358°:n. Heti kun kohteesi kĂ€velee pallon takaosan poikki, naiivi matematiikka nykĂ€isee kameran puoli maailmaa ympĂ€ri. TĂ€mĂ€ yksi ongelma ilmenee jĂ€rjestelmĂ€n neljĂ€ssĂ€ eri kerroksessa, ja jokainen vaatii oman korjauksensa.
Kerros 1 â primitiivit. Kaksi pientĂ€ apufunktiota, joihin koko koodikanta tukeutuu:
static double normalizeYaw(double yaw) {
while (yaw > 180) { yaw -= 360; }
while (yaw < -180) { yaw += 360; }
return yaw;
}
// "CRITICAL for seam-safe tracking ... avoids 180° snaps"
static double shortestYawDelta(double from, double to) {
double delta = to - from;
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
return delta;
}
Kerros 2 â avainruutujen generointi kerÀÀ jatkuvan (rajoittamattoman) yaw-arvon, jotta SLERP interpoloi aina lyhimmĂ€n reitin. Sen sijaan, ettĂ€ rajoittaisimme jokaisen nĂ€ytteen ±180°:een, laskemme lyhimmĂ€t deltat yhteen ja annamme jatkuvan arvon tarkoituksella ylittÀÀ 180°:
final normalizedContinuousYaw = SphericalCoordinates.normalizeYaw(continuousYaw);
final yawDelta = SphericalCoordinates.shortestYawDelta(normalizedContinuousYaw, rawCoords.yaw);
continuousYaw = continuousYaw + yawDelta; // may exceed ±180 on purpose
coords = SphericalCoordinates(yaw: continuousYaw, pitch: rawCoords.pitch);
Kerros 3 â rajauslaatikon rekonstruktio. Kun kĂ€yttĂ€jĂ€ piirtÀÀ sauman yli ulottuvan laatikon, neljĂ€ nurkkaa ilmoittavat yaw-arvot, kuten +170°, +175°, â175°, â170°. SphericalProjection tunnistaa tĂ€mĂ€n (mikĂ€ tahansa nurkka > 90° ja mikĂ€ tahansa nurkka < â90°) ja rakentaa uudelleen jatkuvan yaw-arvon "seam-safe"-rajauslaatikon siirtĂ€mĂ€llĂ€ negatiivisia arvoja +360°:lla ennen sen laajuuden mittaamista.
Kerros 4 â reaaliaikainen soitin tasoittaa kohti kohdettaan shortestYawDelta-funktion avulla, jotta vetĂ€minen sauman poikki ei koskaan nyi.
Jos muistat yhden asian 360-työkalujen rakentamisesta: sauma ei ole reunatapaus, se on lÀpileikkaava huolenaihe. Varaa sille resursseja kaikkialle, missÀ yaw esiintyy.
4. Saada se tuntumaan Insta360/GoPro:lta
Tasainen interpolointi antaa teknisesti oikean kameran. Se ei kuitenkaan anna hyvÀÀ kameraa. Kolme "tuntumatekniikan" osaa kuromassa umpeen tÀtÀ kuilua.
Liikerajoitukset â nopeuden ja kiihtyvyyden rajoittaminen. Raaka seurantadata on nykivÀÀ. KĂ€sittelemme avainruutuja MotionConstraints-rajojen kautta, jotka rajoittavat virtuaalikameran liikkumisnopeutta ja -jyrkkyyttĂ€ (e.g. maxYawSpeed = 120°/s, maxPitchSpeed = 90°/s, maxAcceleration = 180°/sÂČ), kĂ€yttĂ€en cinematic(), responsive() ja production() esiasetuksia. TĂ€mĂ€ on ero "turvakameran lentĂ€vĂ€n kĂ€rpĂ€sen seurannan" ja "vakaakĂ€tisen operaattorin" vĂ€lillĂ€.
Mukautuva FOV â zoomataan kohteen mukaan. Mittaamme kohteen kulmakoon tasasuorakulmaisessa tilassa ja valitsemme FOV:n niin, ettĂ€ se tĂ€yttÀÀ tietyn osuuden kuvasta:
final angularWidth = (bboxWidth / videoWidth) * 360.0;
final angularHeight = (bboxHeight / videoHeight) * 180.0;
final objectAngularSize = math.max(angularWidth, angularHeight);
final idealFov = objectAngularSize / targetFillRatio; // targetFillRatio â 0.4
return idealFov.clamp(minFov, maxFov); // 45°â120°
Liikkeen ennakointi + kasvojenâvartalon yhdistĂ€minen. SiirrĂ€mme kehystystĂ€ noin 15 % rajauslaatikon leveydestĂ€ eteenpĂ€in kulkusuuntaan, jotta kohde ei ole kiinni siinĂ€ reunassa, johon se on kĂ€velemĂ€ssĂ€. Ja kun kasvojentunnistus katkeaa, emme siirry suoraan vartalon keskipisteeseen â vaan lerp-interpoloimme viimeksi tunnetusta kasvojen keskipisteestĂ€ vartalon keskipisteeseen noin 10 ruudun aikana.
5. Kahden videon temppu: esikatseluproksi vs. alkuperÀinen tÀyden resoluution tiedosto
TÀmÀ on arkkitehtoninen pÀÀtös, joka tekee koko asiasta kÀyttökelpoisen oikeissa puhelimissa.
360° GoPro/Insta360-tiedosto on usein 4096Ă2048 HEVC. Edulliset Android-dekooderit tukehtuvat yrittĂ€essÀÀn toistaa sitĂ€ sujuvasti, puhumattakaan kohteen seurannan suorittamisesta sen pÀÀllĂ€. Siksi suoritamme seurannan ja reaaliaikaisen esikatselun transkoodatulla proksilla (~1440Ă720, libx264 -preset veryfast -crf 28), ja varaamme tĂ€yden resoluution alkuperĂ€isen tiedoston vain lopulliseen vientiin.
// "more reliable than MediaMetadataRetriever for 360° videos
// which often use unusual codecs (HEVC, high-res)"
final maxSafeWidth = 2048, maxSafeHeight = 1080;
// 2:1 4K (4096Ă2048) is unsafe -> trigger transcodeForPlayback()
Ongelma, joka sinun on kĂ€siteltĂ€vĂ€: avainruutujen rajauslaatikot lasketaan proksin koordinaateissa ja ne on skaalattava alkuperĂ€isiin koordinaatteihin ennen vientiĂ€. Jos skaalaus menee pieleen, vientisi rajaa hieman eri alueen kuin kĂ€yttĂ€jĂ€ esikatseli. (KĂ€ytĂ€mme myös ffprobea, emme MediaMetadataRetrieveriĂ€, metadatan ja ensimmĂ€isen ruudun poimimiseen â Androidin sisÀÀnrakennettu retriever on epĂ€luotettava 360-kameroiden epĂ€tavallisilla koodekeilla.)
6. GPU-vienti: OpenGL + MediaCodec MethodChannelin kautta
Jokaisen 4K-pallon ruudun uudelleenprojisointi FFmpeg-ohjelman v360-suodattimen kautta puhelimen CPU:lla on aivan liian hidasta. (PidĂ€mme v360-merkkijonon kuitenkin tallessa â se dokumentoi siististi projektion semantiikan, v360=e:flat:... tarkoittaa "tasasuorakulmainen sisÀÀn, litteĂ€ ulos" â mutta se ei ole suora suorituspolku.)
Todellinen vienti antaa avainruudut natiiville OpenGL ES + MediaCodec -renderöijÀlle MethodChannelin kautta:
final result = await _methodChannel.invokeMethod('exportVideoGPU', {
'videoPath': resolvedVideoPath,
'outputPath': outputPath,
'keyframes': keyframesList,
'initialOrientation': initialOrientationMap,
'width': settings.exportWidth,
'height': settings.exportHeight,
'fps': settings.outputFps.toInt(),
'includeAudio': settings.includeAudio,
if (isTrimmed) 'trimStartMs': trimStartMs,
if (isTrimmed) 'trimEndMs': trimEndMs
});
Rajoitamme tÀmÀn ominaisuuden tarkistuksen taakse (Android API 21+, laitteistopohjainen H.264-enkooderi, OpenGL ES 3.0) ja striimaamme edistymisen takaisin EventChannelin kautta. Muutama vaikeasti opittu sudenkuoppa:
- â90° yaw-siirtymĂ€. Dart-konventiomme sijoittaa yaw = 0 tasasuorakulmaisen kuvan keskelle (u = 0.5); OpenGL-renderöijĂ€ sijoittaa yaw = 0 kohtaan u = 0.75. Joten jokaisen avainruudun yaw-arvo uudelleenmÀÀrĂ€tÀÀn â90°:lla (ja kÀÀritÀÀn uudelleen ±180°:een) kanavan yli â symmetrisesti sovellettuna sekĂ€ vientikanavaan ettĂ€ reaaliaikaisen soittimen kanavaan, jotta esikatselu ja vienti ovat yhdenmukaisia.
- EpĂ€onnistuneet avainruutujen mÀÀrĂ€t. Nolla avainruutua â syntetisoidaan kaksi identtistĂ€ alkuperĂ€isestĂ€ suunnasta (staattinen vienti). Yksi avainruutu â duplikoidaan se. RenderöijĂ€ haluaa aina vĂ€hintÀÀn alun ja lopun.
- Leikkaus uudelleenasettelee aikaleimat. Leikattu vienti siirtÀÀ jokaista avainruutua arvolla t â trimStartMs.
FFmpeg-jÀlkikÀsittely, jota kukaan ei odota
Voisit luulla, ettĂ€ GPU-vienti on maaliviiva. Se ei ole â koska Androidin MediaMuxer kirjoittaa moov-atomin tiedoston loppuun ja voi sisĂ€ltÀÀ lĂ€hdeÀÀnen PTS-siirtymĂ€n. Tulos: ExoPlayer (ja Flutterin video_player) jÀÀdyttÀÀ kuvan kellon kĂ€ydessĂ€. Joten GPU-kĂ€sittelyn jĂ€lkeen FFmpeg tekee nopean, hĂ€viöttömĂ€n siivouksen:
// copy video (no re-encode -> zero quality loss), re-encode audio to fix PTS,
// move moov atom to the front for instant playback.
-c:v copy -af "aresample=async=1:first_pts=0" -movflags +faststart
Ja raaka pieni sudenkuoppa, joka sitÀ suojaa: tunnistamme ensin ÀÀniraidan, koska ÀÀnilippujen vÀlittÀminen videolle, jossa ei ole ÀÀniraitaa, saa FFmpeg-ohjelman jumiin ikuisesti joissakin laitteissa. Koko jÀlkikÀsittely on kÀÀritty 60 sekunnin aikakatkaisuun, jossa on keskeytys aikakatkaisun tapahtuessa.
7. Reaaliaikainen esikatselu on myös natiivi soitin
Editorin sisĂ€inen esikatselu ei ole Flutter CustomPaint â se on natiivi OpenGL-pinta, joka on upotettu AndroidView:n kautta (viewType-arvolla kuten 'your_app/spherical360player'), ja sillĂ€ on oma nĂ€kymĂ€kohtainen method/event channel. Dart Spherical360PlayerController peilaa yaw/pitch/roll/fov-arvot ja soveltaa saman â90° siirtymĂ€n ennen lĂ€hettĂ€mistĂ€.
Yksityiskohdat, jotka tekevÀt siitÀ miellyttÀvÀn:
- Kuvanopeudesta riippumaton tasoitus â kerroin = 1 - exp(-nopeus * dt), jotta pehmennys nĂ€yttÀÀ samalta 60 fps:llĂ€ ja 90 fps:llĂ€, saumaturvallisella lyhimmĂ€n polun yaw-arvolla.
- Momentti/inertia heilautuksen jĂ€lkeen, kitkaan perustuvalla vaimenemisella â pois pÀÀltĂ€ toiston aikana, koska toiston aikana kameran on noudatettava avainruutuja, ei kĂ€yttĂ€jĂ€n viimeisintĂ€ pyyhkĂ€isyĂ€.
- Yksi GestureDetector, jonka onScaleUpdate tekee panoroinnin ja nipistyszoomin yhdessĂ€; kaksoisnapautus vaihtaa toiston/tauon vĂ€lillĂ€. FOV rajoittuu 30°â120°:een, pitch ±90°:een ohjaimen rajalla.
Yksi pieni mutta paljastava UX-korjaus: ennen siirtymistÀ "seurannasta" "avainruutujen generointiin" lisÀÀn 120 ms:n viiveen, jotta Flutter todella piirtÀÀ "100 %". Ilman sitÀ lopullinen edistymisruutu ja tilamuutos pÀÀtyvÀt samaan mikrotehtÀvÀerÀÀn, eikÀ kÀyttÀjÀ koskaan nÀe valmistumista.
Tilakone, lyhyesti
Kaikkea tÀtÀ koordinoi Riverpod ReframeNotifier, joka ohjaa tilaluetteloa:
joutokĂ€ynti â videon lataus â transkoodaus
â (tunnistus | kohteen valinta | rajauslaatikon piirto soittimeen | automaattinen tunnistus soittimessa)
â seuranta â avainruutujen generointi â muokkaus â vienti â valmis | virhe
Useat sisÀÀntulopisteet syöttĂ€vĂ€t samaa seurainta: napauta tunnistaaksesi (YOLO-peite), manuaalinen rajauslaatikko tai Insta360-tyylinen suoraan soittimeen piirtĂ€misliike. Ne kaikki yhtyvĂ€t KeyframeGenerator.production().generate(...) â KeyframeSmoother.smooth(...)-funktioihin, ja sieltĂ€ interpoloija ohjaa jokaista esikatseltua ja vietyĂ€ ruutua.
Opetukset
- PidÀ matematiikka puhtaana. Ei Flutter-tuonteja projektio-/interpolointikerroksessa. Se on ainoa syy, miksi tÀmÀ oli testattavissa.
- Quaternion SLERP suunnalle; LERP skalaareille. ĂlĂ€ interpoloi Euler-kulmia, Ă€lĂ€kĂ€ SLERP-interpoloi zoomiasi.
- ±180° sauma on lÀpileikkaava huolenaihe, ei reunatapaus. Se ilmestyy uudelleen primitiiveissÀ, generoinnissa, projektiossa ja soittimessa.
- Yhdenmukaista konventiosi kerran, symmetrisesti. â90° yaw-siirtymĂ€ Dartin ja OpenGL:n vĂ€lillĂ€ sovelletaan molempiin kanavarajoihin, jotta esikatselu == vienti.
- Proksi vuorovaikutukseen, alkuperÀinen tuotokseen. Muista vain skaalata koordinaattisi nÀiden kahden vÀlillÀ.
- "Renderöinti valmis" â "toistaa oikein." Varaa resursseja FFmpeg-jĂ€lkikĂ€sittelyyn moov-atomin sijoittelun ja ÀÀnen PTS-virheiden korjaamiseksi, ja tunnista ÀÀniraidat ennen kuin lisÀÀt ÀÀnilippuja.
- Tuntuma on ominaisuus. Liikerajoitukset, mukautuva FOV ja liikkeen ennakointi erottavat "teknisesti uudelleenkehyksetyn" siitÀ, "miltÀ ammattilainen sen kuvasi".
Tulos: tĂ€ysin laitteessa toimiva 360°-uudelleenkehystyseditori â napauta kohdetta, anna sen seurata, sÀÀdĂ€ avainruutuja ja vie puhdas litteĂ€ video â rakennettu Flutterissa ohuella natiivilla GPU-kerroksella, joka tekee vain sen työn, mihin Dart ei pysty tarpeeksi nopeasti.

