int main(int argc, char *argv[])
{
	#ifdef WIN32
	//_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); // dump leaks at return
	//_CrtSetBreakAlloc(287);
	#endif
	
	string verboseLevelConsole;
	int deviceId, kinectId;
	bool useCamera = false, useKinect = false;
	bool useFileList = false;
	bool useImgs = false;
	bool useDirectory = false;
	bool useLandmarkFiles = false;
	vector<path> inputPaths;
	path inputFilelist;
	path inputDirectory;
	vector<path> inputFilenames;
	path configFilename;
	shared_ptr<ImageSource> imageSource;
	path landmarksDir; // TODO: Make more dynamic wrt landmark format. a) What about the loading-flags (1_Per_Folder etc) we have? b) Expose those flags to cmdline? c) Make a LmSourceLoader and he knows about a LM_TYPE (each corresponds to a Parser/Loader class?)
	string landmarkType;
	path sdmModelFile;
	path faceDetectorFilename;
	bool trackingMode;

	try {
		po::options_description desc("Allowed options");
		desc.add_options()
			("help,h",
				"produce help message")
			("verbose,v", po::value<string>(&verboseLevelConsole)->implicit_value("DEBUG")->default_value("INFO","show messages with INFO loglevel or below."),
				  "specify the verbosity of the console output: PANIC, ERROR, WARN, INFO, DEBUG or TRACE")
			("config,c", po::value<path>(&configFilename)->required(), 
				"path to a config (.cfg) file")
			("input,i", po::value<vector<path>>(&inputPaths)/*->required()*/, 
				"input from one or more files, a directory, or a  .lst/.txt-file containing a list of images")
			("device,d", po::value<int>(&deviceId)->implicit_value(0), 
				"A camera device ID for use with the OpenCV camera driver")
			("kinect,k", po::value<int>(&kinectId)->implicit_value(0), 
				"Windows only: Use a Kinect as camera. Optionally specify a device ID.")
			("model,m", po::value<path>(&sdmModelFile)->required(),
				"A SDM model file to load.")
			("face-detector,f", po::value<path>(&faceDetectorFilename)->required(),
				"Path to an XML CascadeClassifier from OpenCV.")
			("landmarks,l", po::value<path>(&landmarksDir), 
				"load landmark files from the given folder")
			("landmark-type,t", po::value<string>(&landmarkType), 
				"specify the type of landmarks to load: ibug")
			("tracking-mode,r", po::value<bool>(&trackingMode)->default_value(false)->implicit_value(true),
				"If on, V&J will be run to initialize the model only and after the model lost tracking. If off, V&J will be run on every frame/image.")
		;

		po::positional_options_description p;
		p.add("input", -1);

		po::variables_map vm;
		po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm);
		po::notify(vm);

		if (vm.count("help")) {
			cout << "Usage: fitter [options]\n";
			cout << desc;
			return EXIT_SUCCESS;
		}
		if (vm.count("landmarks")) {
			useLandmarkFiles = true;
			if (!vm.count("landmark-type")) {
				cout << "You have specified to use landmark files. Please also specify the type of the landmarks to load via --landmark-type or -t." << endl;
				return EXIT_SUCCESS;
			}
		}

	} catch(std::exception& e) {
		cout << e.what() << endl;
		return EXIT_FAILURE;
	}

	loglevel logLevel;
	if(boost::iequals(verboseLevelConsole, "PANIC")) logLevel = loglevel::PANIC;
	else if(boost::iequals(verboseLevelConsole, "ERROR")) logLevel = loglevel::ERROR;
	else if(boost::iequals(verboseLevelConsole, "WARN")) logLevel = loglevel::WARN;
	else if(boost::iequals(verboseLevelConsole, "INFO")) logLevel = loglevel::INFO;
	else if(boost::iequals(verboseLevelConsole, "DEBUG")) logLevel = loglevel::DEBUG;
	else if(boost::iequals(verboseLevelConsole, "TRACE")) logLevel = loglevel::TRACE;
	else {
		cout << "Error: Invalid loglevel." << endl;
		return EXIT_FAILURE;
	}
	
	Loggers->getLogger("shapemodels").addAppender(make_shared<logging::ConsoleAppender>(logLevel));
	Loggers->getLogger("render").addAppender(make_shared<logging::ConsoleAppender>(logLevel));
	Loggers->getLogger("sdmTracking").addAppender(make_shared<logging::ConsoleAppender>(logLevel));
	Logger appLogger = Loggers->getLogger("sdmTracking");

	appLogger.debug("Verbose level for console output: " + logging::loglevelToString(logLevel));
	appLogger.debug("Using config: " + configFilename.string());

	if (inputPaths.size() > 1) {
		// We assume the user has given several, valid images
		useImgs = true;
		inputFilenames = inputPaths;
	} else if (inputPaths.size() == 1) {
		// We assume the user has given either an image, directory, or a .lst-file
		if (inputPaths[0].extension().string() == ".lst" || inputPaths[0].extension().string() == ".txt") { // check for .lst or .txt first
			useFileList = true;
			inputFilelist = inputPaths.front();
		} else if (boost::filesystem::is_directory(inputPaths[0])) { // check if it's a directory
			useDirectory = true;
			inputDirectory = inputPaths.front();
		} else { // it must be an image
			useImgs = true;
			inputFilenames = inputPaths;
		}
	} else {
		// todo see HeadTracking.cpp
		useCamera = true;
		//appLogger.error("Please either specify one or several files, a directory, or a .lst-file containing a list of images to run the program!");
		//return EXIT_FAILURE;
	}

	if (useFileList==true) {
		appLogger.info("Using file-list as input: " + inputFilelist.string());
		shared_ptr<ImageSource> fileListImgSrc; // TODO VS2013 change to unique_ptr, rest below also
		try {
			fileListImgSrc = make_shared<FileListImageSource>(inputFilelist.string());
		} catch(const std::runtime_error& e) {
			appLogger.error(e.what());
			return EXIT_FAILURE;
		}
		imageSource = fileListImgSrc;
	}
	if (useImgs==true) {
		//imageSource = make_shared<FileImageSource>(inputFilenames);
		//imageSource = make_shared<RepeatingFileImageSource>("C:\\Users\\Patrik\\GitHub\\data\\firstrun\\ws_8.png");
		appLogger.info("Using input images: ");
		vector<string> inputFilenamesStrings;	// Hack until we use vector<path> (?)
		for (const auto& fn : inputFilenames) {
			appLogger.info(fn.string());
			inputFilenamesStrings.push_back(fn.string());
		}
		shared_ptr<ImageSource> fileImgSrc;
		try {
			fileImgSrc = make_shared<FileImageSource>(inputFilenamesStrings);
		} catch(const std::runtime_error& e) {
			appLogger.error(e.what());
			return EXIT_FAILURE;
		}
		imageSource = fileImgSrc;
	}
	if (useDirectory==true) {
		appLogger.info("Using input images from directory: " + inputDirectory.string());
		try {
			imageSource = make_shared<DirectoryImageSource>(inputDirectory.string());
		} catch(const std::runtime_error& e) {
			appLogger.error(e.what());
			return EXIT_FAILURE;
		}
	}
	if (useCamera) {
		imageSource = make_shared<CameraImageSource>(deviceId);
	}
	// Load the ground truth
	// Either a) use if/else for imageSource or labeledImageSource, or b) use an EmptyLandmarkSoure
	shared_ptr<LabeledImageSource> labeledImageSource;
	shared_ptr<NamedLandmarkSource> landmarkSource;
	if (useLandmarkFiles) {
		vector<path> groundtruthDirs; groundtruthDirs.push_back(landmarksDir); // Todo: Make cmdline use a vector<path>
		shared_ptr<LandmarkFormatParser> landmarkFormatParser;
		if(boost::iequals(landmarkType, "lst")) {
			//landmarkFormatParser = make_shared<LstLandmarkFormatParser>();
			//landmarkSource = make_shared<DefaultNamedLandmarkSource>(LandmarkFileGatherer::gather(imageSource, string(), GatherMethod::SEPARATE_FILES, groundtruthDirs), landmarkFormatParser);
		} else if(boost::iequals(landmarkType, "ibug")) {
			landmarkFormatParser = make_shared<IbugLandmarkFormatParser>();
			landmarkSource = make_shared<DefaultNamedLandmarkSource>(LandmarkFileGatherer::gather(imageSource, ".pts", GatherMethod::ONE_FILE_PER_IMAGE_SAME_DIR, groundtruthDirs), landmarkFormatParser);
		} else {
			cout << "Error: Invalid ground truth type." << endl;
			return EXIT_FAILURE;
		}
	} else {
		landmarkSource = make_shared<EmptyLandmarkSource>();
	}
	labeledImageSource = make_shared<NamedLabeledImageSource>(imageSource, landmarkSource);
	ptree pt;
	try {
		boost::property_tree::info_parser::read_info(configFilename.string(), pt);
	} catch(const boost::property_tree::ptree_error& error) {
		appLogger.error(error.what());
		return EXIT_FAILURE;
	}
	
	std::chrono::time_point<std::chrono::system_clock> start, end;
	Mat img;
	const string windowName = "win";

	vector<imageio::ModelLandmark> landmarks;

	cv::namedWindow(windowName);

	SdmLandmarkModel lmModel = SdmLandmarkModel::load(sdmModelFile);
	SdmLandmarkModelFitting modelFitter(lmModel);

	// faceDetectorFilename: e.g. opencv\\sources\\data\\haarcascades\\haarcascade_frontalface_alt2.xml
	cv::CascadeClassifier faceCascade;
	if (!faceCascade.load(faceDetectorFilename.string()))
	{
		cout << "Error loading face detection model." << endl;
		return EXIT_FAILURE;
	}
	
	bool runRigidAlign = true;

	std::ofstream resultsFile("C:\\Users\\Patrik\\Documents\\GitHub\\sdm_lfpw_tr_68lm_10s_5c_RESULTS.txt");
	vector<string> comparisonLandmarks({ "9", "31", "37", "40", "43", "46", "49", "55", "63", "67" });

	while(labeledImageSource->next()) {
		start = std::chrono::system_clock::now();
		appLogger.info("Starting to process " + labeledImageSource->getName().string());
		img = labeledImageSource->getImage();
		Mat landmarksImage = img.clone();

		LandmarkCollection groundtruth = labeledImageSource->getLandmarks();
		vector<shared_ptr<Landmark>> lmv = groundtruth.getLandmarks();
		for (const auto& l : lmv) {
			cv::circle(landmarksImage, l->getPoint2D(), 3, Scalar(255.0f, 0.0f, 0.0f));
		}

		// iBug 68 points. No eye-centers. Calculate them:
		cv::Point2f reye_c = (groundtruth.getLandmark("37")->getPosition2D() + groundtruth.getLandmark("40")->getPosition2D()) / 2.0f;
		cv::Point2f leye_c = (groundtruth.getLandmark("43")->getPosition2D() + groundtruth.getLandmark("46")->getPosition2D()) / 2.0f;
		cv::circle(landmarksImage, reye_c, 3, Scalar(255.0f, 0.0f, 127.0f));
		cv::circle(landmarksImage, leye_c, 3, Scalar(255.0f, 0.0f, 127.0f));
		cv::Scalar interEyeDistance = cv::norm(Vec2f(reye_c), Vec2f(leye_c), cv::NORM_L2);

		Mat imgGray;
		cvtColor(img, imgGray, cv::COLOR_BGR2GRAY);
		vector<cv::Rect> faces;
		float score, notFace = 0.5;
		
		// face detection
		faceCascade.detectMultiScale(img, faces, 1.2, 2, 0, cv::Size(50, 50));
		//faces.push_back({ 172, 199, 278, 278 });
		if (faces.empty()) {
			runRigidAlign = true;
			cv::imshow(windowName, landmarksImage);
			cv::waitKey(5);
			continue;
		}
		for (const auto& f : faces) {
			cv::rectangle(landmarksImage, f, cv::Scalar(0.0f, 0.0f, 255.0f));
		}

		// Check if the face corresponds to the ground-truth:
		Mat gtLmsRowX(1, lmv.size(), CV_32FC1);
		Mat gtLmsRowY(1, lmv.size(), CV_32FC1);
		int idx = 0;
		for (const auto& l : lmv) {
			gtLmsRowX.at<float>(idx) = l->getX();
			gtLmsRowY.at<float>(idx) = l->getY();
			++idx;
		}
		double minWidth, maxWidth, minHeight, maxHeight;
		cv::minMaxIdx(gtLmsRowX, &minWidth, &maxWidth);
		cv::minMaxIdx(gtLmsRowY, &minHeight, &maxHeight);
		float cx = cv::mean(gtLmsRowX)[0];
		float cy = cv::mean(gtLmsRowY)[0] - 30.0f;
		// do this in relation to the IED, not absolute pixel values
		if (std::abs(cx - (faces[0].x+faces[0].width/2.0f)) > 30.0f || std::abs(cy - (faces[0].y+faces[0].height/2.0f)) > 30.0f) {
			//cv::imshow(windowName, landmarksImage);
			//cv::waitKey();
			continue;
		}
		
		Mat modelShape = lmModel.getMeanShape();
		//if (runRigidAlign) {
			modelShape = modelFitter.alignRigid(modelShape, faces[0]);
			//runRigidAlign = false;
		//}
		
	
		// print the mean initialization
		for (int i = 0; i < lmModel.getNumLandmarks(); ++i) {
			cv::circle(landmarksImage, Point2f(modelShape.at<float>(i, 0), modelShape.at<float>(i + lmModel.getNumLandmarks(), 0)), 3, Scalar(255.0f, 0.0f, 255.0f));
		}
		modelShape = modelFitter.optimize(modelShape, imgGray);
		for (int i = 0; i < lmModel.getNumLandmarks(); ++i) {
			cv::circle(landmarksImage, Point2f(modelShape.at<float>(i, 0), modelShape.at<float>(i + lmModel.getNumLandmarks(), 0)), 3, Scalar(0.0f, 255.0f, 0.0f));
		}

		imwrite("C:\\Users\\Patrik\\Documents\\GitHub\\out_sdm_lms\\" + labeledImageSource->getName().filename().string(), landmarksImage);

		resultsFile << "# " << labeledImageSource->getName() << std::endl;
		for (const auto& lmId : comparisonLandmarks) {

			shared_ptr<Landmark> gtlm = groundtruth.getLandmark(lmId); // Todo: Handle case when LM not found
			cv::Point2f gt = gtlm->getPoint2D();
			cv::Point2f det = lmModel.getLandmarkAsPoint(lmId, modelShape);

			float dx = (gt.x - det.x);
			float dy = (gt.y - det.y);
			float diff = std::sqrt(dx*dx + dy*dy);
			diff = diff / interEyeDistance[0]; // normalize by the IED

			resultsFile << diff << " # " << lmId << std::endl;
		}

		
		end = std::chrono::system_clock::now();
		int elapsed_mseconds = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
		appLogger.info("Finished processing. Elapsed time: " + lexical_cast<string>(elapsed_mseconds) + "ms.\n");
		
		//cv::imshow(windowName, landmarksImage);
		//cv::waitKey(5);

	}

	resultsFile.close();

	return 0;
}
Exemplo n.º 2
0
void Benchmark::run(
		shared_ptr<LabeledImageSource> source, shared_ptr<FeatureExtractor> extractor, shared_ptr<TrainableProbabilisticClassifier> classifier,
		float confidenceThreshold, size_t negatives, size_t initialNegatives, ostream& frameOut, ostream& resultOut) const {

	uint32_t frameCount = 0;
	uint64_t extractedPatchCountSum = 0; // total amount of extracted patches
	uint64_t classifiedPatchCountSum = 0; // total amount of classified patches
	uint64_t negativePatchCountSum = 0; // amount of negative patches of all frames except the first ones (where classifier is not usable)
	uint32_t positivePatchCountSum = 0;
	uint64_t updateTimeSum = 0;
	uint64_t extractTimeSum = 0;
	uint64_t falseRejectionsSum = 0;
	uint64_t falseAcceptancesSum = 0;
	uint64_t classifyTimeSum = 0;
	double locationAccuracySum = 0;
	float worstLocationAccuracy = 1;
	uint32_t positiveTrainingCountSum = 0;
	uint32_t negativeTrainingCountSum = 0;
	uint64_t trainingTimeSum = 0;
	cv::Size patchSize;

	bool initialized = false;
	float aspectRatio = 1;
	while (source->next()) {
		Mat image = source->getImage();
		const LandmarkCollection landmarks = source->getLandmarks();
		if (landmarks.isEmpty())
			continue;
		const shared_ptr<Landmark> landmark = landmarks.getLandmark();
		bool hasGroundTruth = landmark->isVisible();
		if (!hasGroundTruth && !initialized)
			continue;

		Rect_<float> groundTruth;
		if (hasGroundTruth) {
			groundTruth = landmark->getRect();
			aspectRatio = groundTruth.width / groundTruth.height;
			if (!initialized) {
				DirectPyramidFeatureExtractor* pyramidExtractor = dynamic_cast<DirectPyramidFeatureExtractor*>(extractor.get());
				if (pyramidExtractor != nullptr) {
					patchSize = pyramidExtractor->getPatchSize();
					double dimension = patchSize.width * patchSize.height;
					double patchHeight = sqrt(dimension / aspectRatio);
					double patchWidth = aspectRatio * patchHeight;
					pyramidExtractor->setPatchSize(cvRound(patchWidth), cvRound(patchHeight));
				}
				initialized = true;
			}
		}

		steady_clock::time_point extractionStart = steady_clock::now();
		extractor->update(image);
		steady_clock::time_point extractionBetween = steady_clock::now();
		shared_ptr<Patch> positivePatch;
		if (hasGroundTruth) {
			positivePatch = extractor->extract(
					cvRound(groundTruth.x + 0.5f * groundTruth.width),
					cvRound(groundTruth.y + 0.5f * groundTruth.height),
					cvRound(groundTruth.width),
					cvRound(groundTruth.height));
			if (!positivePatch)
				continue;
		}
		vector<shared_ptr<Patch>> negativePatches;
		vector<shared_ptr<Patch>> neutralPatches;
		int patchCount = hasGroundTruth ? 1 : 0;
		for (float size = sizeMin; size <= sizeMax; size *= sizeScale) {
			float realHeight = size * image.rows;
			float realWidth = aspectRatio * realHeight;
			int width = cvRound(realWidth);
			int height = cvRound(realHeight);
			int minX = width / 2;
			int minY = height / 2;
			int maxX = image.cols - width + width / 2;
			int maxY = image.rows - height + height / 2;
			float stepX = std::max(1.f, step * realWidth);
			float stepY = std::max(1.f, step * realHeight);
			for (float x = minX; cvRound(x) < maxX; x += stepX) {
				for (float y = minY; cvRound(y) < maxY; y += stepY) {
					shared_ptr<Patch> patch = extractor->extract(cvRound(x), cvRound(y), width, height);
					if (patch) {
						patchCount++;
						if (!hasGroundTruth || computeOverlap(groundTruth, patch->getBounds()) <= allowedOverlap)
							negativePatches.push_back(patch);
						else
							neutralPatches.push_back(patch);
					}
				}
			}
		}
		steady_clock::time_point extractionEnd = steady_clock::now();
		milliseconds updateDuration = duration_cast<milliseconds>(extractionBetween - extractionStart);
		milliseconds extractDuration = duration_cast<milliseconds>(extractionEnd - extractionBetween);

		frameOut << source->getName().filename().generic_string() << ": " << updateDuration.count() << "ms " << patchCount << " " << extractDuration.count() << "ms";
		frameCount++;
		extractedPatchCountSum += patchCount;
		updateTimeSum += updateDuration.count();
		extractTimeSum += extractDuration.count();

		if (classifier->isUsable()) {
			classifiedPatchCountSum += patchCount;
			if (hasGroundTruth)
				positivePatchCountSum++; // amount needed for false acceptance rate, therefore only positive patches that are tested
			negativePatchCountSum += negativePatches.size(); // amount needed for false acceptance rate, therefore only negative patches that are tested

			steady_clock::time_point classificationStart = steady_clock::now();
			// positive patch
			shared_ptr<ClassifiedPatch> classifiedPositivePatch;
			if (hasGroundTruth)
				classifiedPositivePatch = make_shared<ClassifiedPatch>(positivePatch, classifier->classify(positivePatch->getData()));
			// negative patches
			vector<shared_ptr<ClassifiedPatch>> classifiedNegativePatches;
			classifiedNegativePatches.reserve(negativePatches.size());
			for (const shared_ptr<Patch>& patch : negativePatches)
				classifiedNegativePatches.push_back(make_shared<ClassifiedPatch>(patch, classifier->classify(patch->getData())));
			// neutral patches
			vector<shared_ptr<ClassifiedPatch>> classifiedNeutralPatches;
			classifiedNeutralPatches.reserve(neutralPatches.size());
			for (const shared_ptr<Patch>& patch : neutralPatches)
				classifiedNeutralPatches.push_back(make_shared<ClassifiedPatch>(patch, classifier->classify(patch->getData())));
			steady_clock::time_point classificationEnd = steady_clock::now();
			milliseconds classificationDuration = duration_cast<milliseconds>(classificationEnd - classificationStart);

			int falseRejections = !hasGroundTruth || classifiedPositivePatch->isPositive() ? 0 : 1;
			int falseAcceptances = std::count_if(classifiedNegativePatches.begin(), classifiedNegativePatches.end(), [](shared_ptr<ClassifiedPatch>& patch) {
				return patch->isPositive();
			});

			frameOut << " " << falseRejections << "/" << (hasGroundTruth ? "1 " : "0 ") << falseAcceptances << "/" << negativePatches.size() << " " << classificationDuration.count() << "ms";
			falseRejectionsSum += falseRejections;
			falseAcceptancesSum += falseAcceptances;
			classifyTimeSum += classificationDuration.count();

			if (hasGroundTruth) {
				shared_ptr<ClassifiedPatch> bestNegativePatch = *std::max_element(classifiedNegativePatches.begin(), classifiedNegativePatches.end(), [](shared_ptr<ClassifiedPatch>& a, shared_ptr<ClassifiedPatch>& b) {
					return a->getProbability() > b->getProbability();
				});
				shared_ptr<ClassifiedPatch> bestNeutralPatch = *std::max_element(classifiedNeutralPatches.begin(), classifiedNeutralPatches.end(), [](shared_ptr<ClassifiedPatch>& a, shared_ptr<ClassifiedPatch>& b) {
					return a->getProbability() > b->getProbability();
				});
				float overlap = 0;
				if (classifiedPositivePatch->getProbability() >= bestNeutralPatch->getProbability() && classifiedPositivePatch->getProbability() >= bestNegativePatch->getProbability())
					overlap = computeOverlap(groundTruth, classifiedPositivePatch->getPatch()->getBounds());
				else if (bestNeutralPatch->getProbability() >= bestNegativePatch->getProbability())
					overlap = computeOverlap(groundTruth, bestNeutralPatch->getPatch()->getBounds());
				else
					overlap = computeOverlap(groundTruth, bestNegativePatch->getPatch()->getBounds());

				frameOut << " " << cvRound(100 * overlap) << "%";
				locationAccuracySum += overlap;
				worstLocationAccuracy = std::min(worstLocationAccuracy, overlap);
			}

			vector<shared_ptr<ClassifiedPatch>> negativeTrainingCandidates;
			for (shared_ptr<ClassifiedPatch>& patch : classifiedNegativePatches) {
				if (patch->isPositive() || patch->getProbability() > 1 - confidenceThreshold)
					negativeTrainingCandidates.push_back(patch);
			}
			if (negativeTrainingCandidates.size() > negatives) {
				std::partial_sort(negativeTrainingCandidates.begin(), negativeTrainingCandidates.begin() + negatives, negativeTrainingCandidates.end(), [](shared_ptr<ClassifiedPatch>& a, shared_ptr<ClassifiedPatch>& b) {
					return a->getProbability() > b->getProbability();
				});
				negativeTrainingCandidates.resize(negatives);
			}
			vector<Mat> negativeTrainingExamples;
			negativeTrainingExamples.reserve(negativeTrainingCandidates.size());
			for (const shared_ptr<ClassifiedPatch>& patch : negativeTrainingCandidates)
				negativeTrainingExamples.push_back(patch->getPatch()->getData());
			vector<Mat> positiveTrainingExamples;
			if (hasGroundTruth && (!classifiedPositivePatch->isPositive() || classifiedPositivePatch->getProbability() < confidenceThreshold))
				positiveTrainingExamples.push_back(classifiedPositivePatch->getPatch()->getData());

			steady_clock::time_point trainingStart = steady_clock::now();
			classifier->retrain(positiveTrainingExamples, negativeTrainingExamples);
			steady_clock::time_point trainingEnd = steady_clock::now();
			milliseconds trainingDuration = duration_cast<milliseconds>(trainingEnd - trainingStart);

			frameOut << " " << positiveTrainingExamples.size() << "+" << negativeTrainingExamples.size() << " " << trainingDuration.count() << "ms";
			positiveTrainingCountSum += positiveTrainingExamples.size();
			negativeTrainingCountSum += negativeTrainingExamples.size();
			trainingTimeSum += trainingDuration.count();
		} else {
			int step = negativePatches.size() / initialNegatives;
			int first = step / 2;
			vector<Mat> negativeTrainingExamples;
			negativeTrainingExamples.reserve(initialNegatives);
			for (size_t i = first; i < negativePatches.size(); i += step)
				negativeTrainingExamples.push_back(negativePatches[i]->getData());
			vector<Mat> positiveTrainingExamples;
			if (hasGroundTruth)
				positiveTrainingExamples.push_back(positivePatch->getData());

			steady_clock::time_point trainingStart = steady_clock::now();
			classifier->retrain(positiveTrainingExamples, negativeTrainingExamples);
			steady_clock::time_point trainingEnd = steady_clock::now();
			milliseconds trainingDuration = duration_cast<milliseconds>(trainingEnd - trainingStart);

			frameOut << " " << positiveTrainingExamples.size() << "+" << negativeTrainingExamples.size() << " " << trainingDuration.count() << "ms";
			positiveTrainingCountSum += positiveTrainingExamples.size();
			negativeTrainingCountSum += negativeTrainingExamples.size();
			trainingTimeSum += trainingDuration.count();
		}
		frameOut << endl;
	}

	DirectPyramidFeatureExtractor* pyramidExtractor = dynamic_cast<DirectPyramidFeatureExtractor*>(extractor.get());
	if (pyramidExtractor != nullptr) {
		pyramidExtractor->setPatchSize(patchSize.width, patchSize.height);
	}

	if (frameCount == 0) {
		frameOut << "no valid frames" << endl;
		resultOut << "no valid frames" << endl;
	} else if (frameCount == 1 || positivePatchCountSum < 1) {
		frameOut << "too few valid frames" << endl;
		resultOut << "too few valid frames" << endl;
	} else if (extractedPatchCountSum == 0) {
		frameOut << "no valid patches" << endl;
		resultOut << "no valid patches" << endl;
	} else if (negativePatchCountSum == 0) {
		frameOut << "no valid negative patches" << endl;
		resultOut << "no valid negative patches" << endl;
	} else {
		frameOut << frameCount << " " << extractedPatchCountSum << " " << classifiedPatchCountSum << " " << negativePatchCountSum << " " << positivePatchCountSum << " "
				<< updateTimeSum << "ms " << extractTimeSum << "ms " << falseRejectionsSum << " " << falseAcceptancesSum << " "
				<< classifyTimeSum << "ms " << cvRound(100 * locationAccuracySum / positivePatchCountSum) << "% " << cvRound(100 * worstLocationAccuracy) << "% "
				<< positiveTrainingCountSum << " " << negativeTrainingCountSum << " " << trainingTimeSum << "ms" << endl;

		float falseRejectionRate = static_cast<float>(falseRejectionsSum) / static_cast<float>(frameCount - 1);
		float falseAcceptanceRate = static_cast<float>(falseRejectionsSum) / static_cast<float>(negativePatchCountSum);
		float avgPositiveTrainingCount = static_cast<float>(positiveTrainingCountSum) / static_cast<float>(frameCount);
		float avgNegativeTrainingCount = static_cast<float>(negativeTrainingCountSum) / static_cast<float>(frameCount);
		uint64_t normalizedExtractionTime = updateTimeSum / frameCount + (1000 * extractTimeSum) / extractedPatchCountSum;
		uint64_t normalizedClassificationTime = (1000 * classifyTimeSum) / classifiedPatchCountSum;
		uint64_t normalizedTrainingTime = trainingTimeSum / frameCount;
		uint64_t normalizedFrameTime = normalizedExtractionTime + normalizedClassificationTime + normalizedTrainingTime;
		resultOut.setf(std::ios::fixed);
		resultOut.precision(2);
		resultOut << falseRejectionRate << " " << falseAcceptanceRate << " "
				<< cvRound(100 * locationAccuracySum / positivePatchCountSum) << "% " << cvRound(100 * worstLocationAccuracy) << "% "
				<< avgPositiveTrainingCount << " " << avgNegativeTrainingCount << " "
				<< normalizedFrameTime << "ms (" << normalizedExtractionTime << "ms " << normalizedClassificationTime << "ms " << normalizedTrainingTime << "ms)" << endl;
	}
}