using System; using System.Collections.Generic; using System.Linq; using Unity.Collections; using Unity.Mathematics; using Unity.Profiling; using Unity.Simulation; using UnityEngine.Experimental.Rendering; 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. /// /// Keypoints are recorded by this labeler with a state value describing if they are present on the model, /// present but not visible, or visible. A keypoint can be listed as not visible for three reasons: it is outside /// of the camera's view frustum, it is occluded by another object in the scene, or it is occluded by itself, for /// example a raised arm in front a model's face could occlude its eyes from being visible. To calculate self /// occlusion values, the keypoint labeler uses tolerances per keypoint to determine if the keypoint is blocked. /// The initial tolerance value for each keypoint is set per keypoint in the file. /// The tolerance of a custom keypoints can be set with the used to create the keypoint. /// Finally, a component be added to a model to apply a universal scaling /// override to all of the keypoint tolerances defined in a keypoint template. /// [Serializable] public sealed class KeypointLabeler : CameraLabeler { // Smaller texture sizes produce assertion failures in the engine const int k_MinTextureWidth = 8; static ProfilerMarker s_OnEndRenderingMarker = new ProfilerMarker($"KeypointLabeler OnEndRendering"); static ProfilerMarker s_OnVisualizeMarker = new ProfilerMarker($"KeypointLabeler OnVisualize"); /// /// 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; /// /// Controls which objects will have keypoints recorded in the dataset. /// /// public KeypointObjectFilter objectFilter; // ReSharper restore MemberCanBePrivate.Global AnnotationDefinition m_AnnotationDefinition; Texture2D m_MissingTexture; Material m_MaterialDepthCheck; Texture2D m_KeypointPositionsTexture; Texture2D m_KeypointCheckDepthTexture; struct FrameKeypointData { public AsyncAnnotation annotation; public int pointsPerEntry; public List keypoints; public bool isDepthCheckComplete; public bool isInstanceSegmentationCheckComplete; public NativeArray objectInfos; } Dictionary m_FrameKeypointData; List m_KeypointEntriesToReport; 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; private ComputeShader m_KeypointDepthTestShader; private RenderTexture m_ResultsBuffer; private RenderTextureReader m_DepthCheckReader; /// 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_FrameKeypointData = new Dictionary(); m_KeypointEntriesToReport = new List(); m_CurrentFrame = 0; m_KeypointDepthTestShader = (ComputeShader) Resources.Load("KeypointDepthTest"); var depthCheckShader = Shader.Find("Perception/KeypointDepthCheck"); var shaderVariantCollection = new ShaderVariantCollection(); m_MaterialDepthCheck = new Material(depthCheckShader); shaderVariantCollection.Add( new ShaderVariantCollection.ShaderVariant(depthCheckShader, PassType.ScriptableRenderPipeline)); shaderVariantCollection.WarmUp(); perceptionCamera.attachedCamera.depthTextureMode = DepthTextureMode.Depth; #if URP_PRESENT var cameraData = UnityEngine.Rendering.Universal.CameraExtensions.GetUniversalAdditionalCameraData(perceptionCamera.attachedCamera); cameraData.requiresDepthOption = UnityEngine.Rendering.Universal.CameraOverrideOption.On; cameraData.requiresDepthTexture = true; #endif perceptionCamera.InstanceSegmentationImageReadback += OnInstanceSegmentationImageReadback; perceptionCamera.RenderedObjectInfosCalculated += OnRenderedObjectInfoReadback; } void SetupDepthCheckBuffers(int size) { var textureDimensions = TextureDimensions(size); if (m_ResultsBuffer != null && textureDimensions.x == m_ResultsBuffer.width && textureDimensions.y == m_ResultsBuffer.height) return; if (m_ResultsBuffer != null) { m_ResultsBuffer.Release(); m_DepthCheckReader.Dispose(false); } m_KeypointPositionsTexture = new Texture2D(textureDimensions.x, textureDimensions.y, GraphicsFormat.R16G16_SFloat, TextureCreationFlags.None); m_KeypointCheckDepthTexture = new Texture2D(textureDimensions.x, textureDimensions.y, GraphicsFormat.R16_SFloat, TextureCreationFlags.None); m_ResultsBuffer = new RenderTexture(textureDimensions.x, textureDimensions.y, 0, GraphicsFormat.R8G8B8A8_UNorm); m_DepthCheckReader = new RenderTextureReader(m_ResultsBuffer); } 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(int2 pixelLocation, (int x, int y) dimensions) { return pixelLocation.x >= 0 && pixelLocation.x < dimensions.x && pixelLocation.y >= 0 && pixelLocation.y < dimensions.y; } bool PixelsMatch(int x, int y, Color32 idColor, (int x, int y) dimensions, NativeArray data) { var h = dimensions.y - 1 - 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 itself or another object. Self-occlusion has already been checked, so the input keypoint may be state 2, 1, or 0. // We determine if a point is occluded by other objects 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 infinitely 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 pixelLocation = PixelLocationFromScreenPoint(keypoint); if (!PixelOnScreen(pixelLocation, dimensions)) return 0; var pixelMatched = false; for (var y = pixelLocation.y - s_PixelTolerance; y <= pixelLocation.y + s_PixelTolerance; y++) { for (var x = pixelLocation.x - s_PixelTolerance; x <= pixelLocation.x + s_PixelTolerance; x++) { if (!PixelOnScreen(new int2(x, y), dimensions)) continue; pixelMatched = true; if (PixelsMatch(x, y, instanceIdColor, dimensions, data)) { return keypoint.state; } } } return pixelMatched ? 1 : 0; } private static int2 PixelLocationFromScreenPoint(Keypoint keypoint) { var centerX = Mathf.FloorToInt(keypoint.x); var centerY = Mathf.FloorToInt(keypoint.y); int2 pixelLocation = new int2(centerX, centerY); return pixelLocation; } void OnInstanceSegmentationImageReadback(int frameCount, NativeArray data, RenderTexture renderTexture) { if (!m_FrameKeypointData.TryGetValue(frameCount, out var frameKeypointData)) return; var dimensions = (renderTexture.width, renderTexture.height); foreach (var keypointEntry in frameKeypointData.keypoints) { if (InstanceIdToColorMapping.TryGetColorFromInstanceId(keypointEntry.instance_id, out var idColor)) { for (var i = 0; i < keypointEntry.keypoints.Length; i++) { var keypoint = keypointEntry.keypoints[i]; keypoint.state = DetermineKeypointState(keypoint, idColor, dimensions, data); if (keypoint.state == 0) { keypoint.x = 0; keypoint.y = 0; } else { keypoint.x = math.clamp(keypoint.x, 0, dimensions.width - .001f); keypoint.y = math.clamp(keypoint.y, 0, dimensions.height - .001f); } keypointEntry.keypoints[i] = keypoint; } } } frameKeypointData.isInstanceSegmentationCheckComplete = true; m_FrameKeypointData[frameCount] = frameKeypointData; ReportIfComplete(frameCount, frameKeypointData); } private void OnRenderedObjectInfoReadback(int frameCount, NativeArray objectInfos) { if (!m_FrameKeypointData.TryGetValue(frameCount, out var frameKeypointData)) return; frameKeypointData.objectInfos = new NativeArray(objectInfos, Allocator.Persistent); m_FrameKeypointData[frameCount] = frameKeypointData; ReportIfComplete(frameCount, frameKeypointData); } private void ReportIfComplete(int frameCount, FrameKeypointData frameKeypointData) { if (!frameKeypointData.isInstanceSegmentationCheckComplete || !frameKeypointData.isDepthCheckComplete || !frameKeypointData.objectInfos.IsCreated) return; m_KeypointEntriesToReport.Clear(); //filter out objects that are not visible foreach (var entry in frameKeypointData.keypoints) { var include = false; if (objectFilter == KeypointObjectFilter.All) include = true; else { foreach (var objectInfo in frameKeypointData.objectInfos) { if (entry.instance_id == objectInfo.instanceId) { include = true; break; } } if (!include && objectFilter == KeypointObjectFilter.VisibleAndOccluded) include = entry.keypoints.Any(k => k.state == 1); } if (include) m_KeypointEntriesToReport.Add(entry); } m_FrameKeypointData.Remove(frameCount); KeypointsComputed?.Invoke(frameCount, m_KeypointEntriesToReport); frameKeypointData.annotation.ReportValues(m_KeypointEntriesToReport); frameKeypointData.objectInfos.Dispose(); } /// protected override void OnEndRendering(ScriptableRenderContext scriptableRenderContext) { using (s_OnEndRenderingMarker.Auto()) { m_CurrentFrame = Time.frameCount; var annotation = perceptionCamera.SensorHandle.ReportAnnotationAsync(m_AnnotationDefinition); var keypointEntries = new List(); var checkLocations = new NativeList(512, Allocator.Persistent); foreach (var label in LabelManager.singleton.registeredLabels) ProcessLabel(label, keypointEntries, checkLocations); m_FrameKeypointData[m_CurrentFrame] = new FrameKeypointData { annotation = annotation, keypoints = keypointEntries, pointsPerEntry = activeTemplate.keypoints.Length }; if (keypointEntries.Count != 0) DoDepthCheck(scriptableRenderContext, keypointEntries, checkLocations); else { var frameKeypointData = m_FrameKeypointData[m_CurrentFrame]; frameKeypointData.isDepthCheckComplete = true; m_FrameKeypointData[m_CurrentFrame] = frameKeypointData; } checkLocations.Dispose(); } } /// Check self occlusion of each keypoint by passing keypoint location (x & y in one texture) and modified distance from camera (keypoint distance - keypoint threshold distance) /// in an additional texture. The computer shader checks the depth buffer at each passed in location, converts the depth at the pixel to linear space, and then compares it to /// the passed in modified keypoint distance. If the modified keypoint distance is less than the depth buffer distance, the keypoint is visible, else it is blocked by itself. private void DoDepthCheck(ScriptableRenderContext scriptableRenderContext, List keypointEntries, NativeList checkLocations) { var keypointCount = keypointEntries.Count * activeTemplate.keypoints.Length; var commandBuffer = CommandBufferPool.Get("KeypointDepthCheck"); var textureDimensions = TextureDimensions(keypointCount); SetupDepthCheckBuffers(checkLocations.Length); var positionsPixeldata = new NativeArray(textureDimensions.x * textureDimensions.y * 2, Allocator.Temp); var depthPixeldata = new NativeArray(textureDimensions.x * textureDimensions.y, Allocator.Temp); for (int i = 0; i < checkLocations.Length; i++) { var pos = checkLocations[i]; positionsPixeldata[i * 2] = new half(pos.x); positionsPixeldata[i * 2 + 1] = new half(pos.y); depthPixeldata[i] = new half(pos.z); } m_KeypointPositionsTexture.SetPixelData(positionsPixeldata, 0); m_KeypointPositionsTexture.Apply(); m_KeypointCheckDepthTexture.SetPixelData(depthPixeldata, 0); m_KeypointCheckDepthTexture.Apply(); positionsPixeldata.Dispose(); depthPixeldata.Dispose(); m_MaterialDepthCheck.SetTexture("_Positions", m_KeypointPositionsTexture); m_MaterialDepthCheck.SetTexture("_KeypointCheckDepth", m_KeypointCheckDepthTexture); commandBuffer.Blit(null, m_ResultsBuffer, m_MaterialDepthCheck); scriptableRenderContext.ExecuteCommandBuffer(commandBuffer); scriptableRenderContext.Submit(); CommandBufferPool.Release(commandBuffer); m_DepthCheckReader.Capture(scriptableRenderContext, OnDepthCheckReadback); } private static Vector2Int TextureDimensions(int keypointCount) { var width = Math.Max(k_MinTextureWidth, Mathf.NextPowerOfTwo((int)Math.Ceiling(Math.Sqrt(keypointCount)))); var height = width; var textureDimensions = new Vector2Int(width, height); return textureDimensions; } private void OnDepthCheckReadback(int frame, NativeArray data, RenderTexture renderTexture) { DoDepthCheckReadback(frame, data); } private void OnDepthCheckReadback(int frameCount, AsyncGPUReadbackRequest obj) { var data = obj.GetData(); //DoDepthCheckReadback(frameCount, data); } // Go through each keypoint and check if the depth compute shader has determined if it is visible (depth texture // value of 1. private void DoDepthCheckReadback(int frameCount, NativeArray data) { var frameKeypointData = m_FrameKeypointData[frameCount]; var totalLength = frameKeypointData.pointsPerEntry * frameKeypointData.keypoints.Count; Debug.Assert(totalLength < data.Length); for (var i = 0; i < totalLength; i++) { var value = data[i]; if (value.r == 0) { var keypoints = frameKeypointData.keypoints[i / frameKeypointData.pointsPerEntry]; var indexInObject = i % frameKeypointData.pointsPerEntry; var keypoint = keypoints.keypoints[indexInObject]; keypoint.state = 1; keypoints.keypoints[indexInObject] = keypoint; } } frameKeypointData.isDepthCheckComplete = true; m_FrameKeypointData[frameCount] = frameKeypointData; ReportIfComplete(frameCount, frameKeypointData); } // 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 struct 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; } Vector3 ConvertToScreenSpace(Vector3 worldLocation) { var pt = perceptionCamera.attachedCamera.WorldToScreenPoint(worldLocation); pt.y = GetCaptureHeight() - pt.y; if (Mathf.Approximately(pt.y, perceptionCamera.attachedCamera.pixelHeight)) pt.y -= .0001f; if (Mathf.Approximately(pt.x, perceptionCamera.attachedCamera.pixelWidth)) pt.x -= .0001f; return pt; } struct CachedData { public bool status; public Animator animator; public KeypointEntry keypoints; public List<(JointLabel, int)> overrides; public float occlusionScalar; } Dictionary m_KnownStatus; bool TryToGetTemplateIndexForJoint(KeypointTemplate template, JointLabel joint, out int index) { index = -1; foreach (var label in joint.labels) { for (var i = 0; i < template.keypoints.Length; i++) { if (template.keypoints[i].label == label) { index = i; return true; } } } return false; } bool DoesTemplateContainJoint(JointLabel jointLabel) { return TryToGetTemplateIndexForJoint(activeTemplate, jointLabel, out _); } void ProcessLabel(Labeling labeledEntity, List keypointEntries, NativeList checkLocations) { 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)>(), occlusionScalar = 1.0f }; 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; } } var occlusionOverrider = labeledEntity.GetComponentInParent(); if (occlusionOverrider != null) { cached.occlusionScalar = occlusionOverrider.distanceScale; } m_KnownStatus[labeledEntity.instanceId] = cached; } var cachedData = m_KnownStatus[labeledEntity.instanceId]; if (cachedData.status) { var animator = cachedData.animator; var listStart = checkLocations.Length; checkLocations.Resize(checkLocations.Length + activeTemplate.keypoints.Length, NativeArrayOptions.ClearMemory); //grab the slice of the list for the current object to assign positions in var checkLocationsSlice = new NativeSlice(checkLocations, listStart); var cameraPosition = perceptionCamera.transform.position; var cameraforward = perceptionCamera.transform.forward; // 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 bonePosition = bone.position; var occlusionDistance = pt.selfOcclusionDistance * cachedData.occlusionScalar; var jointSelfOcclusionDistance = JointSelfOcclusionDistance(bone, bonePosition, cameraPosition, cameraforward, occlusionDistance); InitKeypoint(bonePosition, cachedData, checkLocationsSlice, i, jointSelfOcclusionDistance); } } } // Go through all of the additional or override points defined by joint labels and get // their locations foreach (var (joint, templateIdx) in cachedData.overrides) { var jointTransform = joint.transform; var jointPosition = jointTransform.position; float resolvedSelfOcclusionDistance; if (joint.overrideSelfOcclusionDistance) resolvedSelfOcclusionDistance = joint.selfOcclusionDistance; else resolvedSelfOcclusionDistance = activeTemplate.keypoints[templateIdx].selfOcclusionDistance; resolvedSelfOcclusionDistance *= cachedData.occlusionScalar; var jointSelfOcclusionDistance = JointSelfOcclusionDistance(joint.transform, jointPosition, cameraPosition, cameraforward, resolvedSelfOcclusionDistance); InitKeypoint(jointPosition, cachedData, checkLocationsSlice, templateIdx, jointSelfOcclusionDistance); } cachedData.keypoints.pose = "unset"; if (cachedData.animator != null) { cachedData.keypoints.pose = GetPose(cachedData.animator); } var cachedKeypointEntry = cachedData.keypoints; var keypointEntry = new KeypointEntry() { instance_id = cachedKeypointEntry.instance_id, keypoints = cachedKeypointEntry.keypoints.ToArray(), label_id = cachedKeypointEntry.label_id, pose = cachedKeypointEntry.pose, template_guid = cachedKeypointEntry.template_guid }; keypointEntries.Add(keypointEntry); } } private float JointSelfOcclusionDistance(Transform transform, Vector3 jointPosition, Vector3 cameraPosition, Vector3 cameraforward, float configuredSelfOcclusionDistance) { var depthOfJoint = Vector3.Dot(jointPosition - cameraPosition, cameraforward); var cameraEffectivePosition = jointPosition - cameraforward * depthOfJoint; var jointRelativeCameraPosition = transform.InverseTransformPoint(cameraEffectivePosition); var jointRelativeCheckPosition = jointRelativeCameraPosition.normalized * configuredSelfOcclusionDistance; var worldSpaceCheckVector = transform.TransformVector(jointRelativeCheckPosition); return worldSpaceCheckVector.magnitude; } private void InitKeypoint(Vector3 position, CachedData cachedData, NativeSlice checkLocations, int idx, float occlusionDistance) { var loc = ConvertToScreenSpace(position); var keypoints = cachedData.keypoints.keypoints; keypoints[idx].index = idx; if (loc.z < 0) { keypoints[idx].x = 0; keypoints[idx].y = 0; keypoints[idx].state = 0; } else { keypoints[idx].x = loc.x; keypoints[idx].y = loc.y; keypoints[idx].state = 2; } //TODO: move this code var pixelLocation = PixelLocationFromScreenPoint(keypoints[idx]); if (pixelLocation.x < 0 || pixelLocation.y < 0) { pixelLocation = new int2(int.MaxValue, int.MaxValue); } checkLocations[idx] = new float3(pixelLocation.x + .5f, pixelLocation.y + .5f, loc.z - occlusionDistance); } 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_KeypointEntriesToReport == null) return; using (s_OnVisualizeMarker.Auto()) { 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_KeypointEntriesToReport) { foreach (var bone in activeTemplate.skeleton) { var joint1 = GetKeypointForJoint(entry, bone.joint1); var joint2 = GetKeypointForJoint(entry, bone.joint2); if (joint1 != null && joint1.Value.state == 2 && joint2 != null && joint2.Value.state == 2) { VisualizationHelper.DrawLine(joint1.Value.x, joint1.Value.y, joint2.Value.x, joint2.Value.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; } } }