Fix : 유로 필터 업데이트 페이셜
This commit is contained in:
parent
bff3acb612
commit
4d84234a88
@ -15,7 +15,8 @@ public class StreamingleFacialReceiverEditor : Editor
|
||||
private VisualElement portButtonsContainer;
|
||||
private Label activePortValue;
|
||||
private VisualElement statusContainer;
|
||||
private VisualElement filteringFields;
|
||||
private VisualElement emaFields;
|
||||
private VisualElement euroFields;
|
||||
|
||||
public override VisualElement CreateInspectorGUI()
|
||||
{
|
||||
@ -37,7 +38,8 @@ public class StreamingleFacialReceiverEditor : Editor
|
||||
statusContainer = root.Q("statusContainer");
|
||||
activePortValue = root.Q<Label>("activePortValue");
|
||||
portButtonsContainer = root.Q("portButtonsContainer");
|
||||
filteringFields = root.Q("filteringFields");
|
||||
emaFields = root.Q("emaFields");
|
||||
euroFields = root.Q("euroFields");
|
||||
|
||||
// Auto-find button
|
||||
var autoFindBtn = root.Q<Button>("autoFindBtn");
|
||||
@ -48,13 +50,13 @@ public class StreamingleFacialReceiverEditor : Editor
|
||||
// Build dynamic port buttons
|
||||
RebuildPortButtons();
|
||||
|
||||
// Track enableFiltering for conditional visibility
|
||||
var enableFilteringProp = serializedObject.FindProperty("enableFiltering");
|
||||
UpdateFilteringVisibility(enableFilteringProp.boolValue);
|
||||
// Track filterMode for conditional visibility of EMA/Euro fields
|
||||
var filterModeProp = serializedObject.FindProperty("filterMode");
|
||||
UpdateFilterModeVisibility(filterModeProp.enumValueIndex);
|
||||
|
||||
root.TrackPropertyValue(enableFilteringProp, prop =>
|
||||
root.TrackPropertyValue(filterModeProp, prop =>
|
||||
{
|
||||
UpdateFilteringVisibility(prop.boolValue);
|
||||
UpdateFilterModeVisibility(prop.enumValueIndex);
|
||||
});
|
||||
|
||||
// Track availablePorts and activePortIndex changes to rebuild port buttons
|
||||
@ -70,10 +72,13 @@ public class StreamingleFacialReceiverEditor : Editor
|
||||
return root;
|
||||
}
|
||||
|
||||
private void UpdateFilteringVisibility(bool enabled)
|
||||
private void UpdateFilterModeVisibility(int modeIndex)
|
||||
{
|
||||
if (filteringFields == null) return;
|
||||
filteringFields.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
// FilterMode: 0=None, 1=EMA, 2=OneEuro
|
||||
if (emaFields != null)
|
||||
emaFields.style.display = modeIndex == 1 ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (euroFields != null)
|
||||
euroFields.style.display = modeIndex == 2 ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void RebuildPortButtons()
|
||||
|
||||
@ -127,13 +127,14 @@
|
||||
|
||||
/* ---- Data Filtering ---- */
|
||||
|
||||
#filteringFields {
|
||||
#emaFields {
|
||||
padding-left: 16px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#filteringFields--hidden {
|
||||
display: none;
|
||||
#euroFields {
|
||||
padding-left: 16px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ---- Facial Intensity ---- */
|
||||
|
||||
@ -36,14 +36,21 @@
|
||||
<!-- Data Filtering -->
|
||||
<ui:VisualElement class="section">
|
||||
<ui:Foldout text="Data Filtering" value="true" class="section-foldout">
|
||||
<uie:PropertyField binding-path="enableFiltering" label="Enable"/>
|
||||
<ui:VisualElement name="filteringFields">
|
||||
<uie:PropertyField binding-path="filterMode" label="Filter Mode"/>
|
||||
<!-- EMA Filter Fields -->
|
||||
<ui:VisualElement name="emaFields">
|
||||
<uie:PropertyField binding-path="smoothingFactor" label="Smoothing"/>
|
||||
<uie:PropertyField binding-path="maxBlendShapeDelta" label="Max BlendShape Delta"/>
|
||||
<uie:PropertyField binding-path="maxRotationDelta" label="Max Rotation Delta"/>
|
||||
<uie:PropertyField binding-path="fastBlendShapeMultiplier" label="Fast BS Multiplier"/>
|
||||
<uie:PropertyField binding-path="spikeToleranceFrames" label="Spike Tolerance"/>
|
||||
</ui:VisualElement>
|
||||
<!-- 1€ Euro Filter Fields -->
|
||||
<ui:VisualElement name="euroFields">
|
||||
<uie:PropertyField binding-path="euroMinCutoff" label="Min Cutoff (Hz)"/>
|
||||
<uie:PropertyField binding-path="euroBeta" label="Beta (Speed Coeff)"/>
|
||||
<uie:PropertyField binding-path="euroDCutoff" label="D Cutoff (Hz)"/>
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
|
||||
|
||||
112
Assets/External/StreamingleFacial/OneEuroFilter.cs
vendored
Normal file
112
Assets/External/StreamingleFacial/OneEuroFilter.cs
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 1€ Euro Filter 구현 (Casiez et al., CHI 2012)
|
||||
/// 속도 기반 적응형 cutoff로 "느린 움직임 = 강한 스무딩, 빠른 움직임 = 즉시 반응"
|
||||
/// </summary>
|
||||
public class OneEuroFilter
|
||||
{
|
||||
private float freq;
|
||||
private float minCutoff;
|
||||
private float beta;
|
||||
private float dCutoff;
|
||||
|
||||
private LowPassFilter xFilter;
|
||||
private LowPassFilter dxFilter;
|
||||
private float lastTimestamp;
|
||||
|
||||
public OneEuroFilter(float frequency, float minCutoff = 1.0f, float beta = 0.0f, float dCutoff = 1.0f)
|
||||
{
|
||||
freq = frequency;
|
||||
this.minCutoff = minCutoff;
|
||||
this.beta = beta;
|
||||
this.dCutoff = dCutoff;
|
||||
|
||||
xFilter = new LowPassFilter(ComputeAlpha(minCutoff));
|
||||
dxFilter = new LowPassFilter(ComputeAlpha(dCutoff));
|
||||
lastTimestamp = -1f;
|
||||
}
|
||||
|
||||
private float ComputeAlpha(float cutoff)
|
||||
{
|
||||
float te = 1.0f / freq;
|
||||
float tau = 1.0f / (2.0f * Mathf.PI * cutoff);
|
||||
return 1.0f / (1.0f + tau / te);
|
||||
}
|
||||
|
||||
public float Filter(float value, float timestamp)
|
||||
{
|
||||
if (lastTimestamp >= 0f && timestamp > lastTimestamp)
|
||||
{
|
||||
freq = 1.0f / (timestamp - lastTimestamp);
|
||||
}
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// 속도(미분) 추정
|
||||
float dvalue = xFilter.HasLastRawValue
|
||||
? (value - xFilter.LastRawValue) * freq
|
||||
: 0.0f;
|
||||
|
||||
// 미분값 필터링
|
||||
float edvalue = dxFilter.FilterWithAlpha(dvalue, ComputeAlpha(dCutoff));
|
||||
|
||||
// 적응형 cutoff: 속도가 빠르면 cutoff↑ (스무딩↓)
|
||||
float cutoff = minCutoff + beta * Mathf.Abs(edvalue);
|
||||
|
||||
// 신호 필터링
|
||||
return xFilter.FilterWithAlpha(value, ComputeAlpha(cutoff));
|
||||
}
|
||||
|
||||
public void UpdateParams(float minCutoff, float beta, float dCutoff)
|
||||
{
|
||||
this.minCutoff = Mathf.Max(0.001f, minCutoff);
|
||||
this.beta = Mathf.Max(0f, beta);
|
||||
this.dCutoff = Mathf.Max(0.001f, dCutoff);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
xFilter = new LowPassFilter(ComputeAlpha(minCutoff));
|
||||
dxFilter = new LowPassFilter(ComputeAlpha(dCutoff));
|
||||
lastTimestamp = -1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1차 지수 이동 평균 저역통과 필터
|
||||
/// </summary>
|
||||
private class LowPassFilter
|
||||
{
|
||||
private float alpha;
|
||||
private float smoothed;
|
||||
private float rawValue;
|
||||
private bool initialized;
|
||||
|
||||
public bool HasLastRawValue => initialized;
|
||||
public float LastRawValue => rawValue;
|
||||
|
||||
public LowPassFilter(float alpha, float initValue = 0f)
|
||||
{
|
||||
this.alpha = Mathf.Clamp01(alpha);
|
||||
smoothed = initValue;
|
||||
rawValue = initValue;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
public float FilterWithAlpha(float value, float newAlpha)
|
||||
{
|
||||
alpha = Mathf.Clamp01(newAlpha);
|
||||
rawValue = value;
|
||||
|
||||
if (!initialized)
|
||||
{
|
||||
smoothed = value;
|
||||
initialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
smoothed = alpha * value + (1.0f - alpha) * smoothed;
|
||||
}
|
||||
return smoothed;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/External/StreamingleFacial/OneEuroFilter.cs.meta
vendored
Normal file
2
Assets/External/StreamingleFacial/OneEuroFilter.cs.meta
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e0dff203e063fc4b92b4c514b5643f0
|
||||
@ -30,10 +30,15 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
public int activePortIndex = 0;
|
||||
public int LOCAL_PORT => availablePorts != null && activePortIndex < availablePorts.Length ? availablePorts[activePortIndex] : 49983;
|
||||
|
||||
// 데이터 필터링 설정
|
||||
// 필터 모드 설정
|
||||
public enum FilterMode { None, EMA, OneEuro }
|
||||
|
||||
[Header("Data Filtering")]
|
||||
[Tooltip("데이터 스무딩/필터링 활성화")]
|
||||
public bool enableFiltering = true;
|
||||
[Tooltip("필터링 모드: None=필터없음, EMA=기존 스무딩+스파이크, OneEuro=1€ 적응형 필터")]
|
||||
public FilterMode filterMode = FilterMode.OneEuro;
|
||||
|
||||
// EMA 필터 설정 (기존 호환)
|
||||
[Header("EMA Filter Settings")]
|
||||
[Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")]
|
||||
[Range(0f, 0.95f)]
|
||||
public float smoothingFactor = 0.1f;
|
||||
@ -50,7 +55,23 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
[Range(1, 5)]
|
||||
public int spikeToleranceFrames = 2;
|
||||
|
||||
// 필터링용 이전 값 저장
|
||||
// 1€ Euro Filter 설정
|
||||
[Header("1€ Euro Filter Settings")]
|
||||
[Tooltip("최소 cutoff 주파수 (Hz). 낮을수록 정지 시 스무딩 강함, 높을수록 반응 빠름")]
|
||||
[Range(0.01f, 10f)]
|
||||
public float euroMinCutoff = 1.0f;
|
||||
[Tooltip("속도 계수. 높을수록 빠른 움직임에 즉시 반응")]
|
||||
[Range(0f, 20f)]
|
||||
public float euroBeta = 0.5f;
|
||||
[Tooltip("미분 cutoff 주파수 (Hz). 속도 추정의 스무딩. 보통 1.0 유지")]
|
||||
[Range(0.1f, 5f)]
|
||||
public float euroDCutoff = 1.0f;
|
||||
|
||||
// Euro 필터 인스턴스 (블렌드쉐이프별)
|
||||
private Dictionary<string, OneEuroFilter> euroFilters = new Dictionary<string, OneEuroFilter>();
|
||||
private float euroMinCutoffPrev, euroBetaPrev, euroDCutoffPrev;
|
||||
|
||||
// EMA 필터링용 이전 값 저장
|
||||
private Dictionary<string, float> prevBlendShapeValues = new Dictionary<string, float>();
|
||||
|
||||
// 연속 스파이크 추적 (같은 방향으로 연속이면 실제 움직임)
|
||||
@ -219,6 +240,21 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
void OnValidate()
|
||||
{
|
||||
intensityMapDirty = true;
|
||||
SyncEuroParams();
|
||||
}
|
||||
|
||||
void SyncEuroParams()
|
||||
{
|
||||
if (euroFilters != null)
|
||||
{
|
||||
foreach (var filter in euroFilters.Values)
|
||||
{
|
||||
filter.UpdateParams(euroMinCutoff, euroBeta, euroDCutoff);
|
||||
}
|
||||
}
|
||||
euroMinCutoffPrev = euroMinCutoff;
|
||||
euroBetaPrev = euroBeta;
|
||||
euroDCutoffPrev = euroDCutoff;
|
||||
}
|
||||
|
||||
void RebuildIntensityOverrideMap()
|
||||
@ -282,7 +318,7 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
weight = Mathf.Clamp(weight, 0f, 100f);
|
||||
|
||||
// 필터링 적용
|
||||
if (enableFiltering)
|
||||
if (filterMode != FilterMode.None)
|
||||
{
|
||||
weight = FilterBlendShapeValue(normalizedName, weight);
|
||||
}
|
||||
@ -539,9 +575,45 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BlendShape 값 필터링: 연속 스파이크 판별 + 카테고리별 임계값 + 프레임독립 EMA
|
||||
/// BlendShape 값 필터링: filterMode에 따라 EMA 또는 1€ Euro Filter 적용
|
||||
/// </summary>
|
||||
float FilterBlendShapeValue(string name, float rawValue)
|
||||
{
|
||||
if (filterMode == FilterMode.OneEuro)
|
||||
return FilterOneEuro(name, rawValue);
|
||||
|
||||
return FilterEMA(name, rawValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1€ Euro Filter: 속도 기반 적응형 cutoff
|
||||
/// </summary>
|
||||
float FilterOneEuro(string name, float rawValue)
|
||||
{
|
||||
float t = Time.time;
|
||||
|
||||
if (!euroFilters.TryGetValue(name, out var filter))
|
||||
{
|
||||
filter = new OneEuroFilter(60f, euroMinCutoff, euroBeta, euroDCutoff);
|
||||
euroFilters[name] = filter;
|
||||
// 첫 샘플은 필터 초기화용 → 그대로 반환
|
||||
filter.Filter(rawValue, t);
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
// 파라미터 변경 감지 → 모든 필터 업데이트
|
||||
if (euroMinCutoffPrev != euroMinCutoff || euroBetaPrev != euroBeta || euroDCutoffPrev != euroDCutoff)
|
||||
{
|
||||
SyncEuroParams();
|
||||
}
|
||||
|
||||
return Mathf.Clamp(filter.Filter(rawValue, t), 0f, 100f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EMA 필터: 연속 스파이크 판별 + 카테고리별 임계값 + 프레임독립 EMA (기존 로직)
|
||||
/// </summary>
|
||||
float FilterEMA(string name, float rawValue)
|
||||
{
|
||||
if (prevBlendShapeValues.TryGetValue(name, out float prevValue))
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user