using System;
using System.Collections.Generic;
using Unity.Profiling;
namespace UnityEngine.Perception.GroundTruth
{
///
/// Static class to procedurally generate a unique color for an instance ID. This algorithm
/// is deterministic, and will always return the same color for a ID, and the same ID for a color. ID 0 is reserved to
/// be an invalid ID and is mapped to color black (0,0,0,255). Invalid IDs always map to black, and black always maps to ID 0.
/// In order to try to create visually contrasting colors for IDs, there are a subset of IDs reserved (1-65)
/// to be generated by applying the golden ration to find the next color in the HSL spectrum. All of these
/// colors, and only theses colors, will be in the alpha channel 255. After the first 65 IDs, the color will be
/// determined by iterating through all available RGB values in the alpha channels from 264 - 1. Alpha channel 0 is marked as invalid.
/// This service will support over 4 billion unique IDs => colors [(256^4) - (256*2) + 64]
///
public static class InstanceIdToColorMapping
{
// ReSharper disable once MemberCanBePrivate.Global
///
/// The max ID supported by this class.
///
public const uint maxId = uint.MaxValue - ((256 * 256 * 256) * 2) + k_HslCount;
static uint[] s_IdToColorCache;
static Dictionary s_ColorToIdCache;
const uint k_HslCount = 1024;
const uint k_ColorsPerAlpha = 256 * 256 * 256;
const uint k_InvalidPackedColor = 255; // packed uint for color (0, 0, 0, 255);
static readonly float k_GoldenRatio = (1 + Mathf.Sqrt(5)) / 2;
const int k_HuesInEachValue = 64;
const uint k_Values = k_HslCount / k_HuesInEachValue;
///
/// The color returned when an instanceId is not mapped to any color, and for clearing ground truth material properties on a .
///
public static readonly Color32 invalidColor = new Color(0, 0, 0, 255);
private static ProfilerMarker k_InitializeMapsMarker = new ProfilerMarker(nameof(InitializeMaps));
internal static void InitializeMaps()
{
using (k_InitializeMapsMarker.Auto())
{
s_IdToColorCache = new uint[k_HslCount + 1];
s_ColorToIdCache = new Dictionary();
s_IdToColorCache[0] = k_InvalidPackedColor;
s_ColorToIdCache[k_InvalidPackedColor] = 0;
for (uint i = 1; i <= k_HslCount; i++)
{
var color = GenerateHSLValueForId(i);
s_IdToColorCache[i] = color;
s_ColorToIdCache.Add(color, i);
}
}
}
static uint GenerateHSLValueForId(uint count)
{
count -= 1;
// assign hue based on golden ratio
var hueId = count % k_HuesInEachValue;
var ratio = hueId * k_GoldenRatio;
var hue = ratio - Mathf.Floor(ratio);
var valueId = count / k_HuesInEachValue;
// avoid value 0
var value = 1 - (float)valueId / (k_Values + 1);
var color = (Color32)Color.HSVToRGB(hue, 1f, value);
color.a = 255;
return GetPackedColorFromColor(color);
}
static uint GetColorForId(uint id)
{
if (id > maxId || id == 0) return k_InvalidPackedColor;
if (id <= k_HslCount)
{
if (s_IdToColorCache == null) InitializeMaps();
return s_IdToColorCache[id];
}
var altered_id = id - k_HslCount;
var rgb = altered_id % k_ColorsPerAlpha;
var alpha= 254 - (altered_id / k_ColorsPerAlpha);
return rgb << 8 | alpha;
}
static bool TryGetIdForColor(uint color, out uint id)
{
if (color == 0 || color == k_InvalidPackedColor)
{
id = 0;
return true;
}
var alpha = color & 0xff;
if (alpha == 255)
{
if (s_ColorToIdCache == null) InitializeMaps();
return s_ColorToIdCache.TryGetValue(color, out id);
}
else
{
var rgb = color >> 8;
id = k_HslCount + rgb + (256 * 256 * 256) * (254 - alpha);
return true;
}
}
static uint GetIdForColor(uint color)
{
if (!TryGetIdForColor(color, out var id))
{
throw new InvalidOperationException($"Passed in color: {color} was not one of the reserved colors for alpha channel 255");
}
return id;
}
///
/// Packs a color32 (RGBA - 1 byte per channel) into a 32bit unsigned integer.
///
/// The RGBA color.
/// The packed unsigned int 32 of the color.
public static uint GetPackedColorFromColor(Color32 color)
{
var tmp = (uint) ((color.r << 24) | (color.g << 16) | (color.b << 8) | (color.a << 0));
return tmp;
}
///
/// Converts a packed color (or unsigned 32bit representation of a color) into an RGBA color.
///
/// The packed color
/// The RGBA color
public static Color32 GetColorFromPackedColor(uint color)
{
return new Color32((byte)(color >> 24), (byte)(color >> 16), (byte)(color >> 8), (byte)color);
}
///
/// Retrieve the color that is mapped to the passed in ID. If the ID is 0 or 255 false will be returned, and
/// color will be set to black.
///
/// The ID of interest.
/// Will be set to the color associated with the passed in ID.
/// Returns true if the ID was mapped to a non-black color, otherwise returns false
public static bool TryGetColorFromInstanceId(uint id, out Color32 color)
{
color = invalidColor;
if (id > maxId) return false;
var packed = GetColorForId(id);
if (packed == k_InvalidPackedColor) return false;
color = GetColorFromPackedColor(packed);
return true;
}
///
/// Retrieve the color that is mapped to the passed in ID. If the ID is 0 or 255 the returned color will be black.
///
/// The ID of interest.
/// The color associated with the passed in ID, or black if no associated color exists.
/// Thrown if the passed in ID is greater than the largest supported ID
public static Color32 GetColorFromInstanceId(uint id)
{
if (id > maxId)
throw new IndexOutOfRangeException($"Passed in index: {id} is greater than max ID: {maxId}");
TryGetColorFromInstanceId(id, out var color);
return color;
}
///
/// Retrieve the ID associated with the passed in color. If the passed in color is black or cannot be mapped to an ID
/// this service will return false, and the out id will be set to 0.
///
/// The color to map to an ID.
/// This value will be updated with the ID for the passed in color.
/// This service will return true if an ID is properly mapped to a color, otherwise it will return false.
public static bool TryGetInstanceIdFromColor(Color32 color, out uint id)
{
var packed = GetPackedColorFromColor(color);
if (!TryGetIdForColor(packed, out id))
{
return false;
}
return id != 0 && id <= maxId;
}
///
/// Retrieve the ID associated with the passed in color. If the passed in color is black this service will return 0.
///
/// The color to map to an ID.
/// The ID for the passed in color.
/// Thrown if the passed in color is mapped to an ID that is greater than the largest supported ID
/// Thrown if the passed in color cannot be mapped to an ID in the alpha 255 range
public static uint GetInstanceIdFromColor(Color32 color)
{
var id = GetIdForColor(GetPackedColorFromColor(color));
if (id > maxId) throw new IndexOutOfRangeException($"Passed in color: {color} maps to an ID: {id} which is greater than max ID: {maxId}");
return id;
}
}
}