☀️ Solar Stereo

59 aligned stereo pairs scheduled across solar angles. Drag the slider to sweep the sun's azimuth.

Session Loading...
Left stereo image
Solar Azimuth: --°  |  Altitude: --°  |  Date: --
Focal Shift -3.3%

📄 Capture Graph Code

G3C.py

"""G3C stereographic recapture, scheduled across daylight solar conditions.

Each session is an aligned stereo pair plus the surrounding photos from both
viewpoints. The server offers each participant a capture time whose solar
angle the dataset is still missing, one distinct time per person.
"""

from datetime import UTC, datetime, timedelta

import capturegraph as cg
import capturegraph.scheduling as cgsh
from server import authoring as cgserver
from server.notifications import hourly_bucket, notification_candidates

SELECTIONS = 3
BOOK = "g3c-schedule"


class StereoSession(cg.Struct):
    user_id: cg.UserID
    weather: cg.Weather
    left: cg.Image
    right: cg.Image
    left_surrounding: cg.Array[cg.Image]
    right_surrounding: cg.Array[cg.Image]
    _metadata: cg.Metadata


class G3C(cg.Struct):
    instructions: cg.Image
    G3C: cg.Map[cg.Date, StereoSession]
    _metadata: cg.Metadata


@cgserver.target
@cg.procedure(G3C)
def g3c(root: cg.Procedure[cg.Path[G3C]]) -> None:
    root._metadata._location |= cg.UserInputLocation()
    root._metadata._thumbnail |= cg.ConvertImageToThumbnail(image=root.instructions.load())

    cg.do(
        cg.ShowInstructions(
            text=(
                "Capture two aligned images from the locations numbered 1 and 2 "
                "in the reference image. Then take panoramic photos from both."
            ),
            image=root.instructions.load(),
        )
    )
    cg.do(cg.ShowLocationGuide(target=root._metadata._location, threshold_meters=100.0))

    session = root.G3C[cg.CaptureTime()]
    session.user_id &= cg.GetUserID()
    session.weather &= cg.CaptureWeather(label="Record current weather")

    session.left &= cg.CaptureImage(label="Capture Left Photo (1)", encoding=cg.ImageEncoding.RAW)
    session.left_surrounding &= cg.CaptureImageSequence(
        label="Capture Panoramic Photos (1)", encoding=cg.ImageEncoding.RAW
    )
    session.right &= cg.CaptureImage(label="Capture Right Photo (2)", encoding=cg.ImageEncoding.RAW)
    session.right_surrounding &= cg.CaptureImageSequence(
        label="Capture Panoramic Photos (2)", encoding=cg.ImageEncoding.RAW
    )
    session._metadata._thumbnail &= cg.ConvertImageToThumbnail(image=session.left)


@cgserver.intercept(G3C, lambda s: s._metadata._next_notification)
def notification(profile: cgserver.RequestProfile, target: G3C) -> cg.Time:
    now = datetime.now(UTC)
    location = target._metadata._location
    if cg.is_missing(location):
        location = profile.location
    if location is None:
        return cg.Time(now + timedelta(hours=1))

    candidates = notification_candidates(
        target.G3C,
        location,
        hourly_bucket(now),
        selections=SELECTIONS,
        daylight_only=True,
    )
    chosen = cgsh.ReservationBook.open(BOOK).reserve(
        profile.user_id,
        [candidate.value for candidate in candidates],
        captured=max(target.G3C.to_dict(), default=None),
        grace=timedelta(minutes=30),
        now=now,
    )
    return cg.Time(chosen if chosen is not None else now + timedelta(hours=1))

🎬 Capture Video