2016-04-28 21:40:36 +02:00
///////////////////////////////////////////////////////////////////////////////
2017-05-09 03:36:23 +02:00
// Copyright (C) 2017, Carnegie Mellon University and University of Cambridge,
2016-04-28 21:40:36 +02:00
// all rights reserved.
//
2017-05-09 03:36:23 +02:00
// ACADEMIC OR NON-PROFIT ORGANIZATION NONCOMMERCIAL RESEARCH USE ONLY
2016-04-28 21:40:36 +02:00
//
2018-05-01 23:00:16 +02:00
// BY USING OR DOWNLOADING THE SOFTWARE, YOU ARE AGREEING TO THE TERMS OF THIS LICENSE AGREEMENT.
2017-05-09 03:36:23 +02:00
// IF YOU DO NOT AGREE WITH THESE TERMS, YOU MAY NOT USE OR DOWNLOAD THE SOFTWARE.
//
// License can be found in OpenFace-license.txt
2016-04-28 21:40:36 +02:00
// * Any publications arising from the use of this software, including but
// not limited to academic journal and conference publications, technical
// reports and manuals, must cite at least one of the following works:
//
// OpenFace: an open source facial behavior analysis toolkit
2018-05-01 23:00:16 +02:00
// Tadas Baltru<72> aitis, Peter Robinson, and Louis-Philippe Morency
// in IEEE Winter Conference on Applications of Computer Vision, 2016
2016-04-28 21:40:36 +02:00
//
// Rendering of Eyes for Eye-Shape Registration and Gaze Estimation
2018-05-01 23:00:16 +02:00
// Erroll Wood, Tadas Baltru<72> aitis, Xucong Zhang, Yusuke Sugano, Peter Robinson, and Andreas Bulling
// in IEEE International. Conference on Computer Vision (ICCV), 2015
2016-04-28 21:40:36 +02:00
//
// Cross-dataset learning and person-speci?c normalisation for automatic Action Unit detection
2018-05-01 23:00:16 +02:00
// Tadas Baltru<72> aitis, Marwa Mahmoud, and Peter Robinson
// in Facial Expression Recognition and Analysis Challenge,
// IEEE International Conference on Automatic Face and Gesture Recognition, 2015
2016-04-28 21:40:36 +02:00
//
// Constrained Local Neural Fields for robust facial landmark detection in the wild.
2018-05-01 23:00:16 +02:00
// Tadas Baltru<72> aitis, Peter Robinson, and Louis-Philippe Morency.
// in IEEE Int. Conference on Computer Vision Workshops, 300 Faces in-the-Wild Challenge, 2013.
2016-04-28 21:40:36 +02:00
//
///////////////////////////////////////////////////////////////////////////////
// FaceTrackingVidMulti.cpp : Defines the entry point for the multiple face tracking console application.
# include "LandmarkCoreIncludes.h"
2017-11-13 18:48:17 +01:00
# include "VisualizationUtils.h"
# include "Visualizer.h"
# include "SequenceCapture.h"
2018-02-23 09:25:19 +01:00
# include <RecorderOpenFace.h>
# include <RecorderOpenFaceParameters.h>
# include <GazeEstimation.h>
# include <FaceAnalyser.h>
2017-11-13 18:48:17 +01:00
2016-04-28 21:40:36 +02:00
# include <fstream>
# include <sstream>
// OpenCV includes
# include <opencv2/videoio/videoio.hpp> // Video write
# include <opencv2/videoio/videoio_c.h> // Video write
# include <opencv2/imgproc.hpp>
# include <opencv2/highgui/highgui.hpp>
# define INFO_STREAM( stream ) \
std : : cout < < stream < < std : : endl
# define WARN_STREAM( stream ) \
std : : cout < < " Warning: " < < stream < < std : : endl
# define ERROR_STREAM( stream ) \
std : : cout < < " Error: " < < stream < < std : : endl
static void printErrorAndAbort ( const std : : string & error )
{
std : : cout < < error < < std : : endl ;
abort ( ) ;
}
# define FATAL_STREAM( stream ) \
printErrorAndAbort ( std : : string ( " Fatal error: " ) + stream )
using namespace std ;
vector < string > get_arguments ( int argc , char * * argv )
{
vector < string > arguments ;
for ( int i = 0 ; i < argc ; + + i )
{
arguments . push_back ( string ( argv [ i ] ) ) ;
}
return arguments ;
}
void NonOverlapingDetections ( const vector < LandmarkDetector : : CLNF > & clnf_models , vector < cv : : Rect_ < double > > & face_detections )
{
// Go over the model and eliminate detections that are not informative (there already is a tracker there)
for ( size_t model = 0 ; model < clnf_models . size ( ) ; + + model )
{
// See if the detections intersect
cv : : Rect_ < double > model_rect = clnf_models [ model ] . GetBoundingBox ( ) ;
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
for ( int detection = face_detections . size ( ) - 1 ; detection > = 0 ; - - detection )
{
double intersection_area = ( model_rect & face_detections [ detection ] ) . area ( ) ;
double union_area = model_rect . area ( ) + face_detections [ detection ] . area ( ) - 2 * intersection_area ;
// If the model is already tracking what we're detecting ignore the detection, this is determined by amount of overlap
if ( intersection_area / union_area > 0.5 )
{
face_detections . erase ( face_detections . begin ( ) + detection ) ;
}
}
}
}
int main ( int argc , char * * argv )
{
vector < string > arguments = get_arguments ( argc , argv ) ;
2018-02-23 09:25:19 +01:00
// no arguments: output usage
if ( arguments . size ( ) = = 1 )
{
cout < < " For command line arguments see: " < < endl ;
cout < < " https://github.com/TadasBaltrusaitis/OpenFace/wiki/Command-line-arguments " ;
return 0 ;
}
2016-04-28 21:40:36 +02:00
LandmarkDetector : : FaceModelParameters det_params ( arguments ) ;
// This is so that the model would not try re-initialising itself
det_params . reinit_video_every = - 1 ;
det_params . curr_face_detector = LandmarkDetector : : FaceModelParameters : : HOG_SVM_DETECTOR ;
vector < LandmarkDetector : : FaceModelParameters > det_parameters ;
det_parameters . push_back ( det_params ) ;
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
// The modules that are being used for tracking
2017-11-13 18:48:17 +01:00
vector < LandmarkDetector : : CLNF > face_models ;
2016-04-28 21:40:36 +02:00
vector < bool > active_models ;
2018-05-01 23:00:16 +02:00
int num_faces_max = 15 ;
2016-04-28 21:40:36 +02:00
2017-11-13 18:48:17 +01:00
LandmarkDetector : : CLNF face_model ( det_parameters [ 0 ] . model_location ) ;
face_model . face_detector_HAAR . load ( det_parameters [ 0 ] . face_detector_location ) ;
face_model . face_detector_location = det_parameters [ 0 ] . face_detector_location ;
2018-05-01 23:00:16 +02:00
2017-11-13 18:48:17 +01:00
face_models . reserve ( num_faces_max ) ;
2016-04-28 21:40:36 +02:00
2017-11-13 18:48:17 +01:00
face_models . push_back ( face_model ) ;
2016-04-28 21:40:36 +02:00
active_models . push_back ( false ) ;
for ( int i = 1 ; i < num_faces_max ; + + i )
{
2017-11-13 18:48:17 +01:00
face_models . push_back ( face_model ) ;
2016-04-28 21:40:36 +02:00
active_models . push_back ( false ) ;
det_parameters . push_back ( det_params ) ;
}
2018-05-01 23:00:16 +02:00
2018-02-23 09:25:19 +01:00
// Load facial feature extractor and AU analyser (make sure it is static, as we don't reidentify faces)
FaceAnalysis : : FaceAnalyserParameters face_analysis_params ( arguments ) ;
face_analysis_params . OptimizeForImages ( ) ;
FaceAnalysis : : FaceAnalyser face_analyser ( face_analysis_params ) ;
2018-03-30 10:36:32 +02:00
if ( ! face_model . eye_model )
{
cout < < " WARNING: no eye model found " < < endl ;
}
if ( face_analyser . GetAUClassNames ( ) . size ( ) = = 0 & & face_analyser . GetAUClassNames ( ) . size ( ) = = 0 )
{
cout < < " WARNING: no Action Unit models found " < < endl ;
}
2017-11-13 18:48:17 +01:00
// Open a sequence
Utilities : : SequenceCapture sequence_reader ;
2016-04-28 21:40:36 +02:00
2017-11-13 20:48:48 +01:00
// A utility for visualizing the results (show just the tracks)
2018-02-23 09:25:19 +01:00
Utilities : : Visualizer visualizer ( arguments ) ;
2016-04-28 21:40:36 +02:00
2017-11-13 18:48:17 +01:00
// Tracking FPS for visualization
Utilities : : FpsTracker fps_tracker ;
fps_tracker . AddFrame ( ) ;
2017-11-22 18:37:26 +01:00
int sequence_number = 0 ;
2017-11-13 18:48:17 +01:00
while ( true ) // this is not a for loop as we might also be reading from a webcam
{
2016-04-28 21:40:36 +02:00
2017-11-13 18:48:17 +01:00
// The sequence reader chooses what to open based on command line arguments provided
2017-11-22 10:03:29 +01:00
if ( ! sequence_reader . Open ( arguments ) )
2018-02-23 09:25:19 +01:00
break ;
2017-11-13 18:48:17 +01:00
INFO_STREAM ( " Device or file opened " ) ;
2016-04-28 21:40:36 +02:00
2017-11-13 18:48:17 +01:00
cv : : Mat captured_image = sequence_reader . GetNextFrame ( ) ;
2016-04-28 21:40:36 +02:00
int frame_count = 0 ;
2018-02-23 09:25:19 +01:00
Utilities : : RecorderOpenFaceParameters recording_params ( arguments , true , sequence_reader . IsWebcam ( ) ,
sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy , sequence_reader . fps ) ;
2018-05-01 23:00:16 +02:00
// for some reason not accepted as cli parameter, as we don't need it: disable it anyway
recording_params . setOutputAUs ( false ) ;
recording_params . setOutputHOG ( false ) ;
recording_params . setOutputAlignedFaces ( false ) ;
recording_params . setOutputTracked ( false ) ;
2018-03-30 10:36:32 +02:00
if ( ! face_model . eye_model )
{
recording_params . setOutputGaze ( false ) ;
2018-05-01 23:00:16 +02:00
}
2018-03-30 10:36:32 +02:00
2018-02-23 09:25:19 +01:00
Utilities : : RecorderOpenFace open_face_rec ( sequence_reader . name , recording_params , arguments ) ;
if ( sequence_reader . IsWebcam ( ) )
{
INFO_STREAM ( " WARNING: using a webcam in feature extraction, forcing visualization of tracking to allow quitting the application (press q) " ) ;
visualizer . vis_track = true ;
}
if ( recording_params . outputAUs ( ) )
{
INFO_STREAM ( " WARNING: using a AU detection in multiple face mode, it might not be as accurate and is experimental " ) ;
}
2016-04-28 21:40:36 +02:00
INFO_STREAM ( " Starting tracking " ) ;
while ( ! captured_image . empty ( ) )
2018-05-01 23:00:16 +02:00
{
2016-04-28 21:40:36 +02:00
// Reading the images
2018-02-23 09:25:19 +01:00
cv : : Mat_ < uchar > grayscale_image = sequence_reader . GetGrayFrame ( ) ;
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
vector < cv : : Rect_ < double > > face_detections ;
bool all_models_active = true ;
2017-11-13 18:48:17 +01:00
for ( unsigned int model = 0 ; model < face_models . size ( ) ; + + model )
2016-04-28 21:40:36 +02:00
{
if ( ! active_models [ model ] )
{
all_models_active = false ;
}
}
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
// Get the detections (every 8th frame and when there are free models available for tracking)
if ( frame_count % 8 = = 0 & & ! all_models_active )
2018-05-01 23:00:16 +02:00
{
2016-04-28 21:40:36 +02:00
if ( det_parameters [ 0 ] . curr_face_detector = = LandmarkDetector : : FaceModelParameters : : HOG_SVM_DETECTOR )
{
vector < double > confidences ;
2017-11-13 18:48:17 +01:00
LandmarkDetector : : DetectFacesHOG ( face_detections , grayscale_image , face_models [ 0 ] . face_detector_HOG , confidences ) ;
2016-04-28 21:40:36 +02:00
}
else
{
2017-11-13 18:48:17 +01:00
LandmarkDetector : : DetectFaces ( face_detections , grayscale_image , face_models [ 0 ] . face_detector_HAAR ) ;
2016-04-28 21:40:36 +02:00
}
}
// Keep only non overlapping detections (also convert to a concurrent vector
2017-11-13 18:48:17 +01:00
NonOverlapingDetections ( face_models , face_detections ) ;
2016-04-28 21:40:36 +02:00
vector < tbb : : atomic < bool > > face_detections_used ( face_detections . size ( ) ) ;
// Go through every model and update the tracking
2017-11-13 18:48:17 +01:00
tbb : : parallel_for ( 0 , ( int ) face_models . size ( ) , [ & ] ( int model ) {
2016-04-28 21:40:36 +02:00
//for(unsigned int model = 0; model < clnf_models.size(); ++model)
//{
bool detection_success = false ;
// If the current model has failed more than 4 times in a row, remove it
2017-11-13 18:48:17 +01:00
if ( face_models [ model ] . failures_in_a_row > 4 )
2018-05-01 23:00:16 +02:00
{
2016-04-28 21:40:36 +02:00
active_models [ model ] = false ;
2017-11-13 18:48:17 +01:00
face_models [ model ] . Reset ( ) ;
2016-04-28 21:40:36 +02:00
}
// If the model is inactive reactivate it with new detections
if ( ! active_models [ model ] )
{
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
for ( size_t detection_ind = 0 ; detection_ind < face_detections . size ( ) ; + + detection_ind )
{
// if it was not taken by another tracker take it (if it is false swap it to true and enter detection, this makes it parallel safe)
if ( face_detections_used [ detection_ind ] . compare_and_swap ( true , false ) = = false )
{
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
// Reinitialise the model
2017-11-13 18:48:17 +01:00
face_models [ model ] . Reset ( ) ;
2016-04-28 21:40:36 +02:00
// This ensures that a wider window is used for the initial landmark localisation
2017-11-13 18:48:17 +01:00
face_models [ model ] . detection_success = false ;
detection_success = LandmarkDetector : : DetectLandmarksInVideo ( grayscale_image , face_detections [ detection_ind ] , face_models [ model ] , det_parameters [ model ] ) ;
2018-02-23 09:25:19 +01:00
2016-04-28 21:40:36 +02:00
// This activates the model
active_models [ model ] = true ;
// break out of the loop as the tracker has been reinitialised
break ;
}
}
}
else
{
// The actual facial landmark detection / tracking
2017-11-13 18:48:17 +01:00
detection_success = LandmarkDetector : : DetectLandmarksInVideo ( grayscale_image , face_models [ model ] , det_parameters [ model ] ) ;
2016-04-28 21:40:36 +02:00
}
} ) ;
2018-05-01 23:00:16 +02:00
2017-11-13 18:48:17 +01:00
// Keeping track of FPS
fps_tracker . AddFrame ( ) ;
2016-04-28 21:40:36 +02:00
2017-11-13 18:48:17 +01:00
visualizer . SetImage ( captured_image , sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy ) ;
2016-04-28 21:40:36 +02:00
2018-05-01 23:00:16 +02:00
std : : stringstream jsonOutput ;
jsonOutput < < " [ " ;
int jsonFaceId = 0 ;
2018-02-23 09:25:19 +01:00
// Go through every model and detect eye gaze, record results and visualise the results
2017-11-13 18:48:17 +01:00
for ( size_t model = 0 ; model < face_models . size ( ) ; + + model )
2016-04-28 21:40:36 +02:00
{
2017-11-13 18:48:17 +01:00
// Visualising the results
if ( active_models [ model ] )
2016-04-28 21:40:36 +02:00
{
2018-02-23 09:25:19 +01:00
2018-05-01 23:00:16 +02:00
// Estimate head pose and eye gaze
2018-02-23 09:25:19 +01:00
cv : : Vec6d pose_estimate = LandmarkDetector : : GetPose ( face_models [ model ] , sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy ) ;
cv : : Point3f gaze_direction0 ( 0 , 0 , 0 ) ; cv : : Point3f gaze_direction1 ( 0 , 0 , 0 ) ; cv : : Vec2d gaze_angle ( 0 , 0 ) ;
// Detect eye gazes
if ( face_models [ model ] . detection_success & & face_model . eye_model )
{
GazeAnalysis : : EstimateGaze ( face_models [ model ] , gaze_direction0 , sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy , true ) ;
GazeAnalysis : : EstimateGaze ( face_models [ model ] , gaze_direction1 , sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy , false ) ;
gaze_angle = GazeAnalysis : : GetGazeAngle ( gaze_direction0 , gaze_direction1 ) ;
}
// Face analysis step
cv : : Mat sim_warped_img ;
cv : : Mat_ < double > hog_descriptor ; int num_hog_rows = 0 , num_hog_cols = 0 ;
// Perform AU detection and HOG feature extraction, as this can be expensive only compute it if needed by output or visualization
if ( recording_params . outputAlignedFaces ( ) | | recording_params . outputHOG ( ) | | recording_params . outputAUs ( ) | | visualizer . vis_align | | visualizer . vis_hog )
{
face_analyser . PredictStaticAUsAndComputeFeatures ( captured_image , face_models [ model ] . detected_landmarks ) ;
face_analyser . GetLatestAlignedFace ( sim_warped_img ) ;
face_analyser . GetLatestHOG ( hog_descriptor , num_hog_rows , num_hog_cols ) ;
}
2018-05-01 23:00:16 +02:00
cv : : Vec6d head_pose = LandmarkDetector : : GetPose ( face_models [ model ] , sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy ) ;
2018-02-23 09:25:19 +01:00
// Visualize the features
visualizer . SetObservationFaceAlign ( sim_warped_img ) ;
visualizer . SetObservationHOG ( hog_descriptor , num_hog_rows , num_hog_cols ) ;
2018-01-19 17:17:22 +01:00
visualizer . SetObservationLandmarks ( face_models [ model ] . detected_landmarks , face_models [ model ] . detection_certainty ) ;
2018-05-01 23:00:16 +02:00
visualizer . SetObservationPose ( head_pose , face_models [ model ] . detection_certainty ) ;
2018-02-23 09:25:19 +01:00
visualizer . SetObservationGaze ( gaze_direction0 , gaze_direction1 , LandmarkDetector : : CalculateAllEyeLandmarks ( face_models [ model ] ) , LandmarkDetector : : Calculate3DEyeLandmarks ( face_models [ model ] , sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy ) , face_models [ model ] . detection_certainty ) ;
2018-03-26 09:26:48 +02:00
visualizer . SetObservationActionUnits ( face_analyser . GetCurrentAUsReg ( ) , face_analyser . GetCurrentAUsClass ( ) ) ;
2018-02-23 09:25:19 +01:00
2018-05-01 23:00:16 +02:00
if ( face_models [ model ] . detection_success & & face_model . eye_model ) {
if ( jsonFaceId > 0 ) {
jsonOutput < < " , " ;
}
jsonFaceId + + ;
2018-05-02 14:24:43 +02:00
jsonOutput < < " { \" fid \" : " ;
jsonOutput < < model < < " , \" confidence \" : " < < face_models [ model ] . detection_certainty ;
// gaze_angle_x, gaze_angle_y Eye gaze direction in radians in world coordinates averaged for both eyes and converted into more easy to use format than gaze vectors. If a person is looking left-right this will results in the change of gaze_angle_x and, if a person is looking up-down this will result in change of gaze_angle_y, if a person is looking straight ahead both of the angles will be close to 0 (within measurement error)
jsonOutput < < " , \" gaze_angle \" : [ " < < gaze_angle [ 0 ] < < " , " < < gaze_angle [ 1 ] < < " ] " ;
jsonOutput < < " , \" head_pos \" : [ " < < head_pose [ 0 ] < < " , " < < head_pose [ 1 ] < < " , " < < head_pose [ 2 ] < < " ] " ;
jsonOutput < < " , \" head_rot \" : [ " < < head_pose [ 3 ] < < " , " < < head_pose [ 4 ] < < " , " < < head_pose [ 5 ] < < " ] " ;
jsonOutput < < " } " ;
2018-05-01 23:00:16 +02:00
}
2018-02-23 09:25:19 +01:00
// Output features
open_face_rec . SetObservationHOG ( face_models [ model ] . detection_success , hog_descriptor , num_hog_rows , num_hog_cols , 31 ) ; // The number of channels in HOG is fixed at the moment, as using FHOG
open_face_rec . SetObservationVisualization ( visualizer . GetVisImage ( ) ) ;
open_face_rec . SetObservationActionUnits ( face_analyser . GetCurrentAUsReg ( ) , face_analyser . GetCurrentAUsClass ( ) ) ;
open_face_rec . SetObservationLandmarks ( face_models [ model ] . detected_landmarks , face_models [ model ] . GetShape ( sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy ) ,
face_models [ model ] . params_global , face_models [ model ] . params_local , face_models [ model ] . detection_certainty , face_models [ model ] . detection_success ) ;
open_face_rec . SetObservationPose ( pose_estimate ) ;
open_face_rec . SetObservationGaze ( gaze_direction0 , gaze_direction1 , gaze_angle , LandmarkDetector : : CalculateAllEyeLandmarks ( face_models [ model ] ) , LandmarkDetector : : Calculate3DEyeLandmarks ( face_models [ model ] , sequence_reader . fx , sequence_reader . fy , sequence_reader . cx , sequence_reader . cy ) ) ;
open_face_rec . SetObservationFaceAlign ( sim_warped_img ) ;
open_face_rec . SetObservationFaceID ( model ) ;
open_face_rec . SetObservationTimestamp ( sequence_reader . time_stamp ) ;
open_face_rec . SetObservationFrameNumber ( sequence_reader . GetFrameNumber ( ) ) ;
open_face_rec . WriteObservation ( ) ;
2016-04-28 21:40:36 +02:00
}
}
2017-11-22 18:37:26 +01:00
visualizer . SetFps ( fps_tracker . GetFPS ( ) ) ;
2018-05-01 23:00:16 +02:00
jsonOutput < < " ] " ;
std : : cout < < jsonOutput . str ( ) < < std : : endl ;
2017-11-22 18:53:15 +01:00
// show visualization and detect key presses
char character_press = visualizer . ShowObservation ( ) ;
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
// restart the trackers
if ( character_press = = ' r ' )
{
2017-11-13 18:48:17 +01:00
for ( size_t i = 0 ; i < face_models . size ( ) ; + + i )
2016-04-28 21:40:36 +02:00
{
2017-11-13 18:48:17 +01:00
face_models [ i ] . Reset ( ) ;
2016-04-28 21:40:36 +02:00
active_models [ i ] = false ;
}
}
// quit the application
else if ( character_press = = ' q ' )
{
2017-11-22 18:53:15 +01:00
return 0 ;
2016-04-28 21:40:36 +02:00
}
// Update the frame count
frame_count + + ;
2017-11-13 18:48:17 +01:00
// Grabbing the next frame in the sequence
captured_image = sequence_reader . GetNextFrame ( ) ;
2016-04-28 21:40:36 +02:00
}
2018-05-01 23:00:16 +02:00
2016-04-28 21:40:36 +02:00
frame_count = 0 ;
// Reset the model, for the next video
2017-11-13 18:48:17 +01:00
for ( size_t model = 0 ; model < face_models . size ( ) ; + + model )
2016-04-28 21:40:36 +02:00
{
2017-11-13 18:48:17 +01:00
face_models [ model ] . Reset ( ) ;
2016-04-28 21:40:36 +02:00
active_models [ model ] = false ;
}
2017-11-22 18:53:15 +01:00
sequence_number + + ;
2016-04-28 21:40:36 +02:00
}
return 0 ;
}