From 8afec3dbd4d9bad9da50d3bce082ebed7cdc8c22 Mon Sep 17 00:00:00 2001 From: Brian Matherly Date: Tue, 14 Jan 2014 07:44:34 -0600 Subject: [PATCH] Updates to vid.stab module. * Clean up serialization/deserialization * results are not published until the analysis step is complete * results are stored in "results" property * Misc changes for MLT conventions and consistency --- src/modules/vid.stab/filter_vidstab.cpp | 495 +++++++++++++----------- 1 file changed, 266 insertions(+), 229 deletions(-) diff --git a/src/modules/vid.stab/filter_vidstab.cpp b/src/modules/vid.stab/filter_vidstab.cpp index 32e06679..4b4fe46f 100644 --- a/src/modules/vid.stab/filter_vidstab.cpp +++ b/src/modules/vid.stab/filter_vidstab.cpp @@ -30,142 +30,154 @@ extern "C" #include #include "common.h" - -typedef struct _stab_data +typedef struct { VSMotionDetect md; - mlt_animation animation; -} StabData; + VSManyLocalMotions mlms; +} vs_analyze; typedef struct { VSTransformData td; VSTransformations trans; -} TransformData; - +} vs_apply; -char* vectors_serializer(mlt_animation animation, int length) +typedef struct { - return mlt_animation_serialize(animation); -} + vs_analyze* analyze_data; + vs_apply* apply_data; +} vs_data; -char* lm_serializer(LocalMotions *lms, int length) -{ - std::ostringstream oss; - int size = vs_vector_size(lms); - for (int i = 0; i < size; ++i) - { - LocalMotion* lm = (LocalMotion*) vs_vector_get(lms, i); - oss << lm->v.x << ' '; - oss << lm->v.y << ' '; - oss << lm->f.x << ' '; - oss << lm->f.y << ' '; - oss << lm->f.size << ' '; - oss << lm->contrast << ' '; - oss << lm->match << ' '; - } - return strdup(oss.str().c_str()); -} +/** Free all data used by a VSManyLocalMotions instance. + */ -int lm_deserialize(LocalMotions *lms, mlt_property property) +static void free_manylocalmotions( VSManyLocalMotions* mlms ) { - std::istringstream iss(mlt_property_get_string(property)); - vs_vector_init(lms, 0); - - while (iss.good()) + for( int i = 0; i < vs_vector_size( mlms ); i++ ) { - LocalMotion lm; - iss >> lm.v.x >> lm.v.y >> lm.f.x >> lm.f.y >> lm.f.size >> lm.contrast >> lm.match; - if (iss.fail()) - { - break; - } - vs_vector_append_dup(lms, &lm, sizeof(lm)); + LocalMotions* lms = (LocalMotions*)vs_vector_get( mlms, i ); + vs_vector_del( lms ); } - return 0; + vs_vector_del( mlms ); } -void lm_destructor(void *lms) -{ - vs_vector_del(static_cast(lms)); -} +/** Serialize a VSManyLocalMotions instance and store it in the properties. + * + * Each LocalMotions instance is converted to a string and stored in an animation. + * Then, the entire animation is serialized and stored in the properties. + * \param properties the filter properties + * \param mlms an initialized VSManyLocalMotions instance + */ -static void serialize_localmotions(StabData* data, LocalMotions &vectors, mlt_position pos) +static void publish_manylocalmotions( mlt_properties properties, VSManyLocalMotions* mlms ) { + mlt_animation animation = mlt_animation_new(); mlt_animation_item_s item; - // Initialize animation item + // Initialize animation item. item.is_key = 1; - item.frame = data->md.frameNum; item.keyframe_type = mlt_keyframe_discrete; item.property = mlt_property_init(); - mlt_property_set_data(item.property, &vectors, 1, lm_destructor, (mlt_serialiser) lm_serializer); - mlt_animation_insert(data->animation, &item); - mlt_property_close(item.property); + // Convert each LocalMotions instance to a string and add it to the animation. + for( int i = 0; i < vs_vector_size( mlms ); i++ ) + { + LocalMotions* lms = (LocalMotions*)vs_vector_get( mlms, i ); + item.frame = i; + int size = vs_vector_size( lms ); + + std::ostringstream oss; + for ( int j = 0; j < size; ++j ) + { + LocalMotion* lm = (LocalMotion*)vs_vector_get( lms, j ); + oss << lm->v.x << ' '; + oss << lm->v.y << ' '; + oss << lm->f.x << ' '; + oss << lm->f.y << ' '; + oss << lm->f.size << ' '; + oss << lm->contrast << ' '; + oss << lm->match << ' '; + } + mlt_property_set_string( item.property, oss.str().c_str() ); + mlt_animation_insert( animation, &item ); + } + + // Serialize and store the animation. + char* motion_str = mlt_animation_serialize( animation ); + mlt_properties_set( properties, "results", motion_str ); + + mlt_property_close( item.property ); + mlt_animation_close( animation ); + free( motion_str ); } -int vectors_deserialize(mlt_animation anim, VSManyLocalMotions *mlms) +/** Get the motions data from the properties and convert it to a VSManyLocalMotions. + * + * Each LocalMotions instance is converted to a string and stored in an animation. + * Then, the entire animation is serialized and stored in the properties. + * \param properties the filter properties + * \param mlms an initialized (but empty) VSManyLocalMotions instance + */ + +static void read_manylocalmotions( mlt_properties properties, VSManyLocalMotions* mlms ) { - int error = 0; mlt_animation_item_s item; item.property = mlt_property_init(); + mlt_animation animation = mlt_animation_new(); + // Get the results property which represents the VSManyLocalMotions + char* motion_str = mlt_properties_get( properties, "results" ); - vs_vector_init(mlms, 1024); // initial number of frames, but it will be increased + mlt_animation_parse( animation, motion_str, 0, 0, NULL ); - int length = mlt_animation_get_length(anim); - for (int i = 0; i < length; ++i) + int length = mlt_animation_get_length( animation ); + + for ( int i = 0; i <= length; ++i ) { LocalMotions lms; + vs_vector_init( &lms, 0 ); + + // Get the animation item that represents the LocalMotions + mlt_animation_get_item( animation, &item, i ); - // read lms - mlt_animation_get_item(anim, &item, i + 1); - if ((error = lm_deserialize(&lms, item.property))) + // Convert the property to a real LocalMotions + std::istringstream iss( mlt_property_get_string( item.property ) ); + while ( iss.good() ) { - break; + LocalMotion lm; + iss >> lm.v.x >> lm.v.y >> lm.f.x >> lm.f.y >> lm.f.size >> lm.contrast >> lm.match; + if ( !iss.fail() ) + { + vs_vector_append_dup( &lms, &lm, sizeof(lm) ); + } } - vs_vector_set_dup(mlms, i, &lms, sizeof(LocalMotions)); + // Add the LocalMotions to the ManyLocalMotions + vs_vector_set_dup( mlms, i, &lms, sizeof(LocalMotions) ); } - mlt_property_close(item.property); - return error; -} - -void destroy_transforms(TransformData *data) -{ - if (data) - { - vsTransformDataCleanup(&data->td); - vsTransformationsCleanup(&data->trans); - delete data; - } + mlt_property_close( item.property ); + mlt_animation_close( animation ); } -TransformData* initialize_transforms(int *width, int *height, mlt_image_format *format, - mlt_properties properties, const char* interps) +static vs_apply* init_apply_data( mlt_filter filter, mlt_frame frame, int width, int height, mlt_image_format format ) { - TransformData *data = new TransformData; - memset(data, 0, sizeof(TransformData)); - - VSPixelFormat pf = convertImageFormat(*format); - VSFrameInfo fi_src, fi_dst; - vsFrameInfoInit(&fi_src, *width, *height, pf); - vsFrameInfoInit(&fi_dst, *width, *height, pf); - - const char* filterName = mlt_properties_get(properties, "mlt_service"); - - VSTransformConfig conf = vsTransformGetDefaultConfig(filterName); - conf.smoothing = mlt_properties_get_int(properties, "smoothing"); - conf.maxShift = mlt_properties_get_int(properties, "maxshift"); - conf.maxAngle = mlt_properties_get_double(properties, "maxangle"); - conf.crop = (VSBorderType) mlt_properties_get_int(properties, "crop"); - conf.zoom = mlt_properties_get_int(properties, "zoom"); - conf.optZoom = mlt_properties_get_int(properties, "optzoom"); - conf.zoomSpeed = mlt_properties_get_double(properties, "zoomspeed"); - conf.relative = mlt_properties_get_int(properties, "relative"); - conf.invert = mlt_properties_get_int(properties, "invert"); - if (mlt_properties_get_int(properties, "tripod") != 0) + mlt_properties properties = MLT_FILTER_PROPERTIES( filter ); + vs_apply* apply_data = (vs_apply*)calloc( 1, sizeof(vs_apply) ); + memset( apply_data, 0, sizeof( vs_apply ) ); + VSPixelFormat pf = convertImageFormat( format ); + + const char* filterName = mlt_properties_get( properties, "mlt_service" ); + VSTransformConfig conf = vsTransformGetDefaultConfig( filterName ); + conf.smoothing = mlt_properties_get_int( properties, "smoothing" ); + conf.maxShift = mlt_properties_get_int( properties, "maxshift" ); + conf.maxAngle = mlt_properties_get_double( properties, "maxangle" ); + conf.crop = (VSBorderType)mlt_properties_get_int( properties, "crop" ); + conf.zoom = mlt_properties_get_int( properties, "zoom" ); + conf.optZoom = mlt_properties_get_int( properties, "optzoom" ); + conf.zoomSpeed = mlt_properties_get_double( properties, "zoomspeed" ); + conf.relative = mlt_properties_get_int( properties, "relative" ); + conf.invert = mlt_properties_get_int( properties, "invert" ); + if ( mlt_properties_get_int( properties, "tripod" ) != 0 ) { // Virtual tripod mode: relative=False, smoothing=0 conf.relative = 0; @@ -173,216 +185,228 @@ TransformData* initialize_transforms(int *width, int *height, mlt_image_format * } // by default a bilinear interpolation is selected + const char *interps = mlt_properties_get( MLT_FRAME_PROPERTIES( frame ), "rescale.interp" ); conf.interpolType = VS_BiLinear; if (strcmp(interps, "nearest") == 0 || strcmp(interps, "neighbor") == 0) conf.interpolType = VS_Zero; else if (strcmp(interps, "tiles") == 0 || strcmp(interps, "fast_bilinear") == 0) conf.interpolType = VS_Linear; - vsTransformDataInit(&data->td, &conf, &fi_src, &fi_dst); - vsTransformationsInit(&data->trans); + // load motions + VSManyLocalMotions mlms; + vs_vector_init( &mlms, mlt_filter_get_length2( filter, frame ) ); + read_manylocalmotions( properties, &mlms ); + + // Convert motions to VSTransformations + VSTransformData* td = &apply_data->td; + VSTransformations* trans = &apply_data->trans; + VSFrameInfo fi_src, fi_dst; + vsFrameInfoInit( &fi_src, width, height, pf ); + vsFrameInfoInit( &fi_dst, width, height, pf ); + vsTransformDataInit( td, &conf, &fi_src, &fi_dst ); + vsTransformationsInit( trans ); + vsLocalmotions2Transforms( td, &mlms, trans ); + vsPreprocessTransforms( td, trans ); - // load transformations - mlt_animation animation = mlt_animation_new(); - char* strAnim = mlt_properties_get(properties, "vectors"); - if (mlt_animation_parse(animation, strAnim, 0, 0, NULL)) - { - mlt_log_warning(NULL, "parse failed\n"); - mlt_animation_close(animation); - destroy_transforms(data); - return NULL; - } + free_manylocalmotions( &mlms ); - VSManyLocalMotions mlms; - if (vectors_deserialize(animation, &mlms)) + return apply_data; +} + +static void destory_apply_data( vs_apply* apply_data ) +{ + if ( apply_data ) { - mlt_animation_close(animation); - destroy_transforms(data); - return NULL; + vsTransformDataCleanup( &apply_data->td ); + vsTransformationsCleanup( &apply_data->trans ); + free( apply_data ); } +} - mlt_animation_close(animation); +static vs_analyze* init_analyze_data( mlt_filter filter, mlt_frame frame, mlt_image_format format, int width, int height ) +{ + mlt_properties properties = MLT_FILTER_PROPERTIES( filter ); + vs_analyze* analyze_data = (vs_analyze*)calloc( 1, sizeof(vs_analyze) ); + memset( analyze_data, 0, sizeof(vs_analyze) ); + + // Initialize the VSManyLocalMotions vector where motion data will be + // stored for each frame. + vs_vector_init( &analyze_data->mlms, mlt_filter_get_length2( filter, frame ) ); - vsLocalmotions2Transforms(&data->td, &mlms, &data->trans); - vsPreprocessTransforms(&data->td, &data->trans); - return data; + // Initialize a VSFrameInfo to be used below + VSPixelFormat pf = convertImageFormat( format ); + VSFrameInfo fi; + vsFrameInfoInit( &fi, width, height, pf ); + + // Initialize a VSMotionDetect + const char* filterName = mlt_properties_get( properties, "mlt_service" ); + VSMotionDetectConfig conf = vsMotionDetectGetDefaultConfig( filterName ); + conf.shakiness = mlt_properties_get_int( properties, "shakiness" ); + conf.accuracy = mlt_properties_get_int( properties, "accuracy" ); + conf.stepSize = mlt_properties_get_int( properties, "stepsize" ); + conf.algo = mlt_properties_get_int( properties, "algo" ); + conf.contrastThreshold = mlt_properties_get_double( properties, "mincontrast" ); + conf.show = mlt_properties_get_int( properties, "show" ); + conf.virtualTripod = mlt_properties_get_int( properties, "tripod" ); + vsMotionDetectInit( &analyze_data->md, &conf, &fi ); + + return analyze_data; } -int get_image_and_transform(mlt_frame frame, uint8_t **image, mlt_image_format *format, int *width, int *height, int writable) +void destory_analyze_data( vs_analyze* analyze_data ) +{ + if ( analyze_data ) + { + vsMotionDetectionCleanup( &analyze_data->md ); + free_manylocalmotions( &analyze_data->mlms ); + free( analyze_data ); + } +} + +static int get_image_and_apply( mlt_filter filter, mlt_frame frame, uint8_t **image, mlt_image_format *format, int *width, int *height, int writable ) { int error = 0; - mlt_filter filter = (mlt_filter) mlt_frame_pop_service(frame); - mlt_properties properties = MLT_FILTER_PROPERTIES(filter); + mlt_properties properties = MLT_FILTER_PROPERTIES( filter ); + vs_data* data = (vs_data*)filter->child; *format = mlt_image_yuv420p; - error = mlt_frame_get_image(frame, image, format, width, height, 1); - if (!error) + error = mlt_frame_get_image( frame, image, format, width, height, 1 ); + if ( !error ) { - // Service locks are for concurrency control - mlt_service_lock(MLT_FILTER_SERVICE(filter)); - - TransformData *data = static_cast(mlt_properties_get_data(properties, "_transform_data", NULL)); + mlt_service_lock( MLT_FILTER_SERVICE( filter ) ); // Handle signal from app to re-init data - if (mlt_properties_get_int(properties, "refresh")) + if ( mlt_properties_get_int(properties, "refresh") ) { mlt_properties_set(properties, "refresh", NULL); - destroy_transforms(data); - data = NULL; + destory_apply_data( data->apply_data ); + data->apply_data = NULL; } - if (!data) + // Init transform data if necessary (first time) + if ( !data->apply_data ) { - const char *interps = mlt_properties_get(MLT_FRAME_PROPERTIES(frame), "rescale.interp"); - data = initialize_transforms(width, height, format, properties, interps); - if(!data) { - mlt_service_unlock(MLT_FILTER_SERVICE(filter)); - return 1; // return error code - } - mlt_properties_set_data(properties, "_transform_data", data, 0, (mlt_destructor) destroy_transforms, NULL); + data->apply_data = init_apply_data( filter, frame, *width, *height, *format ); } - VSTransformData* td = &data->td; + // Apply transformations to this image + VSTransformData* td = &data->apply_data->td; + VSTransformations* trans = &data->apply_data->trans; VSFrame vsFrame; - vsFrameFillFromBuffer(&vsFrame, *image, vsTransformGetSrcFrameInfo(td)); - - // transform frame - data->trans.current = mlt_filter_get_position(filter, frame); - vsTransformPrepare(td, &vsFrame, &vsFrame); - VSTransform t = vsGetNextTransform(td, &data->trans); - vsDoTransform(td, t); - vsTransformFinish(td); - - mlt_service_unlock(MLT_FILTER_SERVICE(filter)); + vsFrameFillFromBuffer( &vsFrame, *image, vsTransformGetSrcFrameInfo( td ) ); + trans->current = mlt_filter_get_position( filter, frame ); + vsTransformPrepare( td, &vsFrame, &vsFrame ); + VSTransform t = vsGetNextTransform( td, trans ); + vsDoTransform( td, t ); + vsTransformFinish( td ); + + mlt_service_unlock( MLT_FILTER_SERVICE( filter ) ); } return error; } -static StabData* init_detect(mlt_properties properties, mlt_image_format *format, int *width, int *height) -{ - StabData *data = new StabData; - memset(data, 0, sizeof(StabData)); - data->animation = mlt_animation_new(); - - VSPixelFormat pf = convertImageFormat(*format); - VSFrameInfo fi; - vsFrameInfoInit(&fi, *width, *height, pf); - - const char* filterName = mlt_properties_get(properties, "mlt_service"); - - VSMotionDetectConfig conf = vsMotionDetectGetDefaultConfig(filterName); - conf.shakiness = mlt_properties_get_int(properties, "shakiness"); - conf.accuracy = mlt_properties_get_int(properties, "accuracy"); - conf.stepSize = mlt_properties_get_int(properties, "stepsize"); - conf.algo = mlt_properties_get_int(properties, "algo"); - conf.contrastThreshold = mlt_properties_get_double(properties, "mincontrast"); - conf.show = mlt_properties_get_int(properties, "show"); - conf.virtualTripod = mlt_properties_get_int(properties, "tripod"); - vsMotionDetectInit(&data->md, &conf, &fi); - - // add vectors to properties - mlt_properties_set_data(properties, "vectors", data->animation, 1, (mlt_destructor) mlt_animation_close, - (mlt_serialiser) vectors_serializer); - return data; -} -void destroy_detect(StabData *data) +static int get_image_and_analyze( mlt_filter filter, mlt_frame frame, uint8_t **image, mlt_image_format *format, int *width, int *height, int writable ) { - if (data) - { - vsMotionDetectionCleanup(&data->md); - delete data; - } -} - -int get_image_and_detect(mlt_frame frame, uint8_t **image, mlt_image_format *format, int *width, int *height, int writable) -{ - mlt_filter filter = (mlt_filter) mlt_frame_pop_service(frame); - mlt_properties properties = MLT_FILTER_PROPERTIES(filter); + mlt_properties properties = MLT_FILTER_PROPERTIES( filter ); + vs_data* data = (vs_data*)filter->child; + mlt_position pos = mlt_filter_get_position( filter, frame ); *format = mlt_image_yuv420p; - writable = writable || mlt_properties_get_int(properties, "show") ? 1 : 0; + writable = writable || mlt_properties_get_int( properties, "show" ) ? 1 : 0; - int error = mlt_frame_get_image(frame, image, format, width, height, writable); - if (!error) + int error = mlt_frame_get_image( frame, image, format, width, height, writable ); + if ( !error ) { // Service locks are for concurrency control - mlt_service_lock(MLT_FILTER_SERVICE(filter)); + mlt_service_lock( MLT_FILTER_SERVICE(filter) ); - StabData *data = static_cast(mlt_properties_get_data(properties, "_stab_data", NULL)); - if (!data) + if ( !data->analyze_data ) { - data = init_detect(properties, format, width, height); - mlt_properties_set_data(properties, "_stab_data", data, 0, (mlt_destructor) destroy_detect, NULL); + data->analyze_data = init_analyze_data( filter, frame, *format, *width, *height ); } - VSMotionDetect* md = &data->md; + // Initialize the VSFrame to be analyzed. + VSMotionDetect* md = &data->analyze_data->md; LocalMotions localmotions; VSFrame vsFrame; - vsFrameFillFromBuffer(&vsFrame, *image, &md->fi); + vsFrameFillFromBuffer( &vsFrame, *image, &md->fi ); - // detect and save motions - vsMotionDetection(md, &localmotions, &vsFrame); - mlt_position pos = mlt_filter_get_position( filter, frame ); - serialize_localmotions(data, localmotions, pos); + // Detect and save motions. + vsMotionDetection( md, &localmotions, &vsFrame ); + vs_vector_set_dup( &data->analyze_data->mlms, pos, &localmotions, sizeof(LocalMotions) ); - mlt_service_unlock(MLT_FILTER_SERVICE(filter)); - } + // Publish the motions if this is the last frame. + if ( pos + 1 == mlt_filter_get_length2( filter, frame ) ) + { + publish_manylocalmotions( properties, &data->analyze_data->mlms ); + } + mlt_service_unlock( MLT_FILTER_SERVICE(filter) ); + } return error; } -static mlt_frame process_filter(mlt_filter filter, mlt_frame frame) +static int get_image( mlt_frame frame, uint8_t **image, mlt_image_format *format, int *width, int *height, int writable ) { - mlt_properties properties = MLT_FILTER_PROPERTIES(filter); - mlt_get_image vidstab_get_image = (mlt_get_image) mlt_properties_get_data( properties, "_vidstab_get_image", NULL ); + mlt_filter filter = (mlt_filter)mlt_frame_pop_service( frame ); + mlt_properties properties = MLT_FILTER_PROPERTIES( filter ); -#if 1 - mlt_position pos = mlt_filter_get_position(filter, frame); - mlt_position length = mlt_filter_get_length2(filter, frame) - 1; - if(pos >= length) + if( mlt_properties_get( properties, "results" ) ) { - mlt_properties_set_data(properties, "_vidstab_get_image", NULL, 0, NULL, NULL); + return get_image_and_apply( filter, frame, image, format, width, height, writable ); } -#endif - - if(vidstab_get_image == NULL) + else { - if(mlt_properties_get(properties, "vectors") == NULL) - { - // vectors are NULL, so use a detect filter - vidstab_get_image = get_image_and_detect; - } else { - // found vectors, so use a transform filter - vidstab_get_image = get_image_and_transform; - } - - mlt_properties_set_data( properties, "_vidstab_get_image", (void*)vidstab_get_image, 0, NULL, NULL ); + return get_image_and_analyze( filter, frame, image, format, width, height, writable ); } +} - mlt_frame_push_service(frame, filter); - mlt_frame_push_get_image(frame, vidstab_get_image); +static mlt_frame process_filter( mlt_filter filter, mlt_frame frame ) +{ + mlt_frame_push_service( frame, filter ); + mlt_frame_push_get_image( frame, get_image ); return frame; } +static void filter_close( mlt_filter filter ) +{ + vs_data* data = (vs_data*)filter->child; + if ( data ) + { + if ( data->analyze_data ) destory_analyze_data( data->analyze_data ); + if ( data->apply_data ) destory_apply_data( data->apply_data ); + free( data ); + } + filter->close = NULL; + filter->child = NULL; + filter->parent.close = NULL; + mlt_service_close( &filter->parent ); +} + extern "C" { -mlt_filter filter_vidstab_init(mlt_profile profile, mlt_service_type type, - const char *id, char *arg) +mlt_filter filter_vidstab_init( mlt_profile profile, mlt_service_type type, const char *id, char *arg ) { mlt_filter filter = mlt_filter_new(); + vs_data* data = (vs_data*)calloc( 1, sizeof(vs_data) ); - if( filter ) + if ( filter && data ) { + data->analyze_data = NULL; + data->apply_data = NULL; + + filter->close = filter_close; + filter->child = data; filter->process = process_filter; mlt_properties properties = MLT_FILTER_PROPERTIES(filter); - //properties for stabilize + //properties for analyze mlt_properties_set(properties, "shakiness", "4"); mlt_properties_set(properties, "accuracy", "4"); mlt_properties_set(properties, "stepsize", "6"); @@ -391,7 +415,7 @@ mlt_filter filter_vidstab_init(mlt_profile profile, mlt_service_type type, mlt_properties_set(properties, "show", "0"); mlt_properties_set(properties, "tripod", "0"); - // properties for transform + // properties for apply mlt_properties_set(properties, "smoothing", "15"); mlt_properties_set(properties, "maxshift", "-1"); mlt_properties_set(properties, "maxangle", "-1"); @@ -404,7 +428,20 @@ mlt_filter filter_vidstab_init(mlt_profile profile, mlt_service_type type, mlt_properties_set(properties, "vid.stab.version", LIBVIDSTAB_VERSION); } + else + { + if( filter ) + { + mlt_filter_close( filter ); + } + + if( data ) + { + free( data ); + } + filter = NULL; + } return filter; } -- 2.39.2