Skip to content

Commit 28b41dd

Browse files
committed
fix no gaze marker after merge
1 parent 02c7750 commit 28b41dd

3 files changed

Lines changed: 182 additions & 39 deletions

File tree

SharpEyes/ViewModels/GazeVideoPlayerViewModelBase.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,17 @@ public bool IsGazeLoaded
5151
/// <summary>Hook for subclasses to raise additional properties that depend on IsGazeLoaded.</summary>
5252
protected virtual void OnGazeLoadedChanged() { }
5353

54-
private bool _isGazeAtNaN = false;
55-
public bool IsGazeAtNaN
54+
public bool FrameHasGaze
5655
{
57-
get => _isGazeAtNaN;
56+
get;
5857
set
5958
{
60-
this.RaiseAndSetIfChanged(ref _isGazeAtNaN, value);
59+
this.RaiseAndSetIfChanged(ref field, value);
6160
this.RaisePropertyChanged("IsGazeEllipseVisible");
6261
}
63-
}
62+
} = true;
6463

65-
public bool IsGazeEllipseVisible => IsGazeLoaded && !IsGazeAtNaN;
64+
public bool IsGazeEllipseVisible => IsGazeLoaded && FrameHasGaze;
6665

6766
private bool _isTTL = false;
6867
public bool IsTTL
@@ -312,19 +311,19 @@ public override void UpdateDisplay()
312311
{
313312
if ((object)GazeData == null || dataFrame >= GazeData.Shape[0])
314313
{
315-
IsGazeAtNaN = true;
314+
FrameHasGaze = false;
316315
}
317316
else
318317
{
319318
double gazeXValue = (double)GazeData[dataFrame, 0];
320319
double gazeYValue = (double)GazeData[dataFrame, 1];
321320
if (Double.IsNaN(gazeXValue) || Double.IsNaN(gazeYValue))
322321
{
323-
IsGazeAtNaN = true;
322+
FrameHasGaze = false;
324323
}
325324
else
326325
{
327-
IsGazeAtNaN = false;
326+
FrameHasGaze = true;
328327
GazeX = gazeXValue;
329328
GazeY = gazeYValue;
330329
}
@@ -334,7 +333,7 @@ public override void UpdateDisplay()
334333
}
335334
else
336335
{
337-
IsGazeAtNaN = false;
336+
FrameHasGaze = true;
338337
IsTTL = false;
339338
foreach (TrailGazePoint point in TrailPoints)
340339
point.IsVisible = false;

SharpEyes/ViewModels/StimulusGazeViewModel.cs

Lines changed: 172 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,18 @@ public bool IsMovingGaze
4040
internal NDArray? RawGazeLocations = null;
4141
internal NDArray? FilteredGazeLocations = null;
4242

43-
internal NDArray? DisplayedGazeLocations => _isGazeFilterEnabled && ((object)FilteredGazeLocations != null)
43+
// the filtered or raw gaze, before manual keyframe corrections are applied
44+
private NDArray? GazeBaseline => _isGazeFilterEnabled && ((object)FilteredGazeLocations != null)
4445
? FilteredGazeLocations
4546
: RawGazeLocations;
4647

48+
// the gaze that is displayed and saved: the baseline with keyframe corrections applied
49+
internal NDArray? CorrectedGazeLocations = null;
50+
51+
internal NDArray? DisplayedGazeLocations => (object)CorrectedGazeLocations != null
52+
? CorrectedGazeLocations
53+
: GazeBaseline;
54+
4755
protected override NDArray? GazeData => DisplayedGazeLocations;
4856

4957
public Color GazeStrokeColor
@@ -114,7 +122,10 @@ public bool IsGazeFilterEnabled
114122
if (value)
115123
ApplyFilter();
116124
else
125+
{
126+
RebuildCorrectedGaze();
117127
RebuildTTLMarkerCache();
128+
}
118129
}
119130
}
120131

@@ -267,6 +278,7 @@ public override async void LoadVideo()
267278
IsGazeLoaded = false;
268279
RawGazeLocations = null;
269280
FilteredGazeLocations = null;
281+
CorrectedGazeLocations = null;
270282
VideoKeyFrames.Clear();
271283
GazeX = 0;
272284
GazeY = 0;
@@ -283,40 +295,45 @@ public override async void LoadVideo()
283295
// after gaze is manually edited, updates it.
284296
public void UpdateGaze()
285297
{
286-
NDArray? gazeLocations = DisplayedGazeLocations;
287-
if ((object)gazeLocations == null || dataFrame == null)
298+
if ((object)DisplayedGazeLocations == null || dataFrame == null)
288299
return;
289300

290-
double deltaX = GazeX - gazeLocations[dataFrame, 0];
291-
double deltaY = GazeY - gazeLocations[dataFrame, 1];
292-
293-
AddKeyFrame();
294-
295-
// if there is a keyframe before, ramp the delta from the previous keyframe
296-
// to this new keyframe
297-
if (PreviousDataKeyFrame.HasValue)
298-
{
299-
int numFrames = dataFrame.Value - PreviousDataKeyFrame.Value;
300-
double multiplier;
301-
for (int i = PreviousDataKeyFrame.Value; i < dataFrame; i++)
302-
{
303-
multiplier = (double)(i - PreviousDataKeyFrame.Value) / numFrames;
304-
gazeLocations[i, 0] += multiplier * deltaX;
305-
gazeLocations[i, 1] += multiplier * deltaY;
306-
}
307-
}
308-
309-
// update all following data frames with this delta
310-
gazeLocations[new Slice(dataFrame.Value, null), 0] += deltaX;
311-
gazeLocations[new Slice(dataFrame.Value, null), 1] += deltaY;
301+
AddKeyFrame(CurrentVideoFrame, GazeX, GazeY);
302+
UpdateCorrectedGazeAround(dataFrame.Value);
312303
}
313304

305+
/// <summary>
306+
/// Adds a keyframe at the current video frame, recording the displayed gaze position.
307+
/// </summary>
314308
public void AddKeyFrame()
315309
{
316310
AddKeyFrame(CurrentVideoFrame);
317311
}
318312

313+
/// <summary>
314+
/// Adds a keyframe at the given video frame, recording the currently displayed gaze position
315+
/// at that frame.
316+
/// </summary>
317+
/// <param name="frame">Video frame at which to add the keyframe</param>
319318
public void AddKeyFrame(int frame)
319+
{
320+
if (!(dataStartFrame.HasValue && (frame >= dataStartFrame.Value) &&
321+
(frame <= dataEndFrame)))
322+
return;
323+
if ((object)DisplayedGazeLocations == null)
324+
return;
325+
int index = VideoTimeToDataIndex(frame);
326+
AddKeyFrame(frame, (double)DisplayedGazeLocations[index, 0], (double)DisplayedGazeLocations[index, 1]);
327+
}
328+
329+
/// <summary>
330+
/// Adds a keyframe at the given video frame with an explicit gaze position, replacing any
331+
/// existing keyframe at the same data index. The keyframe list is kept sorted by video frame.
332+
/// </summary>
333+
/// <param name="frame">Video frame at which to add the keyframe</param>
334+
/// <param name="gazeX">Screen-space X of the gaze at the keyframe</param>
335+
/// <param name="gazeY">Screen-space Y of the gaze at the keyframe</param>
336+
public void AddKeyFrame(int frame, double gazeX, double gazeY)
320337
{
321338
if (dataStartFrame.HasValue && (frame >= dataStartFrame.Value) &&
322339
(frame <= dataEndFrame))
@@ -333,12 +350,130 @@ public void AddKeyFrame(int frame)
333350
}
334351

335352
VideoKeyFrames.Add(new VideoKeyFrame(frame, index, videoReader.FramesToTimecode(frame),
336-
DisplayedGazeLocations[index, 0], DisplayedGazeLocations[index, 1]));
353+
gazeX, gazeY));
337354
VideoKeyFrames =
338355
new ObservableCollection<VideoKeyFrame>(VideoKeyFrames.OrderBy((keyframe) => keyframe.VideoFrame));
339356
}
340357
}
341358

359+
/// <summary>
360+
/// Rebuilds the corrected gaze array from the baseline and keyframes. Frames before the first
361+
/// keyframe stay at the baseline, the correction is linearly interpolated between consecutive
362+
/// keyframes, and the last keyframe's correction is held flat to the end of the data.
363+
/// </summary>
364+
private void RebuildCorrectedGaze()
365+
{
366+
NDArray? baseline = GazeBaseline;
367+
if ((object)baseline == null)
368+
{
369+
CorrectedGazeLocations = null;
370+
return;
371+
}
372+
373+
NDArray corrected = baseline.Clone() as NDArray;
374+
if (VideoKeyFrames.Count > 0)
375+
{
376+
// interpolate the correction between each pair of consecutive keyframes
377+
for (int keyFrameIndex = 0; keyFrameIndex < VideoKeyFrames.Count - 1; keyFrameIndex++)
378+
ApplyInterpolatedSegment(baseline, corrected, VideoKeyFrames[keyFrameIndex],
379+
VideoKeyFrames[keyFrameIndex + 1]);
380+
381+
// hold the last keyframe's correction flat to the end of the data
382+
ApplyFlatTail(baseline, corrected, VideoKeyFrames[VideoKeyFrames.Count - 1]);
383+
}
384+
385+
CorrectedGazeLocations = corrected;
386+
}
387+
388+
/// <summary>
389+
/// Updates only the corrected-gaze segments adjacent to the keyframe at the given data index,
390+
/// after that keyframe was added or moved. Recomputes the segment from the previous keyframe and
391+
/// the segment to the next keyframe, or the flat tail if it is the last keyframe.
392+
/// </summary>
393+
/// <param name="dataIndex">Data index of the keyframe that changed</param>
394+
private void UpdateCorrectedGazeAround(int dataIndex)
395+
{
396+
NDArray? baseline = GazeBaseline;
397+
if ((object)baseline == null || (object)CorrectedGazeLocations == null)
398+
{
399+
RebuildCorrectedGaze();
400+
return;
401+
}
402+
403+
int keyFramePosition = -1;
404+
for (int i = 0; i < VideoKeyFrames.Count; i++)
405+
if (VideoKeyFrames[i].DataIndex == dataIndex)
406+
{
407+
keyFramePosition = i;
408+
break;
409+
}
410+
if (keyFramePosition < 0)
411+
{
412+
RebuildCorrectedGaze();
413+
return;
414+
}
415+
416+
VideoKeyFrame editedKeyFrame = VideoKeyFrames[keyFramePosition];
417+
418+
// recompute the segment behind the edited keyframe
419+
if (keyFramePosition > 0)
420+
ApplyInterpolatedSegment(baseline, CorrectedGazeLocations,
421+
VideoKeyFrames[keyFramePosition - 1], editedKeyFrame);
422+
423+
// recompute the segment ahead of the edited keyframe, or the flat tail if it is last
424+
if (keyFramePosition < VideoKeyFrames.Count - 1)
425+
ApplyInterpolatedSegment(baseline, CorrectedGazeLocations,
426+
editedKeyFrame, VideoKeyFrames[keyFramePosition + 1]);
427+
else
428+
ApplyFlatTail(baseline, CorrectedGazeLocations, editedKeyFrame);
429+
}
430+
431+
/// <summary>
432+
/// Writes the baseline plus a linearly interpolated correction into the corrected array over the
433+
/// half-open range from the first keyframe's data index to the second keyframe's data index. The
434+
/// correction ramps from the start keyframe's offset to the end keyframe's offset.
435+
/// </summary>
436+
/// <param name="baseline">Baseline gaze the correction is added to</param>
437+
/// <param name="corrected">Corrected gaze array written into</param>
438+
/// <param name="startKeyFrame">Keyframe at the start of the segment</param>
439+
/// <param name="endKeyFrame">Keyframe at the end of the segment</param>
440+
private void ApplyInterpolatedSegment(NDArray baseline, NDArray corrected,
441+
VideoKeyFrame startKeyFrame, VideoKeyFrame endKeyFrame)
442+
{
443+
int startIndex = startKeyFrame.DataIndex;
444+
int endIndex = endKeyFrame.DataIndex;
445+
double startDeltaX = startKeyFrame.GazeX - (double)baseline[startIndex, 0];
446+
double startDeltaY = startKeyFrame.GazeY - (double)baseline[startIndex, 1];
447+
double endDeltaX = endKeyFrame.GazeX - (double)baseline[endIndex, 0];
448+
double endDeltaY = endKeyFrame.GazeY - (double)baseline[endIndex, 1];
449+
int numFrames = endIndex - startIndex;
450+
for (int i = startIndex; i < endIndex; i++)
451+
{
452+
double multiplier = numFrames == 0 ? 0.0 : (double)(i - startIndex) / numFrames;
453+
corrected[i, 0] = (double)baseline[i, 0] + startDeltaX + multiplier * (endDeltaX - startDeltaX);
454+
corrected[i, 1] = (double)baseline[i, 1] + startDeltaY + multiplier * (endDeltaY - startDeltaY);
455+
}
456+
}
457+
458+
/// <summary>
459+
/// Writes the baseline plus the keyframe's correction held flat from the keyframe's data index to
460+
/// the end of the data.
461+
/// </summary>
462+
/// <param name="baseline">Baseline gaze the correction is added to</param>
463+
/// <param name="corrected">Corrected gaze array written into</param>
464+
/// <param name="keyFrame">Keyframe whose correction is held flat to the end</param>
465+
private void ApplyFlatTail(NDArray baseline, NDArray corrected, VideoKeyFrame keyFrame)
466+
{
467+
int startIndex = keyFrame.DataIndex;
468+
double deltaX = keyFrame.GazeX - (double)baseline[startIndex, 0];
469+
double deltaY = keyFrame.GazeY - (double)baseline[startIndex, 1];
470+
for (int i = startIndex; i < baseline.Shape[0]; i++)
471+
{
472+
corrected[i, 0] = (double)baseline[i, 0] + deltaX;
473+
corrected[i, 1] = (double)baseline[i, 1] + deltaY;
474+
}
475+
}
476+
342477
private int? PreviousVideoKeyFrame
343478
{
344479
get
@@ -406,12 +541,18 @@ await Task.Run(() =>
406541
EyetrackingFPS = capturedSampleRate;
407542
IsGazeLoaded = true;
408543
gazeFileName = fileName;
409-
if (videoReader != null)
410-
SetCurrentAsDataStart();
411544
if (_isGazeFilterEnabled)
412545
await ApplyFilter();
413546
else
547+
{
548+
RebuildCorrectedGaze();
414549
RebuildTTLMarkerCache();
550+
}
551+
if (videoReader != null)
552+
{
553+
SetCurrentAsDataStart();
554+
UpdateDisplay();
555+
}
415556
IsLoadingGaze = false;
416557
}
417558

@@ -450,6 +591,8 @@ public void SetCurrentAsDataStart()
450591
AddKeyFrame(videoReader.frameCount - 1);
451592
else AddKeyFrame(dataEndFrame.Value - 1);
452593
}
594+
595+
RebuildCorrectedGaze();
453596
}
454597

455598
public void SendToRecentering()
@@ -527,6 +670,7 @@ private async Task ApplyFilter()
527670
OutlierThresholdY,
528671
OutlierThresholdRadius));
529672
FilteredGazeLocations = result;
673+
RebuildCorrectedGaze();
530674
RebuildTTLMarkerCache();
531675

532676
IsProgressBarVisible = false;

SharpEyes/Views/RecenteringUserControl.axaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
Canvas.Left="{Binding GazeCircleLeft}"
8282
Canvas.Top="{Binding GazeCircleTop}"/>
8383
<Panel Width="{Binding VideoWidth}" Height="{Binding VideoHeight}"
84-
IsHitTestVisible="False" IsVisible="{Binding IsGazeAtNaN}">
84+
IsHitTestVisible="False" IsVisible="{Binding !FrameHasGaze}">
8585
<TextBlock Text="No gaze data" HorizontalAlignment="Right" VerticalAlignment="Bottom"
8686
Margin="4" Foreground="Red" FontWeight="Bold"/>
8787
</Panel>

0 commit comments

Comments
 (0)