Skip to content

Commit f08b507

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: jeongseok-meta Differential Revision: D82688034
1 parent 375df42 commit f08b507

File tree

2 files changed

+89
-182
lines changed

2 files changed

+89
-182
lines changed

momentum/marker_tracking/marker_tracker.cpp

Lines changed: 86 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -460,167 +460,24 @@ Eigen::MatrixXf trackPosesPerframe(
460460
const size_t frameStride) {
461461
const size_t numFrames = markerData.size();
462462
MT_CHECK(numFrames > 0, "Input data is empty.");
463-
MT_CHECK(
464-
globalParams.v.size() == character.parameterTransform.numAllModelParameters(),
465-
"Input model parameters {} do not match character model parameters {}",
466-
globalParams.v.size(),
467-
character.parameterTransform.numAllModelParameters());
468-
469-
const ParameterTransform& pt = character.parameterTransform;
470-
const size_t numMarkers = markerData[0].size();
471-
472-
// pose parameters need to exclude "locators"
473-
ParameterSet poseParams = pt.getPoseParameters();
474-
const auto& locatorSet = pt.parameterSets.find("locators");
475-
if (locatorSet != pt.parameterSets.end()) {
476-
poseParams &= ~locatorSet->second;
477-
}
478-
479-
// set up the solver
480-
auto solverFunc = SkeletonSolverFunction(&character.skeleton, &pt);
481-
GaussNewtonSolverOptions solverOptions;
482-
solverOptions.maxIterations = config.maxIter;
483-
solverOptions.minIterations = 2;
484-
solverOptions.doLineSearch = false;
485-
solverOptions.verbose = config.debug;
486-
solverOptions.threshold = 1.f;
487-
solverOptions.regularization = config.regularization;
488-
auto solver = GaussNewtonSolver(solverOptions, &solverFunc);
489-
solver.setEnabledParameters(poseParams);
490-
491-
// parameter limits constraint
492-
auto limitConstrFunc = std::make_shared<LimitErrorFunction>(character);
493-
limitConstrFunc->setWeight(0.1);
494-
solverFunc.addErrorFunction(limitConstrFunc);
495-
496-
// positional constraint function for markers
497-
auto posConstrFunc = std::make_shared<PositionErrorFunction>(character, config.lossAlpha);
498-
posConstrFunc->setWeight(PositionErrorFunction::kLegacyWeight);
499-
solverFunc.addErrorFunction(posConstrFunc);
500-
501-
std::shared_ptr<SkinnedLocatorErrorFunction> skinnedLocatorPosConstrFunc =
502-
std::make_shared<SkinnedLocatorErrorFunction>(character);
503-
skinnedLocatorPosConstrFunc->setWeight(PositionErrorFunction::kLegacyWeight);
504-
solverFunc.addErrorFunction(skinnedLocatorPosConstrFunc);
505463

506-
// floor penetration constraint data; we assume the world is y-up and floor is y=0 for mocap data.
507-
const auto& floorConstraints = createFloorConstraints<float>(
508-
"Floor_",
509-
character.locators,
510-
Vector3f::UnitY(),
511-
/* y offset */ 0.0f,
512-
/* weight */ 5.0f);
513-
auto halfPlaneConstrFunc = std::make_shared<PlaneErrorFunction>(character, /*half plane*/ true);
514-
halfPlaneConstrFunc->setConstraints(floorConstraints);
515-
halfPlaneConstrFunc->setWeight(PlaneErrorFunction::kLegacyWeight);
516-
solverFunc.addErrorFunction(halfPlaneConstrFunc);
517-
518-
// marker constraint data
519-
auto constrData = createConstraintData(markerData, character.locators);
520-
auto skinnedConstrData = createSkinnedConstraintData(markerData, character.skinnedLocators);
521-
522-
// smoothness constraint only for the joints and exclude global dofs because the global transform
523-
// needs to be accurate (may not matter in practice?)
524-
auto smoothConstrFunc = std::make_shared<ModelParametersErrorFunction>(
525-
character, poseParams & ~pt.getRigidParameters());
526-
smoothConstrFunc->setWeight(config.smoothing);
527-
solverFunc.addErrorFunction(smoothConstrFunc);
528-
529-
// add collision error
530-
std::shared_ptr<CollisionErrorFunction> collisionErrorFunction;
531-
if (config.collisionErrorWeight != 0 && character.collision != nullptr) {
532-
collisionErrorFunction = std::make_shared<CollisionErrorFunction>(character);
533-
collisionErrorFunction->setWeight(config.collisionErrorWeight);
534-
solverFunc.addErrorFunction(collisionErrorFunction);
464+
// Generate frame indices from stride
465+
std::vector<size_t> frameIndices;
466+
for (size_t iFrame = 0; iFrame < numFrames; iFrame += frameStride) {
467+
frameIndices.push_back(iFrame);
535468
}
536469

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

557-
{ // scope the ProgressBar so it returns
558-
ProgressBar progress("", numFrames);
559-
for (size_t iFrame = 0; iFrame < numFrames; iFrame += frameStride) {
560-
// reinitialize if not continuous
561-
if (!continuous) {
562-
dof = globalParams.v;
563-
}
476+
// Determine if tracking should be continuous (temporal coherence)
477+
bool isContinuous = (frameStride < 5);
564478

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

626483
/// Track poses independently for specific frame indices with fixed character identity.
@@ -635,13 +492,15 @@ Eigen::MatrixXf trackPosesPerframe(
635492
/// @param initialMotion Initial parameter values (parameters x frames)
636493
/// @param config Tracking configuration settings
637494
/// @param frameIndices Vector of specific frame indices to solve
495+
/// @param isContinuous Whether to use temporal coherence between frames
638496
/// @return Solved motion parameters matrix (parameters x frames) with poses for selected frames
639497
Eigen::MatrixXf trackPosesForFrames(
640498
const gsl::span<const std::vector<Marker>> markerData,
641499
const Character& character,
642500
const MatrixXf& initialMotion,
643501
const TrackingConfig& config,
644-
const std::vector<size_t>& frameIndices) {
502+
const std::vector<size_t>& frameIndices,
503+
bool isContinuous) {
645504
const size_t numFrames = markerData.size();
646505
MT_CHECK(numFrames > 0, "Input data is empty.");
647506
MT_CHECK(
@@ -705,6 +564,17 @@ Eigen::MatrixXf trackPosesForFrames(
705564
auto constrData = createConstraintData(markerData, character.locators);
706565
auto skinnedConstrData = createSkinnedConstraintData(markerData, character.skinnedLocators);
707566

567+
// smoothness constraint only for the joints and exclude global dofs because the global transform
568+
// needs to be accurate (may not matter in practice?)
569+
// Only use temporal smoothness if isContinuous is true
570+
std::shared_ptr<ModelParametersErrorFunction> smoothConstrFunc;
571+
if (isContinuous) {
572+
smoothConstrFunc = std::make_shared<ModelParametersErrorFunction>(
573+
character, poseParams & ~pt.getRigidParameters());
574+
smoothConstrFunc->setWeight(config.smoothing);
575+
solverFunc.addErrorFunction(smoothConstrFunc);
576+
}
577+
708578
// add collision error
709579
std::shared_ptr<CollisionErrorFunction> collisionErrorFunction;
710580
if (config.collisionErrorWeight != 0 && character.collision != nullptr) {
@@ -716,18 +586,32 @@ Eigen::MatrixXf trackPosesForFrames(
716586
// initialize parameters to contain identity information
717587
// the identity fields will be used but untouched during optimization
718588
// globalParams could also be repurposed to pass in initial pose value
719-
std::vector<Eigen::VectorXf> poses(frameIndices.size());
720589
Eigen::VectorXf dof = initialMotion.col(sortedFrames.empty() ? 0 : sortedFrames[0]);
721590
size_t solverFrame = 0;
722591
double priorError = 0.0;
723592
double error = 0.0;
724593

725-
MatrixXf outMotion(pt.numAllModelParameters(), numFrames);
594+
// Use the initial global transform is it's not zero
595+
bool needsInit = true;
596+
const auto rigidParams = pt.getRigidParameters();
597+
for (Eigen::Index i = 0; i < dof.size(); ++i) {
598+
if (rigidParams.test(i) && dof[i] != 0.0f) {
599+
needsInit = false;
600+
}
601+
}
602+
603+
MatrixXf outMotion = initialMotion;
604+
Eigen::Index outputIndex = 0;
726605
{ // scope the ProgressBar so it returns
727-
ProgressBar progress("", sortedFrames.size());
728-
for (size_t fi = 0; fi < sortedFrames.size(); fi++) {
729-
const size_t& iFrame = sortedFrames[fi];
730-
dof = initialMotion.col(iFrame);
606+
ProgressBar progress("Tracking per-frame", sortedFrames.size());
607+
for (const auto iFrame : sortedFrames) {
608+
// For continuous tracking, keep the solved dof from previous frame (temporal coherence)
609+
// For non-continuous tracking, always start from initial motion (independent solving)
610+
if (!isContinuous) {
611+
dof = initialMotion.col(iFrame);
612+
needsInit = true;
613+
}
614+
// For continuous tracking, dof is preserved from previous iteration (or initial value)
731615

732616
if ((constrData.at(iFrame).size() + skinnedConstrData.at(iFrame).size()) >
733617
numMarkers * config.minVisPercent) {
@@ -739,34 +623,52 @@ Eigen::MatrixXf trackPosesForFrames(
739623
skinnedLocatorPosConstrFunc->setConstraints(skinnedConstrData.at(iFrame));
740624

741625
// initialization
742-
solverOptions.maxIterations = 50; // make sure it converges
743-
solver.setOptions(solverOptions);
744-
solver.setEnabledParameters(pt.getRigidParameters());
626+
if (needsInit) { // solve only for the rigid parameters as preprocessing
627+
MT_LOGI_IF(
628+
config.debug && isContinuous,
629+
"Solving for an initial rigid pose at frame {}",
630+
iFrame);
631+
632+
// Set up different config for initialization
633+
solverOptions.maxIterations = 50; // make sure it converges
634+
solver.setOptions(solverOptions);
635+
solver.setEnabledParameters(pt.getRigidParameters());
636+
if (smoothConstrFunc) {
637+
smoothConstrFunc->setWeight(0.0); // turn off smoothing - it doesn't affect rigid dofs
638+
}
639+
640+
solver.solve(dof);
641+
642+
// Recover solver config
643+
solverOptions.maxIterations = config.maxIter;
644+
solver.setOptions(solverOptions);
645+
solver.setEnabledParameters(poseParams);
646+
if (smoothConstrFunc) {
647+
smoothConstrFunc->setWeight(config.smoothing);
648+
}
745649

746-
solver.solve(dof);
650+
needsInit = false;
651+
}
747652

748-
// Recover solver config
749-
solverOptions.maxIterations = config.maxIter;
750-
solver.setOptions(solverOptions);
751-
solver.setEnabledParameters(poseParams);
653+
// set smoothness target as the last pose for continuous tracking
654+
if (smoothConstrFunc) {
655+
smoothConstrFunc->setTargetParameters(dof, smoothConstrFunc->getTargetWeights());
656+
}
752657

753658
priorError += solverFunc.getError(dof);
754659
error += solver.solve(dof);
755660
++solverFrame;
756661
}
757662

758663
// store result
759-
poses[fi] = dof;
664+
while (outputIndex <= iFrame) {
665+
outMotion.col(outputIndex++) = dof;
666+
}
760667
progress.increment();
761668
}
762669

763-
// set results to output
764-
size_t sortedIndex = 0;
765-
for (size_t fi = 0; fi < numFrames; fi++) {
766-
if (sortedIndex < sortedFrames.size() - 1 && fi == sortedFrames[sortedIndex + 1]) {
767-
sortedIndex++;
768-
}
769-
outMotion.col(fi) = poses[sortedIndex];
670+
while (outputIndex < numFrames) {
671+
outMotion.col(outputIndex++) = dof;
770672
}
771673
}
772674
if (config.debug) {
@@ -879,7 +781,8 @@ void calibrateModel(
879781
character,
880782
motion.topRows(transform.numAllModelParameters()),
881783
trackingConfig,
882-
firstFrame);
784+
firstFrame,
785+
false); // Not continuous for calibration keyframes
883786
motion.topRows(transform.numAllModelParameters()) = trackSequence(
884787
markerData,
885788
character,
@@ -988,7 +891,8 @@ void calibrateModel(
988891
character,
989892
motion.topRows(transform.numAllModelParameters()),
990893
trackingConfig,
991-
frameIndices);
894+
frameIndices,
895+
false); // Not continuous for calibration keyframes
992896
} else {
993897
const VectorXf initPose = motion.col(0).head(transform.numAllModelParameters());
994898
motion.topRows(transform.numAllModelParameters()) =
@@ -1117,7 +1021,8 @@ void calibrateLocators(
11171021
character,
11181022
motion.topRows(transform.numAllModelParameters()),
11191023
trackingConfig,
1120-
frameIndices);
1024+
frameIndices,
1025+
false); // Not continuous for calibration keyframes
11211026

11221027
// Solve for both markers and poses.
11231028
// 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)