Skip to content

Commit bf03f63

Browse files
cjshajonnew
andauthored
Add operator for transforming TS4231 position data (#477)
- Add TS4231V1SpatialTransform operator to the base class - Add associated files in design library to implement GUI for facilitating the calibration process - Add SpatialTransform3D class to the base class as the singular class being accessed by the GUI Co-authored-by: jonnew <[email protected]>
1 parent d774b67 commit bf03f63

File tree

6 files changed

+2933
-0
lines changed

6 files changed

+2933
-0
lines changed

OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs

Lines changed: 678 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
using System;
2+
using System.Drawing;
3+
using System.Linq;
4+
using System.Numerics;
5+
using System.Reactive.Linq;
6+
using System.Text;
7+
using System.Windows.Forms;
8+
using Bonsai.Design;
9+
10+
namespace OpenEphys.Onix1.Design
11+
{
12+
/// <summary>
13+
/// Partial class to create a spatial-calibration GUI for <see cref="TS4231V1SpatialTransform.SpatialTransform"/>.
14+
/// </summary>
15+
public partial class SpatialTransformMatrixDialog : Form
16+
{
17+
internal SpatialTransform3D SpatialTransform;
18+
const byte NumMeasurements = 100;
19+
readonly IObservable<TS4231V1PositionDataFrame> PositionDataSource;
20+
IDisposable richTextBoxStatusUpdateSubscription;
21+
IDisposable MeasurementCalculationSubscription;
22+
23+
internal SpatialTransformMatrixDialog(IObservable<TS4231V1PositionDataFrame> dataSource, SpatialTransform3D transformProperties)
24+
{
25+
InitializeComponent();
26+
27+
richTextBoxInstructions.Clear();
28+
richTextBoxInstructions.BulletIndent = 16;
29+
richTextBoxInstructions.SelectedText = "Follow the instructions below to transfom TS4231 position data from a generic base-station reference frame to a user-define reference frame:\n\n";
30+
richTextBoxInstructions.SelectionBullet = true;
31+
richTextBoxInstructions.SelectedText = "Determine a set of 4, well separated XYZ positions in the space in which the headstage will move. These positions should explore a large region of the territory that the headstage will explore and not be confined to a particular plane. Each position defined in this step corresponds to a row in the table below.\n";
32+
richTextBoxInstructions.SelectedText = "For the first position, place the headstage and click the first measure button on the GUI. After the TS4231 coordinate is obtained from the headstage, enter the known User coordinates in the X, Y, and Z text boxes to provide your spatial mapping. Repeat this process for the second, third, and fourth positions to populate the second, third, and fourth rows of the table.\n";
33+
richTextBoxInstructions.SelectedText = "Click \"OK\" to close this GUI and set the spatial transform properties in the workflow.\n";
34+
richTextBoxInstructions.SelectionBullet = false;
35+
richTextBoxInstructions.SelectedText = "\nFor more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation.";
36+
37+
SpatialTransform = transformProperties;
38+
PositionDataSource = dataSource;
39+
40+
var ts4231TextBoxes = new TextBox[] {
41+
textBoxTS4231Coordinate0, textBoxTS4231Coordinate1,
42+
textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 };
43+
var preTransformCoordinates = MatrixToFloatArray(SpatialTransform.A);
44+
for (byte i = 0; i < 4; i++)
45+
ts4231TextBoxes[i].Text = float.IsNaN(preTransformCoordinates[i * 3]) ? "" : $"{preTransformCoordinates[i * 3]}, " +
46+
$"{preTransformCoordinates[i * 3 + 1]}, " +
47+
$"{preTransformCoordinates[i * 3 + 2]}";
48+
49+
var userTextBoxes = new TextBox[] {
50+
textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z,
51+
textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z,
52+
textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z,
53+
textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z };
54+
var postTransformCoordinates = MatrixToFloatArray(SpatialTransform.B);
55+
foreach (var (tb, comp) in Enumerable.Zip(userTextBoxes, postTransformCoordinates, (tb, comp) => (tb, comp)))
56+
tb.Text = float.IsNaN(comp) ? "" : comp.ToString();
57+
58+
IndicateSpatialTransformStatus();
59+
}
60+
61+
void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e)
62+
{
63+
var tag = Convert.ToByte(((TextBox)sender).Tag);
64+
try { SpatialTransform.B = SetMatrixElement(SpatialTransform.B, float.Parse(((TextBox)sender).Text), tag / 3, tag % 3); }
65+
catch { SpatialTransform.B = SetMatrixElement(SpatialTransform.B, float.NaN, tag / 3, tag % 3); }
66+
IndicateSpatialTransformStatus();
67+
}
68+
69+
void ButtonMeasure_Click(object sender, EventArgs e)
70+
{
71+
TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 };
72+
var index = Convert.ToByte(((Button)sender).Tag);
73+
74+
for (byte i = 0; i < 3; i++)
75+
SpatialTransform.A = SetMatrixElement(SpatialTransform.A, float.NaN, index, i);
76+
ts4231TextBoxes[index].Text = "";
77+
78+
if (((Button)sender).Text == "Measure")
79+
{
80+
richTextBoxStatus.SelectionColor = Color.Blue;
81+
richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n");
82+
IndicateSpatialTransformStatus();
83+
textBoxSpatialTransformMatrix.Text = "";
84+
((Button)sender).Text = "Cancel";
85+
EnableButtons(false, index);
86+
87+
var sharedPositionDataGroups = PositionDataSource
88+
.Take(NumMeasurements)
89+
.Timeout(new TimeSpan(0, 0, 5), Observable.Empty<TS4231V1PositionDataFrame>())
90+
.Publish();
91+
92+
richTextBoxStatusUpdateSubscription = sharedPositionDataGroups
93+
.GroupBy(dataFrame => dataFrame.SensorIndex, dataFrame => dataFrame.Position)
94+
.SelectMany(group => group.Count().Select(count => new { Index = group.Key, MeasurementCount = count }))
95+
.Aggregate(
96+
(richTextBoxStatusUpdate: "", Count: 0),
97+
(acc, sensor) =>
98+
{
99+
var richTextBoxStatusUpdateString = $"{acc.richTextBoxStatusUpdate}{sensor.MeasurementCount} samples from sensor {sensor.Index}.\n";
100+
return (richTextBoxStatusUpdateString, acc.Count + sensor.MeasurementCount);
101+
},
102+
acc => (acc.richTextBoxStatusUpdate, Valid: acc.Count == NumMeasurements))
103+
.ObserveOn(new ControlScheduler(this))
104+
.Subscribe(finalResult =>
105+
{
106+
if (finalResult.Valid)
107+
{
108+
richTextBoxStatus.SelectionColor = Color.Black;
109+
richTextBoxStatus.AppendText($"{finalResult.richTextBoxStatusUpdate}Measurement at coordinate {index} complete.\n\n");
110+
}
111+
else
112+
{
113+
richTextBoxStatus.SelectionColor = Color.Red;
114+
richTextBoxStatus.AppendText($"Measurement at coordinate {index} timed out.\n" +
115+
"Confirm the Lighthouse receivers are within range of and unobstructed from Lighthouse transmitters.\n\n");
116+
}
117+
EnableButtons(true, index);
118+
});
119+
120+
MeasurementCalculationSubscription = sharedPositionDataGroups
121+
.Aggregate(
122+
(Sum: Vector3.Zero, Count: 0),
123+
(acc, current) => (acc.Sum + current.Position, acc.Count + 1),
124+
acc =>
125+
{
126+
var measurement = acc.Sum / NumMeasurements;
127+
SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.X, index, 0);
128+
SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.Y, index, 1);
129+
SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.Z, index, 2);
130+
return (Position: measurement, Valid: acc.Count == NumMeasurements);
131+
})
132+
.ObserveOn(new ControlScheduler(this))
133+
.Subscribe(measurement =>
134+
{
135+
((Button)sender).Text = "Measure";
136+
if (measurement.Valid)
137+
{
138+
ts4231TextBoxes[index].Text = $"{measurement.Position.X}, {measurement.Position.Y}, {measurement.Position.Z}";
139+
IndicateSpatialTransformStatus();
140+
}
141+
});
142+
143+
sharedPositionDataGroups.Connect();
144+
}
145+
else
146+
{
147+
richTextBoxStatusUpdateSubscription.Dispose();
148+
MeasurementCalculationSubscription.Dispose();
149+
richTextBoxStatus.SelectionColor = Color.Red;
150+
richTextBoxStatus.AppendText($"Measurement at coordinate {index} cancelled by user.\n\n");
151+
((Button)sender).Text = "Measure";
152+
EnableButtons(true, index);
153+
}
154+
}
155+
156+
void ButtonOK_Click(object sender, EventArgs e)
157+
{
158+
var confirmationMessage = "";
159+
var invalidInput = false;
160+
if (ContainsNaN(SpatialTransform.A) || ContainsNaN(SpatialTransform.B))
161+
{
162+
confirmationMessage = $"At least one entry in the TS4231V1 Calibration GUI form is invalid:\n\n";
163+
164+
for (byte i = 0; i < 4; i++)
165+
if (float.IsNaN(MatrixToFloatArray(SpatialTransform.A)[i * 3]))
166+
confirmationMessage += $" • TS4231 coordinate {i}\n";
167+
168+
var axes = new char[] { 'X', 'Y', 'Z' };
169+
var coordinates = new byte[] { 0, 1, 2, 3 };
170+
171+
for (byte i = 0; i < 12; i++)
172+
if (float.IsNaN(MatrixToFloatArray(SpatialTransform.B)[i]))
173+
confirmationMessage += $" • Component {axes[i % 3]} from user coordinate {coordinates[i / 3]}\n";
174+
175+
confirmationMessage += "\nAny invalid entry will not be saved. ";
176+
invalidInput = true;
177+
}
178+
else if (!Matrix4x4.Invert(SpatialTransform.M, out _))
179+
{
180+
confirmationMessage = $"The calculated spatial transform matrix is non-invertible. ";
181+
invalidInput = true;
182+
}
183+
184+
if (invalidInput)
185+
{
186+
confirmationMessage += "The transformed position data will be NaNs until all entries are valid.\n\n" +
187+
"Would you like to continue?";
188+
if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes)
189+
DialogResult = DialogResult.OK;
190+
}
191+
else
192+
DialogResult = DialogResult.OK;
193+
}
194+
195+
void EnableButtons(bool enable, byte index)
196+
{
197+
var buttons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3, buttonOK, buttonCancel };
198+
Array.ForEach(buttons, button => button.Enabled = enable || (Convert.ToByte(button.Tag) == index));
199+
}
200+
201+
void IndicateSpatialTransformStatus()
202+
{
203+
if (ContainsNaN(SpatialTransform.A) || ContainsNaN(SpatialTransform.B))
204+
{
205+
toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage;
206+
toolStripStatusLabel.Text = "All fields must be properly populated.";
207+
textBoxSpatialTransformMatrix.Text = "";
208+
}
209+
else if (!Matrix4x4.Invert(SpatialTransform.M, out _))
210+
{
211+
toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage;
212+
toolStripStatusLabel.Text = "The calculated spatial transform matrix must be invertible.";
213+
textBoxSpatialTransformMatrix.Text = "";
214+
}
215+
else
216+
{
217+
toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage;
218+
toolStripStatusLabel.Text = "Spatial transform matrix is calculated.";
219+
textBoxSpatialTransformMatrix.Text = Matrix4x4ToPrettyString(SpatialTransform.M);
220+
}
221+
}
222+
223+
static float[] MatrixToFloatArray(Matrix4x4 m) =>
224+
new float[] { m.M11, m.M12, m.M13,
225+
m.M21, m.M22, m.M23,
226+
m.M31, m.M32, m.M33,
227+
m.M41, m.M42, m.M43 };
228+
229+
static bool ContainsNaN(Matrix4x4 m) => MatrixToFloatArray(m).Any(float.IsNaN);
230+
231+
static Matrix4x4 SetMatrixElement(Matrix4x4 m, float value, int coordinate, int component)
232+
{
233+
if (coordinate is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(coordinate) + " must be 0, 1, 2, or 3.");
234+
if (component is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(component) + " must be 0, 1, or 2.");
235+
236+
switch ((coordinate, component))
237+
{
238+
case (0, 0): m.M11 = value; break;
239+
case (0, 1): m.M12 = value; break;
240+
case (0, 2): m.M13 = value; break;
241+
case (1, 0): m.M21 = value; break;
242+
case (1, 1): m.M22 = value; break;
243+
case (1, 2): m.M23 = value; break;
244+
case (2, 0): m.M31 = value; break;
245+
case (2, 1): m.M32 = value; break;
246+
case (2, 2): m.M33 = value; break;
247+
case (3, 0): m.M41 = value; break;
248+
case (3, 1): m.M42 = value; break;
249+
case (3, 2): m.M43 = value; break;
250+
}
251+
return m;
252+
}
253+
void richTextBoxInstructions_ContentsResized(object sender, ContentsResizedEventArgs e)
254+
{
255+
((RichTextBox)sender).Height = e.NewRectangle.Height;
256+
}
257+
258+
static string Matrix4x4ToPrettyString(Matrix4x4 matrix, int decimals = 5, int padding = 15)
259+
{
260+
string format = $"F{decimals}";
261+
262+
string[,] elements = new string[4, 4]
263+
{
264+
{ matrix.M11.ToString(format).PadLeft(padding),
265+
matrix.M12.ToString(format).PadLeft(padding),
266+
matrix.M13.ToString(format).PadLeft(padding),
267+
matrix.M14.ToString(format).PadLeft(padding) },
268+
{ matrix.M21.ToString(format).PadLeft(padding),
269+
matrix.M22.ToString(format).PadLeft(padding),
270+
matrix.M23.ToString(format).PadLeft(padding),
271+
matrix.M24.ToString(format).PadLeft(padding) },
272+
{ matrix.M31.ToString(format).PadLeft(padding),
273+
matrix.M32.ToString(format).PadLeft(padding),
274+
matrix.M33.ToString(format).PadLeft(padding),
275+
matrix.M34.ToString(format).PadLeft(padding) },
276+
{ matrix.M41.ToString(format).PadLeft(padding),
277+
matrix.M42.ToString(format).PadLeft(padding),
278+
matrix.M43.ToString(format).PadLeft(padding),
279+
matrix.M44.ToString(format).PadLeft(padding) }
280+
};
281+
282+
var sb = new StringBuilder();
283+
sb.Append("[[");
284+
285+
for (int row = 0; row < 4; row++)
286+
{
287+
for (int col = 0; col < 4; col++)
288+
{
289+
sb.Append(elements[row, col]);
290+
if (col < 3) sb.Append(",");
291+
}
292+
sb.Append("]");
293+
294+
if (row < 3)
295+
{
296+
sb.Append(",");
297+
sb.AppendLine();
298+
sb.Append(" [");
299+
}
300+
else
301+
sb.Append("]");
302+
}
303+
return sb.ToString();
304+
}
305+
}
306+
}

0 commit comments

Comments
 (0)