Skip to content

Commit

Permalink
Merge pull request #189 from SainsburyWellcomeCentre/file-source
Browse files Browse the repository at this point in the history
Add simulated video source module
  • Loading branch information
glopesdev authored Dec 5, 2023
2 parents ba891c8 + c6715d2 commit e8dc0ce
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 14 deletions.
47 changes: 47 additions & 0 deletions src/Aeon.Acquisition/FileVideoSource.bonsai
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<WorkflowBuilder Version="2.8.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aeon="clr-namespace:Aeon.Acquisition;assembly=Aeon.Acquisition"
xmlns:rx="clr-namespace:Bonsai.Reactive;assembly=Bonsai.Core"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns="https://bonsai-rx.org/2018/workflow">
<Description>Provides a video module simulated from a file for testing and debugging of environments with video pipelines.</Description>
<Workflow>
<Nodes>
<Expression xsi:type="ExternalizedMapping">
<Property Name="Name" DisplayName="TriggerSource" Description="The PWM trigger source used to drive the camera." />
</Expression>
<Expression xsi:type="SubscribeSubject">
<Name>GlobalTrigger</Name>
</Expression>
<Expression xsi:type="ExternalizedMapping">
<Property Name="FileName" />
</Expression>
<Expression xsi:type="Combinator">
<Combinator xsi:type="aeon:VideoFileCapture" />
</Expression>
<Expression xsi:type="ExternalizedMapping">
<Property Name="Name" DisplayName="FrameEvents" Description="The name of the output sequence containing all frame events from the video source." />
</Expression>
<Expression xsi:type="rx:PublishSubject">
<Name>FrameEvents</Name>
</Expression>
<Expression xsi:type="WorkflowOutput" />
<Expression xsi:type="ExternalizedMapping">
<Property Name="Name" DisplayName="TriggerFrequency" Description="The frequency of the trigger source." />
</Expression>
<Expression xsi:type="SubscribeSubject" TypeArguments="sys:Double">
<Name>GlobalTriggerFrequency</Name>
</Expression>
</Nodes>
<Edges>
<Edge From="0" To="1" Label="Source1" />
<Edge From="1" To="3" Label="Source1" />
<Edge From="2" To="3" Label="Source2" />
<Edge From="3" To="5" Label="Source1" />
<Edge From="4" To="5" Label="Source2" />
<Edge From="5" To="6" Label="Source1" />
<Edge From="7" To="8" Label="Source1" />
</Edges>
</Workflow>
</WorkflowBuilder>
52 changes: 38 additions & 14 deletions src/Aeon.Acquisition/VideoFileCapture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,21 @@ public class VideoFileCapture : Source<Timestamped<VideoDataFrame>>
{
[Description("The path to the file used to source the video frames.")]
[Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
public string Path { get; set; }

[Description("Specifies the color conversion to use when reading the video frames.")]
public ColorConversion? ColorConversion { get; set; } = OpenCV.Net.ColorConversion.Bgr2Gray;
public string FileName { get; set; }

public override IObservable<Timestamped<VideoDataFrame>> Generate()
{
var videoFileName = Path;
var colorConversion = ColorConversion;
var videoFileName = FileName;
if (string.IsNullOrEmpty(videoFileName))
{
throw new InvalidOperationException("A valid file name must be specified");
}

return Observable.Defer(() =>
{
var grayscale = new Grayscale();
var capture = new FileCapture { FileName = videoFileName };
var metadataFileName = System.IO.Path.ChangeExtension(videoFileName, ".csv");
var metadataFileName = Path.ChangeExtension(videoFileName, ".csv");
var metadataContents = File.ReadAllLines(metadataFileName).Skip(1).Select(row =>
{
var values = row.Split(',');
Expand All @@ -49,19 +46,46 @@ public override IObservable<Timestamped<VideoDataFrame>> Generate()
}).ToArray();
var frames = capture.Generate();
if (colorConversion.HasValue)
{
var convertColor = new ConvertColor { Conversion = colorConversion.GetValueOrDefault() };
frames = convertColor.Process(frames);
}
return frames.Select((frame, index) =>
return grayscale.Process(frames).Select((frame, index) =>
{
var (seconds, frameID, frameTimestamp) = metadataContents[index];
var dataFrame = new VideoDataFrame(frame, frameID, frameTimestamp);
return Timestamped.Create(dataFrame, seconds);
});
});
}

public IObservable<Timestamped<VideoDataFrame>> Generate<TPayload>(IObservable<Timestamped<TPayload>> source)
{
var videoFileName = FileName;
if (string.IsNullOrEmpty(videoFileName))
{
throw new InvalidOperationException("A valid file name must be specified");
}

const string ImageExtensions = ".png;.bmp;.jpg;.jpeg;.tif;.tiff;.exr";
var extension = Path.GetExtension(videoFileName);
return source.Publish(trigger =>
{
var frameID = 0L;
Timestamped<VideoDataFrame> TimestampFrame(Timestamped<TPayload> timestamped, IplImage frame)
{
var dataFrame = new VideoDataFrame(frame, frameID++, (long)(timestamped.Seconds * 1e6));
return Timestamped.Create(dataFrame, timestamped.Seconds);
}
if (!string.IsNullOrEmpty(extension) && ImageExtensions.Contains(extension))
{
var capture = new LoadImage { FileName = videoFileName, Mode = LoadImageFlags.Grayscale };
return trigger.CombineLatest(capture.Generate(), TimestampFrame);
}
else
{
var grayscale = new Grayscale();
var capture = new FileCapture { FileName = videoFileName, Loop = true };
return trigger.Zip(grayscale.Process(capture.Generate(trigger)), TimestampFrame);
}
});
}
}
}

0 comments on commit e8dc0ce

Please sign in to comment.