tmk/cpp/algo/tmkfv.cpp (529 lines of code) (raw):

// ================================================================ // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved // ================================================================ // ================================================================ // Wrapper class for TMK feature-vectors. It includes methods for // computing them on a streaming basis from frame-features, one frame-feature // at a time, as well as methods for manipulating them when loaded from disk. // ================================================================ #include <tmk/cpp/algo/tmkfv.h> #include <tmk/cpp/lib/vec.h> #if defined(_WIN32) #define _USE_MATH_DEFINES #endif #include <cmath> #include <stdexcept> namespace facebook { namespace tmk { namespace algo { // ---------------------------------------------------------------- // See comments in tmkfv.h. static float computePairScoreNormalizer( const FourierCoefficients& fourierCoefficients) { if (fourierCoefficients.size() == 0) { return 1.0; } else { float pairScoreNormalizer = fourierCoefficients[0]; for (int j = 1; j < fourierCoefficients.size(); j++) { pairScoreNormalizer += 2.0 * fourierCoefficients[j]; } return pairScoreNormalizer; } } // ---------------------------------------------------------------- // Constructor for beginning to compute TMK feature vectors from // framewise hashes. TMKFeatureVectors::TMKFeatureVectors( facebook::tmk::io::TMKFramewiseAlgorithm algorithm, int framesPerSecond, const Periods& periods, const FourierCoefficients& fourierCoefficients, int frameFeatureDimension) : _algorithm(algorithm), _framesPerSecond(framesPerSecond), _periods(periods), _fourierCoefficients(fourierCoefficients), _frameFeatureDimension(frameFeatureDimension), _frameFeatureCount(0) { int numPeriods = periods.size(); int numFourierCoefficients = fourierCoefficients.size(); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _pureAverageFeature = FrameFeature(frameFeatureDimension); for (int j = 0; j < frameFeatureDimension; j++) { _pureAverageFeature[j] = 0.0; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _cosFeatures = facebook::tmk::libvec::allocateRank3( numPeriods, numFourierCoefficients, frameFeatureDimension); _sinFeatures = facebook::tmk::libvec::allocateRank3( numPeriods, numFourierCoefficients, frameFeatureDimension); _pairScoreNormalizer = computePairScoreNormalizer(fourierCoefficients); } // ---------------------------------------------------------------- // Constructor for loading precomputed data from a file. Here it's possible for // the dimensions to be inconsistent, so we intend this constructor to be // private. TMKFeatureVectors::TMKFeatureVectors( facebook::tmk::io::TMKFramewiseAlgorithm algorithm, int framesPerSecond, int frameFeatureCount, const Periods& periods, const FourierCoefficients& fourierCoefficients, const FrameFeature& pureAverageFeature, const FeaturesByPeriodsAndFourierCoefficients& cosFeatures, const FeaturesByPeriodsAndFourierCoefficients& sinFeatures) : _algorithm(algorithm), _framesPerSecond(framesPerSecond), _periods(periods), _fourierCoefficients(fourierCoefficients), _frameFeatureDimension(pureAverageFeature.size()), _frameFeatureCount(frameFeatureCount), _pureAverageFeature(pureAverageFeature), _cosFeatures(cosFeatures), _sinFeatures(sinFeatures), _pairScoreNormalizer(computePairScoreNormalizer(fourierCoefficients)) {} // ---------------------------------------------------------------- // Supporting method for the above private constructor. Invariants are // documented in detail within tmkfv.h. Recall though from there that periods // are 1D (call it P) vector of int, fourier coefficients are 1D (call it C) // vector of float, pure-average feature is 1D (call it D) vector of float, and // cosine and sine features are 3D P x C x D. Note that P and/or C can be zero. std::shared_ptr<TMKFeatureVectors> TMKFeatureVectors::tryCreateFromPrecomputed( facebook::tmk::io::TMKFramewiseAlgorithm algorithm, int framesPerSecond, int frameFeatureCount, const Periods& periods, const FourierCoefficients& fourierCoefficients, const FrameFeature& pureAverageFeature, const FeaturesByPeriodsAndFourierCoefficients& cosFeatures, const FeaturesByPeriodsAndFourierCoefficients& sinFeatures) { int P = periods.size(); int C = fourierCoefficients.size(); int D = pureAverageFeature.size(); bool ok = facebook::tmk::libvec::checkDimensionsRank3(cosFeatures, P, C, D) && facebook::tmk::libvec::checkDimensionsRank3(sinFeatures, P, C, D); if (ok) { return std::shared_ptr<TMKFeatureVectors>(new TMKFeatureVectors( algorithm, framesPerSecond, frameFeatureCount, periods, fourierCoefficients, pureAverageFeature, cosFeatures, sinFeatures)); } else { return nullptr; } } // ---------------------------------------------------------------- bool TMKFeatureVectors::areCompatible( const TMKFeatureVectors& fva, const TMKFeatureVectors& fvb) { if (fva._algorithm != fvb._algorithm) { fprintf( stderr, "TMK: algorithm \"%s\" != \"%s\".\n", facebook::tmk::io::algorithmToName(fva._algorithm).c_str(), facebook::tmk::io::algorithmToName(fvb._algorithm).c_str()); return false; } if (fva._framesPerSecond != fvb._framesPerSecond) { fprintf( stderr, "TMK: frames per second %d != %d.\n", fva._framesPerSecond, fvb._framesPerSecond); return false; } if (fva._periods.size() != fvb._periods.size()) { fprintf( stderr, "TMK: period-count %d != %d.\n", (int)fva._periods.size(), (int)fvb._periods.size()); return false; } int np = fva._periods.size(); for (int i = 0; i < np; i++) { if (fva._periods[i] != fvb._periods[i]) { fprintf( stderr, "TMK: period[%d] %d != %d.\n", i, fva._periods[i], fvb._periods[i]); return false; } } if (fva._fourierCoefficients.size() != fvb._fourierCoefficients.size()) { fprintf( stderr, "TMK: fourier-coefficient-count %d != %d.\n", (int)fva._fourierCoefficients.size(), (int)fvb._fourierCoefficients.size()); return false; } for (int i = 0; i < fva._fourierCoefficients.size(); i++) { float ca = fva._fourierCoefficients[i]; float cb = fvb._fourierCoefficients[i]; float m = std::fmax(std::fabs(ca), std::fabs(cb)); if (m > 0) { float relerr = std::fabs((ca - cb) / m); if (relerr > 1e-6) { fprintf( stderr, "TMK: fourier coefficient %d %.7e != %.7e.\n", i, ca, cb); return false; } } } // Should have been caught by the algorithm-magic check, but, if someone // generated data files with the same algorithm name and somehow different // frame-feature dimensions, we would want to catch that. if (fva._frameFeatureDimension != fvb._frameFeatureDimension) { fprintf( stderr, "TMK: frame-feature dimension %d != %d.\n", fva._frameFeatureDimension, fvb._frameFeatureDimension); return false; } return true; } // ---------------------------------------------------------------- // The TMK-output feature vectors are indexed by the periods T and // the fourier-coefficient index j (0 to m-1). // See comments in tmkfv.h for algebraic derivations. void TMKFeatureVectors::ingestFrameFeature( const FrameFeature& frameFeature, int t // frame number ) { if (frameFeature.size() != _frameFeatureDimension) { throw std::runtime_error( "Incompatible frame-feature dimensions " + std::to_string(frameFeature.size()) + ", " + std::to_string(_frameFeatureDimension)); } for (int k = 0; k < frameFeature.size(); k++) { _pureAverageFeature[k] += frameFeature[k]; } FrameFeature normalizedFrameFeature(frameFeature); facebook::tmk::libvec::L2NormalizeVector(normalizedFrameFeature); for (int i = 0; i < _periods.size(); i++) { int T = _periods[i]; for (int k = 0; k < normalizedFrameFeature.size(); k++) { _cosFeatures[i][0][k] += normalizedFrameFeature[k]; } for (int j = 1; j < _fourierCoefficients.size(); j++) { double arg = 2.0 * M_PI * (double)j * (double)t / (double)T; double cosArg = cos(arg); double sinArg = sin(arg); for (int k = 0; k < normalizedFrameFeature.size(); k++) { _cosFeatures[i][j][k] += normalizedFrameFeature[k] * cosArg; _sinFeatures[i][j][k] += normalizedFrameFeature[k] * sinArg; } } } _frameFeatureCount++; } // ---------------------------------------------------------------- // See comments in tmkfv.h for algebraic derivations. void TMKFeatureVectors::finishFrameFeatureIngest() { if (_frameFeatureCount > 0) { facebook::tmk::libvec::scalarDivide( _pureAverageFeature, (float)_frameFeatureCount); for (int i = 0; i < _periods.size(); i++) { for (int j = 0; j < _fourierCoefficients.size(); j++) { facebook::tmk::libvec::L2NormalizeVector(_cosFeatures[i][j]); facebook::tmk::libvec::L2NormalizeVector(_sinFeatures[i][j]); facebook::tmk::libvec::scalarMultiply( _cosFeatures[i][j], std::sqrt(_fourierCoefficients[j])); facebook::tmk::libvec::scalarMultiply( _sinFeatures[i][j], std::sqrt(_fourierCoefficients[j])); } } } } // ---------------------------------------------------------------- // Poullot parameters // https://www.dropbox.com/s/uaav1l2hj7m6hj4/v2-circu-p381-poullot.pdf?dl=0 // TODO(T28927988) // Shigan Chu and Nathan Hurst are working on learned // parameters which we expect to be better. Periods TMKFeatureVectors::makePoullotPeriods() { return Periods{2731, 4391, 9767, 14653}; } // ---------------------------------------------------------------- // These are the Poullot parameters as described in // https://www.dropbox.com/s/uaav1l2hj7m6hj4/v2-circu-p381-poullot.pdf?dl=0 // // TODO(T28927988) // Shigan Chu and Nathan Hurst are working on learned // parameters which we expect to be better. // // The Poullot values are a simple formula. Issue: can't find // std::cyl_bessel_if et al. within C++ header libraries despite half an hour // of googling+futzingwith. // // And it's not worth my time since these are just temporary until I get // trained TMK coefficients computed -- which will (like these) just end up // being tabulated anyway. // // So I might as well tabulate. // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The C++ code I would write: // // vector<float> coefficients(m); // float sinhBeta = sinh(beta); // coefficients[0] = (cyl_bessel_if(0, beta) - exp(-beta)) / (2*sinhBeta); // for (int i = 1; i < m; i++) { // coefficients[i] = cyl_bessel_if(i, beta) / sinhBeta; // } // return coefficients; // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // And the Python code: // // from scipy.special import iv as bessel // from math import sinh, exp // // m = 32 // beta = 32 // // a0 = (bessel(0, beta) - exp(-beta)) / (2 * sinh(beta)) // print 0,a0 // for i in range(1, m): // ai = bessel(i, beta) / sinh(beta) // print i, ai FourierCoefficients TMKFeatureVectors::makePoullotFourierCoefficients() { return std::vector<float>{ 0.0708041893112, 0.13937789309, 0.132897260304, 0.122765735552, 0.109878684888, 0.09529606433, 0.0800986647852, 0.0652590650356, 0.0515478238322, 0.0394851531195, 0.0293374252025, 0.0211492623679, 0.0147973073245, 0.0100512818746, 0.0066306408014, 0.00424947117334, 0.0026467615764, 0.00160270959695, 0.000943882629639, 0.000540841638603, 0.000301633183798, 0.000163800158855, 8.66454753015e-05, 4.46626303151e-05, 2.24429442235e-05, 1.09982139799e-05, 5.25823487999e-06, 2.45358229988e-06, 1.11781474895e-06, 4.97406489221e-07, 2.16265487234e-07, 9.19087006565e-08}; } // ---------------------------------------------------------------- bool TMKFeatureVectors::writeToOutputStream(FILE* fp, const char* programName) const { bool write_rc = false; if (!facebook::tmk::io::writeFeatureVectorFileHeader( fp, _algorithm, // provenance _framesPerSecond, // provenance _periods.size(), // a.k.a. P _fourierCoefficients.size(), // a.k.a m _frameFeatureDimension, // a.k.a d _frameFeatureCount, // informational: frame count (time-resampled) programName)) { // ... in the hashed video perror("fwrite"); return false; } write_rc = facebook::tmk::io::writeIntVector(_periods, fp); if (!write_rc) { fprintf(stderr, "%s: failed to write periods vector.\n", programName); return false; } write_rc = facebook::tmk::io::writeFloatVector(_fourierCoefficients, fp); if (!write_rc) { fprintf( stderr, "%s: failed to write fourier-coefficients feature vector.\n", programName); return false; } write_rc = facebook::tmk::io::writeFloatVector(_pureAverageFeature, fp); if (!write_rc) { fprintf( stderr, "%s: failed to write pure-average feature vector.\n", programName); return false; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for (int i = 0; i < _periods.size(); i++) { for (int j = 0; j < _fourierCoefficients.size(); j++) { write_rc = facebook::tmk::io::writeFloatVector(_cosFeatures[i][j], fp); if (!write_rc) { fprintf( stderr, "%s: failed to write feature vector %d.\n", programName, 0); return false; } } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for (int i = 0; i < _periods.size(); i++) { for (int j = 0; j < _fourierCoefficients.size(); j++) { write_rc = facebook::tmk::io::writeFloatVector(_sinFeatures[i][j], fp); if (!write_rc) { fprintf( stderr, "%s: failed to write feature vector %d.\n", programName, 0); return false; } } } return true; } // ---------------------------------------------------------------- bool TMKFeatureVectors::writeToOutputFile( const char* fileName, const char* programName) const { FILE* fp = fopen(fileName, "wb"); if (fp == nullptr) { perror("fopen"); fprintf( stderr, "%s: could not open \"%s\" for write.\n", programName, fileName); return false; } bool rc = writeToOutputStream(fp, programName); if (fclose(fp) != 0) { perror("fclose"); fprintf( stderr, "%s: could not close \"%s\" after write.\n", programName, fileName); return false; } return rc; } // ---------------------------------------------------------------- std::shared_ptr<TMKFeatureVectors> TMKFeatureVectors::readFromInputStream( FILE* fp, const char* programName) { io::FeatureVectorFileHeader header; bool read_rc; facebook::tmk::io::TMKFramewiseAlgorithm algorithm; if (!facebook::tmk::io::readFeatureVectorFileHeader( fp, &header, algorithm, programName)) { return nullptr; } if (algorithm == facebook::tmk::io::TMKFramewiseAlgorithm::UNRECOGNIZED) { fprintf(stderr, "%s: failed to recognized algorithm.\n", programName); return nullptr; } int framesPerSecond = header.framesPerSecond; int numPeriods = header.numPeriods; int numFourierCoefficients = header.numFourierCoefficients; int frameFeatureDimension = header.frameFeatureDimension; int frameFeatureCount = header.frameFeatureCount; Periods periods(numPeriods); FourierCoefficients fourierCoefficients(numFourierCoefficients); FrameFeature pureAverageFeature(frameFeatureDimension); // TODO(T25190142): include frameCount bool eofUnusedHere = false; read_rc = facebook::tmk::io::readIntVector(periods, fp); if (!read_rc) { fprintf(stderr, "%s: failed to read periods vector.\n", programName); return nullptr; } read_rc = facebook::tmk::io::readFloatVector( fourierCoefficients, fp, eofUnusedHere); if (!read_rc) { fprintf( stderr, "%s: failed to read fourier-coefficients feature vector.\n", programName); return nullptr; } read_rc = facebook::tmk::io::readFloatVector(pureAverageFeature, fp, eofUnusedHere); if (!read_rc) { fprintf( stderr, "%s: failed to read pure-average feature vector.\n", programName); return nullptr; } FeaturesByPeriodsAndFourierCoefficients cosFeatures = facebook::tmk::libvec::allocateRank3( numPeriods, numFourierCoefficients, frameFeatureDimension); FeaturesByPeriodsAndFourierCoefficients sinFeatures = facebook::tmk::libvec::allocateRank3( numPeriods, numFourierCoefficients, frameFeatureDimension); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for (int i = 0; i < cosFeatures.size(); i++) { for (int j = 0; j < cosFeatures[i].size(); j++) { read_rc = facebook::tmk::io::readFloatVector( cosFeatures[i][j], fp, eofUnusedHere); if (!read_rc) { fprintf( stderr, "%s: failed to read feature vector %d.\n", programName, 0); return nullptr; } } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for (int i = 0; i < sinFeatures.size(); i++) { for (int j = 0; j < sinFeatures[i].size(); j++) { read_rc = facebook::tmk::io::readFloatVector( sinFeatures[i][j], fp, eofUnusedHere); if (!read_rc) { fprintf( stderr, "%s: failed to read feature vector %d.\n", programName, 0); return nullptr; } } } return tryCreateFromPrecomputed( algorithm, framesPerSecond, frameFeatureCount, periods, fourierCoefficients, pureAverageFeature, cosFeatures, sinFeatures); } // ---------------------------------------------------------------- std::shared_ptr<TMKFeatureVectors> TMKFeatureVectors::readFromInputFile( const char* fileName, const char* programName) { FILE* fp = fopen(fileName, "rb"); if (fp == nullptr) { perror("fopen"); fprintf( stderr, "%s: could not open \"%s\" for read.\n", programName, fileName); return nullptr; } auto pfv = readFromInputStream(fp, programName); (void)fclose(fp); return pfv; } // ---------------------------------------------------------------- void TMKFeatureVectors::L2NormalizePureAverageFeature() { facebook::tmk::libvec::L2NormalizeVector(_pureAverageFeature); } // ---------------------------------------------------------------- // See comments in tmkfv.h for algebraic derivations. void TMKFeatureVectors::findPairOffsetsModuloPeriods( const TMKFeatureVectors& fva, const TMKFeatureVectors& fvb, BestOffsets& bestOffsets, ValuesAtBestOffsets& valuesAtBestOffsets, bool printDetails) { int numPeriods = fva._periods.size(); int numFourierCoefficients = fva._fourierCoefficients.size(); // We assume areCompatible has already been called on fva and fvb. bestOffsets = BestOffsets(numPeriods); valuesAtBestOffsets = ValuesAtBestOffsets(numPeriods); if (numPeriods == 0 || numFourierCoefficients == 0) { return; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We assume all cos/sin features are already L2-normalized. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - std::vector<std::vector<float>> dotCosCos = facebook::tmk::libvec::allocateRank2(numPeriods, numFourierCoefficients); std::vector<std::vector<float>> dotSinSin = facebook::tmk::libvec::allocateRank2(numPeriods, numFourierCoefficients); std::vector<std::vector<float>> dotSinCos = facebook::tmk::libvec::allocateRank2(numPeriods, numFourierCoefficients); std::vector<std::vector<float>> dotCosSin = facebook::tmk::libvec::allocateRank2(numPeriods, numFourierCoefficients); for (int i = 0; i < numPeriods; i++) { for (int j = 0; j < numFourierCoefficients; j++) { const std::vector<float>& fvaCos = fva._cosFeatures[i][j]; const std::vector<float>& fvaSin = fva._sinFeatures[i][j]; const std::vector<float>& fvbCos = fvb._cosFeatures[i][j]; const std::vector<float>& fvbSin = fvb._sinFeatures[i][j]; dotCosCos[i][j] = facebook::tmk::libvec::computeDot(fvaCos, fvbCos); dotSinSin[i][j] = facebook::tmk::libvec::computeDot(fvaSin, fvbSin); dotSinCos[i][j] = facebook::tmk::libvec::computeDot(fvaSin, fvbCos); dotCosSin[i][j] = facebook::tmk::libvec::computeDot(fvaCos, fvbSin); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for (int i = 0; i < numPeriods; i++) { int period = fva._periods[i]; std::vector<float> K_deltas(period); for (int offset = 0; offset < period; offset++) { float delta = 2.0 * M_PI * (float)offset / (float)period; float K_delta = dotCosCos[i][0]; #if 0 // Direct evaluation for (int j = 1; j < numFourierCoefficients; j++) { K_delta += cos(j * delta) * (dotCosCos[i][j] + dotSinSin[i][j]); K_delta += sin(j * delta) * (dotSinCos[i][j] - dotCosSin[i][j]); } #else /** * We noticed that sin and cos were the dominant cost to this function. * We can try using an incremental algorithm for equally spaced exp(1j) * as recommended in Numerical Recipes. A good demonstration of precision: * http://steve.hollasch.net/cgindex/math/inccos.html * this is based on the addition formulae. We replace cos(d) with * sin(d/2) to avoid truncation error using the double angle formula. * * Initial tests show 1/3 improvement in performance. **/ float cos_jd = 1; float sin_jd = 0; float beta = sin(delta); float alpha = sin(delta / 2); alpha = 2 * alpha * alpha; for (int j = 1; j < numFourierCoefficients; j++) { // a bit magical because we're starting at j = 1 float n_cos_jd = (alpha * cos_jd) + (beta * sin_jd); float n_sin_jd = (alpha * sin_jd) - (beta * cos_jd); cos_jd -= n_cos_jd; sin_jd -= n_sin_jd; K_delta += cos_jd * (dotCosCos[i][j] + dotSinSin[i][j]); K_delta += sin_jd * (dotSinCos[i][j] - dotCosSin[i][j]); } #endif if (printDetails) { printf("TODK %d %d %.6f %.6f\n", period, offset, delta, K_delta); } K_deltas[offset] = K_delta; } // Now find the offset with the largest K_delta for this period. // This is the best alignment of the two videos modulo this period. float max_K_delta = K_deltas[0]; float maxIndex = 0; for (int offset = 1; offset < period; offset++) { if (K_deltas[offset] > max_K_delta) { max_K_delta = K_deltas[offset]; maxIndex = offset; } } bestOffsets[i] = maxIndex; valuesAtBestOffsets[i] = max_K_delta; } } // ---------------------------------------------------------------- float TMKFeatureVectors::computeLevel1Score( const TMKFeatureVectors& fva, const TMKFeatureVectors& fvb) { return facebook::tmk::libvec::computeCosSim( fva.getPureAverageFeature(), fvb.getPureAverageFeature()); } // ---------------------------------------------------------------- float TMKFeatureVectors::computeLevel2Score( const TMKFeatureVectors& fva, const TMKFeatureVectors& fvb) { BestOffsets bestOffsets; ValuesAtBestOffsets valuesAtBestOffsets; TMKFeatureVectors::findPairOffsetsModuloPeriods( fva, fvb, bestOffsets, valuesAtBestOffsets, false); #if 1 return facebook::tmk::libvec::computeMax(valuesAtBestOffsets) / fva._pairScoreNormalizer; #else return facebook::tmk::libvec::computeSum(valuesAtBestOffsets) / pva.getNumPeriods / fva._pairScoreNormalizer; #endif } // ---------------------------------------------------------------- bool TMKFeatureVectors::compare( const TMKFeatureVectors& fva, const TMKFeatureVectors& fvb, float tolerance) { if (TMKFeatureVectors::areCompatible(fva, fvb)) { if (facebook::tmk::libvec::compareVectors( fva._pureAverageFeature, fvb._pureAverageFeature, tolerance)) { bool cos_equal = facebook::tmk::libvec::compareVectorsRank3( fva._cosFeatures, fvb._cosFeatures, tolerance); bool sin_equal = facebook::tmk::libvec::compareVectorsRank3( fva._sinFeatures, fvb._sinFeatures, tolerance); return cos_equal && sin_equal; } return false; } return false; } } // namespace algo } // namespace tmk } // namespace facebook