using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using UnityEngine.Rendering;
namespace UnityEngine.Perception.GroundTruth
{
///
/// Produces keypoint annotations for a humanoid model. This labeler supports generic
/// . Template values are mapped to rigged
/// . Custom joints can be
/// created by applying to empty game objects at a body
/// part's location.
///
[Serializable]
public sealed class KeypointLabeler : CameraLabeler
{
///
/// The active keypoint template. Required to annotate keypoint data.
///
public KeypointTemplate activeTemplate;
///
public override string description
{
get => "Produces keypoint annotations for all visible labeled objects that have a humanoid animation avatar component.";
protected set { }
}
///
protected override bool supportsVisualization => true;
// ReSharper disable MemberCanBePrivate.Global
///
/// The GUID id to associate with the annotations produced by this labeler.
///
public string annotationId = "8b3ef246-daa7-4dd5-a0e8-a943f6e7f8c2";
///
/// The which associates objects with labels.
///
public IdLabelConfig idLabelConfig;
// ReSharper restore MemberCanBePrivate.Global
AnnotationDefinition m_AnnotationDefinition;
Texture2D m_MissingTexture;
Dictionary keypoints)> m_AsyncAnnotations;
List m_ToReport;
int m_CurrentFrame;
///
/// Action that gets triggered when a new frame of key points are computed.
///
public event Action> KeypointsComputed;
///
/// Creates a new key point labeler. This constructor creates a labeler that
/// is not valid until a and
/// are assigned.
///
public KeypointLabeler() { }
///
/// Creates a new key point labeler.
///
/// The Id label config for the labeler
/// The active keypoint template
public KeypointLabeler(IdLabelConfig config, KeypointTemplate template)
{
this.idLabelConfig = config;
this.activeTemplate = template;
}
///
/// Array of animation pose labels which map animation clip times to ground truth pose labels.
///
public List animationPoseConfigs;
///
protected override void Setup()
{
if (idLabelConfig == null)
throw new InvalidOperationException($"{nameof(KeypointLabeler)}'s idLabelConfig field must be assigned");
m_AnnotationDefinition = DatasetCapture.RegisterAnnotationDefinition("keypoints", new []{TemplateToJson(activeTemplate)},
"pixel coordinates of keypoints in a model, along with skeletal connectivity data", id: new Guid(annotationId));
// Texture to use in case the template does not contain a texture for the joints or the skeletal connections
m_MissingTexture = new Texture2D(1, 1);
m_KnownStatus = new Dictionary();
m_AsyncAnnotations = new Dictionary)>();
m_ToReport = new List();
m_CurrentFrame = 0;
perceptionCamera.InstanceSegmentationImageReadback += OnInstanceSegmentationImageReadback;
}
bool AreEqual(Color32 lhs, Color32 rhs)
{
return lhs.r == rhs.r && lhs.g == rhs.g && lhs.b == rhs.b && lhs.a == rhs.a;
}
bool PixelOnScreen(int x, int y, (int x, int y) dimensions)
{
return x >= 0 && x < dimensions.x && y >= 0 && y < dimensions.y;
}
bool PixelsMatch(int x, int y, Color32 idColor, (int x, int y) dimensions, NativeArray data)
{
var h = dimensions.y - y;
var pixelColor = data[h * dimensions.x + x];
return AreEqual(pixelColor, idColor);
}
static int s_PixelTolerance = 1;
// Determine the state of a keypoint. A keypoint is considered visible (state = 2) if it is on screen and not occluded
// by another object. The way that we determine if a point is occluded is by checking the pixel location of the keypoint
// against the instance segmentation mask for the frame. The instance segmentation mask provides the instance id of the
// visible object at a pixel location. Which means, if the keypoint does not match the visible pixel, then another
// object is in front of the keypoint occluding it from view. An important note here is that the keypoint is an infintely small
// point in space, which can lead to false negatives due to rounding issues if the keypoint is on the edge of an object or very
// close to the edge of the screen. Because of this we will test not only the keypoint pixel, but also the immediate surrounding
// pixels to determine if the pixel is really visible. This method returns 1 if the pixel is not visible but on screen, and 0
// if the pixel is off of the screen (taken the tolerance into account).
int DetermineKeypointState(Keypoint keypoint, Color32 instanceIdColor, (int x, int y) dimensions, NativeArray data)
{
if (keypoint.state == 0) return 0;
var centerX = Mathf.RoundToInt(keypoint.x);
var centerY = Mathf.RoundToInt(keypoint.y);
var pixelOnScreen = false;
for (var y = centerY - s_PixelTolerance; y <= centerY + s_PixelTolerance; y++)
{
for (var x = centerX - s_PixelTolerance; x <= centerX + s_PixelTolerance; x++)
{
if (!PixelOnScreen(x, y, dimensions)) continue;
pixelOnScreen = true;
if (PixelsMatch(x, y, instanceIdColor, dimensions, data))
{
return 2;
}
}
}
return pixelOnScreen ? 1 : 0;
}
void OnInstanceSegmentationImageReadback(int frameCount, NativeArray data, RenderTexture renderTexture)
{
if (!m_AsyncAnnotations.TryGetValue(frameCount, out var asyncAnnotation))
return;
m_AsyncAnnotations.Remove(frameCount);
var dimensions = (renderTexture.width, renderTexture.height);
m_ToReport.Clear();
foreach (var keypointSet in asyncAnnotation.keypoints)
{
if (InstanceIdToColorMapping.TryGetColorFromInstanceId(keypointSet.Key, out var idColor))
{
var shouldReport = false;
foreach (var keypoint in keypointSet.Value.keypoints)
{
keypoint.state = DetermineKeypointState(keypoint, idColor, dimensions, data);
if (keypoint.state == 0)
{
keypoint.x = 0;
keypoint.y = 0;
}
else
{
shouldReport = true;
}
}
if (shouldReport)
m_ToReport.Add(keypointSet.Value);
}
}
KeypointsComputed?.Invoke(frameCount, m_ToReport);
asyncAnnotation.annotation.ReportValues(m_ToReport);
}
///
///
protected override void OnEndRendering(ScriptableRenderContext scriptableRenderContext)
{
m_CurrentFrame = Time.frameCount;
var annotation = perceptionCamera.SensorHandle.ReportAnnotationAsync(m_AnnotationDefinition);
var keypoints = new Dictionary();
m_AsyncAnnotations[m_CurrentFrame] = (annotation, keypoints);
foreach (var label in LabelManager.singleton.registeredLabels)
ProcessLabel(label);
}
// ReSharper disable InconsistentNaming
// ReSharper disable NotAccessedField.Global
// ReSharper disable NotAccessedField.Local
///
/// Record storing all of the keypoint data of a labeled gameobject.
///
[Serializable]
public class KeypointEntry
{
///
/// The label id of the entity
///
public int label_id;
///
/// The instance id of the entity
///
public uint instance_id;
///
/// The template that the points are based on
///
public string template_guid;
///
/// Pose ground truth for the current set of keypoints
///
public string pose = "unset";
///
/// Array of all of the keypoints
///
public Keypoint[] keypoints;
}
///
/// The values of a specific keypoint
///
[Serializable]
public class Keypoint
{
///
/// The index of the keypoint in the template file
///
public int index;
///
/// The keypoint's x-coordinate pixel location
///
public float x;
///
/// The keypoint's y-coordinate pixel location
///
public float y;
///
/// The state of the point,
/// 0 = not present,
/// 1 = keypoint is present but not visible,
/// 2 = keypoint is present and visible
///
public int state;
}
// ReSharper restore InconsistentNaming
// ReSharper restore NotAccessedField.Global
// ReSharper restore NotAccessedField.Local
float GetCaptureHeight()
{
var targetTexture = perceptionCamera.attachedCamera.targetTexture;
return targetTexture != null ?
targetTexture.height : Screen.height;
}
// Converts a coordinate from world space into pixel space
Vector3 ConvertToScreenSpace(Vector3 worldLocation)
{
var pt = perceptionCamera.attachedCamera.WorldToScreenPoint(worldLocation);
pt.y = GetCaptureHeight() - pt.y;
return pt;
}
struct CachedData
{
public bool status;
public Animator animator;
public KeypointEntry keypoints;
public List<(JointLabel, int)> overrides;
}
Dictionary m_KnownStatus;
bool TryToGetTemplateIndexForJoint(KeypointTemplate template, JointLabel joint, out int index)
{
index = -1;
foreach (var jointTemplate in joint.templateInformation.Where(jointTemplate => jointTemplate.template == template))
{
for (var i = 0; i < template.keypoints.Length; i++)
{
if (template.keypoints[i].label == jointTemplate.label)
{
index = i;
return true;
}
}
}
return false;
}
bool DoesTemplateContainJoint(JointLabel jointLabel)
{
foreach (var template in jointLabel.templateInformation)
{
if (template.template == activeTemplate)
{
if (activeTemplate.keypoints.Any(i => i.label == template.label))
{
return true;
}
}
}
return false;
}
void ProcessLabel(Labeling labeledEntity)
{
if (!idLabelConfig.TryGetLabelEntryFromInstanceId(labeledEntity.instanceId, out var labelEntry))
return;
// Cache out the data of a labeled game object the first time we see it, this will
// save performance each frame. Also checks to see if a labeled game object can be annotated.
if (!m_KnownStatus.ContainsKey(labeledEntity.instanceId))
{
var cached = new CachedData()
{
status = false,
animator = null,
keypoints = new KeypointEntry(),
overrides = new List<(JointLabel, int)>()
};
var entityGameObject = labeledEntity.gameObject;
cached.keypoints.instance_id = labeledEntity.instanceId;
cached.keypoints.label_id = labelEntry.id;
cached.keypoints.template_guid = activeTemplate.templateID;
cached.keypoints.keypoints = new Keypoint[activeTemplate.keypoints.Length];
for (var i = 0; i < cached.keypoints.keypoints.Length; i++)
{
cached.keypoints.keypoints[i] = new Keypoint { index = i, state = 0 };
}
var animator = entityGameObject.transform.GetComponentInChildren();
if (animator != null)
{
cached.animator = animator;
cached.status = true;
}
foreach (var joint in entityGameObject.transform.GetComponentsInChildren())
{
if (TryToGetTemplateIndexForJoint(activeTemplate, joint, out var idx))
{
cached.overrides.Add((joint, idx));
cached.status = true;
}
}
m_KnownStatus[labeledEntity.instanceId] = cached;
}
var cachedData = m_KnownStatus[labeledEntity.instanceId];
if (cachedData.status)
{
var animator = cachedData.animator;
var keypoints = cachedData.keypoints.keypoints;
// Go through all of the rig keypoints and get their location
for (var i = 0; i < activeTemplate.keypoints.Length; i++)
{
var pt = activeTemplate.keypoints[i];
if (pt.associateToRig)
{
var bone = animator.GetBoneTransform(pt.rigLabel);
if (bone != null)
{
var loc = ConvertToScreenSpace(bone.position);
keypoints[i].index = i;
keypoints[i].x = loc.x;
keypoints[i].y = loc.y;
keypoints[i].state = 2;
}
}
}
// Go through all of the additional or override points defined by joint labels and get
// their locations
foreach (var (joint, idx) in cachedData.overrides)
{
var loc = ConvertToScreenSpace(joint.transform.position);
keypoints[idx].index = idx;
keypoints[idx].x = loc.x;
keypoints[idx].y = loc.y;
keypoints[idx].state = 2;
}
cachedData.keypoints.pose = "unset";
if (cachedData.animator != null)
{
cachedData.keypoints.pose = GetPose(cachedData.animator);
}
m_AsyncAnnotations[m_CurrentFrame].keypoints[labeledEntity.instanceId] = cachedData.keypoints;
}
}
string GetPose(Animator animator)
{
var info = animator.GetCurrentAnimatorClipInfo(0);
if (info != null && info.Length > 0)
{
var clip = info[0].clip;
var timeOffset = animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
if (animationPoseConfigs != null)
{
foreach (var p in animationPoseConfigs)
{
if (p != null && p.animationClip == clip)
{
var time = timeOffset;
var label = p.GetPoseAtTime(time);
return label;
}
}
}
}
return "unset";
}
Keypoint GetKeypointForJoint(KeypointEntry entry, int joint)
{
if (joint < 0 || joint >= entry.keypoints.Length) return null;
return entry.keypoints[joint];
}
///
protected override void OnVisualize()
{
if (m_ToReport == null) return;
var jointTexture = activeTemplate.jointTexture;
if (jointTexture == null) jointTexture = m_MissingTexture;
var skeletonTexture = activeTemplate.skeletonTexture;
if (skeletonTexture == null) skeletonTexture = m_MissingTexture;
foreach (var entry in m_ToReport)
{
foreach (var bone in activeTemplate.skeleton)
{
var joint1 = GetKeypointForJoint(entry, bone.joint1);
var joint2 = GetKeypointForJoint(entry, bone.joint2);
if (joint1 != null && joint1.state == 2 && joint2 != null && joint2.state == 2)
{
VisualizationHelper.DrawLine(joint1.x, joint1.y, joint2.x, joint2.y, bone.color, 8, skeletonTexture);
}
}
foreach (var keypoint in entry.keypoints)
{
if (keypoint.state == 2)
VisualizationHelper.DrawPoint(keypoint.x, keypoint.y, activeTemplate.keypoints[keypoint.index].color, 8, jointTexture);
}
}
}
// ReSharper disable InconsistentNaming
// ReSharper disable NotAccessedField.Local
[Serializable]
struct JointJson
{
public string label;
public int index;
public Color color;
}
[Serializable]
struct SkeletonJson
{
public int joint1;
public int joint2;
public Color color;
}
[Serializable]
struct KeypointJson
{
public string template_id;
public string template_name;
public JointJson[] key_points;
public SkeletonJson[] skeleton;
}
// ReSharper restore InconsistentNaming
// ReSharper restore NotAccessedField.Local
KeypointJson TemplateToJson(KeypointTemplate input)
{
var json = new KeypointJson();
json.template_id = input.templateID;
json.template_name = input.templateName;
json.key_points = new JointJson[input.keypoints.Length];
json.skeleton = new SkeletonJson[input.skeleton.Length];
for (var i = 0; i < input.keypoints.Length; i++)
{
json.key_points[i] = new JointJson
{
label = input.keypoints[i].label,
index = i,
color = input.keypoints[i].color
};
}
for (var i = 0; i < input.skeleton.Length; i++)
{
json.skeleton[i] = new SkeletonJson()
{
joint1 = input.skeleton[i].joint1,
joint2 = input.skeleton[i].joint2,
color = input.skeleton[i].color
};
}
return json;
}
}
}