MicrocosmWorksחדשנות ותכנון קוסמוס דיגיטלי
אודותצור קשר
MicrocosmWorksמחדשים ומתכננים קוסמוס דיגיטלי

מספקים פתרונות IT חשובים. אנו נלהבים מטכנולוגיה, אבטחה ועוזרים לעסקים לצמוח באמצעות תשתית IT אמינה וחדשנית.

[email protected]
+91 7011868196
New Delhi, India

מרכז צמיחה AI

מרכז AIחדשנות סטארטאפמאיץ ארגוני

פתרונות

כל הפתרונותאפליקציות בריאות וכושרפלטפורמת וידאו AIפיתוח סוכני AI

משאבים

תובנותמדריכי תעשייהתוכניות מקרה שימושתבניות ארכיטקטורהמחקרי מקרה

חברה

אודותינוצור קשרהעבודה שלנו

שירותים

ייעוץ דיגיטליתשתית ענןפיתוח SaaSפיתוח AIטכנולוגיית וידאו
פיתוח ERPהתאמה אישית של Zohoפיתוח Odooאינטגרציה של Salesforceפיתוח CRM מותאם אישית
אינטגרציה של QuickBooksפתרונות IoTפיתוח בלוקצ'יין
ייעוץ סייברתמיכה טכנית - L3

© 2026 MicrocosmWorks. כל הזכויות שמורות.

מדיניות פרטיותתנאי שירות
חזרה לתובנות
IoT Development

מיסגור מחדש של וידאו 360 ב-Flutter: היטל כדורי, keyframes מסוג קווטרניון, וייצוא ב-GPU

מיסגור מחדש של צילום 360 בפורמט equirectangular ב-Flutter באמצעות היטל כדורי, keyframes מסוג קווטרניון, וייצוא מואץ ב-GPU.

Saurav Kumar Gupta's image Saurav Kumar Gupta
•
July 10, 2026
•
עודכן August 19, 2026
•
5 min read
Developer using Flutter to edit and reframe a 360° video with spherical projection.webp
5 min read

מיסגור מחדש של וידאו 360° ב-Flutter: היטל כדורי, keyframes מסוג קווטרניון, וייצוא ב-GPU

איך הפכנו צילום 360° גולמי בפורמט equirectangular לווידאו שטוח, ממוסגר וחלק – כולו בתוך אפליקציית Flutter.

הבעיה שאף אחד לא מזהיר מפניה

לווידאו רגיל יש דבר אחד ברור להראות לכם: המסגרת שהמצלמה כיוונה אליה. לווידאו 360° אין דבר כזה. המצלמה הקליטה הכל — כדור מלא של פיקסלים המאוחסנים כמלבן equirectangular שטוח (תמונת 2:1 שבה ציר ה-X הוא קו אורך וציר ה-Y הוא קו רוחב). לפני שתוכלו לשים את הצילום הזה ב-Instagram, YouTube, או במסך טלפון, מישהו צריך לענות על שאלה שהמצלמה סירבה במכוון לענות עליה:

לאן הצופה צריך להסתכל, ומתי?

"מיסגור מחדש" (Reframing) הוא הפעולה של הטסת מצלמה וירטואלית דרך הכדור הזה לאורך זמן — הזזה אופקית (panning), הטיה (tilting), זום (zooming) — ורינדור וידאו שטוח רגיל ביחס 16:9 (או 9:16, או 2.35:1). זו התכונה שבבסיס GoPro Player, Insta360 Studio, והפלאגין GoPro VR Reframe של Adobe Premiere.

בנינו את זה כולו ב-Flutter. פוסט זה סוקר את החלקים שהיו קשים באמת: מתמטיקת ההיטל (projection math), אינטרפולציה ללא gimbal lock, התפר ±180°, וצינור ייצוא GPU שצריך לשרוד מפענחי Android אמיתיים.

זרימת העבודה מקצה לקצה:

לכידה/ייבוא וידאו 360
  → המרה (transcode) לפרוקסי תצוגה מקדימה קל משקל
  → המשתמש מקיש / מצייר / מזהה אוטומטית נושא
  → מעקב אובייקטים (object tracking) מייצר נתיב
  → יצירה + החלקת keyframes
  → אינטרפולציית SLERP של כיוון המצלמה עבור כל פריימ (תצוגה מקדימה חיה)
  → ייצוא GPU מקורי לווידאו שטוח
  → מעבר FFmpeg לאחר (תיקון moov atom + A/V sync)

מבט מהיר על הארכיטקטורה

שמרנו על המערכת בשלוש שכבות נקיות כך שהמתמטיקה ניתנת לבדיקה בנפרד מ-Flutter:

שכבהממוקם באחריות
Modelslib/data/models/reframe/SphericalCoordinates/Quaternion, CameraOrientation/MotionConstraints, ReframeKeyframe, ReframeProject
Serviceslib/services/reframe/SphericalProjection (ray casting), CoordinateConverter, KeyframeInterpolator/Generator/Smoother, ReframeExportService
Presentationlib/presentation/.../reframe/Riverpod ReframeNotifier, the native OpenGL Spherical360Player, bbox-drawing overlay, keyframe timeline

כלל הזהב: כל הטריגונומטריה היא Dart טהור ללא ייבואי Flutter. המשמעות היא שכל פונקציית היטל ואינטרפולציה ניתנת לבדיקת יחידה (unit-testable) ללא עץ widget, ואותה מתמטיקה מניעה הן את התצוגה המקדימה החיה והן את הייצוא.

1. לב העניין: equirectangular ⟷ כדורי

הכל מתחיל במיפוי אחד. פריימ equirectangular הוא פשוט מלבן שבו:

  • X (0 → רוחב) סורק את קו האורך (yaw) מ-−180° ל- +180°
  • Y (0 → גובה) סורק את קו הרוחב (pitch) מ- +90° (למעלה/זניט) ל- −90° (למטה/נאדיר)

SphericalCoordinates הוא קטן במכוון — רק yaw ו-pitch. Roll, FOV ו-zoom שייכים ל-*מצלמה*, לא ל-*כיוון*. הנה ההמרה שעליה נשענת כל התכונה:

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),
  );
}

ההפוך הוא תמונת המראה (normalizedX = yaw/360 + 0.5), ואנו משתמשים בו באופן קבוע כדי לצייר מלבן תוחם (bounding box) שעוקבים אחריו בחזרה על הנגן.

"הקש על מה שאתה רואה" — הטלת קרן פרספקטיבה (perspective ray casting)

המשתמש לא מקיים אינטראקציה עם המלבן ה-equirectangular. הוא מקיים אינטראקציה עם תצוגת פרספקטיבה מרונדרת. לכן, כאשר הוא מקיש על נקודה בנגן, עלינו להטיל קרן דרך מצלמת pinhole וירטואלית ולמצוא איזה כיוון על הכדור הם פגעו בו. זהו 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) לפני asin נראה פרנואידי, אבל סחף נקודה צפה (floating-point drift) יחזיר לכם 1.0000001 ו-asin יחזיר NaN — ו-NaN יחיד ירעיל כל פריימ במורד הזרם. קיבוע הגנתי סביב פונקציות טריגונומטריות הפוכות הוא נושא חוזר בבסיס קוד זה.

2. תנועת מצלמה חלקה: קווטרניונים, לא זוויות אוילר

הגישה הנאיבית היא לאחסן yaw/pitch בכל keyframe ולבצע אינטרפולציה ליניארית של המספרים. זה נראה נורא. אינטרפולציה של זוויות אוילר גורמת ל-gimbal lock, מהירות זוויתית לא אחידה, וקפיצות מכוערות ליד הקטבים.

אנו מבצעים אינטרפולציה של כיוון כקווטרניון באמצעות SLERP (אינטרפולציה ליניארית כדורית). כל כיוון מומר לקווטרניון באמצעות מוסכמת ZYX Euler:

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);
}

ול-SLERP עצמו יש שני שסתומי בטיחות ברמת ייצור שכל יישום זקוק להם:

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();
}

שני פרטים בלתי ניתנים למשא ומתן:

  1. חצי הכדור הקצר ביותר — q ו-−q מייצגים את אותו כיוון. אם המכפלה הסקלרית (dot product) שלילית, אתם מנטרלים קלט אחד או שהמצלמה שלכם לוקחת את הדרך הארוכה סביב הכדור.
  2. גיבוי LERP כמעט מקביל — כאשר שני keyframes כמעט זהים, sinTheta0 → 0 ואתם מחלקים כמעט באפס. מעבר ל-LERP רגיל מעל dot > 0.9995 מונע את ההתפוצצות.

החלטת עיצוב עדינה אך חשובה: אנו מבצעים SLERP לכיוון, אבל LERP ל-FOV/זום ו-angle-LERP ל-roll. אינטרפולציה כדורית היא הכלי הנכון ל-*כיוון*; זום הוא פשוט סקלר וצריך לנוע באופן ליניארי:

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);

💡 כאשר ממירים בחזרה מקווטרניון ל-yaw/pitch, אנו משתמשים ב-atan2 במקום asin "ליציבות נומרית (נמנע מבעיות תחום asin)." לאחר SLERP, הקווטרניון יכול להיות מעט מאוד לא מנורמל, ו-atan2 מתדרדר בחן בעוד asin זורק NaN.

Easing: מיוצר בעבודת יד, במעבר הנכנס

כל keyframe מכיל את עקומת ה-easing עבור המעבר אליו. העקומות נכתבות ידנית כך שהן זהות בכל פלטפורמה:

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°: הבאג שרודף כל תכונת 360

הנה המלכודת. Yaw חי על מעגל: +179° ו-−179° מרוחקים ב-2°, לא ב-358°. ברגע שהנושא שלכם עובר מצד אחד של הכדור לצד השני, מתמטיקה נאיבית מקפיצה את המצלמה חצי דרך סביב העולם. בעיה יחידה זו מופיעה בארבע שכבות שונות של המערכת, וכל אחת דורשת תיקון משלה.

שכבה 1 — פרימיטיבים. שני כלים קטנים שכל בסיס הקוד נשען עליהם:

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;
}

שכבה 2 — יצירת keyframe צוברת yaw רציף (בלתי מוגבל) כך ש-SLERP תמיד מבצע אינטרפולציה בדרך הקצרה. במקום לקבוע כל דגימה ל-±180°, אנו סוכמים את הדלתאות הקצרות ביותר ומניחים לערך הרץ לחרוג מ-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);

שכבה 3 — שחזור bounding box. כאשר המשתמש מצייר תיבה שחוצה את התפר, ארבע הפינות מדווחות על yaws כמו +170°, +175°, −175°, −170°. SphericalProjection מזהה זאת (כל פינה > 90° וכל פינה < −90°) ובונה מחדש bbox "בטוח לתפר" עם yaw רציף על ידי הזזת הערכים השליליים ב- +360° לפני מדידת הטווח שלו.

שכבה 4 — הנגן החי מחליק לעבר המטרה שלו באמצעות shortestYawDelta כך שגרירה לאורך התפר לעולם לא גורמת לקפיצות פתאומיות.

אם אתם זוכרים דבר אחד על בניית כלי 360: התפר אינו מקרה קצה (edge case), הוא עניין חוצה רוחב (cross-cutting concern). תקצבו אותו בכל מקום שבו מופיע yaw.

4. לגרום לזה להרגיש כמו Insta360/GoPro

אינטרפולציה חלקה נותנת לכם מצלמה נכונה מבחינה טכנית. היא לא נותנת לכם מצלמה טובה. שלושה רכיבים של "הנדסת תחושה" סוגרים את הפער הזה.

אילוצי תנועה — הגבלת מהירות ותאוצה. נתוני מעקב גולמיים רועדים (jittery). אנו מעבירים keyframes דרך MotionConstraints, אשר מגביל את מהירות וחדות תנועת המצלמה הווירטואלית (לדוגמה: maxYawSpeed = 120°/s, maxPitchSpeed = 90°/s, maxAcceleration = 180°/s²), עם הגדרות קבועות מראש (presets) של cinematic(), responsive() ו-production(). זהו ההבדל בין "מצלמת אבטחה שעוקבת אחרי זבוב" לבין "מפעיל עם יד יציבה".

FOV אדפטיבי — זום להתאמת הנושא. אנו מודדים את הגודל הזוויתי של הנושא במרחב equirectangular ובוחרים FOV כך שהוא ימלא חלק יעד מהפריימ:

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°

הובלת תנועה + שילוב פנים→גוף. אנו מזיזים את המיסגור בכ-15% מרוחב ה-bbox קדימה בכיוון התנועה, כך שהנושא לא יהיה צמוד לקצה שאליו הוא מתקדם. וכאשר זיהוי הפנים נכשל, איננו קופצים למרכז הגוף — אנו מבצעים LERP ממרכז הפנים האחרון הידוע למרכז הגוף במשך כ-10 פריימים.

5. טריק שני הווידאו: פרוקסי תצוגה מקדימה מול מקור ברזולוציה מלאה

זוהי ההחלטה הארכיטקטונית שהופכת את העניין כולו לשמיש בטלפונים אמיתיים.

קובץ GoPro/Insta360 360° הוא לעיתים קרובות 4096×2048 HEVC. מפענחי Android תקציביים נחנקים בניסיון להפעיל זאת בצורה חלקה, קל וחומר תוך כדי הפעלת מעקב אובייקטים (object tracking) מעל. לכן אנו מריצים מעקב ותצוגה מקדימה חיה על פרוקסי שהומר (~1440×720, libx264 -preset veryfast -crf 28), ושומרים את המקור ברזולוציה מלאה לייצוא הסופי בלבד.

// "יותר אמין מ-MediaMetadataRetriever עבור סרטוני 360°
//  אשר משתמשים לעיתים קרובות בקודקים לא שגרתיים (HEVC, ברזולוציה גבוהה)"
final maxSafeWidth = 2048, maxSafeHeight = 1080;
// 2:1 4K (4096×2048) אינו בטוח -> מפעיל transcodeForPlayback()

המלכוד שאתם חייבים לטפל בו: bounding boxes של keyframe מחושבים בקואורדינטות פרוקסי וחייבים לעבור סקאלה לקואורדינטות המקוריות לפני הייצוא. אם תבצעו את הסקאלה הזו באופן שגוי, הייצוא שלכם ימסגר אזור שונה במקצת מזה שהמשתמש צפה בו בתצוגה המקדימה. (אנו משתמשים גם ב-ffprobe, לא ב-MediaMetadataRetriever, עבור מטא נתונים וחילוץ פריימ ראשון — המאחזר המובנה של Android לא יציב (flaky) עם הקודקים הלא שגרתיים שמצלמות 360 מפיקות).

6. ייצוא GPU: OpenGL + MediaCodec מעל MethodChannel

היטול מחדש של כל פריימ מכדור 4K דרך פילטר v360 של FFmpeg על מעבד טלפון איטי מדי. (אנו כן שומרים את מחרוזת v360 — היא מתעדת בצורה מסודרת את סמנטיקת ההיטל, v360=e:flat:... שמשמעותה "equirectangular פנימה, שטוח החוצה" — אבל זה לא הנתיב החי).

הייצוא האמיתי מעביר את ה-keyframes לרנדרר (renderer) מקורי של OpenGL ES + MediaCodec דרך MethodChannel:

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
,
});

אנו מגבילים זאת מאחורי בדיקת יכולת (Android API 21+, מקודד חומרה H.264, OpenGL ES 3.0) ומזרימים התקדמות בחזרה דרך EventChannel. כמה מלכודות שנלמדו בדרך הקשה:

  • הסטת yaw של −90°. מוסכמת Dart שלנו ממקמת yaw = 0 ב-*מרכז* תמונת ה-equirectangular (u = 0.5); רנדרר ה-OpenGL ממקם yaw = 0 ב-u = 0.75. לכן כל yaw של keyframe ממופה מחדש ב-−90° (ונעטף מחדש ל-±180°) במעבר דרך הערוץ — מיושם *סימטרית* הן לערוץ הייצוא והן לערוץ הנגן החי, כך שהתצוגה המקדימה והייצוא תואמים.
  • מספר keyframe מנוון. אפס keyframes → סינתזה של שניים זהים מהכיוון ההתחלתי (ייצוא סטטי). keyframe אחד → שכפולו. הרנדרר תמיד רוצה לפחות התחלה וסוף.
  • חיתוך (Trimming) משנה את בסיס חותמות הזמן. ייצוא חתוך מזיז כל keyframe ב-t − trimStartMs.

המעבר המאוחר של FFmpeg שאף אחד לא מצפה לו

ייתכן שתחשבו שייצוא GPU הוא קו הסיום. אבל לא — מכיוון ש-MediaMuxer של Android כותב את ה-moov atom ב-*סוף* הקובץ ויכול לשאת את היסט ה-PTS של השמע מהמקור. התוצאה: ExoPlayer (ו-video_player של Flutter) מקפיא את התמונה בזמן שהשעון ממשיך לרוץ. לכן, לאחר מעבר ה-GPU, FFmpeg מבצע ניקוי מהיר וללא אובדן איכות:

// 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

ומלכוד קטן ואכזרי השומר עליו: אנו בודקים קודם קיומו של ערוץ שמע, מכיוון שהעברת דגלי שמע לווידאו שאין לו ערוץ שמע גורמת ל-FFmpeg להיתקע לנצח במכשירים מסוימים. כל המעבר שלאחר מכן עטוף בפסק זמן של 60 שניות עם ביטול בעת פסק זמן.

7. התצוגה המקדימה החיה היא גם נגן מקורי

התצוגה המקדימה בעורך אינה Flutter CustomPaint — היא משטח OpenGL מקורי המוטמע באמצעות AndroidView (עם viewType כמו 'your_app/spherical360player'), עם ערוץ method/event משלו לכל תצוגה. ה-Dart Spherical360PlayerController משקף yaw/pitch/roll/fov ומיישם את אותה הסטת −90° לפני השליחה.

פרטי התחושה שהופכים אותו לנעים:

  • החלקה בלתי תלויה בקצב הפריימים — factor = 1 - exp(-speed * dt) כך שה-easing נראה זהה ב-60fps וב-90fps, עם yaw בטוח-לתפר בדרך הקצרה ביותר מובנה.
  • מומנטום/אינרציה לאחר הטלה, עם דעיכה מבוססת חיכוך — מושבת במהלך ההשמעה, מכיוון שבמהלך ההשמעה המצלמה חייבת לציית ל-keyframes, לא להקפצה האחרונה של המשתמש.
  • GestureDetector אחד שפעולת onScaleUpdate שלו מבצעת pan ו-pinch-zoom יחד; לחיצה כפולה מחליפה בין הפעלה/השהיה. FOV מוגבל ל-30°–120°, pitch ל-±90°, בגבול הבקר.

תיקון UX קטן אך משמעותי: לפני המעבר מ"מעקב" ל"יצירת keyframes", אנו מכניסים עיכוב של 120 מילישניות כדי ש-Flutter יצייר בפועל "100%". בלעדיו, פריימ ההתקדמות הסופי ושינוי הסטטוס נוחתים באותה אצווה של microtask והמשתמש לעולם לא רואה השלמה.

מכונת מצבים, בקצרה

כל זה מתואם על ידי Riverpod ReframeNotifier המניע enum סטטוס:

idle → loadingVideo → transcoding
    → (detecting | selectingObject | drawingBboxOnPlayer | autoDetectingOnPlayer)
    → tracking → generatingKeyframes → editing → exporting → completed | error

נקודות כניסה מרובות מזינות את אותו עוקב: הקש-לזיהוי (YOLO overlay), bbox ידני, או מחוות הציור-ישירות-על-הנגן בסגנון Insta360. כולם מתכנסים ל-KeyframeGenerator.production().generate(...) → KeyframeSmoother.smooth(...), ומשם האינטרפולטור מניע כל פריימ בתצוגה מקדימה ופריימ מיוצא.

לקחים שנלמדו

  1. שמרו על המתמטיקה טהורה. אין ייבואי Flutter בשכבת ההיטל/אינטרפולציה. זו הסיבה היחידה שהדבר היה ניתן לבדיקה.
  2. SLERP קווטרניון לכיוון; LERP לסקלרים. אל תבצעו אינטרפולציה לזוויות אוילר, ואל תבצעו SLERP לזום שלכם.
  3. תפר ±180° הוא עניין חוצה רוחב, לא מקרה קצה (edge case). הוא מופיע מחדש בפרימיטיבים, יצירה, היטל, והנגן.
  4. התאימו את המוסכמות שלכם פעם אחת, באופן סימטרי. הסטת yaw של −90° בין Dart ל-OpenGL מיושמת בשני גבולות הערוצים כך שתצוגה מקדימה == ייצוא.
  5. פרוקסי לאינטראקציה, מקור לפלט. רק זכרו לשנות קנה מידה לקואורדינטות שלכם בין השניים.
  6. "רינדור הושלם" ≠ "מתנגן נכון." תקצבו מעבר FFmpeg מאוחר לתיקון מיקום moov atom ו-audio PTS, ובדקו לפני שתוסיפו דגלי שמע.
  7. "תחושה" היא תכונה. אילוצי תנועה, FOV אדפטיבי, והובלת תנועה הם מה שמפריד בין "ממוסגר מחדש טכנית" לבין "נראה כאילו מקצוען צילם את זה".

התוצאה: עורך מיסגור מחדש (reframe editor) של 360° מובנה במכשיר — הקישו על נושא, תנו לו לעקוב, כווננו את ה-keyframes, וייצאו וידאו שטוח ונקי — בנוי ב-Flutter עם שכבת GPU מקורית דקה המבצעת רק את העבודה ש-Dart לא יכול לבצע מספיק מהר.

Flutter360VideoOpenGLQuaternions
Saurav Kumar Gupta's image

אודות המחבר

Saurav Kumar Gupta

AI & Cloud Solutions Expert at MicrocosmWorks

Building innovative AI-powered solutions and helping businesses transform through cutting-edge technology.

רוצים ללמוד עוד?

צרו קשר לדון כיצד נוכל לעזור ליישם פתרונות אלו עבור העסק שלכם.

צרו קשר

שאלות נפוצות

360° video reframing is the process of converting equirectangular 360° footage into a standard flat video by controlling a virtual camera's direction, zoom, and field of view over time.

Quaternions enable smooth camera rotations without gimbal lock or abrupt transitions, making them ideal for creating natural-looking camera movements in 360° video reframing.

Flutter uses spherical projection for live previews, quaternion-based interpolation for camera motion, and a native OpenGL GPU pipeline to export high-quality reframed videos efficiently.

Accurate reframing requires handling spherical projection, seamless rotation across the ±180° boundary, smooth camera interpolation, and synchronized GPU rendering to produce stable, cinematic video output.

GPU acceleration significantly reduces rendering time by processing projection and video encoding on the graphics hardware, making high-resolution 360° video export practical on mobile devices.

Comments (0)

Share your thoughts and join the conversation

Leave a Comment

Your email will not be published

No comments yet

Be the first to share your thoughts!