Skip to content

Commit f8ea248

Browse files
cdtwiggfacebook-github-bot
authored andcommitted
Refactor to remove duplicate code in marker_tracking. (#609)
Summary: We have two functions, trackPosesPerframe and trackPosesForFrames, which are largely identical. This kept messing me up because I would add functionality in one of the functions or the other and then be confused why nothing had changed. Looking at the two functions, they perform the same function, we just need to pass in the appropriate set of frames into trackPosesForFrames and add support for temporal coherence. Reviewed By: cstollmeta, jeongseok-meta Differential Revision: D82688034
1 parent 3814de0 commit f8ea248

File tree

2 files changed

+100
-182
lines changed

2 files changed

+100
-182
lines changed

momentum/marker_tracking/marker_tracker.cpp

Lines changed: 97 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,23 @@ Eigen::MatrixXf trackSequence(
441441
return outMotion;
442442
}
443443

444+
/// Check if the global transform is zero by checking if any rigid parameters are non-zero.
445+
///
446+
/// This is used to determine whether initialization is needed for pose tracking.
447+
/// If all rigid parameters are zero, we need to solve for an initial rigid transform.
448+
///
449+
/// @param dof The parameter vector to check
450+
/// @param rigidParams Parameter set defining which parameters are rigid/global
451+
/// @return True if global transform is zero (needs initialization), false otherwise
452+
bool isGlobalTransformZero(const Eigen::VectorXf& dof, const ParameterSet& rigidParams) {
453+
for (Eigen::Index i = 0; i < dof.size(); ++i) {
454+
if (rigidParams.test(i) && dof[i] != 0.0f) {
455+
return false;
456+
}
457+
}
458+
return true;
459+
}
460+
444461
/// Track poses independently per frame with fixed character identity.
445462
///
446463
/// This is the main production tracking function used after character calibration.
@@ -462,167 +479,24 @@ Eigen::MatrixXf trackPosesPerframe(
462479
const size_t frameStride) {
463480
const size_t numFrames = markerData.size();
464481
MT_CHECK(numFrames > 0, "Input data is empty.");
465-
MT_CHECK(
466-
globalParams.v.size() == character.parameterTransform.numAllModelParameters(),
467-
"Input model parameters {} do not match character model parameters {}",
468-
globalParams.v.size(),
469-
character.parameterTransform.numAllModelParameters());
470482

471-
const ParameterTransform& pt = character.parameterTransform;
472-
const size_t numMarkers = markerData[0].size();
473-
474-
// pose parameters need to exclude "locators"
475-
ParameterSet poseParams = pt.getPoseParameters();
476-
const auto& locatorSet = pt.parameterSets.find("locators");
477-
if (locatorSet != pt.parameterSets.end()) {
478-
poseParams &= ~locatorSet->second;
483+
// Generate frame indices from stride
484+
std::vector<size_t> frameIndices;
485+
for (size_t iFrame = 0; iFrame < numFrames; iFrame += frameStride) {
486+
frameIndices.push_back(iFrame);
479487
}
480488

481-
// set up the solver
482-
auto solverFunc = SkeletonSolverFunction(character, pt);
483-
GaussNewtonSolverOptions solverOptions;
484-
solverOptions.maxIterations = config.maxIter;
485-
solverOptions.minIterations = 2;
486-
solverOptions.doLineSearch = false;
487-
solverOptions.verbose = config.debug;
488-
solverOptions.threshold = 1.f;
489-
solverOptions.regularization = config.regularization;
490-
auto solver = GaussNewtonSolver(solverOptions, &solverFunc);
491-
solver.setEnabledParameters(poseParams);
492-
493-
// parameter limits constraint
494-
auto limitConstrFunc = std::make_shared<LimitErrorFunction>(character);
495-
limitConstrFunc->setWeight(0.1);
496-
solverFunc.addErrorFunction(limitConstrFunc);
497-
498-
// positional constraint function for markers
499-
auto posConstrFunc = std::make_shared<PositionErrorFunction>(character, config.lossAlpha);
500-
posConstrFunc->setWeight(PositionErrorFunction::kLegacyWeight);
501-
solverFunc.addErrorFunction(posConstrFunc);
502-
503-
std::shared_ptr<SkinnedLocatorErrorFunction> skinnedLocatorPosConstrFunc =
504-
std::make_shared<SkinnedLocatorErrorFunction>(character);
505-
skinnedLocatorPosConstrFunc->setWeight(PositionErrorFunction::kLegacyWeight);
506-
solverFunc.addErrorFunction(skinnedLocatorPosConstrFunc);
507-
508-
// floor penetration constraint data; we assume the world is y-up and floor is y=0 for mocap data.
509-
const auto& floorConstraints = createFloorConstraints<float>(
510-
"Floor_",
511-
character.locators,
512-
Vector3f::UnitY(),
513-
/* y offset */ 0.0f,
514-
/* weight */ 5.0f);
515-
auto halfPlaneConstrFunc = std::make_shared<PlaneErrorFunction>(character, /*half plane*/ true);
516-
halfPlaneConstrFunc->setConstraints(floorConstraints);
517-
halfPlaneConstrFunc->setWeight(PlaneErrorFunction::kLegacyWeight);
518-
solverFunc.addErrorFunction(halfPlaneConstrFunc);
519-
520-
// marker constraint data
521-
auto constrData = createConstraintData(markerData, character.locators);
522-
auto skinnedConstrData = createSkinnedConstraintData(markerData, character.skinnedLocators);
523-
524-
// smoothness constraint only for the joints and exclude global dofs because the global transform
525-
// needs to be accurate (may not matter in practice?)
526-
auto smoothConstrFunc = std::make_shared<ModelParametersErrorFunction>(
527-
character, poseParams & ~pt.getRigidParameters());
528-
smoothConstrFunc->setWeight(config.smoothing);
529-
solverFunc.addErrorFunction(smoothConstrFunc);
530-
531-
// add collision error
532-
std::shared_ptr<CollisionErrorFunction> collisionErrorFunction;
533-
if (config.collisionErrorWeight != 0 && character.collision != nullptr) {
534-
collisionErrorFunction = std::make_shared<CollisionErrorFunction>(character);
535-
collisionErrorFunction->setWeight(config.collisionErrorWeight);
536-
solverFunc.addErrorFunction(collisionErrorFunction);
489+
// Convert globalParams to initial motion matrix
490+
MatrixXf initialMotion(globalParams.v.size(), numFrames);
491+
for (size_t i = 0; i < numFrames; ++i) {
492+
initialMotion.col(i) = globalParams.v;
537493
}
538494

539-
MatrixXf motion(pt.numAllModelParameters(), numFrames);
540-
// initialize parameters to contain identity information
541-
// the identity fields will be used but untouched during optimization
542-
// globalParams could also be repurposed to pass in initial pose value
543-
Eigen::VectorXf dof = globalParams.v;
544-
size_t solverFrame = 0;
545-
double error = 0.0;
546-
// Use the initial global transform is it's not zero
547-
bool needsInit = dof.head(6).isZero(0); // TODO: assume first six dofs are global dofs
548-
549-
// When the frames are not continuous, we sometimes run into an issue when the desired joint
550-
// rotation between two consecutive frames is large (eg. larger than 180). If we initialize from
551-
// the previous result, the smaller rotation will be wrongly chosen, and we cannot recover from
552-
// this mistake. To prevent this, we will solve each frame completely independently when they are
553-
// not continuous.
554-
bool continuous = (frameStride < 5);
555-
if (!continuous) {
556-
needsInit = true;
557-
}
495+
// Determine if tracking should be continuous (temporal coherence)
496+
bool isContinuous = (frameStride < 5);
558497

559-
{ // scope the ProgressBar so it returns
560-
ProgressBar progress("", numFrames);
561-
for (size_t iFrame = 0; iFrame < numFrames; iFrame += frameStride) {
562-
// reinitialize if not continuous
563-
if (!continuous) {
564-
dof = globalParams.v;
565-
}
566-
567-
if ((constrData.at(iFrame).size() + skinnedConstrData.at(iFrame).size()) >
568-
config.minVisPercent * numMarkers) {
569-
// add positional constraints
570-
posConstrFunc->clearConstraints(); // clear constraint data from the previous frame
571-
posConstrFunc->setConstraints(constrData.at(iFrame));
572-
573-
skinnedLocatorPosConstrFunc->clearConstraints();
574-
skinnedLocatorPosConstrFunc->setConstraints(skinnedConstrData.at(iFrame));
575-
576-
// initialization
577-
// TODO: run on first frame or tracking failure
578-
if (needsInit) { // solve only for the rigid parameters as preprocessing
579-
MT_LOGI_IF(
580-
config.debug && continuous, "Solving for an initial rigid pose at frame {}", iFrame);
581-
582-
// Set up different config for initialization
583-
solverOptions.maxIterations = 50; // make sure it converges
584-
solver.setOptions(solverOptions);
585-
solver.setEnabledParameters(pt.getRigidParameters());
586-
smoothConstrFunc->setWeight(0.0); // turn off smoothing - it doesn't affect rigid dofs
587-
588-
solver.solve(dof);
589-
590-
// Recover solver config
591-
solverOptions.maxIterations = config.maxIter;
592-
solver.setOptions(solverOptions);
593-
solver.setEnabledParameters(poseParams);
594-
smoothConstrFunc->setWeight(config.smoothing);
595-
596-
if (continuous) {
597-
needsInit = false;
598-
}
599-
}
600-
601-
// set smoothness target as the last pose -- dof holds parameter values from last (good)
602-
// frame it will serve as a small regularization to rest pose for the first frame
603-
// TODO: API needs improvement
604-
smoothConstrFunc->setTargetParameters(dof, smoothConstrFunc->getTargetWeights());
605-
606-
error += solver.solve(dof);
607-
++solverFrame;
608-
}
609-
610-
// set result to output; fill in frames within a stride
611-
// note that dof contains complete parameter info with identity
612-
for (size_t jDelta = 0; jDelta < frameStride && iFrame + jDelta < numFrames; ++jDelta) {
613-
motion.col(iFrame + jDelta) = dof;
614-
}
615-
progress.increment(frameStride);
616-
}
617-
}
618-
if (config.debug) {
619-
if (solverFrame > 0) {
620-
MT_LOGI("Average per-frame residual: {}", error / solverFrame);
621-
} else {
622-
MT_LOGW("no valid frames to solve");
623-
}
624-
}
625-
return motion;
498+
return trackPosesForFrames(
499+
markerData, character, initialMotion, config, frameIndices, isContinuous);
626500
}
627501

628502
/// Track poses independently for specific frame indices with fixed character identity.
@@ -637,13 +511,15 @@ Eigen::MatrixXf trackPosesPerframe(
637511
/// @param initialMotion Initial parameter values (parameters x frames)
638512
/// @param config Tracking configuration settings
639513
/// @param frameIndices Vector of specific frame indices to solve
514+
/// @param isContinuous Whether to use temporal coherence between frames
640515
/// @return Solved motion parameters matrix (parameters x frames) with poses for selected frames
641516
Eigen::MatrixXf trackPosesForFrames(
642517
const std::span<const std::vector<Marker>> markerData,
643518
const Character& character,
644519
const MatrixXf& initialMotion,
645520
const TrackingConfig& config,
646-
const std::vector<size_t>& frameIndices) {
521+
const std::vector<size_t>& frameIndices,
522+
bool isContinuous) {
647523
const size_t numFrames = markerData.size();
648524
MT_CHECK(numFrames > 0, "Input data is empty.");
649525
MT_CHECK(
@@ -707,6 +583,17 @@ Eigen::MatrixXf trackPosesForFrames(
707583
auto constrData = createConstraintData(markerData, character.locators);
708584
auto skinnedConstrData = createSkinnedConstraintData(markerData, character.skinnedLocators);
709585

586+
// smoothness constraint only for the joints and exclude global dofs because the global transform
587+
// needs to be accurate (may not matter in practice?)
588+
// Only use temporal smoothness if isContinuous is true
589+
std::shared_ptr<ModelParametersErrorFunction> smoothConstrFunc;
590+
if (isContinuous) {
591+
smoothConstrFunc = std::make_shared<ModelParametersErrorFunction>(
592+
character, poseParams & ~pt.getRigidParameters());
593+
smoothConstrFunc->setWeight(config.smoothing);
594+
solverFunc.addErrorFunction(smoothConstrFunc);
595+
}
596+
710597
// add collision error
711598
std::shared_ptr<CollisionErrorFunction> collisionErrorFunction;
712599
if (config.collisionErrorWeight != 0 && character.collision != nullptr) {
@@ -718,18 +605,26 @@ Eigen::MatrixXf trackPosesForFrames(
718605
// initialize parameters to contain identity information
719606
// the identity fields will be used but untouched during optimization
720607
// globalParams could also be repurposed to pass in initial pose value
721-
std::vector<Eigen::VectorXf> poses(frameIndices.size());
722608
Eigen::VectorXf dof = initialMotion.col(sortedFrames.empty() ? 0 : sortedFrames[0]);
723609
size_t solverFrame = 0;
724610
double priorError = 0.0;
725611
double error = 0.0;
726612

727-
MatrixXf outMotion(pt.numAllModelParameters(), numFrames);
613+
// Use the initial global transform if it's not zero
614+
bool needsInit = isGlobalTransformZero(dof, pt.getRigidParameters());
615+
616+
MatrixXf outMotion = initialMotion;
617+
Eigen::Index outputIndex = 0;
728618
{ // scope the ProgressBar so it returns
729-
ProgressBar progress("", sortedFrames.size());
730-
for (size_t fi = 0; fi < sortedFrames.size(); fi++) {
731-
const size_t& iFrame = sortedFrames[fi];
732-
dof = initialMotion.col(iFrame);
619+
ProgressBar progress("Tracking per-frame", sortedFrames.size());
620+
for (const auto iFrame : sortedFrames) {
621+
// For continuous tracking, keep the solved dof from previous frame (temporal coherence)
622+
// For non-continuous tracking, always start from initial motion (independent solving)
623+
if (!isContinuous) {
624+
dof = initialMotion.col(iFrame);
625+
needsInit = true;
626+
}
627+
// For continuous tracking, dof is preserved from previous iteration (or initial value)
733628

734629
if ((constrData.at(iFrame).size() + skinnedConstrData.at(iFrame).size()) >
735630
numMarkers * config.minVisPercent) {
@@ -741,34 +636,52 @@ Eigen::MatrixXf trackPosesForFrames(
741636
skinnedLocatorPosConstrFunc->setConstraints(skinnedConstrData.at(iFrame));
742637

743638
// initialization
744-
solverOptions.maxIterations = 50; // make sure it converges
745-
solver.setOptions(solverOptions);
746-
solver.setEnabledParameters(pt.getRigidParameters());
639+
if (needsInit) { // solve only for the rigid parameters as preprocessing
640+
MT_LOGI_IF(
641+
config.debug && isContinuous,
642+
"Solving for an initial rigid pose at frame {}",
643+
iFrame);
747644

748-
solver.solve(dof);
645+
// Set up different config for initialization
646+
solverOptions.maxIterations = 50; // make sure it converges
647+
solver.setOptions(solverOptions);
648+
solver.setEnabledParameters(pt.getRigidParameters());
649+
if (smoothConstrFunc) {
650+
smoothConstrFunc->setWeight(0.0); // turn off smoothing - it doesn't affect rigid dofs
651+
}
652+
653+
solver.solve(dof);
654+
655+
// Recover solver config
656+
solverOptions.maxIterations = config.maxIter;
657+
solver.setOptions(solverOptions);
658+
solver.setEnabledParameters(poseParams);
659+
if (smoothConstrFunc) {
660+
smoothConstrFunc->setWeight(config.smoothing);
661+
}
749662

750-
// Recover solver config
751-
solverOptions.maxIterations = config.maxIter;
752-
solver.setOptions(solverOptions);
753-
solver.setEnabledParameters(poseParams);
663+
needsInit = false;
664+
}
665+
666+
// set smoothness target as the last pose for continuous tracking
667+
if (smoothConstrFunc) {
668+
smoothConstrFunc->setTargetParameters(dof, smoothConstrFunc->getTargetWeights());
669+
}
754670

755671
priorError += solverFunc.getError(dof);
756672
error += solver.solve(dof);
757673
++solverFrame;
758674
}
759675

760676
// store result
761-
poses[fi] = dof;
677+
while (outputIndex <= iFrame) {
678+
outMotion.col(outputIndex++) = dof;
679+
}
762680
progress.increment();
763681
}
764682

765-
// set results to output
766-
size_t sortedIndex = 0;
767-
for (size_t fi = 0; fi < numFrames; fi++) {
768-
if (sortedIndex < sortedFrames.size() - 1 && fi == sortedFrames[sortedIndex + 1]) {
769-
sortedIndex++;
770-
}
771-
outMotion.col(fi) = poses[sortedIndex];
683+
while (outputIndex < numFrames) {
684+
outMotion.col(outputIndex++) = dof;
772685
}
773686
}
774687
if (config.debug) {
@@ -881,7 +794,8 @@ void calibrateModel(
881794
character,
882795
motion.topRows(transform.numAllModelParameters()),
883796
trackingConfig,
884-
firstFrame);
797+
firstFrame,
798+
false); // Not continuous for calibration keyframes
885799
motion.topRows(transform.numAllModelParameters()) = trackSequence(
886800
markerData,
887801
character,
@@ -990,7 +904,8 @@ void calibrateModel(
990904
character,
991905
motion.topRows(transform.numAllModelParameters()),
992906
trackingConfig,
993-
frameIndices);
907+
frameIndices,
908+
false); // Not continuous for calibration keyframes
994909
} else {
995910
const VectorXf initPose = motion.col(0).head(transform.numAllModelParameters());
996911
motion.topRows(transform.numAllModelParameters()) =
@@ -1119,7 +1034,8 @@ void calibrateLocators(
11191034
character,
11201035
motion.topRows(transform.numAllModelParameters()),
11211036
trackingConfig,
1122-
frameIndices);
1037+
frameIndices,
1038+
false); // Not continuous for calibration keyframes
11231039

11241040
// Solve for both markers and poses.
11251041
// TODO: add a small regularization to prevent too large a change

momentum/marker_tracking/marker_tracker.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Eigen::MatrixXf trackPosesPerframe(
155155
/// too.
156156
/// @param[in] config Solving options.
157157
/// @param[in] frameIndices Frame indices of the frames to be solved.
158+
/// @param[in] isContinuous Whether to use temporal coherence between frames.
158159
///
159160
/// @return The solved motion. It has the same length as markerData. It repeats the same solved pose
160161
/// within a frame stride.
@@ -163,7 +164,8 @@ Eigen::MatrixXf trackPosesForFrames(
163164
const momentum::Character& character,
164165
const Eigen::MatrixXf& initialMotion,
165166
const TrackingConfig& config,
166-
const std::vector<size_t>& frameIndices);
167+
const std::vector<size_t>& frameIndices,
168+
bool isContinuous = false);
167169

168170
/// Calibrate body proportions and locator offsets of a character from input marker data.
169171
///

0 commit comments

Comments
 (0)