Code Walkthrough: Unity Bounding Box 2D Toolkit Package
Introduction
The Unity Bounding Box 2D Toolkit package provides an easy-to-use and customizable solution to work with and visualize 2D bounding boxes on a Unity canvas.
Some of my tutorials involve using 2D object detection models in Unity applications. This package makes that shared functionality more modular and reusable, allowing me to streamline my tutorial content. Check out the demo video below to see this package in action.
In this post, I’ll walk through the package code, providing a solid understanding of its components and their roles.
Package Overview
The package contains three C# scripts and prefabs to construct 2D bounding boxes.
C# Scripts
BBox2DUtils.cs
: This script provides functionality to work with 2D bounding boxes for object detection tasks.BoundingBox2DVisualizer.cs
: This script creates, updates, and manages UI elements for visualizing 2D bounding boxes.AddCustomDefineSymbol.cs
: An Editor script that automatically adds a custom scripting define symbol to the project after the package installs.
Prefabs
BoundingBoxBorderPrefab.prefab
: The BoundingBox2DVisualizer script uses this prefab to construct the sides of bounding boxes.DotPrefab.prefab
: An Image prefab used to display a dot at the center of bounding boxesLabelPrefab.prefab
: A text prefab used to display the class label associated probability score for a bounding box.LabelBackgroundPrefab.prefab
: An Image prefab used as the background for the LabelPrefab.BBox2DVisualizer.prefab
: This prefab helps simplify adding 2D bounding box visualization to a Unity scene. The prefab already has the BoundingBox2DVisualizer script attached and has a child Canvas component.
Code Explanation
In this section, we will delve deeper into the Unity Bounding Box 2D Toolkit package by examining the purpose and functionality of each C# script.
BBox2DUtils.cs
The BBox2DUtils.cs script provides functionality to work with 2D bounding boxes for object detection tasks. It contains two structs and a utility class. The complete code is available on GitHub at the link below.
BBox2D
struct
This struct contains the coordinates (x0, y0), width, height, class index, and probability value for a 2D bounding box.
/// <summary>
/// A struct that represents a 2D bounding box.
/// </summary>
public struct BBox2D
{
public float x0;
public float y0;
public float width;
public float height;
public int index;
public float prob;
/// <summary>
/// Initializes a new instance of the BBox2D struct.
/// </summary>
/// <param name="x0">The x-coordinate of the top-left corner.</param>
/// <param name="y0">The y-coordinate of the top-left corner.</param>
/// <param name="width">The width of the bounding box.</param>
/// <param name="height">The height of the bounding box.</param>
/// <param name="index">The class index of the object.</param>
/// <param name="prob">The probability of the object belonging to the given class.</param>
public BBox2D(float x0, float y0, float width, float height, int index, float prob)
{
this.x0 = x0;
this.y0 = y0;
this.width = width;
this.height = height;
this.index = index;
this.prob = prob;
}
}
BBox2DInfo
struct
This struct contains a BBox2D object, class label, and color.
/// <summary>
/// A struct for 2D bounding box information.
/// </summary>
public struct BBox2DInfo
{
public BBox2D bbox;
public string label;
public Color color;
/// <summary>
/// Initializes a new instance of the BBox2DInfo struct.
/// </summary>
/// <param name="boundingBox">The 2D bounding box.</param>
/// <param name="label">The class label.</param>
/// <param name="width">The bounding box color.</param>
public BBox2DInfo(BBox2D boundingBox, string label = "", Color color = new Color())
{
this.bbox = boundingBox;
this.label = label;
this.color = color;
}
}
BBox2DUtility
class
This class provides various utility methods for working with bounding boxes.
CalcUnionArea
This method calculates the union area between two bounding boxes.
/// <summary>
/// Calculates the union area between two bounding boxes.
/// </summary>
/// <param name="a">The first bounding box.</param>
/// <param name="b">The second bounding box.</param>
/// <returns>The union area between the two bounding boxes.</returns>
public static float CalcUnionArea(BBox2D a, BBox2D b)
{
// Calculate the coordinates and dimensions of the union area
float x = Mathf.Min(a.x0, b.x0);
float y = Mathf.Min(a.y0, b.y0);
float w = Mathf.Max(a.x0 + a.width, b.x0 + b.width) - x;
float h = Mathf.Max(a.y0 + a.height, b.y0 + b.height) - y;
// Calculate the union area of two bounding boxes
return w * h;
}
CalcInterArea
This method calculates the intersection area between two bounding boxes.
/// <summary>
/// Calculates the intersection area between two bounding boxes.
/// </summary>
/// <param name="a">The first bounding box.</param>
/// <param name="b">The second bounding box.</param>
/// <returns>The intersection area between the two bounding boxes.</returns>
public static float CalcInterArea(BBox2D a, BBox2D b)
{
// Calculate the coordinates and dimensions of the intersection area
float x = Mathf.Max(a.x0, b.x0);
float y = Mathf.Max(a.y0, b.y0);
float w = Mathf.Min(a.x0 + a.width, b.x0 + b.width) - x;
float h = Mathf.Min(a.y0 + a.height, b.y0 + b.height) - y;
// Calculate the intersection area of two bounding boxes
return w * h;
}
NMSSortedBoxes
This method performs Non-Maximum Suppression (NMS) on a sorted list of bounding box proposals, retaining only those with an intersection over union (IoU) value below the threshold.
/// <summary>
/// Performs Non-Maximum Suppression (NMS) on a sorted list of bounding box proposals.
/// </summary>
/// <param name="proposals">A sorted list of BBox2D objects representing the bounding box proposals.</param>
/// <param name="nms_thresh">The NMS threshold for filtering proposals (default is 0.45).</param>
/// <returns>A list of integers representing the indices of the retained proposals.</returns>
public static List<int> NMSSortedBoxes(List<BBox2D> proposals, float nms_thresh = 0.45f)
{
// Iterate through the proposals and perform non-maximum suppression
<int> proposal_indices = new List<int>();
List
for (int i = 0; i < proposals.Count; i++)
{
// Calculate the intersection and union areas
= proposals[i];
BBox2D a bool keep = proposal_indices.All(j =>
{
= proposals[j];
BBox2D b float inter_area = CalcInterArea(a, b);
float union_area = CalcUnionArea(a, b);
// Keep the proposal if its IoU with all previous proposals is below the NMS threshold
return inter_area / union_area <= nms_thresh;
});
// If the proposal passes the NMS check, add its index to the list
if (keep) proposal_indices.Add(i);
}
return proposal_indices;
}
ScaleBoundingBox
This method scales and optionally mirrors the bounding box of a detected object to match the in-game screen and display resolutions.
/// <summary>
/// Scales and optionally mirrors the bounding box of a detected object to match the in-game screen and display resolutions.
/// </summary>
/// <param name="boundingBox">A BBox2D object containing the bounding box information for a detected object.</param>
/// <param name="inputDims">The dimensions of the input image used for object detection.</param>
/// <param name="screenDims">The dimensions of the in-game screen where the bounding boxes will be displayed.</param>
/// <param name="offset">An offset to apply to the bounding box coordinates when scaling.</param>
/// <param name="mirrorScreen">A boolean flag to indicate if the bounding boxes should be mirrored horizontally (default is false).</param>
public static BBox2D ScaleBoundingBox(BBox2D boundingBox, Vector2Int inputDims, Vector2 screenDims, Vector2Int offset, bool mirrorScreen)
{
// The smallest dimension of the screen
float minScreenDim = Mathf.Min(screenDims.x, screenDims.y);
// The smallest input dimension
int minInputDim = Mathf.Min(inputDims.x, inputDims.y);
// Calculate the scale value between the in-game screen and input dimensions
float minImgScale = minScreenDim / minInputDim;
// Calculate the scale value between the in-game screen and display
float displayScaleX = Screen.width / screenDims.x;
float displayScaleY = Screen.height / screenDims.y;
float displayScale = Mathf.Min(displayScaleX, displayScaleY);
// Scale bounding box to in-game screen resolution and flip the bbox coordinates vertically
float x0 = (boundingBox.x0 + offset.x) * minImgScale;
float y0 = (inputDims.y - (boundingBox.y0 - offset.y)) * minImgScale;
float width = boundingBox.width * minImgScale;
float height = boundingBox.height * minImgScale;
// Mirror bounding box across screen
if (mirrorScreen)
{
= screenDims.x - x0 - width;
x0 }
// Scale bounding box to display resolution
.x0 = x0 * displayScale;
boundingBox.y0 = y0 * displayScale;
boundingBox.width = width * displayScale;
boundingBox.height = height * displayScale;
boundingBox
// Offset the bounding box coordinates based on the difference between the in-game screen and display
.x0 += (Screen.width - screenDims.x * displayScale) / 2;
boundingBox.y0 += (Screen.height - screenDims.y * displayScale) / 2;
boundingBox
return boundingBox;
}
BoundingBox2DVisualizer.cs
The BoundingBox2DVisualizer
script is a Unity C# MonoBehaviour class that displays 2D bounding boxes and labels on a Unity canvas. It creates, updates, and manages UI elements for visualizing them based on the provided BBox2DInfo array. This class supports customizable settings such as bounding box transparency and the ability to toggle the display of bounding boxes. The complete code is available on GitHub at the link below.
Serialized Fields
The BoundingBox2DVisualizer class contains serialized fields referencing UI components, prefabs, and settings.
// UI components
[Header("Components")]
[Tooltip("Container for holding the bounding box UI elements")]
[SerializeField] private RectTransform boundingBoxContainer;
[Tooltip("Container for holding the label UI elements")]
[SerializeField] private RectTransform labelContainer;
// Prefabs for creating UI elements
[Header("Prefabs")]
[Tooltip("Prefab for the bounding box UI element")]
[SerializeField] private RectTransform boundingBoxPrefab;
[Tooltip("Prefab for the label UI element")]
[SerializeField] private TMP_Text labelPrefab;
[Tooltip("Prefab for the label background UI element")]
[SerializeField] private Image labelBackgroundPrefab;
[Tooltip("Prefab for the dot UI element")]
[SerializeField] private Image dotPrefab;
// Settings for customizing the bounding box visualizer
[Header("Settings")]
[Tooltip("Flag to control whether bounding boxes should be displayed or not")]
[SerializeField] private bool displayBoundingBoxes = true;
[Tooltip("Transparency value for the bounding boxes, ranging from 0 (completely transparent) to 1 (completely opaque)")]
[SerializeField, Range(0f, 1f)] private float bboxTransparency = 1f;
GUID Constants
These are the GUIDs of the default assets. They are used to set default values for the bounding box, label, label background, and dot prefabs in the Unity Editor.
// GUIDs of the default assets for the bounding box, label, label background, and dot prefabs
private const string BoundingBoxPrefabGUID = "be0edeacc0f249fab31ac75426ad8a2a";
private const string LabelPrefabGUID = "4e39b47d4b984862aeab14255855fcc9";
private const string LabelBackgroundPrefabGUID = "9074ea186151430084312ba891bad58e";
private const string DotPrefabGUID = "3eb64b4f1a4e4e2595066ed269be9532";
Lists for UI Elements
// Lists for storing and managing instantiated UI elements
private List<RectTransform> boundingBoxes = new List<RectTransform>(); // List of instantiated bounding box UI elements
private List<TMP_Text> labels = new List<TMP_Text>(); // List of instantiated label UI elements
private List<Image> labelBackgrounds = new List<Image>(); // List of instantiated label background UI elements
private List<Image> dots = new List<Image>(); // List of instantiated dot UI elements
Reset
This method sets the default assets from the project using their GUIDs. It uses AssetDatabase
to find them and set the default values. This method will only work in the Unity Editor, not in a build.
/// <summary>
/// Reset is called when the user hits the Reset button in the Inspector's context menu
/// or when adding the component the first time. This function is only called in editor mode.
/// </summary>
private void Reset()
{
// Load default assets only in the Unity Editor, not in a build
#if UNITY_EDITOR
= LoadDefaultAsset<RectTransform>(BoundingBoxPrefabGUID);
boundingBoxPrefab = LoadDefaultAsset<TMP_Text>(LabelPrefabGUID);
labelPrefab = LoadDefaultAsset<Image>(LabelBackgroundPrefabGUID);
labelBackgroundPrefab = LoadDefaultAsset<Image>(DotPrefabGUID);
dotPrefab #endif
}
LoadDefaultAsset
This method provides a generic way to load default assets for the specified fields using their GUIDs.
/// <summary>
/// Loads the default asset for the specified type using its GUID.
/// </summary>
/// <typeparam name="T">The type of asset to be loaded.</typeparam>
/// <param name="guid">The GUID of the default asset.</param>
/// <returns>The loaded asset of the specified type.</returns>
/// <remarks>
/// This method is only executed in the Unity Editor, not in builds.
/// </remarks>
private T LoadDefaultAsset<T>(string guid) where T : UnityEngine.Object
{
#if UNITY_EDITOR
// Load the asset from the AssetDatabase using its GUID
return UnityEditor.AssetDatabase.LoadAssetAtPath<T>(UnityEditor.AssetDatabase.GUIDToAssetPath(guid));
#else
return null;
#endif
}
UpdateBoundingBoxVisualizations
This method updates the visualization of bounding boxes based on the given BBox2DInfo
array.
/// <summary>
/// Update the visualization of bounding boxes based on the given BBox2DInfo array.
/// </summary>
/// <param name="bboxInfoArray">An array of BBox2DInfo objects containing bounding box information</param>
public void UpdateBoundingBoxVisualizations(BBox2DInfo[] bboxInfoArray)
{
// Depending on the displayBoundingBoxes flag, either update or disable bounding box UI elements
if (displayBoundingBoxes)
{
UpdateBoundingBoxes(bboxInfoArray);
}
else
{
// Disable bounding boxes, labels, and label backgrounds for all existing UI elements
for (int i = 0; i < boundingBoxes.Count; i++)
{
[i].gameObject.SetActive(false);
boundingBoxes[i].gameObject.SetActive(false);
labelBackgrounds[i].gameObject.SetActive(false);
labels[i].gameObject.SetActive(false);
dots}
}
}
ScreenToCanvasPoint
This method converts a screen point to a local one in the RectTransform
space of the given canvas.
/// <summary>
/// Convert a screen point to a local point in the RectTransform space of the given canvas.
/// </summary>
/// <param name="canvas">The RectTransform object of the canvas</param>
/// <param name="screenPoint">The screen point to be converted</param>
/// <returns>A Vector2 object representing the local point in the RectTransform space of the canvas</returns>
private Vector2 ScreenToCanvasPoint(RectTransform canvas, Vector2 screenPoint)
{
.ScreenPointToLocalPointInRectangle(canvas, screenPoint, null, out Vector2 localPoint);
RectTransformUtilityreturn localPoint;
}
UpdateBoundingBoxes
This method updates bounding box UI elements to match the provided BBox2DInfo
array. It creates or removes bounding box UI elements to match the number of detected objects and updates bounding boxes, labels, and label backgrounds. It also disables UI elements if not needed.
/// <summary>
/// Update bounding box UI elements to match the provided BBox2DInfo array.
/// </summary>
/// <param name="bboxInfoArray">An array of BBox2DInfo objects containing bounding box information</param>
private void UpdateBoundingBoxes(BBox2DInfo[] bboxInfoArray)
{
// Create or remove bounding box UI elements to match the number of detected objects
while (boundingBoxes.Count < bboxInfoArray.Length)
{
= Instantiate(boundingBoxPrefab, boundingBoxContainer);
RectTransform newBoundingBox .Add(newBoundingBox);
boundingBoxes
= Instantiate(labelBackgroundPrefab, labelContainer);
Image newLabelBackground .Add(newLabelBackground);
labelBackgrounds
= Instantiate(labelPrefab, labelContainer);
TMP_Text newLabel .Add(newLabel);
labels
= Instantiate(dotPrefab, boundingBoxContainer);
Image newDot .Add(newDot);
dots}
// Update bounding boxes, labels, and label backgrounds for each detected object, or disable UI elements if not needed
for (int i = 0; i < boundingBoxes.Count; i++)
{
if (i < bboxInfoArray.Length)
{
= bboxInfoArray[i];
BBox2DInfo bboxInfo
// Get UI elements for the current bounding box, label, and label background
= boundingBoxes[i];
RectTransform boundingBox = labels[i];
TMP_Text label = labelBackgrounds[i];
Image labelBackground = dots[i];
Image dot
UpdateBoundingBox(boundingBox, bboxInfo);
UpdateLabelAndBackground(label, labelBackground, bboxInfo);
UpdateDot(dot, bboxInfo);
// Enable bounding box, label, and label background UI elements
.gameObject.SetActive(true);
boundingBox.gameObject.SetActive(true);
labelBackground.gameObject.SetActive(true);
label[i].gameObject.SetActive(true);
dots}
else
{
// Disable UI elements for extra bounding boxes, labels, and label backgrounds
[i].gameObject.SetActive(false);
boundingBoxes[i].gameObject.SetActive(false);
labelBackgrounds[i].gameObject.SetActive(false);
labels[i].gameObject.SetActive(false);
dots}
}
}
UpdateBoundingBox
This method updates the bounding box UI element with the information from the given BBox2DInfo
object. It converts the screen point to a local one in the RectTransform
space of the bounding box container and sets the color of the bounding box with the specified transparency.
/// <summary>
/// Update the bounding box UI element with the information from the given BBox2DInfo object.
/// </summary>
/// <param name="boundingBox">The RectTransform object representing the bounding box UI element</param>
/// <param name="bboxInfo">The BBox2DInfo object containing the information for the bounding box</param>
private void UpdateBoundingBox(RectTransform boundingBox, BBox2DInfo bboxInfo)
{
// Convert the screen point to a local point in the RectTransform space of the bounding box container
= ScreenToCanvasPoint(boundingBoxContainer, new Vector2(bboxInfo.bbox.x0, bboxInfo.bbox.y0));
Vector2 localPosition .anchoredPosition = localPosition;
boundingBox.sizeDelta = new Vector2(bboxInfo.bbox.width, bboxInfo.bbox.height);
boundingBox
// Set the color of the bounding box with the specified transparency
= GetColorWithTransparency(bboxInfo.color);
Color color [] sides = boundingBox.GetComponentsInChildren<Image>();
Imageforeach (Image side in sides)
{
.color = color;
side}
}
UpdateLabelAndBackground
This method updates the label and label background UI elements with the information from the provided BBox2DInfo
object. It sets the label text, position, color, and the label’s background position, size, and color with the specified transparency.
/// <summary>
/// Update the label and label background UI elements with the information from the given BBox2DInfo object.
/// </summary>
/// <param name="label">The TMP_Text object representing the label UI element</param>
/// <param name="labelBackground">The Image object representing the label background UI element</param>
/// <param name="bboxInfo">The BBox2DInfo object containing the information for the label and label background</param>
private void UpdateLabelAndBackground(TMP_Text label, Image labelBackground, BBox2DInfo bboxInfo)
{
// Convert the screen point to a local point in the RectTransform space of the bounding box container
= ScreenToCanvasPoint(boundingBoxContainer, new Vector2(bboxInfo.bbox.x0, bboxInfo.bbox.y0));
Vector2 localPosition
// Set the label text and position
.text = $"{bboxInfo.label}: {(bboxInfo.bbox.prob * 100).ToString("0.##")}%";
label.rectTransform.anchoredPosition = new Vector2(localPosition.x, localPosition.y - label.preferredHeight);
label
// Set the label color based on the grayscale value of the bounding box color
= GetColorWithTransparency(bboxInfo.color);
Color color .color = color.grayscale > 0.5 ? Color.black : Color.white;
label
// Set the label background position and size
.rectTransform.anchoredPosition = new Vector2(localPosition.x, localPosition.y - label.preferredHeight);
labelBackground.rectTransform.sizeDelta = new Vector2(Mathf.Max(label.preferredWidth, bboxInfo.bbox.width), label.preferredHeight);
labelBackground
// Set the label background color with the specified transparency
.color = color;
labelBackground}
UpdateDot
This method updates the dot UI element based on the provided BBox2DInfo object.
/// <summary>
/// Update the dot UI element with the information from the given BBox2DInfo object.
/// </summary>
/// <param name="dot">The Image object representing the dot UI element</param>
/// <param name="bboxInfo">The BBox2DInfo object containing the information for the bounding box</param>
private void UpdateDot(Image dot, BBox2DInfo bboxInfo)
{
// Calculate the center of the bounding box
= new Vector2(bboxInfo.bbox.x0 + bboxInfo.bbox.width / 2, bboxInfo.bbox.y0 - bboxInfo.bbox.height / 2);
Vector2 center
// Convert the screen point to a local point in the RectTransform space of the bounding box container
= ScreenToCanvasPoint(boundingBoxContainer, center);
Vector2 localPosition
// Set the dot position
.rectTransform.anchoredPosition = localPosition;
dot
// Set the dot color with the specified transparency
= GetColorWithTransparency(bboxInfo.color);
Color color .color = color;
dot}
GetColorWithTransparency
This method is a utility function that returns a new color based on the input color with the adjusted transparency.
/// <summary>
/// Get a new color based on the input color with the adjusted transparency.
/// </summary>
/// <param name="color">The input color to be modified</param>
/// <returns>A new color with the specified transparency</returns>
private Color GetColorWithTransparency(Color color)
{
.a = bboxTransparency;
colorreturn color;
}
AddCustomDefineSymbol.cs
This Editor script contains a class that adds a custom define symbol to the project. We can use this custom symbol to prevent code that relies on this package from executing unless the Bounding Box 2D Toolkit package is present. The complete code is available on GitHub at the link below.
using UnityEditor;
using UnityEngine;
namespace CJM.BBox2DToolkit
{
public class DependencyDefineSymbolAdder
{
private const string CustomDefineSymbol = "CJM_BBOX_2D_TOOLKIT";
[InitializeOnLoadMethod]
public static void AddCustomDefineSymbol()
{
// Get the currently selected build target group
var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
// Retrieve the current scripting define symbols for the selected build target group
var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
// Check if the CustomDefineSymbol is already present in the defines string
if (!defines.Contains(CustomDefineSymbol))
{
// Append the CustomDefineSymbol to the defines string, separated by a semicolon
+= $";{CustomDefineSymbol}";
defines // Set the updated defines string as the new scripting define symbols for the selected build target group
.SetScriptingDefineSymbolsForGroup(buildTargetGroup, defines);
PlayerSettings// Log a message in the Unity console to inform the user that the custom define symbol has been added
.Log($"Added custom define symbol '{CustomDefineSymbol}' to the project.");
Debug}
}
}
}
Conclusion
This post provided an in-depth walkthrough of the code for the Unity Bounding Box 2D Toolkit package. The package provides an easy-to-use and customizable solution to work with and visualize 2D bounding boxes on a Unity canvas.
You can continue to explore the package by going to its GitHub repository linked below, where you will also find instructions for installing it using the Unity Package Manager.
- GitHub Repository: unity-bounding-box-2d-toolkit
You can find the code for the demo project shown in the video at the beginning of this post linked below.
- Barracuda Inference YOLOX Demo: A simple Unity project demonstrating how to perform object detection with the
barracuda-inference-yolox
package.
I’m Christian Mills, a deep learning consultant specializing in practical AI implementations. I help clients leverage cutting-edge AI technologies to solve real-world problems.
Interested in working together? Fill out my Quick AI Project Assessment form or learn more about me.