Newer
Older
oak-d_proto / pose3d_utils.py
@T.Nakaguchi T.Nakaguchi 21 days ago 8 KB 最初のコミット
from __future__ import annotations

import numpy as np


JOINT_NAMES = [
    "Nose",
    "LEye",
    "REye",
    "LEar",
    "REar",
    "LShoulder",
    "RShoulder",
    "LElbow",
    "RElbow",
    "LWrist",
    "RWrist",
    "LHip",
    "RHip",
    "LKnee",
    "RKnee",
    "LAnkle",
    "RAnkle",
]


SKELETON = [
    (0, 1),
    (0, 2),
    (1, 3),
    (2, 4),

    (5, 6),

    (5, 7),
    (7, 9),

    (6, 8),
    (8, 10),

    (5, 11),
    (6, 12),

    (11, 12),

    (11, 13),
    (13, 15),

    (12, 14),
    (14, 16),
]


# ============================================================
# 3D alpha-beta tracker
# ============================================================

class JointTracker3D:

    def __init__(
        self,
        nominal_fps=30.0,
        alpha=0.55,
        beta=0.08,
        max_speed_mps=6.0,
        base_gate_m=0.10,
        max_missed_frames=6,
    ):

        self.nominal_fps = (
            nominal_fps
        )

        self.alpha = alpha
        self.beta = beta

        self.max_speed_mps = (
            max_speed_mps
        )

        self.base_gate_m = (
            base_gate_m
        )

        self.max_missed_frames = (
            max_missed_frames
        )

        self.position = None

        self.velocity = np.zeros(
            3,
            dtype=np.float64
        )

        self.last_time = None

        self.missed = 0

    def reset(self):

        self.position = None

        self.velocity[:] = 0.0

        self.last_time = None

        self.missed = 0

    def get_predicted_z(self):

        if self.position is None:
            return None

        return float(
            self.position[2]
        )

    def update(
        self,
        measurement,
        now
    ):

        # First observation
        if self.position is None:

            if measurement is None:
                return None, False

            self.position = (
                measurement.astype(
                    np.float64
                )
            )

            self.velocity[:] = 0.0

            self.last_time = now

            self.missed = 0

            return (
                self.position.copy(),
                True
            )

        # dt
        if self.last_time is None:

            dt = (
                1.0
                / self.nominal_fps
            )

        else:

            dt = (
                now
                - self.last_time
            )

        dt = float(
            np.clip(
                dt,
                1.0 / 120.0,
                0.20
            )
        )

        # Constant velocity prediction
        predicted = (
            self.position
            + self.velocity * dt
        )

        accepted = False

        if measurement is not None:

            error = (
                measurement
                - predicted
            )

            error_norm = float(
                np.linalg.norm(error)
            )

            gate = (
                self.base_gate_m
                + self.max_speed_mps * dt
            )

            # Accept measurement
            if error_norm <= gate:

                self.position = (
                    predicted
                    + self.alpha * error
                )

                self.velocity = (
                    self.velocity
                    + (self.beta / dt)
                    * error
                )

                speed = float(
                    np.linalg.norm(
                        self.velocity
                    )
                )

                if (
                    speed
                    > self.max_speed_mps
                ):

                    self.velocity *= (
                        self.max_speed_mps
                        / speed
                    )

                self.missed = 0

                accepted = True

            # Reject sudden jump
            else:

                self.position = (
                    predicted
                )

                self.velocity *= 0.90

                self.missed += 1

        # Pose/Depth missing
        else:

            self.position = predicted

            self.velocity *= 0.90

            self.missed += 1

        self.last_time = now

        if (
            self.missed
            > self.max_missed_frames
        ):

            self.reset()

            return None, False

        return (
            self.position.copy(),
            accepted
        )


# ============================================================
# Robust Depth sampling
# ============================================================

def robust_joint_depth(
    depth_mm,
    u,
    v,
    previous_z_m,
    *,
    min_depth_m=0.30,
    max_depth_m=5.00,
    roi_radius=4,
    min_pixels=4,
    previous_depth_gate_m=0.40,
    center_depth_gate_m=0.25,
):

    h, w = depth_mm.shape

    u_i = int(round(u))
    v_i = int(round(v))

    if not (
        0 <= u_i < w
        and 0 <= v_i < h
    ):
        return None

    x0 = max(
        0,
        u_i - roi_radius
    )

    x1 = min(
        w,
        u_i + roi_radius + 1
    )

    y0 = max(
        0,
        v_i - roi_radius
    )

    y1 = min(
        h,
        v_i + roi_radius + 1
    )

    patch = depth_mm[
        y0:y1,
        x0:x1
    ].astype(
        np.float32
    )

    min_mm = (
        min_depth_m * 1000.0
    )

    max_mm = (
        max_depth_m * 1000.0
    )

    values = patch[
        (patch > min_mm)
        &
        (patch < max_mm)
    ]

    if len(values) < min_pixels:
        return None

    # --------------------------------------------------------
    # Temporal depth continuity
    # --------------------------------------------------------

    if previous_z_m is not None:

        previous_mm = (
            previous_z_m
            * 1000.0
        )

        near_previous = values[
            np.abs(
                values
                - previous_mm
            )
            <
            previous_depth_gate_m
            * 1000.0
        ]

        if (
            len(near_previous)
            >= min_pixels
        ):

            values = (
                near_previous
            )

    # --------------------------------------------------------
    # Spatial continuity
    # --------------------------------------------------------

    else:

        cx0 = max(
            0,
            u_i - 1
        )

        cx1 = min(
            w,
            u_i + 2
        )

        cy0 = max(
            0,
            v_i - 1
        )

        cy1 = min(
            h,
            v_i + 2
        )

        center = depth_mm[
            cy0:cy1,
            cx0:cx1
        ].astype(
            np.float32
        )

        center = center[
            (center > min_mm)
            &
            (center < max_mm)
        ]

        if len(center) >= 2:

            center_depth = float(
                np.median(center)
            )

            near_center = values[
                np.abs(
                    values
                    - center_depth
                )
                <
                center_depth_gate_m
                * 1000.0
            ]

            if (
                len(near_center)
                >= min_pixels
            ):

                values = (
                    near_center
                )

    # --------------------------------------------------------
    # MAD outlier rejection
    # --------------------------------------------------------

    median = float(
        np.median(values)
    )

    mad = float(
        np.median(
            np.abs(
                values - median
            )
        )
    )

    robust_sigma = (
        1.4826 * mad
    )

    tolerance_mm = max(
        40.0,
        3.0 * robust_sigma
    )

    filtered = values[
        np.abs(
            values - median
        )
        <= tolerance_mm
    ]

    if len(filtered) >= min_pixels:
        values = filtered

    return (
        float(
            np.median(values)
        )
        / 1000.0
    )


# ============================================================
# Projection
# ============================================================

def pixel_to_xyz(
    u,
    v,
    z_m,
    K
):

    fx = K[0, 0]
    fy = K[1, 1]

    cx = K[0, 2]
    cy = K[1, 2]

    x = (
        (u - cx)
        * z_m
        / fx
    )

    y = (
        (v - cy)
        * z_m
        / fy
    )

    return np.array(
        [
            x,
            y,
            z_m
        ],
        dtype=np.float64
    )


def xyz_to_pixel(
    xyz,
    K
):

    if xyz is None:
        return None

    x, y, z = xyz

    if z <= 0:
        return None

    fx = K[0, 0]
    fy = K[1, 1]

    cx = K[0, 2]
    cy = K[1, 2]

    u = (
        fx * x / z
        + cx
    )

    v = (
        fy * y / z
        + cy
    )

    return (
        int(round(u)),
        int(round(v))
    )