您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
474 行
19 KiB
474 行
19 KiB
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Unity.Mathematics;
|
|
using UnityEditor.UIElements;
|
|
using UnityEditor.VersionControl;
|
|
using UnityEditorInternal;
|
|
using UnityEngine;
|
|
using UnityEngine.Perception.GroundTruth;
|
|
using UnityEngine.UIElements;
|
|
using System.Threading.Tasks;
|
|
using Newtonsoft.Json.Linq;
|
|
using Task = System.Threading.Tasks.Task;
|
|
|
|
namespace UnityEditor.Perception.GroundTruth
|
|
{
|
|
abstract class LabelConfigEditor<T> : Editor where T : ILabelEntry
|
|
{
|
|
private string m_UxmlDir = "Packages/com.unity.perception/Editor/GroundTruth/Uxml/";
|
|
private string m_UxmlPath;
|
|
|
|
private int m_AddedLabelsItemHeight = 37;
|
|
private int m_OtherLabelsItemHeight = 27;
|
|
|
|
protected abstract LabelConfig<T> TargetLabelConfig { get; }
|
|
|
|
private List<string> m_AddedLabels = new List<string>();
|
|
public List<string> AddedLabels => m_AddedLabels;
|
|
|
|
protected SerializedProperty m_SerializedLabelsArray;
|
|
|
|
private static HashSet<string> allLabelsInProject = new HashSet<string>();
|
|
private List<string> m_LabelsNotPresentInConfig = new List<string>();
|
|
|
|
private VisualElement m_Root;
|
|
protected ListView m_LabelListView;
|
|
private ListView m_NonPresentLabelsListView;
|
|
|
|
private Button m_SaveButton;
|
|
private Button m_AddNewLabelButton;
|
|
protected Button m_MoveUpButton;
|
|
protected Button m_MoveDownButton;
|
|
protected VisualElement m_MoveButtons;
|
|
private Button m_ImportFromFileButton;
|
|
private Button m_ExportToFileButton;
|
|
|
|
public void OnEnable()
|
|
{
|
|
m_UxmlPath = m_UxmlDir + "LabelConfig_Main.uxml";
|
|
m_Root = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(m_UxmlPath).CloneTree();
|
|
m_LabelListView = m_Root.Q<ListView>("labels-listview");
|
|
|
|
m_NonPresentLabelsListView = m_Root.Q<ListView>("labels-in-project-listview");
|
|
m_SaveButton = m_Root.Q<Button>("save-button");
|
|
m_AddNewLabelButton = m_Root.Q<Button>("add-label");
|
|
m_MoveUpButton = m_Root.Q<Button>("move-up-button");
|
|
m_MoveDownButton = m_Root.Q<Button>("move-down-button");
|
|
m_MoveButtons = m_Root.Q<VisualElement>("move-buttons");
|
|
m_ImportFromFileButton = m_Root.Q<Button>("import-file-button");
|
|
m_ExportToFileButton = m_Root.Q<Button>("export-file-button");
|
|
|
|
m_SaveButton.SetEnabled(false);
|
|
m_SerializedLabelsArray = serializedObject.FindProperty(IdLabelConfig.labelEntriesFieldName);
|
|
|
|
UpdateMoveButtonState();
|
|
|
|
RefreshAddedLabels();
|
|
SetupPresentLabelsListView();
|
|
|
|
RefreshLabelsMasterList();
|
|
RefreshNonPresentLabels();
|
|
SetupNonPresentLabelsListView();
|
|
|
|
OnEnableExtended();
|
|
|
|
m_AddNewLabelButton.clicked += () => { AddNewLabel(m_SerializedLabelsArray, m_AddedLabels); };
|
|
|
|
m_LabelListView.RegisterCallback<ClickEvent>(evt => { UpdateMoveButtonState(); });
|
|
|
|
m_ImportFromFileButton.clicked += () =>
|
|
{
|
|
var path = EditorUtility.OpenFilePanel("Import label configuration from file", "", "json");
|
|
if (path.Length != 0)
|
|
{
|
|
var fileContent = File.ReadAllText(path);
|
|
var jsonObj = JObject.Parse(fileContent);
|
|
ImportFromJson(jsonObj);
|
|
}
|
|
};
|
|
|
|
m_ExportToFileButton.clicked += () =>
|
|
{
|
|
var path = EditorUtility.SaveFilePanel("Export label configuration to file", "", this.name, "json");
|
|
if (path.Length != 0)
|
|
{
|
|
string fileContents = ExportToJson(this);
|
|
var writer = File.CreateText(path);
|
|
|
|
writer.Write(fileContents);
|
|
writer.Flush();
|
|
writer.Close();
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
protected abstract void OnEnableExtended();
|
|
|
|
public abstract void PostRemoveOperations();
|
|
|
|
public override VisualElement CreateInspectorGUI()
|
|
{
|
|
serializedObject.Update();
|
|
RefreshListDataAndPresenation();
|
|
return m_Root;
|
|
}
|
|
|
|
static void RefreshLabelsMasterList()
|
|
{
|
|
allLabelsInProject.Clear();
|
|
|
|
var allPrefabPaths = GetAllPrefabsInProject();
|
|
foreach (var path in allPrefabPaths)
|
|
{
|
|
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
|
var labeling = asset.GetComponent<Labeling>();
|
|
if (labeling)
|
|
{
|
|
allLabelsInProject.UnionWith(labeling.labels);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RefreshNonPresentLabels()
|
|
{
|
|
m_LabelsNotPresentInConfig.Clear();
|
|
m_LabelsNotPresentInConfig.AddRange(allLabelsInProject);
|
|
m_LabelsNotPresentInConfig.RemoveAll(label => m_AddedLabels.Contains(label));
|
|
}
|
|
|
|
private static IEnumerable<string> GetAllPrefabsInProject()
|
|
{
|
|
var allPaths = AssetDatabase.GetAllAssetPaths();
|
|
return allPaths.Where(path => path.EndsWith(".prefab")).ToList();
|
|
}
|
|
|
|
private void UpdateMoveButtonState()
|
|
{
|
|
var selectedIndex = m_LabelListView.selectedIndex;
|
|
m_MoveDownButton.SetEnabled(selectedIndex > -1);
|
|
m_MoveUpButton.SetEnabled(selectedIndex > -1);
|
|
}
|
|
|
|
public void RefreshListDataAndPresenation()
|
|
{
|
|
serializedObject.Update();
|
|
RefreshAddedLabels();
|
|
RefreshNonPresentLabels();
|
|
m_NonPresentLabelsListView.Refresh();
|
|
RefreshListViewHeight();
|
|
m_LabelListView.Refresh();
|
|
}
|
|
|
|
private void ScrollToBottomAndSelectLastItem()
|
|
{
|
|
m_LabelListView.SetSelection(m_LabelListView.itemsSource.Count - 1);
|
|
UpdateMoveButtonState();
|
|
|
|
m_Root.schedule.Execute(() => { m_LabelListView.ScrollToItem(-1); })
|
|
.StartingIn(
|
|
10); //to circumvent the delay in the listview's internal scrollview updating its geometry (when new items are added).
|
|
}
|
|
|
|
protected void RefreshAddedLabels()
|
|
{
|
|
m_AddedLabels.Clear();
|
|
m_AddedLabels.AddRange(TargetLabelConfig.labelEntries.Select(entry => entry.label));
|
|
}
|
|
|
|
protected virtual void SetupPresentLabelsListView()
|
|
{
|
|
m_LabelListView.itemsSource = m_AddedLabels;
|
|
m_LabelListView.itemHeight = m_AddedLabelsItemHeight;
|
|
m_LabelListView.selectionType = SelectionType.Single;
|
|
|
|
m_LabelListView.RegisterCallback<AttachToPanelEvent>(evt => { RefreshListViewHeight(); });
|
|
}
|
|
|
|
private void SetupNonPresentLabelsListView()
|
|
{
|
|
m_NonPresentLabelsListView.itemsSource = m_LabelsNotPresentInConfig;
|
|
|
|
VisualElement MakeItem()
|
|
{
|
|
var element = new NonPresentLabelElement<T>(this, m_SerializedLabelsArray);
|
|
return element;
|
|
}
|
|
|
|
void BindItem(VisualElement e, int i)
|
|
{
|
|
if (e is NonPresentLabelElement<T> nonPresentLabel)
|
|
{
|
|
nonPresentLabel.m_Label.text = m_LabelsNotPresentInConfig[i];
|
|
}
|
|
}
|
|
|
|
m_NonPresentLabelsListView.bindItem = BindItem;
|
|
m_NonPresentLabelsListView.makeItem = MakeItem;
|
|
m_NonPresentLabelsListView.itemHeight = m_OtherLabelsItemHeight;
|
|
m_NonPresentLabelsListView.selectionType = SelectionType.None;
|
|
}
|
|
|
|
// public void SetSaveButtonEnabled(bool enabled)
|
|
// {
|
|
// m_SaveButton.SetEnabled(enabled);
|
|
// }
|
|
|
|
protected void RefreshListViewHeight()
|
|
{
|
|
m_LabelListView.style.minHeight =
|
|
Mathf.Clamp(m_LabelListView.itemsSource.Count * m_LabelListView.itemHeight, 300, 600);
|
|
}
|
|
|
|
string FindNewLabelString(List<string> labels)
|
|
{
|
|
string baseLabel = "New Label";
|
|
string label = baseLabel;
|
|
int count = 1;
|
|
while (labels.Contains(label))
|
|
{
|
|
label = baseLabel + "_" + count++;
|
|
}
|
|
|
|
return label;
|
|
}
|
|
|
|
private void AddNewLabel(SerializedProperty serializedArray, List<string> presentLabels)
|
|
{
|
|
AddLabel(serializedArray, FindNewLabelString(presentLabels));
|
|
}
|
|
|
|
public void AddLabel(SerializedProperty serializedArray, string labelToAdd)
|
|
{
|
|
if (m_AddedLabels.Contains(labelToAdd)) //label has already been added, cannot add again
|
|
return;
|
|
|
|
AppendLabelEntryToSerializedArray(serializedArray, CreateLabelEntryFromLabelString(serializedArray, labelToAdd));
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
RefreshListDataAndPresenation();
|
|
ScrollToBottomAndSelectLastItem();
|
|
}
|
|
|
|
protected abstract T CreateLabelEntryFromLabelString(SerializedProperty serializedArray, string labelToAdd);
|
|
|
|
|
|
// Import a list of IdLabelEntry objects to the target object's serialized entries array. This function assumes the labelsToAdd list does not contain any duplicate labels.
|
|
protected virtual void ImportLabelEntryListIntoSerializedArray(SerializedProperty serializedArray,
|
|
List<T> labelEntriesToAdd)
|
|
{
|
|
serializedArray.ClearArray();
|
|
|
|
foreach (var entry in labelEntriesToAdd)
|
|
{
|
|
AppendLabelEntryToSerializedArray(serializedArray, entry);
|
|
}
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
RefreshListDataAndPresenation();
|
|
}
|
|
protected abstract void AppendLabelEntryToSerializedArray(SerializedProperty serializedArray, T labelEntry);
|
|
|
|
|
|
protected abstract T ImportFromJsonExtended(string labelString, JObject jsonObject, List<T> previousEntries, bool preventDuplicateIdentifiers = true);
|
|
|
|
protected const string InvalidLabel = "INVALID_LABEL";
|
|
void ImportFromJson(JObject jsonObj)
|
|
{
|
|
var importedLabelEntries = new List<T>();
|
|
if (jsonObj.TryGetValue("LabelEntryType", out var type))
|
|
{
|
|
if (type.Value<string>() == typeof(T).Name)
|
|
{
|
|
if (jsonObj.TryGetValue("LabelEntries", out var labelsArrayToken))
|
|
{
|
|
JArray labelsArray = JArray.Parse(labelsArrayToken.ToString());
|
|
if (labelsArray != null)
|
|
{
|
|
foreach (var labelEntryToken in labelsArray)
|
|
{
|
|
if (labelEntryToken is JObject entryObject)
|
|
{
|
|
if (entryObject.TryGetValue("Label", out var labelToken))
|
|
{
|
|
string labelString = labelToken.Value<string>();
|
|
|
|
if (importedLabelEntries.FindAll(entry => entry.label == labelString)
|
|
.Count > 0)
|
|
{
|
|
Debug.LogError("File contains a duplicate Label: " + labelString);
|
|
return;
|
|
}
|
|
|
|
T labelEntry = ImportFromJsonExtended(labelString, entryObject, importedLabelEntries);
|
|
|
|
if (labelEntry.label == InvalidLabel)
|
|
return;
|
|
|
|
importedLabelEntries.Add(labelEntry);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("Error reading Label for Label Entry" + labelEntryToken +
|
|
" from file. Please check the formatting.");
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("Error reading Label Entry " + labelEntryToken +
|
|
" from file. Please check the formatting.");
|
|
return;
|
|
}
|
|
}
|
|
ImportLabelEntryListIntoSerializedArray(m_SerializedLabelsArray, importedLabelEntries);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError(
|
|
"Could not read list of Label Entries from file. Please check the formatting.");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("Specified LabelEntryType does not match " + typeof(T).Name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("LabelEntryType not found.");
|
|
}
|
|
}
|
|
|
|
|
|
protected abstract void AddLabelIdentifierToJson(SerializedProperty labelEntry, JObject jObj);
|
|
|
|
private string ExportToJson(LabelConfigEditor<T> editor)
|
|
{
|
|
JObject result = new JObject();
|
|
result.Add("LabelEntryType", typeof(T).Name);
|
|
|
|
JArray labelEntries = new JArray();
|
|
for (int i = 0; i < m_SerializedLabelsArray.arraySize; i++)
|
|
{
|
|
var entry = m_SerializedLabelsArray.GetArrayElementAtIndex(i);
|
|
JObject entryJobj = new JObject();
|
|
entryJobj.Add("Label", entry.FindPropertyRelative(nameof(ILabelEntry.label)).stringValue);
|
|
AddLabelIdentifierToJson(entry, entryJobj);
|
|
labelEntries.Add(entryJobj);
|
|
}
|
|
|
|
result.Add("LabelEntries", labelEntries);
|
|
|
|
return result.ToString();
|
|
}
|
|
}
|
|
|
|
internal abstract class LabelElementInLabelConfig<T> : VisualElement where T : ILabelEntry
|
|
{
|
|
protected const string UxmlDir = "Packages/com.unity.perception/Editor/GroundTruth/Uxml/";
|
|
protected abstract string UxmlPath { get; }
|
|
|
|
private Button m_RemoveButton;
|
|
private Button m_MoveUpButton;
|
|
private Button m_MoveDownButton;
|
|
|
|
public TextField m_LabelTextField;
|
|
|
|
public int m_IndexInList;
|
|
|
|
protected SerializedProperty m_LabelsArray;
|
|
|
|
//protected ListView m_LabelsListView;
|
|
protected LabelConfigEditor<T> m_LabelConfigEditor;
|
|
|
|
protected LabelElementInLabelConfig(LabelConfigEditor<T> editor, SerializedProperty labelsArray)
|
|
{
|
|
m_LabelConfigEditor = editor;
|
|
m_LabelsArray = labelsArray;
|
|
//m_LabelsListView = labelsListView;
|
|
|
|
Init();
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree(this);
|
|
m_LabelTextField = this.Q<TextField>("label-value");
|
|
m_RemoveButton = this.Q<Button>("remove-button");
|
|
m_MoveUpButton = this.Q<Button>("move-up-button");
|
|
m_MoveDownButton = this.Q<Button>("move-down-button");
|
|
|
|
m_LabelTextField.isDelayed = true;
|
|
|
|
InitExtended();
|
|
|
|
m_LabelTextField.RegisterValueChangedCallback<string>((cEvent) =>
|
|
{
|
|
if (m_LabelConfigEditor.AddedLabels.Contains(cEvent.newValue) && m_LabelConfigEditor.AddedLabels.IndexOf(cEvent.newValue) != m_IndexInList)
|
|
{
|
|
//The listview recycles child visual elements and that causes the RegisterValueChangedCallback event to be called when scrolling.
|
|
//Therefore, we need to make sure we are not in this code block just because of scrolling, but because the user is actively changing one of the labels.
|
|
//The m_LabelConfigEditor.AddedLabels.IndexOf(cEvent.newValue) != m_IndexInList check is for this purpose.
|
|
|
|
Debug.LogError("A label with the string " + cEvent.newValue + " has already been added to this label configuration.");
|
|
m_LabelsArray.GetArrayElementAtIndex(m_IndexInList).FindPropertyRelative(nameof(ILabelEntry.label))
|
|
.stringValue = cEvent.previousValue; //since the textfield is bound to this property, it has already changed the property, so we need to revert the proprty.
|
|
m_LabelsArray.serializedObject.ApplyModifiedProperties();
|
|
m_LabelConfigEditor.RefreshListDataAndPresenation();
|
|
return;
|
|
}
|
|
|
|
|
|
//even though the textfield is already bound to the relevant property, we need to explicitly set the
|
|
//property here too in order to make "hasModifiedProperties" return the right value in the next line. Otherwise it will always be false.
|
|
m_LabelsArray.GetArrayElementAtIndex(m_IndexInList).FindPropertyRelative(nameof(ILabelEntry.label))
|
|
.stringValue = cEvent.newValue;
|
|
if (m_LabelsArray.serializedObject.hasModifiedProperties)
|
|
{
|
|
//the value change event is called even when the listview recycles its child elements for re-use during scrolling, therefore, we should check to make sure there are modified properties, otherwise we would be doing the refresh for no reason (reduces scrolling performance)
|
|
m_LabelsArray.serializedObject.ApplyModifiedProperties();
|
|
m_LabelConfigEditor.RefreshListDataAndPresenation();
|
|
}
|
|
});
|
|
|
|
m_RemoveButton.clicked += () =>
|
|
{
|
|
m_LabelsArray.DeleteArrayElementAtIndex(m_IndexInList);
|
|
m_LabelConfigEditor.PostRemoveOperations();
|
|
m_LabelConfigEditor.serializedObject.ApplyModifiedProperties();
|
|
m_LabelConfigEditor.RefreshListDataAndPresenation();
|
|
};
|
|
}
|
|
|
|
protected abstract void InitExtended();
|
|
|
|
public void UpdateMoveButtonVisibility(SerializedProperty labelsArray)
|
|
{
|
|
m_MoveDownButton.visible = m_IndexInList != labelsArray.arraySize - 1;
|
|
m_MoveUpButton.visible = m_IndexInList != 0;
|
|
}
|
|
}
|
|
|
|
class NonPresentLabelElement<T> : VisualElement where T : ILabelEntry
|
|
{
|
|
private string m_UxmlDir = "Packages/com.unity.perception/Editor/GroundTruth/Uxml/";
|
|
private string m_UxmlPath;
|
|
private Button m_AddButton;
|
|
public Label m_Label;
|
|
|
|
public NonPresentLabelElement(LabelConfigEditor<T> editor, SerializedProperty labelsArray)
|
|
{
|
|
m_UxmlPath = m_UxmlDir + "SuggestedLabelElement.uxml";
|
|
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(m_UxmlPath).CloneTree(this);
|
|
m_Label = this.Q<Label>("label-value");
|
|
m_AddButton = this.Q<Button>("add-button");
|
|
|
|
m_AddButton.clicked += () => { editor.AddLabel(labelsArray, m_Label.text); };
|
|
}
|
|
}
|
|
}
|