Skip to content

Commit 2fc6dc1

Browse files
authored
Merge pull request #2198 from opensim-org/express_C3D_forces_at_optional_location
Express c3d forces at optional location
2 parents 6fafb28 + f4c8863 commit 2fc6dc1

17 files changed

+996
-62
lines changed

Bindings/Java/tests/TestC3DFileAdapter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ class TestC3DFileAdapter {
55
public static void test_C3DFileAdapter() {
66
C3DFileAdapter c3dAdapter = new C3DFileAdapter();
77
StdMapStringTimeSeriesTableVec3 tables =
8-
c3dAdapter.read("walking5.c3d");
8+
c3dAdapter.read("walking5.c3d",
9+
C3DFileAdapter.ForceLocation.CenterOfPressure);
910

1011
// Marker data read from C3D.
1112
TimeSeriesTableVec3 markerTable = tables.get("markers");

Bindings/Python/tests/test_DataAdapter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_C3DFileAdapter(self):
3636
except AttributeError:
3737
# C3D support not available. OpenSim was not compiled with BTK.
3838
return
39-
tables = adapter.read(os.path.join(test_dir, 'walking2.c3d'))
39+
tables = adapter.read(os.path.join(test_dir, 'walking2.c3d'), 0)
4040
markers = tables['markers']
4141
forces = tables['forces']
4242

@@ -45,7 +45,7 @@ def test_C3DFileAdapter(self):
4545
assert forces.getNumRows() == 9992
4646
assert forces.getNumColumns() == 6
4747

48-
tables = adapter.read(os.path.join(test_dir, 'walking5.c3d'))
48+
tables = adapter.read(os.path.join(test_dir, 'walking5.c3d'), 1)
4949

5050
# Marker data read from C3D.
5151
markers = tables['markers']

Bindings/common.i

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,27 @@ namespace OpenSim {
382382
%include <OpenSim/Common/CSVFileAdapter.h>
383383
%include <OpenSim/Common/C3DFileAdapter.h>
384384

385+
%extend OpenSim::C3DFileAdapter {
386+
Tables read(const std::string& fileName, unsigned int wrt) {
387+
C3DFileAdapter::ForceLocation location;
388+
switch(wrt) {
389+
case 0:
390+
location = C3DFileAdapter::ForceLocation::OriginOfForcePlate;
391+
break;
392+
case 1:
393+
location = C3DFileAdapter::ForceLocation::CenterOfPressure;
394+
break;
395+
case 2:
396+
location = C3DFileAdapter::ForceLocation::PointOfWrenchApplication;
397+
break;
398+
default:
399+
throw OpenSim::Exception{
400+
"An invalid C3DFileAdapter::ForceLocation was provided."};
401+
}
402+
return C3DFileAdapter::read(fileName, location);
403+
};
404+
};
405+
385406
namespace OpenSim {
386407
%ignore TableSource_::TableSource_(TableSource_ &&);
387408
}

OpenSim/Auxiliary/auxiliaryTestFunctions.h

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,73 @@
3333
#include <fstream>
3434
#include <string>
3535
#include <regex>
36+
#include <type_traits>
3637

37-
template <typename T>
38-
void ASSERT_EQUAL(T expected,
38+
/**
39+
* ASSERT_EQUAL is a general utility for comparing two values and throwing
40+
* an Exception with a caller defined message when values are not equivalent.
41+
* Note, ASSERT_EQUAL is typically used to verify that some found value matches
42+
* a given expected or standard value. If the expected value is NaN,
43+
* ASSERT_EQUAL will NOT throw if the found value is also NaN. This is
44+
* particularly helpful for comparing motion capture data where missing data
45+
* are denoted by NaN values. If NaNs are not acceptable for your test, then
46+
* the expected value should not be NaN. In the case of floating point values
47+
* (or containers of floating points) a tolerance of the same value type is
48+
* required.
49+
*/
50+
template <typename T,
51+
typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr >
52+
void ASSERT_EQUAL(T expected,
3953
T found,
4054
T tolerance,
4155
std::string file = "",
4256
int line = -1,
4357
std::string message = "") {
58+
// if both values are NaN treat them as being equivalent for the
59+
// sake of comparing experimental data and results where NaNs are
60+
// possible
61+
if(SimTK::isNaN(found) && SimTK::isNaN(expected))
62+
return;
63+
if (found < expected - tolerance || found > expected + tolerance)
64+
throw OpenSim::Exception(message, file, line);
65+
}
66+
67+
template <typename T,
68+
typename std::enable_if<std::is_integral<T>::value>::type* = nullptr >
69+
void ASSERT_EQUAL(T expected,
70+
T found,
71+
std::string file = "",
72+
int line = -1,
73+
std::string message = "") {
74+
if (found != expected)
75+
throw OpenSim::Exception(message, file, line);
76+
}
77+
78+
template <typename T,
79+
typename std::enable_if<!std::is_arithmetic<T>::value>::type* = nullptr >
80+
void ASSERT_EQUAL(T expected,
81+
T found,
82+
T tolerance,
83+
std::string file = "",
84+
int line = -1,
85+
std::string message = "") {
86+
// if both values are NaN treat them as equivalent
87+
if (found.isNaN() && expected.isNaN() )
88+
return;
4489
if (found < expected - tolerance || found > expected + tolerance)
4590
throw OpenSim::Exception(message, file, line);
4691
}
92+
4793
template<int M, typename ELT, int STRIDE>
4894
void ASSERT_EQUAL(const SimTK::Vec<M, ELT, STRIDE>& vecA,
4995
const SimTK::Vec<M, ELT, STRIDE>& vecB,
5096
const std::string& file = "",
5197
int line = -1,
5298
const std::string& message = "") {
5399
try {
100+
// if both values are NaN treat them as being equivalent
101+
if (vecA.isNaN() && vecB.isNaN())
102+
return;
54103
SimTK_TEST_EQ(vecA, vecB);
55104
} catch(const SimTK::Exception::Assert&) {
56105
throw OpenSim::Exception(message, file, line);
@@ -64,12 +113,37 @@ void ASSERT_EQUAL(const SimTK::Vec<M, ELT, STRIDE>& vecA,
64113
int line = -1,
65114
const std::string& message = "") {
66115
try {
116+
// if both values are NaN treat them as being equivalent
117+
if (vecA.isNaN() && vecB.isNaN())
118+
return;
67119
SimTK_TEST_EQ_TOL(vecA, vecB, tolerance);
68120
} catch(const SimTK::Exception::Assert&) {
69121
throw OpenSim::Exception(message, file, line);
70122
}
71123
}
72124

125+
template<typename Container, typename T>
126+
void ASSERT_EQUAL( const Container& vecA,
127+
const Container& vecB,
128+
T tolerance,
129+
std::string file = "",
130+
int line = -1,
131+
std::string message = "") {
132+
133+
if (vecA.size() != vecB.size()) {
134+
throw OpenSim::Exception(message, file, line);
135+
}
136+
else {
137+
for (auto i = 0; i < vecA.size(); ++i) {
138+
// if both values are NaN treat them as being equivalent
139+
if ( SimTK::isNaN(vecA[i]) && SimTK::isNaN(vecB[i]) )
140+
continue;
141+
if (vecA[i] < vecB[i] - tolerance || vecA[i] > vecB[i] + tolerance) {
142+
throw OpenSim::Exception(message, file, line);
143+
}
144+
}
145+
}
146+
}
73147

74148
inline void ASSERT(bool cond,
75149
std::string file="",

OpenSim/Common/C3DFileAdapter.cpp

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ C3DFileAdapter::clone() const {
4444
}
4545

4646
C3DFileAdapter::Tables
47-
C3DFileAdapter::read(const std::string& fileName) {
48-
auto abstables = C3DFileAdapter{}.extendRead(fileName);
47+
C3DFileAdapter::read(const std::string& fileName, ForceLocation wrt)
48+
{
49+
C3DFileAdapter c3dreader{};
50+
c3dreader.setLocationForForceExpression(wrt);
51+
52+
auto abstables = c3dreader.extendRead(fileName);
4953
auto marker_table =
5054
std::static_pointer_cast<TimeSeriesTableVec3>(abstables.at(_markers));
5155
auto force_table =
@@ -108,13 +112,23 @@ C3DFileAdapter::extendRead(const std::string& fileName) const {
108112

109113
double time_step{1.0 / acquisition->GetPointFrequency()};
110114
for(int f = 0; f < marker_nrow; ++f) {
111-
SimTK::RowVector_<SimTK::Vec3> row{marker_pts->GetItemNumber()};
115+
SimTK::RowVector_<SimTK::Vec3> row{ marker_pts->GetItemNumber(),
116+
SimTK::Vec3(SimTK::NaN) };
112117
int m{0};
113118
for(auto it = marker_pts->Begin(); it != marker_pts->End(); ++it) {
114119
auto pt = *it;
115-
row[m++] = SimTK::Vec3{pt->GetValues().coeff(f, 0),
116-
pt->GetValues().coeff(f, 1),
117-
pt->GetValues().coeff(f, 2)};
120+
// BTK reads empty values as zero, but sets a "residual" value
121+
// to -1 and it is how it knows to export these values as
122+
// blank, instead of 0, when exporting to .trc
123+
// See: BTKCore/Code/IO/btkTRCFileIO.cpp#L359-L360
124+
// Read in value if it is not zero or residual is not -1
125+
if (!pt->GetValues().row(f).isZero() || //not precisely zero
126+
(pt->GetResiduals().coeff(f) != -1) ) {//residual is not -1
127+
row[m] = SimTK::Vec3{ pt->GetValues().coeff(f, 0),
128+
pt->GetValues().coeff(f, 1),
129+
pt->GetValues().coeff(f, 2) };
130+
}
131+
++m;
118132
}
119133

120134
marker_matrix.updRow(f) = row;
@@ -169,6 +183,8 @@ C3DFileAdapter::extendRead(const std::string& fileName) const {
169183
// Get ground reaction wrenches for the force platform.
170184
auto ground_reaction_wrench_filter =
171185
btk::GroundReactionWrenchFilter::New();
186+
ground_reaction_wrench_filter->setLocation(
187+
btk::GroundReactionWrenchFilter::Location(getLocationForForceExpression()));
172188
ground_reaction_wrench_filter->SetInput(*platform);
173189
auto wrench_collection = ground_reaction_wrench_filter->GetOutput();
174190
ground_reaction_wrench_filter->Update();

OpenSim/Common/C3DFileAdapter.h

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,52 @@ class OSIMCOMMON_API C3DFileAdapter : public FileAdapter {
3737
typedef std::vector<Event> EventTable;
3838
typedef std::map<std::string, std::shared_ptr<TimeSeriesTableVec3>> Tables;
3939

40+
/** Enumerated list of locations in which read in forces are expressed.
41+
Measurement from force plates can be expressed by the C3DFileAdapter
42+
either at the OriginOfForcePlate (the default), CenterOfPressure, or
43+
the PointOfWrenchApplication. It is an optional argument to
44+
C3DFileAdapter::read().
45+
46+
In the case of the CenterOfPressure (COP), the underlying assumptions
47+
are that the ground plane (in which COP is defined) passes through the
48+
lab origin (0,0,0) with the Z-axis as its normal vector.
49+
50+
The PointOfWrenchApplication (PWA) does not assume a plane of contact.
51+
The PWA is an equivalent wrench in the lab frame and computed according
52+
to Shimba 1984.
53+
Takeshi Shimba, An estimation of center of gravity from force platform
54+
data, Journal of Biomechanics, 17(1), pp53-60, 1984.
55+
56+
<b>C++ example</b>
57+
\code{.cpp}
58+
auto tables = C3DFileAdapter::read("myData.c3d",
59+
C3DFileAdapter::ForceLocation::CenterOfPressure);
60+
\endcode
61+
62+
<b>Python example</b>
63+
\code{.py}
64+
import opensim
65+
tables = C3DFileAdapter.read("myData.c3d",
66+
opensim.C3DFileAdapter.ForceLocation_CenterOfPressure)
67+
\endcode
68+
69+
<b>Java example</b>
70+
\code{.java}
71+
tables = C3DFileAdapter.read("myData.c3d",
72+
C3DFileAdapter.ForceLocation.CenterOfPressure);
73+
\endcode
74+
75+
<b>MATLAB example</b>
76+
\code{.m}
77+
tables = C3DFileAdapter.read("myData.c3d", 1);
78+
\endcode
79+
*/
80+
enum class ForceLocation {
81+
OriginOfForcePlate = 0, ///< 0 : the origin of the force-plate
82+
CenterOfPressure = 1, ///< 1 : the center of pressure
83+
PointOfWrenchApplication = 2 ///< 2 : PWA as defined by Shimba, 1984
84+
};
85+
4086
C3DFileAdapter() = default;
4187
C3DFileAdapter(const C3DFileAdapter&) = default;
4288
C3DFileAdapter(C3DFileAdapter&&) = default;
@@ -45,14 +91,27 @@ class OSIMCOMMON_API C3DFileAdapter : public FileAdapter {
4591
~C3DFileAdapter() = default;
4692

4793
C3DFileAdapter* clone() const override;
94+
95+
void setLocationForForceExpression(const ForceLocation location) {
96+
_location = location;
97+
}
98+
99+
const ForceLocation getLocationForForceExpression() const {
100+
return _location;
101+
}
48102

49103
/** Read in a C3D file into separate markers and forces tables of type
50104
TimeSeriesTableVec3. The markers table has each column labeled by its
51105
corresponding marker name. For the forces table, the data are grouped
52106
by sensor (force-plate #) in force, point and moment order, with the
53-
respective *f#*, *p#* and *m#* column labels. */
107+
respective *f#*, *p#* and *m#* column labels. C3DFileAdpater provides
108+
options for expressing the force-plate measurements either as the
109+
net force and moments expressed at the ForcePlateOrigin, the
110+
CentereOfPressure, or the PointOfWrenchApplication (see above).
111+
*/
54112
static
55-
Tables read(const std::string& fileName);
113+
Tables read(const std::string& fileName,
114+
ForceLocation wrt = ForceLocation::OriginOfForcePlate);
56115

57116
static
58117
void write(const Tables& markerTable, const std::string& fileName);
@@ -69,6 +128,8 @@ class OSIMCOMMON_API C3DFileAdapter : public FileAdapter {
69128
private:
70129
static const std::unordered_map<std::string, std::size_t> _unit_index;
71130

131+
ForceLocation _location{ ForceLocation::OriginOfForcePlate };
132+
72133
};
73134

74135
} // namespace OpenSim

OpenSim/Common/DataTable.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ class DataTable_ : public AbstractDataTable {
782782

783783
if(index < getNumRows() - 1)
784784
for(size_t r = index; r < getNumRows() - 1; ++r)
785-
_depData.updRow((int)index) = _depData.row((int)(index + 1));
785+
_depData.updRow((int)r) = _depData.row((int)(r + 1));
786786

787787
_depData.resizeKeep(_depData.nrow() - 1, _depData.ncol());
788788
_indData.erase(_indData.begin() + index);

0 commit comments

Comments
 (0)