383 lines
18 KiB
C#
383 lines
18 KiB
C#
using Aisu.SIM1.Face.Core;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
namespace Aisu.SIM1.Face.Samples.FaceDetectionDemo
|
|
{
|
|
/// <summary>
|
|
/// 人脸检测 Demo 场景 UI 动态构建器。
|
|
/// </summary>
|
|
public sealed class FaceDetectionDemoBootstrap : MonoBehaviour
|
|
{
|
|
private Font m_font; // UI 字体。
|
|
|
|
private void Awake()
|
|
{
|
|
Build();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建 MVP 测试页面。
|
|
/// </summary>
|
|
public void Build()
|
|
{
|
|
m_font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
|
EnsureEventSystem();
|
|
|
|
var manager = FindObjectOfType<FaceModuleManager>() ?? new GameObject("FaceModuleManager").AddComponent<FaceModuleManager>();
|
|
var camera = gameObject.AddComponent<FaceDetectionDemoCamera>();
|
|
var logger = gameObject.AddComponent<FaceDetectionDemoLogger>();
|
|
var userList = gameObject.AddComponent<FaceDetectionDemoUserList>();
|
|
var controller = gameObject.AddComponent<FaceDetectionDemoController>();
|
|
|
|
var canvasGo = Create("FaceDetectionDemoCanvas", transform);
|
|
var canvas = canvasGo.AddComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvasGo.AddComponent<GraphicRaycaster>();
|
|
var scaler = canvasGo.AddComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1280, 720);
|
|
|
|
var root = Create("Root", canvasGo.transform);
|
|
Stretch(root.GetComponent<RectTransform>());
|
|
var rootLayout = root.AddComponent<VerticalLayoutGroup>();
|
|
rootLayout.padding = new RectOffset(16, 16, 16, 16);
|
|
rootLayout.spacing = 10;
|
|
rootLayout.childControlWidth = true;
|
|
rootLayout.childForceExpandWidth = true;
|
|
|
|
var title = Text(root.transform, "SI-M1 Face Detection Demo", 28, TextAnchor.MiddleLeft);
|
|
title.color = new Color(0.05f, 0.07f, 0.1f);
|
|
|
|
var statusText = Text(root.transform, "摄像头未打开 | 模组未连接", 20, TextAnchor.MiddleLeft);
|
|
statusText.color = new Color(0.12f, 0.18f, 0.24f);
|
|
|
|
var body = Create("Body", root.transform);
|
|
body.AddComponent<LayoutElement>().flexibleHeight = 1f;
|
|
var bodyLayout = body.AddComponent<HorizontalLayoutGroup>();
|
|
bodyLayout.spacing = 12;
|
|
bodyLayout.childControlHeight = true;
|
|
bodyLayout.childForceExpandHeight = true;
|
|
|
|
var left = Panel(body.transform, "LeftPanel", new Color(0.95f, 0.97f, 1f));
|
|
left.AddComponent<LayoutElement>().flexibleWidth = 1f;
|
|
var leftLayout = left.AddComponent<VerticalLayoutGroup>();
|
|
leftLayout.padding = new RectOffset(10, 10, 10, 10);
|
|
leftLayout.spacing = 8;
|
|
leftLayout.childControlWidth = true;
|
|
leftLayout.childForceExpandWidth = true;
|
|
|
|
var right = Panel(body.transform, "RightPanel", new Color(0.98f, 0.98f, 0.96f));
|
|
right.AddComponent<LayoutElement>().preferredWidth = 430;
|
|
var rightLayout = right.AddComponent<VerticalLayoutGroup>();
|
|
rightLayout.padding = new RectOffset(10, 10, 10, 10);
|
|
rightLayout.spacing = 8;
|
|
rightLayout.childControlWidth = true;
|
|
rightLayout.childForceExpandWidth = true;
|
|
|
|
var cameraDropdown = Dropdown(left.transform, "未发现摄像头");
|
|
var cameraRow = Row(left.transform);
|
|
var refreshCameraButton = Button(cameraRow.transform, "刷新摄像头");
|
|
var openCameraButton = Button(cameraRow.transform, "打开摄像头");
|
|
var closeCameraButton = Button(cameraRow.transform, "关闭摄像头");
|
|
|
|
var previewFrame = Panel(left.transform, "CameraPreview", Color.black);
|
|
previewFrame.AddComponent<LayoutElement>().flexibleHeight = 1f;
|
|
var preview = Create("Preview", previewFrame.transform).AddComponent<RawImage>();
|
|
preview.color = Color.white;
|
|
var previewRect = preview.GetComponent<RectTransform>();
|
|
previewRect.anchorMin = Vector2.zero;
|
|
previewRect.anchorMax = Vector2.one;
|
|
previewRect.offsetMin = Vector2.zero;
|
|
previewRect.offsetMax = Vector2.zero;
|
|
var aspect = preview.gameObject.AddComponent<AspectRatioFitter>();
|
|
aspect.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
|
|
|
|
Text(right.transform, "串口 / 模组", 22, TextAnchor.MiddleLeft);
|
|
var serialDropdown = Dropdown(right.transform, "手动输入串口");
|
|
var serialInput = Input(right.transform, "串口名,例如 COM3 或 /dev/ttyUSB0");
|
|
var baudInput = Input(right.transform, "115200");
|
|
var serialRow = Row(right.transform);
|
|
var refreshSerialButton = Button(serialRow.transform, "刷新串口");
|
|
var connectButton = Button(serialRow.transform, "连接模组");
|
|
var disconnectButton = Button(serialRow.transform, "断开模组");
|
|
|
|
Text(right.transform, "人脸操作", 22, TextAnchor.MiddleLeft);
|
|
var userNameInput = Input(right.transform, "User");
|
|
var adminToggle = Toggle(right.transform, "管理员", false);
|
|
var faceRow = Row(right.transform);
|
|
var enrollButton = Button(faceRow.transform, "录入人脸");
|
|
var verifyButton = Button(faceRow.transform, "检测人脸");
|
|
|
|
var resultText = Text(right.transform, "结果: -", 20, TextAnchor.UpperLeft);
|
|
resultText.gameObject.AddComponent<LayoutElement>().preferredHeight = 58;
|
|
|
|
Text(right.transform, "人脸数据", 22, TextAnchor.MiddleLeft);
|
|
var userButtonRow = Row(right.transform);
|
|
var refreshUsersButton = Button(userButtonRow.transform, "刷新用户列表");
|
|
var userScroll = ScrollView(right.transform, out var userContent);
|
|
userScroll.GetComponent<LayoutElement>().preferredHeight = 170;
|
|
|
|
Text(root.transform, "操作日志", 22, TextAnchor.MiddleLeft);
|
|
var logRow = Row(root.transform);
|
|
var clearLogButton = Button(logRow.transform, "清空日志");
|
|
var logScroll = ScrollView(root.transform, out var logContent);
|
|
logScroll.GetComponent<LayoutElement>().preferredHeight = 150;
|
|
var logText = Text(logContent, string.Empty, 18, TextAnchor.UpperLeft);
|
|
logText.font = m_font;
|
|
logText.color = Color.white;
|
|
logText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
logText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
logText.gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;
|
|
logContent.gameObject.GetComponent<Image>().color = new Color(0.07f, 0.08f, 0.09f);
|
|
|
|
camera.Bind(preview, aspect, logger);
|
|
logger.Bind(logText, logScroll);
|
|
userList.Bind(userContent, m_font, controller.DeleteUser);
|
|
controller.Bind(manager, camera, logger, userList, cameraDropdown, serialDropdown, serialInput, baudInput, userNameInput, adminToggle, statusText, resultText);
|
|
|
|
refreshCameraButton.onClick.AddListener(controller.RefreshCameras);
|
|
openCameraButton.onClick.AddListener(controller.OpenCamera);
|
|
closeCameraButton.onClick.AddListener(controller.CloseCamera);
|
|
refreshSerialButton.onClick.AddListener(controller.RefreshSerialPorts);
|
|
connectButton.onClick.AddListener(controller.ConnectModule);
|
|
disconnectButton.onClick.AddListener(controller.DisconnectModule);
|
|
enrollButton.onClick.AddListener(controller.EnrollFace);
|
|
verifyButton.onClick.AddListener(controller.VerifyFace);
|
|
refreshUsersButton.onClick.AddListener(controller.RefreshUsers);
|
|
clearLogButton.onClick.AddListener(logger.Clear);
|
|
}
|
|
|
|
private static void EnsureEventSystem()
|
|
{
|
|
if (FindObjectOfType<EventSystem>() != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var eventSystem = new GameObject("EventSystem");
|
|
eventSystem.AddComponent<EventSystem>();
|
|
eventSystem.AddComponent<StandaloneInputModule>();
|
|
}
|
|
|
|
private GameObject Panel(Transform parent, string name, Color color)
|
|
{
|
|
var go = Create(name, parent);
|
|
go.AddComponent<Image>().color = color;
|
|
return go;
|
|
}
|
|
|
|
private GameObject Row(Transform parent)
|
|
{
|
|
var row = Create("Row", parent);
|
|
var layout = row.AddComponent<HorizontalLayoutGroup>();
|
|
layout.spacing = 8;
|
|
layout.childControlWidth = true;
|
|
layout.childForceExpandWidth = true;
|
|
row.AddComponent<LayoutElement>().preferredHeight = 44;
|
|
return row;
|
|
}
|
|
|
|
private Button Button(Transform parent, string text)
|
|
{
|
|
var go = Create(text, parent);
|
|
go.AddComponent<Image>().color = new Color(0.78f, 0.86f, 0.95f);
|
|
var button = go.AddComponent<Button>();
|
|
go.AddComponent<LayoutElement>().preferredHeight = 42;
|
|
var label = Text(go.transform, text, 20, TextAnchor.MiddleCenter);
|
|
Stretch(label.GetComponent<RectTransform>());
|
|
return button;
|
|
}
|
|
|
|
private InputField Input(Transform parent, string placeholder)
|
|
{
|
|
var go = Create("InputField", parent);
|
|
go.AddComponent<Image>().color = Color.white;
|
|
go.AddComponent<LayoutElement>().preferredHeight = 42;
|
|
var input = go.AddComponent<InputField>();
|
|
|
|
var text = Text(go.transform, string.Empty, 20, TextAnchor.MiddleLeft);
|
|
var textRect = text.GetComponent<RectTransform>();
|
|
Stretch(textRect);
|
|
textRect.offsetMin = new Vector2(10, 0);
|
|
textRect.offsetMax = new Vector2(-10, 0);
|
|
input.textComponent = text;
|
|
|
|
var placeholderText = Text(go.transform, placeholder, 20, TextAnchor.MiddleLeft);
|
|
placeholderText.color = new Color(0.45f, 0.48f, 0.52f);
|
|
var placeholderRect = placeholderText.GetComponent<RectTransform>();
|
|
Stretch(placeholderRect);
|
|
placeholderRect.offsetMin = new Vector2(10, 0);
|
|
placeholderRect.offsetMax = new Vector2(-10, 0);
|
|
input.placeholder = placeholderText;
|
|
return input;
|
|
}
|
|
|
|
private Toggle Toggle(Transform parent, string label, bool value)
|
|
{
|
|
var row = Row(parent);
|
|
row.GetComponent<HorizontalLayoutGroup>().childForceExpandWidth = false;
|
|
var toggleGo = Create("Toggle", row.transform);
|
|
toggleGo.AddComponent<LayoutElement>().preferredWidth = 34;
|
|
var toggle = toggleGo.AddComponent<Toggle>();
|
|
var background = Create("Background", toggleGo.transform).AddComponent<Image>();
|
|
background.color = Color.white;
|
|
Stretch(background.GetComponent<RectTransform>());
|
|
var checkmark = Create("Checkmark", background.transform).AddComponent<Image>();
|
|
checkmark.color = new Color(0.18f, 0.45f, 0.85f);
|
|
var checkRect = checkmark.GetComponent<RectTransform>();
|
|
checkRect.anchorMin = new Vector2(0.2f, 0.2f);
|
|
checkRect.anchorMax = new Vector2(0.8f, 0.8f);
|
|
checkRect.offsetMin = Vector2.zero;
|
|
checkRect.offsetMax = Vector2.zero;
|
|
toggle.targetGraphic = background;
|
|
toggle.graphic = checkmark;
|
|
toggle.isOn = value;
|
|
Text(row.transform, label, 20, TextAnchor.MiddleLeft).gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;
|
|
return toggle;
|
|
}
|
|
|
|
private Dropdown Dropdown(Transform parent, string initial)
|
|
{
|
|
var go = Create("Dropdown", parent);
|
|
var image = go.AddComponent<Image>();
|
|
image.color = Color.white;
|
|
go.AddComponent<LayoutElement>().preferredHeight = 42;
|
|
var dropdown = go.AddComponent<Dropdown>();
|
|
dropdown.targetGraphic = image;
|
|
var label = Text(go.transform, initial, 20, TextAnchor.MiddleLeft);
|
|
var labelRect = label.GetComponent<RectTransform>();
|
|
Stretch(labelRect);
|
|
labelRect.offsetMin = new Vector2(10, 0);
|
|
labelRect.offsetMax = new Vector2(-30, 0);
|
|
var arrow = Text(go.transform, "v", 18, TextAnchor.MiddleCenter);
|
|
var arrowRect = arrow.GetComponent<RectTransform>();
|
|
arrowRect.anchorMin = new Vector2(1f, 0f);
|
|
arrowRect.anchorMax = Vector2.one;
|
|
arrowRect.pivot = new Vector2(1f, 0.5f);
|
|
arrowRect.sizeDelta = new Vector2(30, 0);
|
|
arrowRect.anchoredPosition = Vector2.zero;
|
|
dropdown.template = CreateDropdownTemplate(go.transform, out var itemText);
|
|
dropdown.captionText = label;
|
|
dropdown.itemText = itemText;
|
|
dropdown.options.Add(new Dropdown.OptionData(initial));
|
|
dropdown.value = 0;
|
|
return dropdown;
|
|
}
|
|
|
|
private RectTransform CreateDropdownTemplate(Transform parent, out Text itemText)
|
|
{
|
|
var template = Panel(parent, "Template", Color.white);
|
|
template.SetActive(false);
|
|
var templateRect = template.GetComponent<RectTransform>();
|
|
templateRect.anchorMin = new Vector2(0f, 0f);
|
|
templateRect.anchorMax = new Vector2(1f, 0f);
|
|
templateRect.pivot = new Vector2(0.5f, 1f);
|
|
templateRect.sizeDelta = new Vector2(0f, 180f);
|
|
templateRect.anchoredPosition = new Vector2(0f, 2f);
|
|
var scrollRect = template.AddComponent<ScrollRect>();
|
|
scrollRect.horizontal = false;
|
|
|
|
var viewport = Panel(template.transform, "Viewport", new Color(1f, 1f, 1f, 0.98f));
|
|
viewport.AddComponent<Mask>().showMaskGraphic = false;
|
|
Stretch(viewport.GetComponent<RectTransform>());
|
|
|
|
var contentGo = Create("Content", viewport.transform);
|
|
var contentRect = contentGo.GetComponent<RectTransform>();
|
|
contentRect.anchorMin = new Vector2(0f, 1f);
|
|
contentRect.anchorMax = Vector2.one;
|
|
contentRect.pivot = new Vector2(0.5f, 1f);
|
|
contentRect.offsetMin = Vector2.zero;
|
|
contentRect.offsetMax = Vector2.zero;
|
|
contentGo.AddComponent<VerticalLayoutGroup>().childForceExpandWidth = true;
|
|
contentGo.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
var itemGo = Create("Item", contentGo.transform);
|
|
var itemToggle = itemGo.AddComponent<Toggle>();
|
|
itemGo.AddComponent<LayoutElement>().preferredHeight = 36;
|
|
var itemBackground = itemGo.AddComponent<Image>();
|
|
itemBackground.color = Color.white;
|
|
itemToggle.targetGraphic = itemBackground;
|
|
|
|
var checkmark = Text(itemGo.transform, "✓", 18, TextAnchor.MiddleCenter);
|
|
var checkRect = checkmark.GetComponent<RectTransform>();
|
|
checkRect.anchorMin = new Vector2(0f, 0f);
|
|
checkRect.anchorMax = new Vector2(0f, 1f);
|
|
checkRect.pivot = new Vector2(0f, 0.5f);
|
|
checkRect.sizeDelta = new Vector2(28f, 0f);
|
|
checkRect.anchoredPosition = Vector2.zero;
|
|
itemToggle.graphic = checkmark;
|
|
|
|
var itemLabel = Text(itemGo.transform, "Option", 18, TextAnchor.MiddleLeft);
|
|
var itemLabelRect = itemLabel.GetComponent<RectTransform>();
|
|
Stretch(itemLabelRect);
|
|
itemLabelRect.offsetMin = new Vector2(32f, 0f);
|
|
itemLabelRect.offsetMax = new Vector2(-8f, 0f);
|
|
|
|
scrollRect.viewport = viewport.GetComponent<RectTransform>();
|
|
scrollRect.content = contentRect;
|
|
itemText = itemLabel;
|
|
return templateRect;
|
|
}
|
|
|
|
private ScrollRect ScrollView(Transform parent, out Transform content)
|
|
{
|
|
var root = Panel(parent, "ScrollView", new Color(0.12f, 0.13f, 0.15f));
|
|
var scrollRect = root.AddComponent<ScrollRect>();
|
|
scrollRect.horizontal = false;
|
|
root.AddComponent<LayoutElement>().preferredHeight = 160;
|
|
|
|
var viewport = Panel(root.transform, "Viewport", new Color(0.12f, 0.13f, 0.15f));
|
|
viewport.AddComponent<Mask>().showMaskGraphic = false;
|
|
Stretch(viewport.GetComponent<RectTransform>());
|
|
|
|
var contentGo = Panel(viewport.transform, "Content", new Color(0.12f, 0.13f, 0.15f));
|
|
content = contentGo.transform;
|
|
var contentRect = contentGo.GetComponent<RectTransform>();
|
|
contentRect.anchorMin = new Vector2(0, 1);
|
|
contentRect.anchorMax = new Vector2(1, 1);
|
|
contentRect.pivot = new Vector2(0.5f, 1f);
|
|
contentRect.offsetMin = new Vector2(8, 0);
|
|
contentRect.offsetMax = new Vector2(-8, 0);
|
|
contentGo.AddComponent<VerticalLayoutGroup>().spacing = 6;
|
|
contentGo.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
scrollRect.viewport = viewport.GetComponent<RectTransform>();
|
|
scrollRect.content = contentRect;
|
|
return scrollRect;
|
|
}
|
|
|
|
private Text Text(Transform parent, string value, int size, TextAnchor alignment)
|
|
{
|
|
var go = Create("Text", parent);
|
|
var text = go.AddComponent<Text>();
|
|
text.font = m_font;
|
|
text.fontSize = size;
|
|
text.color = Color.black;
|
|
text.alignment = alignment;
|
|
text.text = value;
|
|
text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
text.verticalOverflow = VerticalWrapMode.Overflow;
|
|
return text;
|
|
}
|
|
|
|
private static GameObject Create(string name, Transform parent)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|
go.transform.SetParent(parent, false);
|
|
return go;
|
|
}
|
|
|
|
private static void Stretch(RectTransform rect)
|
|
{
|
|
rect.anchorMin = Vector2.zero;
|
|
rect.anchorMax = Vector2.one;
|
|
rect.offsetMin = Vector2.zero;
|
|
rect.offsetMax = Vector2.zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
|