From c1db7e1148a19b6c55579cdee827636ef00047cc Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 16 May 2023 14:20:07 -0400 Subject: [PATCH 01/51] Add ability to read fields into std::optional --- arrows/core/csv_io.cxx | 111 +++++++++++++++++++++++++----- arrows/core/csv_io.h | 6 +- arrows/core/tests/test_csv_io.cxx | 27 ++++++++ doc/release-notes/master.txt | 4 ++ 4 files changed, 129 insertions(+), 19 deletions(-) diff --git a/arrows/core/csv_io.cxx b/arrows/core/csv_io.cxx index 1b3c3bfba5..e8c6b51f5d 100644 --- a/arrows/core/csv_io.cxx +++ b/arrows/core/csv_io.cxx @@ -338,6 +338,52 @@ struct str_to_int64< true > return std::stoll( std::forward< Args >( args )... ); }; }; +// ---------------------------------------------------------------------------- +template< class T > +struct is_optional : std::false_type +{}; + +// ---------------------------------------------------------------------------- +template< class T > +struct is_optional< std::optional< T > > : std::true_type +{}; + +// ---------------------------------------------------------------------------- +template< class T > +constexpr bool is_optional_v = is_optional< T >::value; + +// ---------------------------------------------------------------------------- +template< class T > +struct decay_optional +{ + using type = T; +}; + +// ---------------------------------------------------------------------------- +template< class T > +struct decay_optional< std::optional< T > > +{ + using type = T; +}; + +// ---------------------------------------------------------------------------- +template< class T > +using decay_optional_t = typename decay_optional< T >::type; + +// ---------------------------------------------------------------------------- +template< class T > +T bad_parse( std::string const& s ) +{ + if constexpr( is_optional_v< T > ) + { + return std::nullopt; + } + else + { + throw csv_reader::parse_error( s, typeid( T ) ); + } +} + } // ---------------------------------------------------------------------------- @@ -374,7 +420,8 @@ ::read() if constexpr( std::is_arithmetic_v< T > || std::is_same_v< T, std::string > || - std::is_same_v< T, csv::skipf_t > ) + std::is_same_v< T, csv::skipf_t > || + is_optional_v< T > ) { if( !m_first_field && m_is.peek() == m_delim ) { @@ -384,6 +431,7 @@ ::read() m_first_field = false; auto are_in_quotes = m_is.peek() == m_quote; + auto const were_in_quotes = are_in_quotes; if( are_in_quotes ) { // Skip opening quote character @@ -501,15 +549,27 @@ ::read() return {}; } - // If all we wanted was a string, just return that + // Get unquoted field text std::string s = ss.str(); - if constexpr( std::is_same_v< T, std::string > ) + + // Check for empty field + if constexpr( is_optional_v< T > ) + { + if( s.empty() && !were_in_quotes ) + { + return std::nullopt; + } + } + using decayed_t = decay_optional_t< T >; + + // If all we wanted was a string, just return that + if constexpr( std::is_same_v< decayed_t, std::string > ) { return s; } // Convert string to boolean - if constexpr( std::is_same_v< T, bool > ) + if constexpr( std::is_same_v< decayed_t, bool > ) { if( s == "0" || s == "false" ) { @@ -520,23 +580,23 @@ ::read() return true; } - throw parse_error( s, typeid( T ) ); + return bad_parse< T >( s ); } // Numbers should not have leading whitespace - if constexpr( std::is_arithmetic_v< T > ) + if constexpr( std::is_arithmetic_v< decayed_t > ) { if( s.empty() || std::isspace( s[ 0 ] ) ) { - throw parse_error( s, typeid( T ) ); + return bad_parse< T >( s ); } } // Convert string to integer - if constexpr( std::is_integral_v< T > ) + if constexpr( std::is_integral_v< decayed_t > ) { // Parse into a 64-bit integer - using parser_t = str_to_int64< std::is_signed_v< T > >; + using parser_t = str_to_int64< std::is_signed_v< decayed_t > >; parser_t parser; typename parser_t::type value = 0; size_t offset = 0; @@ -546,15 +606,15 @@ ::read() } catch( std::exception const& ) { - throw parse_error( s, typeid( T ) ); + return bad_parse< T >( s ); } // Check if reducing to the desired integer size would overflow - if( value < std::numeric_limits< T >::lowest() || - value > std::numeric_limits< T >::max() || + if( value < std::numeric_limits< decayed_t >::lowest() || + value > std::numeric_limits< decayed_t >::max() || !offset || offset != s.size() ) { - throw parse_error( s, typeid( T ) ); + return bad_parse< T >( s ); } // Downcast to correct type @@ -563,14 +623,16 @@ ::read() // Parse to floating point // Didn't use std::is_floating_point_v because we don't handle long double - if constexpr( std::is_same_v< T, float > || std::is_same_v< T, double > ) + if constexpr( + std::is_same_v< decayed_t, float > || + std::is_same_v< decayed_t, double > ) { - T ( *parser )( char const*, char** ) = nullptr; - if constexpr( std::is_same_v< T, float > ) + decayed_t ( *parser )( char const*, char** ) = nullptr; + if constexpr( std::is_same_v< decayed_t, float > ) { parser = &std::strtof; } - if constexpr( std::is_same_v< T, double > ) + if constexpr( std::is_same_v< decayed_t, double > ) { parser = &std::strtod; } @@ -580,7 +642,7 @@ ::read() auto const value = parser( begin, &end ); if( end == begin || end != &*s.end() ) { - throw parse_error( s, typeid( T ) ); + return bad_parse< T >( s ); } return value; } @@ -709,18 +771,31 @@ ::is_at_field() template KWIVER_ALGO_CORE_EXPORT T csv_reader::read< T >(); INSTANTIATE_READ( std::string ) +INSTANTIATE_READ( std::optional< std::string > ) INSTANTIATE_READ( bool ) +INSTANTIATE_READ( std::optional< bool > ) INSTANTIATE_READ( char ) +INSTANTIATE_READ( std::optional< char > ) INSTANTIATE_READ( uint8_t ) +INSTANTIATE_READ( std::optional< uint8_t > ) INSTANTIATE_READ( uint16_t ) +INSTANTIATE_READ( std::optional< uint16_t > ) INSTANTIATE_READ( uint32_t ) +INSTANTIATE_READ( std::optional< uint32_t > ) INSTANTIATE_READ( uint64_t ) +INSTANTIATE_READ( std::optional< uint64_t > ) INSTANTIATE_READ( int8_t ) +INSTANTIATE_READ( std::optional< int8_t > ) INSTANTIATE_READ( int16_t ) +INSTANTIATE_READ( std::optional< int16_t > ) INSTANTIATE_READ( int32_t ) +INSTANTIATE_READ( std::optional< int32_t > ) INSTANTIATE_READ( int64_t ) +INSTANTIATE_READ( std::optional< int64_t > ) INSTANTIATE_READ( float ) +INSTANTIATE_READ( std::optional< float > ) INSTANTIATE_READ( double ) +INSTANTIATE_READ( std::optional< double > ) INSTANTIATE_READ( csv::skipf_t ) INSTANTIATE_READ( csv::endl_t ) INSTANTIATE_READ( csv::comment_t ) diff --git a/arrows/core/csv_io.h b/arrows/core/csv_io.h index 3e33179889..2cda353717 100644 --- a/arrows/core/csv_io.h +++ b/arrows/core/csv_io.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -290,7 +291,10 @@ class KWIVER_ALGO_CORE_EXPORT csv_reader : public csv_io_base /// csv::comment_t to begin reading the contents of a comment as regular /// fields. Otherwise, comments will be silently skipped. Passing \c /// csv::endl_t has the same effect as \c next_line(). Passing \c - /// csv::skipf_t has the same effect as \c skip_field(). + /// csv::skipf_t has the same effect as \c skip_field(). If \p T is a \c + /// std::optional, \c std::nullopt will be returned if the field is empty or + /// if parsing the field fails. Therefore, \c parse_error will not be thrown + /// if \p T is a \c std::optional. /// /// \throws parse_error If parsing from the current field to type \p T fails. /// The current field is skipped if this occurs. A string representation of diff --git a/arrows/core/tests/test_csv_io.cxx b/arrows/core/tests/test_csv_io.cxx index a240efb8ea..d52f807833 100644 --- a/arrows/core/tests/test_csv_io.cxx +++ b/arrows/core/tests/test_csv_io.cxx @@ -377,3 +377,30 @@ TEST ( csv_io, read_comment ) reader.skip_line(); EXPECT_TRUE( reader.is_at_eof() ); } + +// ---------------------------------------------------------------------------- +TEST ( csv_io, read_optional ) +{ + std::stringstream ss{ ",,,,,A,Maybe,1L,-10,five,0,true,\"\",5,7.0" }; + csv_reader reader( ss ); + + EXPECT_EQ( std::nullopt, reader.read< std::optional< char > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< bool > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< std::string > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< uint16_t > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< float > >() ); + + EXPECT_EQ( std::nullopt, reader.read< std::optional< char > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< bool > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< uint16_t > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< uint16_t > >() ); + EXPECT_EQ( std::nullopt, reader.read< std::optional< float > >() ); + + EXPECT_EQ( 0, reader.read< std::optional< char > >() ); + EXPECT_EQ( true, reader.read< std::optional< bool > >() ); + EXPECT_EQ( std::string{}, reader.read< std::optional< std::string > >() ); + EXPECT_EQ( 5, reader.read< std::optional< uint16_t > >() ); + EXPECT_EQ( 7.0f, reader.read< std::optional< float > >() ); + + EXPECT_TRUE( reader.is_at_eol() ); +} diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index af7ea9690d..6c97154332 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -6,3 +6,7 @@ over the previous v1.8.0 release. Updates ------- + +Arrows: Core + +* Implemented the csv_reader reading std::optional fields. From 93db03e967b92371ea3c7062fe565297349652a7 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Wed, 10 May 2023 18:49:36 -0400 Subject: [PATCH 02/51] Better define comparison behavior with NaNs --- arrows/klv/klv_lengthy.cxx | 47 +---------------------- arrows/klv/klv_lengthy.h | 35 +---------------- arrows/klv/klv_util.h | 73 +++++++++++++++++++++++++++++++++--- arrows/klv/klv_value.cxx | 27 +------------ doc/release-notes/master.txt | 4 ++ 5 files changed, 75 insertions(+), 111 deletions(-) diff --git a/arrows/klv/klv_lengthy.cxx b/arrows/klv/klv_lengthy.cxx index c7bac10cd5..d637924baf 100644 --- a/arrows/klv/klv_lengthy.cxx +++ b/arrows/klv/klv_lengthy.cxx @@ -58,52 +58,7 @@ operator<<( std::ostream& os, klv_lengthy< T > const& value ) } // ---------------------------------------------------------------------------- -template < class T > -bool -operator<( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ) -{ - return lhs.value < rhs.value; -} - -// ---------------------------------------------------------------------------- -template < class T > -bool -operator>( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ) -{ - return lhs.value > rhs.value; -} - -// ---------------------------------------------------------------------------- -template < class T > -bool -operator<=( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ) -{ - return lhs.value <= rhs.value; -} - -// ---------------------------------------------------------------------------- -template < class T > -bool -operator>=( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ) -{ - return lhs.value >= rhs.value; -} - -// ---------------------------------------------------------------------------- -template < class T > -bool -operator==( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ) -{ - return lhs.value == rhs.value; -} - -// ---------------------------------------------------------------------------- -template < class T > -bool -operator!=( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ) -{ - return lhs.value != rhs.value; -} +DEFINE_TEMPLATE_CMP( klv_lengthy< T >, &klv_lengthy< T >::value ) // ---------------------------------------------------------------------------- #define KLV_INSTANTIATE( T ) \ diff --git a/arrows/klv/klv_lengthy.h b/arrows/klv/klv_lengthy.h index ac64a333a0..39c66b3409 100644 --- a/arrows/klv/klv_lengthy.h +++ b/arrows/klv/klv_lengthy.h @@ -43,40 +43,7 @@ std::ostream& operator<<( std::ostream& os, klv_lengthy< T > const& value ); // ---------------------------------------------------------------------------- -template < class T > -KWIVER_ALGO_KLV_EXPORT -bool -operator<( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ); - -// ---------------------------------------------------------------------------- -template < class T > -KWIVER_ALGO_KLV_EXPORT -bool -operator>( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ); - -// ---------------------------------------------------------------------------- -template < class T > -KWIVER_ALGO_KLV_EXPORT -bool -operator<=( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ); - -// ---------------------------------------------------------------------------- -template < class T > -KWIVER_ALGO_KLV_EXPORT -bool -operator>=( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ); - -// ---------------------------------------------------------------------------- -template < class T > -KWIVER_ALGO_KLV_EXPORT -bool -operator==( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ); - -// ---------------------------------------------------------------------------- -template < class T > -KWIVER_ALGO_KLV_EXPORT -bool -operator!=( klv_lengthy< T > const& lhs, klv_lengthy< T > const& rhs ); +DECLARE_TEMPLATE_CMP( klv_lengthy< T > ) } // namespace klv diff --git a/arrows/klv/klv_util.h b/arrows/klv/klv_util.h index 72e41e9b3a..55c4fa2eed 100644 --- a/arrows/klv/klv_util.h +++ b/arrows/klv/klv_util.h @@ -85,12 +85,68 @@ operator<<( std::ostream& os, std::set< T > const& value ) return os; } +// ---------------------------------------------------------------------------- +template< class T > +struct wrap_cmp_nan +{ + explicit wrap_cmp_nan( T const& value ) : value{ value } {} + + bool operator<( wrap_cmp_nan< T > const& other ) const + { + if constexpr( std::is_floating_point_v< T > ) + { + return value < other.value || + ( std::isnan( value ) && !std::isnan( other.value ) ); + } + else + { + return value < other.value; + } + } + + bool operator>( wrap_cmp_nan< T > const& other ) const + { + return other < *this; + } + + bool operator<=( wrap_cmp_nan< T > const& other ) const + { + return !( other < *this ); + } + + bool operator>=( wrap_cmp_nan< T > const& other ) const + { + return !( *this < other ); + } + + bool operator==( wrap_cmp_nan< T > const& other ) const + { + if constexpr( std::is_floating_point_v< T > ) + { + return value == other.value || + ( std::isnan( value ) && std::isnan( other.value ) ); + } + else + { + return value == other.value; + } + } + + bool operator!=( wrap_cmp_nan< T > const& other ) const + { + return !( *this == other ); + } + + T const& value; +}; + // ---------------------------------------------------------------------------- template< class T, class... Args > bool struct_lt( T const& lhs, T const& rhs, Args T::*... args ) { - return std::tie( ( lhs.*args )... ) < std::tie( ( rhs.*args )... ); + return std::make_tuple( wrap_cmp_nan( lhs.*args )... ) < + std::make_tuple( wrap_cmp_nan( rhs.*args )... ); } // ---------------------------------------------------------------------------- @@ -98,7 +154,8 @@ template< class T, class... Args > bool struct_gt( T const& lhs, T const& rhs, Args T::*... args ) { - return std::tie( ( lhs.*args )... ) > std::tie( ( rhs.*args )... ); + return std::make_tuple( wrap_cmp_nan( lhs.*args )... ) > + std::make_tuple( wrap_cmp_nan( rhs.*args )... ); } // ---------------------------------------------------------------------------- @@ -106,7 +163,8 @@ template< class T, class... Args > bool struct_le( T const& lhs, T const& rhs, Args T::*... args ) { - return std::tie( ( lhs.*args )... ) <= std::tie( ( rhs.*args )... ); + return std::make_tuple( wrap_cmp_nan( lhs.*args )... ) <= + std::make_tuple( wrap_cmp_nan( rhs.*args )... ); } // ---------------------------------------------------------------------------- @@ -114,7 +172,8 @@ template< class T, class... Args > bool struct_ge( T const& lhs, T const& rhs, Args T::*... args ) { - return std::tie( ( lhs.*args )... ) >= std::tie( ( rhs.*args )... ); + return std::make_tuple( wrap_cmp_nan( lhs.*args )... ) >= + std::make_tuple( wrap_cmp_nan( rhs.*args )... ); } // ---------------------------------------------------------------------------- @@ -122,7 +181,8 @@ template< class T, class... Args > bool struct_eq( T const& lhs, T const& rhs, Args T::*... args ) { - return std::tie( ( lhs.*args )... ) == std::tie( ( rhs.*args )... ); + return std::make_tuple( wrap_cmp_nan( lhs.*args )... ) == + std::make_tuple( wrap_cmp_nan( rhs.*args )... ); } // ---------------------------------------------------------------------------- @@ -130,7 +190,8 @@ template< class T, class... Args > bool struct_ne( T const& lhs, T const& rhs, Args T::*... args ) { - return std::tie( ( lhs.*args )... ) != std::tie( ( rhs.*args )... ); + return std::make_tuple( wrap_cmp_nan( lhs.*args )... ) != + std::make_tuple( wrap_cmp_nan( rhs.*args )... ); } // ---------------------------------------------------------------------------- diff --git a/arrows/klv/klv_value.cxx b/arrows/klv/klv_value.cxx index 64976d9d32..14fdef2e52 100644 --- a/arrows/klv/klv_value.cxx +++ b/arrows/klv/klv_value.cxx @@ -12,6 +12,7 @@ #include "klv_packet.h" #include "klv_series.hpp" #include "klv_set.h" +#include "klv_util.h" #include @@ -21,30 +22,6 @@ namespace arrows { namespace klv { -namespace { - -// ---------------------------------------------------------------------------- -template < class T, - typename std::enable_if< !std::is_floating_point< T >::value, - bool >::type = true > -bool -equal_or_nan( T const& lhs, T const& rhs ) -{ - return lhs == rhs; -} - -// ---------------------------------------------------------------------------- -template < class T, - typename std::enable_if< std::is_floating_point< T >::value, - bool >::type = true > -bool -equal_or_nan( T const& lhs, T const& rhs ) -{ - return lhs == rhs || ( std::isnan( lhs ) && std::isnan( rhs ) ); -} - -} // namespace - // ---------------------------------------------------------------------------- klv_bad_value_cast ::klv_bad_value_cast( std::type_info const& requested_type, @@ -130,7 +107,7 @@ class klv_value::internal_ : public internal_base auto const& rhs_item = dynamic_cast< internal_< T > const& >( rhs ).m_item; // Second, compare values - return equal_or_nan( lhs.m_item, rhs_item ); + return wrap_cmp_nan( lhs.m_item ) == wrap_cmp_nan( rhs_item ); } std::ostream& diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 6c97154332..ed342fe618 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -10,3 +10,7 @@ Updates Arrows: Core * Implemented the csv_reader reading std::optional fields. + +Arrows: KLV + +* Ensured that NaN comparisons happen consistently across all data structures. From 09e4f8bccd369efe3c559fa8e280f2d9a61530c1 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Fri, 2 Jun 2023 15:14:40 -0400 Subject: [PATCH 03/51] Add check for out-of-bounds IMAP --- arrows/klv/klv_read_write.cxx | 9 ++++++++- arrows/klv/klv_read_write.h | 3 ++- doc/release-notes/master.txt | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/arrows/klv/klv_read_write.cxx b/arrows/klv/klv_read_write.cxx index 85c4dead65..6c0bc48c14 100644 --- a/arrows/klv/klv_read_write.cxx +++ b/arrows/klv/klv_read_write.cxx @@ -265,7 +265,14 @@ klv_read_imap( // Return exactly zero if applicable, overriding rounding errors. IMAP // specification considers this important auto const precision = klv_imap_precision( interval, length ); - return ( std::abs( value ) < precision / 2.0 ) ? 0.0 : value; + value = ( std::abs( value ) < precision / 2.0 ) ? 0.0 : value; + + if( !interval.contains( value, true, true ) ) + { + VITAL_THROW( kv::metadata_type_overflow, "value outside IMAP bounds" ); + } + + return value; } // ---------------------------------------------------------------------------- diff --git a/arrows/klv/klv_read_write.h b/arrows/klv/klv_read_write.h index 381fa98c8c..546fd4c88d 100644 --- a/arrows/klv/klv_read_write.h +++ b/arrows/klv/klv_read_write.h @@ -335,7 +335,8 @@ klv_write_float( double value, klv_write_iter_t& data, size_t length ); /// \returns Floating-point number decoded from \p data buffer. /// /// \throws metadata_type_overflow When \p length is greater than the size of a -/// \c uint64_t or the span of \p interval is too large for a \c double to hold. +/// \c uint64_t, or the span of \p interval is too large for a \c double to +/// hold, or the result value would fall outside \p interval. KWIVER_ALGO_KLV_EXPORT double klv_read_imap( diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index ed342fe618..78ea8ce5eb 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -14,3 +14,5 @@ Arrows: Core Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. + +* Throw an exception when reading an out-of-bounds IMAP value. From 0d3b84154693e10f7a39ca02ff4aecd50cd95696 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Wed, 24 May 2023 18:28:17 -0400 Subject: [PATCH 04/51] Consistently catch video open failures in applets --- arrows/core/applets/dump_klv.cxx | 6 ++---- arrows/core/applets/transcode.cxx | 16 +++++++++++++++- arrows/klv/applets/compare_klv.cxx | 15 ++++++++++++++- doc/release-notes/master.txt | 4 ++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/arrows/core/applets/dump_klv.cxx b/arrows/core/applets/dump_klv.cxx index a2bc5aa999..a6e66236d7 100644 --- a/arrows/core/applets/dump_klv.cxx +++ b/arrows/core/applets/dump_klv.cxx @@ -223,14 +223,12 @@ ::run() } catch ( kv::video_exception const& e ) { - std::cerr << "Video Exception-Couldn't open \"" << video_file << "\"" << std::endl - << e.what() << std::endl; + std::cerr << e.what() << std::endl; return EXIT_FAILURE; } catch ( kv::file_not_found_exception const& e ) { - std::cerr << "Couldn't open \"" << video_file << "\"" << std::endl - << e.what() << std::endl; + std::cerr << e.what() << std::endl; return EXIT_FAILURE; } diff --git a/arrows/core/applets/transcode.cxx b/arrows/core/applets/transcode.cxx index 053bffb52a..51d4d237e3 100644 --- a/arrows/core/applets/transcode.cxx +++ b/arrows/core/applets/transcode.cxx @@ -136,7 +136,21 @@ ::run() std::cerr << "Failed to initialize video input." << std::endl; exit( EXIT_FAILURE ); } - input->open( input_filename ); + + try + { + input->open( input_filename ); + } + catch( kv::video_runtime_exception const& e ) + { + std::cerr << e.what() << std::endl; + exit( EXIT_FAILURE ); + } + catch( kv::file_not_found_exception const& e ) + { + std::cerr << e.what() << std::endl; + exit( EXIT_FAILURE ); + } check_input( input, cmd_args ); auto const video_settings = input->implementation_settings(); diff --git a/arrows/klv/applets/compare_klv.cxx b/arrows/klv/applets/compare_klv.cxx index cbb9a052ef..e97796ff02 100644 --- a/arrows/klv/applets/compare_klv.cxx +++ b/arrows/klv/applets/compare_klv.cxx @@ -107,7 +107,20 @@ class video_source : public vital::metadata_istream exit( EXIT_FAILURE ); } - m_video->open( filepath.string() ); + try + { + m_video->open( filepath.string() ); + } + catch( vital::video_runtime_exception const& e ) + { + std::cerr << e.what() << std::endl; + exit( EXIT_FAILURE ); + } + catch( vital::file_not_found_exception const& e ) + { + std::cerr << e.what() << std::endl; + exit( EXIT_FAILURE ); + } m_is.emplace( *m_video ); } diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 78ea8ce5eb..ad3d5b217f 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -11,8 +11,12 @@ Arrows: Core * Implemented the csv_reader reading std::optional fields. +* Made the transcode applet's failure to open a video result in a more graceful exit. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. * Throw an exception when reading an out-of-bounds IMAP value. + +* Made the compare-klv applet's failure to open a video result in a more graceful exit. From bb4c47b67ad2f067545282656824c1c662668da7 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Fri, 2 Jun 2023 16:12:40 -0400 Subject: [PATCH 05/51] Only initialize image writer if we're going to use it --- arrows/core/applets/dump_klv.cxx | 14 +++++++++----- doc/release-notes/master.txt | 2 ++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/arrows/core/applets/dump_klv.cxx b/arrows/core/applets/dump_klv.cxx index a6e66236d7..fe2a12432e 100644 --- a/arrows/core/applets/dump_klv.cxx +++ b/arrows/core/applets/dump_klv.cxx @@ -173,10 +173,13 @@ ::run() "metadata_serializer", config, metadata_serializer_ptr ); kva::metadata_map_io::get_nested_algo_configuration( "metadata_serializer", config, metadata_serializer_ptr ); - kva::image_io::set_nested_algo_configuration( - "image_writer", config, image_writer ); - kva::image_io::get_nested_algo_configuration( - "image_writer", config, image_writer ); + if( cmd_args.count( "frames" ) ) + { + kva::image_io::set_nested_algo_configuration( + "image_writer", config, image_writer ); + kva::image_io::get_nested_algo_configuration( + "image_writer", config, image_writer ); + } // Check to see if we are to dump config if ( cmd_args.count("output") ) @@ -209,7 +212,8 @@ ::run() return EXIT_FAILURE; } - if( !kva::image_io::check_nested_algo_configuration( + if( cmd_args.count( "frames" ) && + !kva::image_io::check_nested_algo_configuration( "image_writer", config ) ) { std::cerr << "Invalid image_writer config" << std::endl; diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index ad3d5b217f..473387e8be 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -20,3 +20,5 @@ Arrows: KLV * Throw an exception when reading an out-of-bounds IMAP value. * Made the compare-klv applet's failure to open a video result in a more graceful exit. + +* Disabled initialization of image writer in dump-klv if no images are to be written. From 259e33aa9a1cf09290480d3f90f532017d53036d Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Fri, 2 Jun 2023 16:26:22 -0400 Subject: [PATCH 06/51] Account for missing frames in compare-klv --- arrows/klv/applets/compare_klv.cxx | 33 ++++++++++++++++-------------- doc/release-notes/master.txt | 2 ++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/arrows/klv/applets/compare_klv.cxx b/arrows/klv/applets/compare_klv.cxx index e97796ff02..9bdf2554bd 100644 --- a/arrows/klv/applets/compare_klv.cxx +++ b/arrows/klv/applets/compare_klv.cxx @@ -674,10 +674,22 @@ ::run() // Loop through frames while( !lhs_is->at_end() || !rhs_is->at_end() ) { + // Determine frame numbers + auto const lhs_frame_number = + lhs_is->at_end() ? INT64_MAX : lhs_is->frame_number(); + auto const rhs_frame_number = + rhs_is->at_end() ? INT64_MAX : rhs_is->frame_number(); + auto const frame_number = std::min( lhs_frame_number, rhs_frame_number ); + d->breadcrumbs.emplace_back( + std::string{} + "frame (" + std::to_string( frame_number ) + ")" ); + // Extract information about this frame's KLV auto const build_data = - []( vital::metadata_istream& is ) -> std::vector< istream_data > { - if( is.at_end() ) + [ frame_number ]( + vital::metadata_istream& is, vital::frame_id_t this_frame_number ) + -> std::vector< istream_data > + { + if( is.at_end() || this_frame_number > frame_number ) { return {}; } @@ -689,17 +701,8 @@ ::run() } return data; }; - auto lhs_data = build_data( *lhs_is ); - auto rhs_data = build_data( *rhs_is ); - - // Determine frame numbers - auto const lhs_frame_number = - lhs_is->at_end() ? 0 : lhs_is->frame_number(); - auto const rhs_frame_number = - rhs_is->at_end() ? 0 : rhs_is->frame_number(); - auto const frame_number = std::max( lhs_frame_number, rhs_frame_number ); - d->breadcrumbs.emplace_back( - std::string{} + "frame (" + std::to_string( frame_number ) + ")" ); + auto lhs_data = build_data( *lhs_is, lhs_frame_number ); + auto rhs_data = build_data( *rhs_is, rhs_frame_number ); // Score each possible pair of packets on their similarity std::multimap< std::vector< size_t >, possible_pair > ranked_pairs; @@ -833,11 +836,11 @@ ::run() // Next frame d->breadcrumbs.pop_back(); - if( !lhs_is->at_end() ) + if( !lhs_is->at_end() && lhs_frame_number == frame_number ) { lhs_is->next_frame(); } - if( !rhs_is->at_end() ) + if( !rhs_is->at_end() && rhs_frame_number == frame_number ) { rhs_is->next_frame(); } diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 473387e8be..86ffe88f9e 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -22,3 +22,5 @@ Arrows: KLV * Made the compare-klv applet's failure to open a video result in a more graceful exit. * Disabled initialization of image writer in dump-klv if no images are to be written. + +* Accounted for missing or extra frames in compare-klv. From 5b88911e784fabf66930b1140554b265634a6871 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 11 Apr 2023 15:40:36 -0400 Subject: [PATCH 07/51] Implement metadata_map_io_csv.load_() --- arrows/core/metadata_map_io_csv.cxx | 377 ++++++++++++++---- .../core/tests/test_metadata_map_io_csv.cxx | 123 ++++-- doc/release-notes/master.txt | 2 + 3 files changed, 391 insertions(+), 111 deletions(-) diff --git a/arrows/core/metadata_map_io_csv.cxx b/arrows/core/metadata_map_io_csv.cxx index 37b96ce7e7..1a280ea0d8 100644 --- a/arrows/core/metadata_map_io_csv.cxx +++ b/arrows/core/metadata_map_io_csv.cxx @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,10 @@ namespace core { class metadata_map_io_csv::priv { public: + std::optional< kv::metadata_value > read_csv_item( + core::csv_reader& csv_is, + kv::vital_metadata_tag tag ); + void write_csv_item( core::csv_writer& csv_os, kv::vital_metadata_tag tag, @@ -57,6 +62,33 @@ class metadata_map_io_csv::priv namespace { +constexpr auto crs = kv::SRID::lat_lon_WGS84; + +// ---------------------------------------------------------------------------- +struct read_visitor { + template< class T > + std::optional< kv::metadata_value > + operator()() const + { + if constexpr( + std::is_same_v< T, kv::geo_point > || + std::is_same_v< T, kv::geo_polygon > ) + { + throw std::logic_error( "Complex type given to csv field reader" ); + } + else + { + if( auto const value = csv_is.read< std::optional< T > >() ) + { + return *value; + } + return std::nullopt; + } + } + + core::csv_reader& csv_is; +}; + // ---------------------------------------------------------------------------- struct write_visitor { template< class T > @@ -94,45 +126,107 @@ get_column_count( std::type_info const& type ) } // ---------------------------------------------------------------------------- -// Get the special name for a particular subvalue, if it exists -std::string const* -get_special_column_name( kv::vital_metadata_tag tag, size_t index ) +struct column_id +{ + kv::vital_metadata_tag tag; + size_t index; + + bool + operator<( column_id const& other ) const + { + return std::tie( tag, index ) < std::tie( other.tag, other.index ); + } + + bool + operator==( column_id const& other ) const + { + return std::tie( tag, index ) == std::tie( other.tag, other.index ); + } +}; + +// ---------------------------------------------------------------------------- +struct special_column_name +{ + column_id id; + std::string name; +}; + +// ---------------------------------------------------------------------------- +std::vector< special_column_name > const& +special_column_names() { - static std::map< kv::vital_metadata_tag, - std::vector< std::string > > const map = { - { kv::VITAL_META_SENSOR_LOCATION, - { "Sensor Geodetic Longitude (EPSG:4326)", - "Sensor Geodetic Latitude (EPSG:4326)", - "Sensor Geodetic Altitude (meters)", } }, - { kv::VITAL_META_FRAME_CENTER, - { "Geodetic Frame Center Longitude (EPSG:4326)", - "Geodetic Frame Center Latitude (EPSG:4326)", - "Geodetic Frame Center Elevation (meters)", } }, - { kv::VITAL_META_TARGET_LOCATION, - { "Target Geodetic Location Longitude (EPSG:4326)", - "Target Geodetic Location Latitude (EPSG:4326)", - "Target Geodetic Location Elevation (meters)", } }, - { kv::VITAL_META_CORNER_POINTS, - { "Upper Left Corner Longitude (EPSG:4326)", - "Upper Left Corner Latitude (EPSG:4326)", - "Upper Right Corner Longitude (EPSG:4326)", - "Upper Right Corner Latitude (EPSG:4326)", - "Lower Right Corner Longitude (EPSG:4326)", - "Lower Right Corner Latitude (EPSG:4326)", - "Lower Left Corner Longitude (EPSG:4326)", - "Lower Left Corner Latitude (EPSG:4326)", } }, + static std::vector< special_column_name > const names = { + { { kv::VITAL_META_SENSOR_LOCATION, 0 }, + "Sensor Geodetic Longitude (EPSG:4326)" }, + { { kv::VITAL_META_SENSOR_LOCATION, 1 }, + "Sensor Geodetic Latitude (EPSG:4326)" }, + { { kv::VITAL_META_SENSOR_LOCATION, 2 }, + "Sensor Geodetic Altitude (meters)" }, + + { { kv::VITAL_META_FRAME_CENTER, 0 }, + "Target Geodetic Location Longitude (EPSG:4326)" }, + { { kv::VITAL_META_FRAME_CENTER, 1 }, + "Target Geodetic Location Latitude (EPSG:4326)" }, + { { kv::VITAL_META_FRAME_CENTER, 2 }, + "Target Geodetic Location Altitude (meters)" }, + + { { kv::VITAL_META_CORNER_POINTS, 0 }, + "Upper Left Corner Longitude (EPSG:4326)" }, + { { kv::VITAL_META_CORNER_POINTS, 1 }, + "Upper Left Corner Latitude (EPSG:4326)" }, + { { kv::VITAL_META_CORNER_POINTS, 2 }, + "Upper Right Corner Longitude (EPSG:4326)" }, + { { kv::VITAL_META_CORNER_POINTS, 3 }, + "Upper Right Corner Latitude (EPSG:4326)" }, + { { kv::VITAL_META_CORNER_POINTS, 4 }, + "Lower Right Corner Longitude (EPSG:4326)" }, + { { kv::VITAL_META_CORNER_POINTS, 5 }, + "Lower Right Corner Latitude (EPSG:4326)" }, + { { kv::VITAL_META_CORNER_POINTS, 6 }, + "Lower Left Corner Longitude (EPSG:4326)" }, + { { kv::VITAL_META_CORNER_POINTS, 7 }, + "Lower Left Corner Latitude (EPSG:4326)" }, }; - auto const it = map.find( tag ); - return ( it != map.end() ) ? &it->second.at( index ) : nullptr; + return names; +} + +// ---------------------------------------------------------------------------- +// Get the special name for a particular subvalue, if it exists. +std::string const* +get_special_column_name( column_id const& id ) +{ + for( auto const& entry : special_column_names() ) + { + if( entry.id == id ) + { + return &entry.name; + } + } + return nullptr; +} + +// ---------------------------------------------------------------------------- +// Get the subvalue for a particular special column name, if it exists. +column_id const* +get_special_column_id( std::string const& name ) +{ + for( auto const& entry : special_column_names() ) + { + if( entry.name == name ) + { + return &entry.id; + } + } + return nullptr; } // ---------------------------------------------------------------------------- // Get the name to be used as the header title for the given subvalue. std::string -get_column_name( kv::vital_metadata_tag tag, size_t index, bool use_enum_name ) +get_column_name( column_id const& id, bool use_enum_name ) { - auto const& traits = kv::tag_traits_by_tag( tag ); + auto const& traits = kv::tag_traits_by_tag( id.tag ); auto const column_count = get_column_count( traits.type() ); std::stringstream ss; if( use_enum_name ) @@ -140,12 +234,12 @@ get_column_name( kv::vital_metadata_tag tag, size_t index, bool use_enum_name ) ss << traits.enum_name(); if( column_count > 1 ) { - ss << '.' << index; + ss << '.' << id.index; } } else { - auto const special_name = get_special_column_name( tag, index ); + auto const special_name = get_special_column_name( id ); if( special_name ) { ss << *special_name; @@ -155,7 +249,7 @@ get_column_name( kv::vital_metadata_tag tag, size_t index, bool use_enum_name ) ss << traits.name(); if( column_count > 1 ) { - ss << '.' << index; + ss << '.' << id.index; } } } @@ -163,6 +257,41 @@ get_column_name( kv::vital_metadata_tag tag, size_t index, bool use_enum_name ) return ss.str(); } +// ---------------------------------------------------------------------------- +// Determine what subvalue is being requested via the given string. +column_id +parse_column_id( std::string const& s ) +{ + if( auto const special_id = get_special_column_id( s ) ) + { + return *special_id; + } + + // Format of s will be: NAME.INDEX or just NAME (index defaults to 0) + // NAME will be either enum_name or regular name of a vital tag + column_id result = { kv::VITAL_META_UNKNOWN, 0 }; + auto const separator_pos = s.rfind( '.' ); + auto name = s; + if( separator_pos != s.npos ) + { + try + { + result.index = std::stoi( s.substr( separator_pos + 1 ) ); + name = s.substr( 0, separator_pos ); + } + catch( std::invalid_argument const& e ) + { + // Maybe there was a period in the name? + } + } + if( ( result.tag = kv::tag_traits_by_enum_name( name ).tag() ) == + kv::VITAL_META_UNKNOWN ) + { + result.tag = kv::tag_traits_by_name( name ).tag(); + } + return result; +} + // ---------------------------------------------------------------------------- struct subvalue_visitor { @@ -182,7 +311,7 @@ kv::metadata_value subvalue_visitor ::operator()< kv::geo_point >( kv::geo_point const& value ) const { - return value.location( kv::SRID::lat_lon_WGS84 )( index ); + return value.location( crs )( index ); } // ---------------------------------------------------------------------------- @@ -191,7 +320,7 @@ kv::metadata_value subvalue_visitor ::operator()< kv::geo_polygon >( kv::geo_polygon const& value ) const { - return value.polygon( kv::SRID::lat_lon_WGS84 ).at( index / 2 )( index % 2 ); + return value.polygon( crs ).at( index / 2 )( index % 2 ); } // ---------------------------------------------------------------------------- @@ -203,50 +332,95 @@ get_subvalue( kv::metadata_value const& value, size_t index ) } // ---------------------------------------------------------------------------- -struct column_id +struct set_subvalue_visitor { - kv::vital_metadata_tag tag; - size_t index; - - bool - operator<( column_id const& other ) const + template< class T > + void + operator()() const { - if( tag < other.tag ) { return true; } - if( tag > other.tag ) { return false; } - return index < other.index; + constexpr auto nan = std::numeric_limits< double >::quiet_NaN(); + if constexpr( std::is_same_v< T, kv::geo_point > ) + { + static T const default_value{ kv::vector_3d{ nan, nan, nan }, crs }; + auto original_value = + metadata.has( column.tag ) + ? metadata.find( column.tag ).get< T >() + : default_value; + auto internal_value = original_value.location( crs ); + internal_value( column.index ) = std::get< double >( value ); + original_value.set_location( internal_value, crs ); + metadata.add( column.tag, original_value ); + } + else if constexpr( std::is_same_v< T, kv::geo_polygon > ) + { + static T const default_value{ + std::vector( 4, kv::vector_2d{ nan, nan } ), crs }; + auto original_value = + metadata.has( column.tag ) + ? metadata.find( column.tag ).get< T >() + : default_value; + auto internal_value = original_value.polygon( crs ).get_vertices(); + internal_value.at( column.index / 2 )( column.index % 2 ) = + std::get< double >( value ); + original_value.set_polygon( internal_value, crs ); + metadata.add( column.tag, original_value ); + } + else + { + metadata.add( column.tag, value ); + } } + + column_id const& column; + kv::metadata_value const& value; + kv::metadata& metadata; }; +} // namespace + // ---------------------------------------------------------------------------- -// Determine what subvalue is being requested via the given string. -column_id -parse_column_id( std::string const& s ) +std::optional< kv::metadata_value > +metadata_map_io_csv::priv +::read_csv_item( core::csv_reader& csv_is, kv::vital_metadata_tag tag ) { - // Format of s will be: NAME.INDEX or just NAME (index defaults to 0) - // NAME will be either enum_name or regular name of a vital tag - column_id result = { kv::VITAL_META_UNKNOWN, 0 }; - auto const separator_pos = s.rfind( '.' ); - auto name = s; - if( separator_pos != s.npos ) + if( tag == kv::VITAL_META_VIDEO_MICROSECONDS ) { - try + auto const maybe_s = csv_is.read< std::optional< std::string > >(); + if( !maybe_s ) { - result.index = std::stoi( s.substr( separator_pos + 1 ) ); - name = s.substr( 0, separator_pos ); + return std::nullopt; } - catch( std::invalid_argument const& e ) + auto const& s = *maybe_s; + + static std::regex const pattern( "(\\d{2}):(\\d{2}):(\\d{2}).(\\d{6})" ); + std::smatch match; + if( !std::regex_match( s, match, pattern ) ) { - // Maybe there was a period in the name? + return std::nullopt; } + uint64_t microseconds = 0; + auto const convert = + [ &match, µseconds ]( size_t index, uint64_t factor ) { + microseconds *= factor; + microseconds += std::stoull( match.str( index ) ); + }; + convert( 1, 0 ); + convert( 2, 60 ); + convert( 3, 60 ); + convert( 4, 1000000 ); + return microseconds; } - if( ( result.tag = kv::tag_traits_by_enum_name( name ).tag() ) == - kv::VITAL_META_UNKNOWN ) + else { - result.tag = kv::tag_traits_by_name( name ).tag(); - } - return result; -} + auto const* type = &kv::tag_traits_by_tag( tag ).type(); + if( *type == typeid( kv::geo_point ) || *type == typeid( kv::geo_polygon ) ) + { + type = &typeid( double ); + } + return kv::visit_metadata_types_return< + std::optional< kv::metadata_value >, read_visitor >( { csv_is }, *type ); + } } // ---------------------------------------------------------------------------- @@ -373,9 +547,75 @@ ::get_configuration() const // ---------------------------------------------------------------------------- kv::metadata_map_sptr metadata_map_io_csv -::load_( VITAL_UNUSED std::istream& fin, std::string const& filename ) const +::load_( std::istream& is, std::string const& filename ) const { - throw kv::file_write_exception( filename, "not implemented" ); + // Check that output file is valid + if( !is ) + { + VITAL_THROW( + kv::invalid_file, filename, "Insufficient permissions or moved file" ); + } + + // Initialize reader + core::csv_reader csv_is{ is }; + + // Parse column names + std::vector< column_id > column_ids{ + { kv::VITAL_META_VIDEO_FRAME_NUMBER, 0 } }; + if( csv_is.read< std::string >() != "Frame ID" ) + { + VITAL_THROW( + kv::invalid_file, filename, "First column must be 'Frame ID'" ); + } + while( !csv_is.is_at_eol() ) + { + auto const name = csv_is.read< std::string >(); + column_ids.emplace_back( parse_column_id( name ) ); + } + + // Parse remaining lines + vital::simple_metadata_map::map_metadata_t result; + while( !csv_is.is_at_eof() ) + { + csv_is.next_line(); + + // Parse each column in turn + std::map< column_id, kv::metadata_value > values; + for( auto const& column : column_ids ) + { + if( auto const value = d_->read_csv_item( csv_is, column.tag ) ) + { + if( !values.emplace( column, *value ).second ) + { + LOG_WARN( logger(), + "Dropping duplicate value for column: " + << get_column_name( column, true ) ); + } + } + } + + // Create an empty metadata packet for this frame + using frame_number_t = + vital::type_of_tag< vital::VITAL_META_VIDEO_FRAME_NUMBER >; + auto const& frame_number_value = + values.at( { vital::VITAL_META_VIDEO_FRAME_NUMBER, 0 } ); + auto const frame_number = std::get< frame_number_t >( frame_number_value ); + auto& metadata = + *result.emplace( frame_number, kv::metadata_vector{} ).first->second + .emplace_back( std::make_shared< kv::metadata >() ); + + // Fill that metadata packet with the values, correctly handling + // multi-column fields + for( auto const& entry : values ) + { + auto const& tag_type = + vital::tag_traits_by_tag( entry.first.tag ).type(); + kv::visit_metadata_types< set_subvalue_visitor >( + { entry.first, entry.second, metadata }, tag_type ); + } + } + + return std::make_shared< kv::simple_metadata_map >( result ); } // ---------------------------------------------------------------------------- @@ -430,7 +670,7 @@ ::save_( std::ostream& os, info.id = parse_column_id( name ); info.name = name_override.empty() - ? get_column_name( info.id.tag, info.id.index, d_->write_enum_names ) + ? get_column_name( info.id, d_->write_enum_names ) : name_override; if( info.id.tag != kv::VITAL_META_UNKNOWN ) @@ -447,8 +687,7 @@ ::save_( std::ostream& os, { for( auto const& id : remaining_ids ) { - auto const name = - get_column_name( id.tag, id.index, d_->write_enum_names ); + auto const name = get_column_name( id, d_->write_enum_names ); infos.push_back( { id, name } ); } } diff --git a/arrows/core/tests/test_metadata_map_io_csv.cxx b/arrows/core/tests/test_metadata_map_io_csv.cxx index 29e43787b9..d86dc972f8 100644 --- a/arrows/core/tests/test_metadata_map_io_csv.cxx +++ b/arrows/core/tests/test_metadata_map_io_csv.cxx @@ -23,52 +23,91 @@ main( int argc, char** argv ) } // ---------------------------------------------------------------------------- -TEST( metadata_map_io_csv, save ) +class metadata_map_csv : public ::testing::Test { - // Test data with one of each type - kv::metadata_map::map_metadata_t map = { - { 4, { std::make_shared< kv::metadata >() } }, - { 7, { std::make_shared< kv::metadata >(), - std::make_shared< kv::metadata >() } } }; - - map[ 4 ][ 0 ]->add< kv::VITAL_META_UNIX_TIMESTAMP >( 1 ); - map[ 4 ][ 0 ]->add< kv::VITAL_META_VIDEO_DATA_STREAM_INDEX >( 1 ); - map[ 4 ][ 0 ]->add< kv::VITAL_META_SENSOR_HORIZONTAL_FOV >( 60.7 ); - map[ 4 ][ 0 ]->add< kv::VITAL_META_PLATFORM_DESIGNATION >( "\"Platform,\"" ); - map[ 4 ][ 0 ]->add< kv::VITAL_META_SENSOR_LOCATION >( - { kv::vector_2d{ 2.0, 3.0 }, kv::SRID::lat_lon_WGS84 } ); - map[ 4 ][ 0 ]->add< kv::VITAL_META_CORNER_POINTS >( - { { kv::vector_2d{ 0.0, 3.0 }, - kv::vector_2d{ 2.0, 3.0 }, - kv::vector_2d{ 2.0, 6.0 }, - kv::vector_2d{ 0.0, 6.0 } }, kv::SRID::lat_lon_WGS84 } ); - - map[ 7 ][ 0 ]->add< kv::VITAL_META_UNIX_TIMESTAMP >( 3 ); - map[ 7 ][ 0 ]->add< kv::VITAL_META_VIDEO_DATA_STREAM_INDEX >( 1 ); - - map[ 7 ][ 1 ]->add< kv::VITAL_META_UNIX_TIMESTAMP >( 5 ); - map[ 7 ][ 1 ]->add< kv::VITAL_META_VIDEO_DATA_STREAM_INDEX >( 2 ); - - // Create CSV writer - metadata_map_io_csv io; +protected: + void SetUp() override + { + // Set write_enum_names so this test still works if the field names change + auto const config = io.get_configuration(); + config->set_value( "write_enum_names", true ); + io.set_configuration( config ); - // Set write_enum_names so this test still works if the field names change - auto const config = io.get_configuration(); - config->set_value( "write_enum_names", true ); - io.set_configuration( config ); + map = { + { 4, { std::make_shared< kv::metadata >() } }, + { 7, { std::make_shared< kv::metadata >(), + std::make_shared< kv::metadata >() } } }; - // Write to CSV + map[ 4 ][ 0 ]->add< kv::VITAL_META_VIDEO_FRAME_NUMBER >( 4 ); + map[ 4 ][ 0 ]->add< kv::VITAL_META_UNIX_TIMESTAMP >( 1 ); + map[ 4 ][ 0 ]->add< kv::VITAL_META_VIDEO_DATA_STREAM_INDEX >( 1 ); + map[ 4 ][ 0 ]->add< kv::VITAL_META_SENSOR_HORIZONTAL_FOV >( 60.7 ); + map[ 4 ][ 0 ]->add< kv::VITAL_META_PLATFORM_DESIGNATION >( "\"Platform,\"" ); + map[ 4 ][ 0 ]->add< kv::VITAL_META_SENSOR_LOCATION >( + { kv::vector_2d{ 2.0, 3.0 }, kv::SRID::lat_lon_WGS84 } ); + map[ 4 ][ 0 ]->add< kv::VITAL_META_CORNER_POINTS >( + { { kv::vector_2d{ 0.0, 3.0 }, + kv::vector_2d{ 2.0, 3.0 }, + kv::vector_2d{ 2.0, 6.0 }, + kv::vector_2d{ 0.0, 6.0 } }, kv::SRID::lat_lon_WGS84 } ); + + map[ 7 ][ 0 ]->add< kv::VITAL_META_VIDEO_FRAME_NUMBER >( 7 ); + map[ 7 ][ 0 ]->add< kv::VITAL_META_UNIX_TIMESTAMP >( 3 ); + map[ 7 ][ 0 ]->add< kv::VITAL_META_VIDEO_MICROSECONDS >( 123456789012 ); + map[ 7 ][ 0 ]->add< kv::VITAL_META_VIDEO_DATA_STREAM_INDEX >( 1 ); + + map[ 7 ][ 1 ]->add< kv::VITAL_META_VIDEO_FRAME_NUMBER >( 7 ); + map[ 7 ][ 1 ]->add< kv::VITAL_META_UNIX_TIMESTAMP >( 5 ); + map[ 7 ][ 1 ]->add< kv::VITAL_META_VIDEO_DATA_STREAM_INDEX >( 2 ); + + example_csv = + "Frame ID,UNIX_TIMESTAMP,PLATFORM_DESIGNATION,VIDEO_DATA_STREAM_INDEX," + "VIDEO_MICROSECONDS," + "SENSOR_LOCATION.0,SENSOR_LOCATION.1,SENSOR_LOCATION.2," + "SENSOR_HORIZONTAL_FOV," + "CORNER_POINTS.0,CORNER_POINTS.1,CORNER_POINTS.2,CORNER_POINTS.3," + "CORNER_POINTS.4,CORNER_POINTS.5,CORNER_POINTS.6,CORNER_POINTS.7\n" + "4,1,\"\"\"Platform,\"\"\",1,,2,3,0,60.7,0,3,2,3,2,6,0,6\n" + "7,3,,1,34:17:36.789012,,,,,,,,,,,,\n" + "7,5,,2,,,,,,,,,,,,,\n"; + } + + metadata_map_io_csv io; + kv::metadata_map::map_metadata_t map; std::stringstream ss; + std::string example_csv; +}; + +// ---------------------------------------------------------------------------- +TEST_F( metadata_map_csv, save ) +{ + // Write to CSV io.save_( ss, std::make_shared< kv::simple_metadata_map >( map ), "" ); - std::string const expected_result = - "Frame ID,UNIX_TIMESTAMP,PLATFORM_DESIGNATION,VIDEO_DATA_STREAM_INDEX," - "SENSOR_LOCATION.0,SENSOR_LOCATION.1,SENSOR_LOCATION.2," - "SENSOR_HORIZONTAL_FOV," - "CORNER_POINTS.0,CORNER_POINTS.1,CORNER_POINTS.2,CORNER_POINTS.3," - "CORNER_POINTS.4,CORNER_POINTS.5,CORNER_POINTS.6,CORNER_POINTS.7\n" - "4,1,\"\"\"Platform,\"\"\",1,2,3,0,60.7,0,3,2,3,2,6,0,6\n" - "7,3,,1,,,,,,,,,,,,\n" - "7,5,,2,,,,,,,,,,,,\n"; - EXPECT_EQ( expected_result, ss.str() ); + EXPECT_EQ( example_csv, ss.str() ); +} + +// ---------------------------------------------------------------------------- +TEST_F( metadata_map_csv, load ) +{ + // Read from CSV + ss.str( example_csv ); + auto const result_map = io.load_( ss, "" )->metadata(); + + auto true_it = map.cbegin(); + auto result_it = result_map.cbegin(); + while( true_it != map.cend() && result_it != result_map.cend() ) + { + EXPECT_EQ( true_it->first, result_it->first ); + EXPECT_TRUE( std::equal( + true_it->second.begin(), true_it->second.end(), + result_it->second.begin(), result_it->second.end(), + []( auto const& lhs, auto const& rhs ){ return *lhs == *rhs; } ) ) + << "Frame " << true_it->first << " not equal"; + ++true_it; + ++result_it; + } + + EXPECT_EQ( true_it, map.cend() ); + EXPECT_EQ( result_it, result_map.cend() ); } diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 86ffe88f9e..7547b1b3e7 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -24,3 +24,5 @@ Arrows: KLV * Disabled initialization of image writer in dump-klv if no images are to be written. * Accounted for missing or extra frames in compare-klv. + +* Implemented metadata_map_io_csv.load_(). From dd32517ad4decaacae142ac977fde2fb2aa8cf72 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Fri, 26 May 2023 09:45:42 -0400 Subject: [PATCH 08/51] Acquire first frame before querying video settings --- arrows/core/applets/transcode.cxx | 8 ++++---- doc/release-notes/master.txt | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/arrows/core/applets/transcode.cxx b/arrows/core/applets/transcode.cxx index 51d4d237e3..16bc9f569b 100644 --- a/arrows/core/applets/transcode.cxx +++ b/arrows/core/applets/transcode.cxx @@ -153,6 +153,9 @@ ::run() } check_input( input, cmd_args ); + // Acquire first frame, which may help produce more accurate video settings + kv::timestamp timestamp; + input->next_frame( timestamp ); auto const video_settings = input->implementation_settings(); // Setup video output @@ -165,10 +168,7 @@ ::run() output->open( output_filename, video_settings.get() ); // Transcode frames - kv::timestamp timestamp; - for( input->next_frame( timestamp ); - !input->end_of_video(); - input->next_frame( timestamp ) ) + for( ; !input->end_of_video(); input->next_frame( timestamp ) ) { // Transcode metadata if( cmd_args.count( "copy-metadata" ) ) diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 7547b1b3e7..e6eb5cd1be 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -13,6 +13,9 @@ Arrows: Core * Made the transcode applet's failure to open a video result in a more graceful exit. +* Made the transcode applet ask for video settings slightly later, when they + might be more accurate. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From bc22c7041bf1fcc639f54d753078cdfb5de7148c Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Thu, 11 May 2023 11:06:16 -0400 Subject: [PATCH 09/51] Make flint/imap not override incorrect lengths --- arrows/klv/klv_data_format.cxx | 20 ++++++++++++++++---- doc/release-notes/master.txt | 3 +++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/arrows/klv/klv_data_format.cxx b/arrows/klv/klv_data_format.cxx index de944467a9..e10cf72151 100644 --- a/arrows/klv/klv_data_format.cxx +++ b/arrows/klv/klv_data_format.cxx @@ -392,7 +392,10 @@ size_t klv_float_format ::length_of_typed( klv_lengthy< double > const& value ) const { - return m_length_constraints.fixed_or( value.length ); + return + value.length + ? value.length + : m_length_constraints.fixed_or( m_length_constraints.suggested() ); } // ---------------------------------------------------------------------------- @@ -449,7 +452,10 @@ size_t klv_sflint_format ::length_of_typed( klv_lengthy< double > const& value ) const { - return m_length_constraints.fixed_or( value.length ); + return + value.length + ? value.length + : m_length_constraints.fixed_or( m_length_constraints.suggested() ); } // ---------------------------------------------------------------------------- @@ -517,7 +523,10 @@ size_t klv_uflint_format ::length_of_typed( klv_lengthy< double > const& value ) const { - return m_length_constraints.fixed_or( value.length ); + return + value.length + ? value.length + : m_length_constraints.fixed_or( m_length_constraints.suggested() ); } // ---------------------------------------------------------------------------- @@ -584,7 +593,10 @@ size_t klv_imap_format ::length_of_typed( klv_lengthy< double > const& value ) const { - return m_length_constraints.fixed_or( value.length ); + return + value.length + ? value.length + : m_length_constraints.fixed_or( m_length_constraints.suggested() ); } // ---------------------------------------------------------------------------- diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index e6eb5cd1be..968c7732f4 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -29,3 +29,6 @@ Arrows: KLV * Accounted for missing or extra frames in compare-klv. * Implemented metadata_map_io_csv.load_(). + +* Modified flint/IMAP behavior to print a warning when writing values with + incorrect lengths instead of correcting the length and possibly losing data. From 9a7f6e6ee45ece0bbad271a5379875bddc949755 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 12 Jun 2023 10:59:17 -0500 Subject: [PATCH 10/51] Enforce dts validity when writing --- arrows/ffmpeg/ffmpeg_video_output.cxx | 28 +++++++++++++++++++++++++-- doc/release-notes/master.txt | 4 ++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index 10763377f7..a1d6ec240e 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -70,6 +70,7 @@ class ffmpeg_video_output::impl codec_context_uptr codec_context; AVCodec const* codec; sws_context_uptr image_conversion_context; + int64_t prev_video_dts; }; impl(); @@ -413,7 +414,8 @@ ::open_video_state( metadata_stream{ nullptr }, codec_context{ nullptr }, codec{ nullptr }, - image_conversion_context{ nullptr } + image_conversion_context{ nullptr }, + prev_video_dts{ AV_NOPTS_VALUE } { // Allocate output format context { @@ -740,8 +742,30 @@ ::add_image( kv::video_raw_image const& image ) dynamic_cast< ffmpeg_video_raw_image const& >( image ); for( auto const& packet : ffmpeg_image.packets ) { + // Ensure this packet has sensible timestamps or FFmpeg will complain + if( packet->pts == AV_NOPTS_VALUE || packet->dts == AV_NOPTS_VALUE || + packet->dts <= prev_video_dts || packet->dts > packet->pts ) + { + LOG_ERROR( + parent->logger, + "Dropping video packet with invalid dts/pts " + << packet->dts << "/" << packet->pts << " " + << "with prev dts " << prev_video_dts ); + continue; + } + + // Copy the packet so we can switch the video stream index + packet_uptr tmp_packet{ + throw_error_null( + av_packet_clone( packet.get() ), "Could not copy video packet" ) }; + tmp_packet->stream_index = video_stream->index; + + // Record this DTS for next time + prev_video_dts = packet->dts; + + // Write the packet throw_error_code( - av_interleaved_write_frame( format_context.get(), packet.get() ), + av_interleaved_write_frame( format_context.get(), tmp_packet.get() ), "Could not write frame to file" ); } ++frame_count; diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 968c7732f4..282d126d5b 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -16,6 +16,10 @@ Arrows: Core * Made the transcode applet ask for video settings slightly later, when they might be more accurate. +Arrows: FFmpeg + +* Added check for incoming raw video packets' timestamps and stream indices. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From fe9e4d77e7305142b4d1409ecfa2a440edd2bfdf Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 20 Jun 2023 10:47:06 -0500 Subject: [PATCH 11/51] Copy input video start timestamp when transcoding --- arrows/ffmpeg/ffmpeg_video_input.cxx | 1 + arrows/ffmpeg/ffmpeg_video_output.cxx | 4 ++++ arrows/ffmpeg/ffmpeg_video_settings.cxx | 14 ++++++++++---- arrows/ffmpeg/ffmpeg_video_settings.h | 1 + doc/release-notes/master.txt | 2 ++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index 1b27c96901..d035bb911c 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -1558,6 +1558,7 @@ ::implementation_settings() const ffmpeg_video_settings_uptr result{ new ffmpeg_video_settings{} }; result->frame_rate = frame_rate(); result->klv_stream_count = klv_streams.size(); + result->start_timestamp = format_context->start_time; if( codec_context ) { diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index a1d6ec240e..25dfbe1909 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -398,6 +398,7 @@ ::implementation_settings() const avcodec_parameters_from_context( result->parameters.get(), d->video->codec_context.get() ); result->klv_stream_count = 0; // TODO + result->start_timestamp = d->video->format_context->start_time; return kwiver::vital::video_settings_uptr{ result }; } @@ -428,6 +429,9 @@ ::open_video_state( } output_format = format_context->oformat; + // Set timestamp value to start at + format_context->output_ts_offset = settings.start_timestamp; + // Prioritization scheme for codecs: // (1) Match ffmpeg settings passed to constructor if present // (2) Match configuration setting if present diff --git a/arrows/ffmpeg/ffmpeg_video_settings.cxx b/arrows/ffmpeg/ffmpeg_video_settings.cxx index a1b450697d..2046f0ef17 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.cxx +++ b/arrows/ffmpeg/ffmpeg_video_settings.cxx @@ -20,7 +20,8 @@ ffmpeg_video_settings ::ffmpeg_video_settings() : frame_rate{ 0, 1 }, parameters{ avcodec_parameters_alloc() }, - klv_stream_count{ 0 } + klv_stream_count{ 0 }, + start_timestamp{ AV_NOPTS_VALUE } { if( !parameters ) { @@ -34,7 +35,8 @@ ffmpeg_video_settings ::ffmpeg_video_settings( ffmpeg_video_settings const& other ) : frame_rate{ other.frame_rate }, parameters{ avcodec_parameters_alloc() }, - klv_stream_count{ other.klv_stream_count } + klv_stream_count{ other.klv_stream_count }, + start_timestamp{ other.start_timestamp } { throw_error_code( avcodec_parameters_copy( parameters.get(), other.parameters.get() ), @@ -46,7 +48,8 @@ ffmpeg_video_settings ::ffmpeg_video_settings( ffmpeg_video_settings&& other ) : frame_rate{ std::move( other.frame_rate ) }, parameters{ std::move( other.parameters ) }, - klv_stream_count{ std::move( other.klv_stream_count ) } + klv_stream_count{ std::move( other.klv_stream_count ) }, + start_timestamp{ other.start_timestamp } {} // ---------------------------------------------------------------------------- @@ -57,7 +60,8 @@ ::ffmpeg_video_settings( size_t klv_stream_count ) : frame_rate( frame_rate ), parameters{ avcodec_parameters_alloc() }, - klv_stream_count{ klv_stream_count } + klv_stream_count{ klv_stream_count }, + start_timestamp{ AV_NOPTS_VALUE } { if( !parameters ) { @@ -84,6 +88,7 @@ ::operator=( ffmpeg_video_settings const& other ) avcodec_parameters_copy( parameters.get(), other.parameters.get() ), "Could not copy codec parameters" ); klv_stream_count = other.klv_stream_count; + start_timestamp = other.start_timestamp; return *this; } @@ -95,6 +100,7 @@ ::operator=( ffmpeg_video_settings&& other ) frame_rate = std::move( other.frame_rate ); parameters = std::move( other.parameters ); klv_stream_count = std::move( other.klv_stream_count ); + start_timestamp = std::move( other.start_timestamp ); return *this; } diff --git a/arrows/ffmpeg/ffmpeg_video_settings.h b/arrows/ffmpeg/ffmpeg_video_settings.h index 2530f323ae..5dc596ed45 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.h +++ b/arrows/ffmpeg/ffmpeg_video_settings.h @@ -47,6 +47,7 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings AVRational frame_rate; codec_parameters_uptr parameters; size_t klv_stream_count; + int64_t start_timestamp; // In AV_TIME_BASE units }; using ffmpeg_video_settings_uptr = std::unique_ptr< ffmpeg_video_settings >; diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 282d126d5b..a442e7f691 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -20,6 +20,8 @@ Arrows: FFmpeg * Added check for incoming raw video packets' timestamps and stream indices. +* Added functionality to copy the input video's start timestamp when transcoding. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From 5b8038ec0ce9eb08fe94832b6f07f53ba552fff5 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Fri, 26 May 2023 09:59:36 -0400 Subject: [PATCH 12/51] Allow writing too-large integers with a warning --- arrows/klv/klv_data_format.cxx | 6 ++++-- doc/release-notes/master.txt | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/arrows/klv/klv_data_format.cxx b/arrows/klv/klv_data_format.cxx index e10cf72151..a320418ca5 100644 --- a/arrows/klv/klv_data_format.cxx +++ b/arrows/klv/klv_data_format.cxx @@ -238,7 +238,8 @@ size_t klv_uint_format ::length_of_typed( uint64_t const& value ) const { - return m_length_constraints.fixed_or( klv_int_length( value ) ); + auto const int_length = klv_int_length( value ); + return std::max( m_length_constraints.fixed_or( 1 ), int_length ); } // ---------------------------------------------------------------------------- @@ -277,7 +278,8 @@ size_t klv_sint_format ::length_of_typed( int64_t const& value ) const { - return m_length_constraints.fixed_or( klv_int_length( value ) ); + auto const int_length = klv_int_length( value ); + return std::max( m_length_constraints.fixed_or( 1 ), int_length ); } // ---------------------------------------------------------------------------- diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 282d126d5b..cf94b0dc99 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -34,5 +34,6 @@ Arrows: KLV * Implemented metadata_map_io_csv.load_(). -* Modified flint/IMAP behavior to print a warning when writing values with - incorrect lengths instead of correcting the length and possibly losing data. +* Modified flint, IMAP, and integer behavior to print a warning when writing + values with incorrect lengths instead of correcting the length and possibly + losing data. From 80e6ec00f80330a18428b67cb5f6800edf7a5b7f Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 30 May 2023 19:12:22 -0400 Subject: [PATCH 13/51] Remove imagery_enabled --- arrows/ffmpeg/ffmpeg_video_input.cxx | 185 ++++++++---------- .../ffmpeg/tests/test_video_input_ffmpeg.cxx | 33 ---- doc/release-notes/master.txt | 2 + 3 files changed, 79 insertions(+), 141 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index d035bb911c..d43ae6ba35 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -357,7 +357,6 @@ class ffmpeg_video_input::priv hardware_device_context_uptr hardware_device_context; - bool imagery_enabled; bool klv_enabled; bool use_misp_timestamps; bool smooth_klv_packets; @@ -393,7 +392,6 @@ ::priv( ffmpeg_video_input& parent ) : parent( parent ), logger{ kv::get_logger( "ffmpeg_video_input" ) }, hardware_device_context{ nullptr }, - imagery_enabled{ true }, klv_enabled{ true }, use_misp_timestamps{ false }, smooth_klv_packets{ false }, @@ -556,11 +554,6 @@ kv::image_container_sptr ffmpeg_video_input::priv::frame_state ::convert_image() { - if( !parent->parent->imagery_enabled ) - { - return nullptr; - } - if( image ) { return image; @@ -844,74 +837,71 @@ ::open_video_state( priv& parent, std::string const& path ) } } - if( parent.imagery_enabled ) - { - // Dig up information about the video's codec - auto const video_params = video_stream->codecpar; - auto const codec_id = video_params->codec_id; - LOG_INFO( - logger, "Video requires codec type: " << pretty_codec_name( codec_id ) ); - - // Codec prioritization scheme: - // (1) Choose hardware over software codecs - auto const codec_cmp = - [ & ]( AVCodec const* lhs, AVCodec const* rhs ) -> bool { - return - std::make_tuple( is_hardware_codec( lhs ) ) > - std::make_tuple( is_hardware_codec( rhs ) ); - }; - std::multiset< - AVCodec const*, std::function< bool( AVCodec const*, AVCodec const* ) > > - possible_codecs{ codec_cmp }; - - // Find all compatible CUDA codecs + // Dig up information about the video's codec + auto const video_params = video_stream->codecpar; + auto const codec_id = video_params->codec_id; + LOG_INFO( + logger, "Video requires codec type: " << pretty_codec_name( codec_id ) ); + + // Codec prioritization scheme: + // (1) Choose hardware over software codecs + auto const codec_cmp = + [ & ]( AVCodec const* lhs, AVCodec const* rhs ) -> bool { + return + std::make_tuple( is_hardware_codec( lhs ) ) > + std::make_tuple( is_hardware_codec( rhs ) ); + }; + std::multiset< + AVCodec const*, std::function< bool( AVCodec const*, AVCodec const* ) > > + possible_codecs{ codec_cmp }; + + // Find all compatible CUDA codecs #ifdef KWIVER_ENABLE_FFMPEG_CUDA - if( parent.cuda_device() ) - { - auto const cuda_codecs = cuda_find_decoders( *video_params ); - possible_codecs.insert( cuda_codecs.begin(), cuda_codecs.end() ); - } + if( parent.cuda_device() ) + { + auto const cuda_codecs = cuda_find_decoders( *video_params ); + possible_codecs.insert( cuda_codecs.begin(), cuda_codecs.end() ); + } #endif - // Find all compatible software codecs - AVCodec const* codec_ptr = nullptr; + // Find all compatible software codecs + AVCodec const* codec_ptr = nullptr; #if LIBAVCODEC_VERSION_MAJOR > 57 - for( void* it = nullptr; ( codec_ptr = av_codec_iterate( &it ) ); ) + for( void* it = nullptr; ( codec_ptr = av_codec_iterate( &it ) ); ) #else - while( ( codec_ptr = av_codec_next( codec_ptr ) ) ) + while( ( codec_ptr = av_codec_next( codec_ptr ) ) ) #endif + { + if( codec_ptr->id == codec_id && + av_codec_is_decoder( codec_ptr ) && + !is_hardware_codec( codec_ptr ) && + !( codec_ptr->capabilities & AV_CODEC_CAP_EXPERIMENTAL ) ) { - if( codec_ptr->id == codec_id && - av_codec_is_decoder( codec_ptr ) && - !is_hardware_codec( codec_ptr ) && - !( codec_ptr->capabilities & AV_CODEC_CAP_EXPERIMENTAL ) ) - { - possible_codecs.emplace( codec_ptr ); - } + possible_codecs.emplace( codec_ptr ); } + } - // Find the first compatible codec that works, in priority order - for( auto const possible_codec : possible_codecs ) + // Find the first compatible codec that works, in priority order + for( auto const possible_codec : possible_codecs ) + { + codec = possible_codec; + if( try_codec() ) { - codec = possible_codec; - if( try_codec() ) - { - break; - } - else - { - codec = nullptr; - } + break; + } + else + { + codec = nullptr; } - - throw_error_null( - codec, - "Could not open video with any known input codec. ", - possible_codecs.size(), " codecs were tried. ", - "Required codec type: ", pretty_codec_name( codec_id ) ); - LOG_INFO( - logger, "Successfully loaded codec: " << pretty_codec_name( codec ) ); } + + throw_error_null( + codec, + "Could not open video with any known input codec. ", + possible_codecs.size(), " codecs were tried. ", + "Required codec type: ", pretty_codec_name( codec_id ) ); + LOG_INFO( + logger, "Successfully loaded codec: " << pretty_codec_name( codec ) ); } // ---------------------------------------------------------------------------- @@ -1140,24 +1130,15 @@ ::advance() if( read_err == AVERROR_EOF ) { // End of input. Tell this to decoder - if( parent->imagery_enabled ) - { - avcodec_send_packet( codec_context.get(), nullptr ); - new_frame.is_draining = true; - } - else - { - at_eof = true; - return false; - } + avcodec_send_packet( codec_context.get(), nullptr ); + new_frame.is_draining = true; } else { throw_error_code( read_err, "Could not read next packet from file" ); // Video packet - if( parent->imagery_enabled && - packet->stream_index == video_stream->index ) + if( packet->stream_index == video_stream->index ) { // Record packet as raw image new_frame.raw_image->packets.emplace_back( @@ -1216,30 +1197,27 @@ ::advance() } } - if( parent->imagery_enabled ) + // Receive decoded frame + auto const recv_err = + avcodec_receive_frame( codec_context.get(), new_frame.frame.get() ); + switch( recv_err ) { - // Receive decoded frame - auto const recv_err = - avcodec_receive_frame( codec_context.get(), new_frame.frame.get() ); - switch( recv_err ) - { - case 0: - // Success - frame = std::move( new_frame ); - break; - case AVERROR_EOF: - // End of file - at_eof = true; - break; - case AVERROR_INVALIDDATA: - case AVERROR( EAGAIN ): - // Acceptable errors - break; - default: - // Unacceptable errors - throw_error_code( recv_err, "Decoder returned error" ); - break; - } + case 0: + // Success + frame = std::move( new_frame ); + break; + case AVERROR_EOF: + // End of file + at_eof = true; + break; + case AVERROR_INVALIDDATA: + case AVERROR( EAGAIN ): + // Acceptable errors + break; + default: + // Unacceptable errors + throw_error_code( recv_err, "Decoder returned error" ); + break; } } @@ -1261,7 +1239,7 @@ ::advance() stream.advance( backup_timestamp, max_pts, max_pos ); } - return !parent->imagery_enabled || frame.has_value(); + return frame.has_value(); } // ---------------------------------------------------------------------------- @@ -1622,12 +1600,6 @@ ::get_configuration() const "deinterlacing only to frames which are interlaced. " "See details at https://ffmpeg.org/ffmpeg-filters.html" ); - config->set_value( - "imagery_enabled", d->imagery_enabled, - "When set to false, will not attempt to process any imagery found in the " - "video file. This may be useful if only processing metadata." - ); - config->set_value( "klv_enabled", d->klv_enabled, "When set to false, will not attempt to process any KLV metadata found in " @@ -1696,9 +1668,6 @@ ::set_configuration( kv::config_block_sptr in_config ) config->get_value< std::string >( "filter_desc", d->filter_description ); - d->imagery_enabled = - config->get_value< bool >( "imagery_enabled", d->imagery_enabled ); - d->klv_enabled = config->get_value< bool >( "klv_enabled", d->klv_enabled ); diff --git a/arrows/ffmpeg/tests/test_video_input_ffmpeg.cxx b/arrows/ffmpeg/tests/test_video_input_ffmpeg.cxx index be69d8d9e5..80bffde4e8 100644 --- a/arrows/ffmpeg/tests/test_video_input_ffmpeg.cxx +++ b/arrows/ffmpeg/tests/test_video_input_ffmpeg.cxx @@ -148,39 +148,6 @@ TEST_F ( ffmpeg_video_input, frame_image ) EXPECT_EQ( decode_barcode( *frame ), 1 ); } -// ---------------------------------------------------------------------------- -// Verify that disabling imagery processing acts as expected and doesn't break -// anything else. -TEST_F( ffmpeg_video_input, imagery_disabled ) -{ - ffmpeg::ffmpeg_video_input input; - - auto config = input.get_configuration(); - config->set_value< bool >( "imagery_enabled", false ); - input.set_configuration( config ); - input.open( aphill_video_path ); - - EXPECT_FALSE( input.good() ); - EXPECT_EQ( input.frame_image(), nullptr ); - - size_t frame_count = 0; - kv::timestamp ts; - while( input.next_frame( ts ) ) - { - ++frame_count; - EXPECT_TRUE( input.good() ); - EXPECT_EQ( input.frame_image(), nullptr ); - EXPECT_EQ( ts.get_frame(), frame_count ); - - auto const md = input.frame_metadata(); - ASSERT_FALSE( md.empty() ); - ASSERT_TRUE( md.at( 0 )->has( kv::VITAL_META_UNIX_TIMESTAMP ) ); - } - - input.close(); - EXPECT_FALSE( input.good() ); -} - // ---------------------------------------------------------------------------- // Verify that disabling KLV processing acts as expected and doesn't break // anything else. diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index a442e7f691..8dbded61b4 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -22,6 +22,8 @@ Arrows: FFmpeg * Added functionality to copy the input video's start timestamp when transcoding. +* Removed imagery_enabled option from ffmpeg_video_input. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From 35cadf8bbcd814b6e3300ac338a6545c9fe5c943 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 20 Jun 2023 16:09:05 -0500 Subject: [PATCH 14/51] Handle changing video size or pixel format --- arrows/ffmpeg/ffmpeg_video_input.cxx | 85 ++++++++++++++++++++++++---- doc/release-notes/master.txt | 2 + 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index d43ae6ba35..d27feaf5d7 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -308,11 +308,24 @@ class ffmpeg_video_input::priv }; struct open_video_state { + struct filter_parameters { + explicit filter_parameters( AVCodecContext const& codec_context ); + explicit filter_parameters( AVFrame const& frame ); + + bool operator==( filter_parameters const& other ) const; + bool operator!=( filter_parameters const& other ) const; + + int width; + int height; + AVPixelFormat pix_fmt; + AVRational sample_aspect_ratio; + }; + open_video_state( priv& parent, std::string const& path ); ~open_video_state(); bool try_codec(); - void init_filters(); + void init_filters( filter_parameters const& parameters ); bool advance(); void seek( kv::frame_id_t frame_number ); void set_video_metadata( kv::metadata& md ); @@ -338,6 +351,7 @@ class ffmpeg_video_input::priv filter_graph_uptr filter_graph; AVFilterContext* filter_sink_context; AVFilterContext* filter_source_context; + std::optional< filter_parameters > filter_params; sws_context_uptr image_conversion_context; @@ -575,6 +589,13 @@ ::convert_image() // Run the frame through the filter graph if( parent->filter_source_context && parent->filter_sink_context ) { + // Check for parameter changes + open_video_state::filter_parameters const frame_params{ *frame }; + if( frame_params != *parent->filter_params ) + { + parent->init_filters( frame_params ); + } + int recv_err; do{ throw_error_code( @@ -715,6 +736,48 @@ ::convert_metadata() return *metadata; } +// ---------------------------------------------------------------------------- +ffmpeg_video_input::priv::open_video_state::filter_parameters +::filter_parameters( AVCodecContext const& codec_context ) + : width{ codec_context.width }, + height{ codec_context.height }, + pix_fmt{ + codec_context.hw_device_ctx + ? codec_context.sw_pix_fmt + : codec_context.pix_fmt }, + sample_aspect_ratio{ codec_context.sample_aspect_ratio } +{} + +// ---------------------------------------------------------------------------- +ffmpeg_video_input::priv::open_video_state::filter_parameters +::filter_parameters( AVFrame const& frame ) + : width{ frame.width }, + height{ frame.height }, + pix_fmt{ static_cast< AVPixelFormat >( frame.format ) }, + sample_aspect_ratio{ frame.sample_aspect_ratio } +{} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input::priv::open_video_state::filter_parameters +::operator==( filter_parameters const& other ) const +{ + return + width == other.width && + height == other.height && + pix_fmt == other.pix_fmt && + sample_aspect_ratio.num == other.sample_aspect_ratio.num && + sample_aspect_ratio.den == other.sample_aspect_ratio.den; +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input::priv::open_video_state::filter_parameters +::operator!=( filter_parameters const& other ) const +{ + return !( *this == other ); +} + // ---------------------------------------------------------------------------- ffmpeg_video_input::priv::open_video_state ::open_video_state( priv& parent, std::string const& path ) @@ -949,7 +1012,7 @@ ::try_codec() } // Initialize filter graph - init_filters(); + init_filters( filter_parameters{ *codec_context } ); // Start time taken from the first decodable frame throw_error_code( @@ -1015,7 +1078,7 @@ ::try_codec() // ---------------------------------------------------------------------------- void ffmpeg_video_input::priv::open_video_state -::init_filters() +::init_filters( filter_parameters const& parameters ) { // Check for empty filter string if( std::all_of( @@ -1032,18 +1095,14 @@ ::init_filters() // Create the input buffer { - auto const pix_fmt = - codec_context->hw_device_ctx - ? codec_context->sw_pix_fmt - : codec_context->pix_fmt; std::stringstream ss; - ss << "video_size=" << codec_context->width << "x" - << codec_context->height - << ":pix_fmt=" << pix_fmt + ss << "video_size=" << parameters.width << "x" + << parameters.height + << ":pix_fmt=" << parameters.pix_fmt << ":time_base=" << video_stream->time_base.num << "/" << video_stream->time_base.den - << ":pixel_aspect=" << codec_context->sample_aspect_ratio.num << "/" - << codec_context->sample_aspect_ratio.den; + << ":pixel_aspect=" << parameters.sample_aspect_ratio.num << "/" + << parameters.sample_aspect_ratio.den; throw_error_code( avfilter_graph_create_filter( &filter_source_context, avfilter_get_by_name( "buffer" ), @@ -1095,6 +1154,8 @@ ::init_filters() throw_error_code( avfilter_graph_config( filter_graph.get(), NULL ), "Could not configure filter graph" ); + + filter_params.emplace( parameters ); } // ---------------------------------------------------------------------------- diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 9077324e8d..9ad3fb2f1e 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -24,6 +24,8 @@ Arrows: FFmpeg * Removed imagery_enabled option from ffmpeg_video_input. +* Added proper handling for changing size or pixel format mid-video. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From 1accf2ede01b91a3a43abd35ff40ca784bc35889 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 20 Jun 2023 16:20:09 -0500 Subject: [PATCH 15/51] Improve initial file probing --- arrows/ffmpeg/ffmpeg_video_input.cxx | 45 +++++++++++++++------------- doc/release-notes/master.txt | 2 ++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index d27feaf5d7..c923b27f7a 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -799,31 +799,23 @@ ::open_video_state( priv& parent, std::string const& path ) frame{}, at_eof{ false } { - // Open the file - { - AVFormatContext* ptr = nullptr; - throw_error_code( - avformat_open_input( &ptr, path.c_str(), NULL, NULL ), - "Could not open input stream" ); - format_context.reset( ptr ); - } - - // Try to probe the file for stream information - constexpr size_t max_probe_tries = 3; - format_context->probesize = 5'000'000; // 5 MB - format_context->max_analyze_duration = 10 * AV_TIME_BASE; // 10 seconds +// Try to probe the file for stream information + constexpr size_t max_probe_tries = 4; + int64_t probesize = 5'000'000; // 5 MB + int64_t max_analyze_duration = 10 * AV_TIME_BASE; // 10 seconds + uint64_t increase_factor = 100; for( auto const i : kvr::iota( max_probe_tries ) ) { - // Increase how much of file to analyze on later attempts - if( i != 0 ) + // Open the file { - LOG_ERROR( - logger, - "Could not find a valid video stream in the input on attempt " << i - << " of " << max_probe_tries ); - format_context->probesize *= 3; - format_context->max_analyze_duration *= 3; + AVFormatContext* ptr = nullptr; + throw_error_code( + avformat_open_input( &ptr, path.c_str(), NULL, NULL ), + "Could not open input stream" ); + format_context.reset( ptr ); } + format_context->probesize = probesize; + format_context->max_analyze_duration = max_analyze_duration; // Get the stream information by reading a bit of the file throw_error_code( @@ -883,6 +875,17 @@ ::open_video_state( priv& parent, std::string const& path ) // Success! break; } + + // Increase how much of file to analyze on later attempts + LOG_ERROR( + logger, + "Could not find a valid video stream in the input on attempt " + << ( i + 1 ) << " of " << max_probe_tries ); + probesize *= increase_factor; + max_analyze_duration *= increase_factor; + + // Clear state + klv_streams.clear(); } // Confirm stream characteristics diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 9ad3fb2f1e..977ef7b5c1 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -26,6 +26,8 @@ Arrows: FFmpeg * Added proper handling for changing size or pixel format mid-video. +* Improved probing behavior and increased duration. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From 6b4aa2cd43e13928e8cd306bd4c2c21f5103baeb Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Wed, 21 Jun 2023 10:10:20 -0500 Subject: [PATCH 16/51] Remove now-unnecessary seek --- arrows/ffmpeg/ffmpeg_video_input.cxx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index c923b27f7a..83fe7145cf 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -1017,12 +1017,6 @@ ::try_codec() // Initialize filter graph init_filters( filter_parameters{ *codec_context } ); - // Start time taken from the first decodable frame - throw_error_code( - av_seek_frame( - format_context.get(), video_stream->index, 0, AVSEEK_FLAG_FRAME ), - "Could not seek to beginning of video" ); - // Read frames until we can successfully decode one to get start timestamp { packet_uptr tmp_packet{ From 66fac70dceb9de21c8df5cda04753830d4e76042 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 26 Jun 2023 12:14:27 -0500 Subject: [PATCH 17/51] Clear video state on every seek attempt --- arrows/ffmpeg/ffmpeg_video_input.cxx | 16 ++++++++-------- doc/release-notes/master.txt | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index 83fe7145cf..a9645f861b 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -1310,14 +1310,6 @@ ::seek( kv::frame_id_t frame_number ) return; } - // Clear current state - at_eof = false; - frame.reset(); - for( auto& stream : klv_streams ) - { - stream.reset(); - } - // Get to the desired frame by seeking some number of frames before it, then // iterating forward. If we still don't have a frame image for that frame, // try again by seeking even further back. Finding the last keyframe is @@ -1328,6 +1320,14 @@ ::seek( kv::frame_id_t frame_number ) constexpr size_t maximum_attempts = 5; for( size_t i = 0; i < maximum_attempts; ++i ) { + // Clear current state + at_eof = false; + frame.reset(); + for( auto& stream : klv_streams ) + { + stream.reset(); + } + // Increasing backstep intervals on further tries size_t const backstep = i ? ( ( 1 << ( i - 1 ) ) * backstep_size ) : 1; diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 977ef7b5c1..2f8e0f1b70 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -28,6 +28,8 @@ Arrows: FFmpeg * Improved probing behavior and increased duration. +* Ensured proper clearing of video state even when seek attempts fail. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From a6b065c7d97cf024f8c4868722be4dac750f2886 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Fri, 28 Jul 2023 17:18:27 -0400 Subject: [PATCH 18/51] Fix style --- python/kwiver/vital/util/python_exceptions.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/kwiver/vital/util/python_exceptions.h b/python/kwiver/vital/util/python_exceptions.h index 704856816c..3def6587ba 100644 --- a/python/kwiver/vital/util/python_exceptions.h +++ b/python/kwiver/vital/util/python_exceptions.h @@ -51,7 +51,7 @@ void VITAL_PYTHON_UTIL_EXPORT python_print_exception(); } \ catch ( std::exception const& e ) \ { \ - pybind11::gil_scoped_acquire acquire; \ + pybind11::gil_scoped_acquire acquire; \ (void) acquire; \ PyErr_SetString( PyExc_RuntimeError, e.what() ); \ \ From 4482afe1b8fedeea14af9b3e202ab234bae2cec1 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Fri, 28 Jul 2023 17:18:37 -0400 Subject: [PATCH 19/51] Fix spacing --- .../trampoline/detected_object_set_output_trampoline.txx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/kwiver/vital/algo/trampoline/detected_object_set_output_trampoline.txx b/python/kwiver/vital/algo/trampoline/detected_object_set_output_trampoline.txx index a43a196610..4e9ccde528 100644 --- a/python/kwiver/vital/algo/trampoline/detected_object_set_output_trampoline.txx +++ b/python/kwiver/vital/algo/trampoline/detected_object_set_output_trampoline.txx @@ -71,10 +71,10 @@ class detected_object_set_output_trampoline : void complete() override { PYBIND11_OVERLOAD( - void, - kwiver::vital::algo::detected_object_set_output, - complete, - ); + void, + kwiver::vital::algo::detected_object_set_output, + complete, + ); } void From a422409b83f78f31cda486e448e8009513e75427 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Fri, 28 Jul 2023 17:18:52 -0400 Subject: [PATCH 20/51] Remove tabs --- .../detected_object_set_input_trampoline.txx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/kwiver/vital/algo/trampoline/detected_object_set_input_trampoline.txx b/python/kwiver/vital/algo/trampoline/detected_object_set_input_trampoline.txx index c13ba56f78..0bcd9bb7a9 100644 --- a/python/kwiver/vital/algo/trampoline/detected_object_set_input_trampoline.txx +++ b/python/kwiver/vital/algo/trampoline/detected_object_set_input_trampoline.txx @@ -56,14 +56,14 @@ class detected_object_set_input_trampoline : pybind11::gil_scoped_acquire gil; pybind11::function overload = pybind11::get_overload(static_cast(this), "read_set"); if (overload) { - auto o = overload(); - if (pybind11::isinstance(o)) { - return false; - } - std::tie(set, image_path) = o.cast>(); - return true; + auto o = overload(); + if (pybind11::isinstance(o)) { + return false; + } + std::tie(set, image_path) = o.cast>(); + return true; } else { - pybind11::pybind11_fail("Tried to call pure virtual function \"dosi::read_set\""); + pybind11::pybind11_fail("Tried to call pure virtual function \"dosi::read_set\""); } } @@ -73,7 +73,7 @@ class detected_object_set_input_trampoline : void, dosi, open, - filename + filename ); } From 246e53e19767b2135ca8ad8e3b501a20ae348a87 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 25 Sep 2023 16:48:30 -0500 Subject: [PATCH 21/51] Expose FFmpeg codec options --- arrows/ffmpeg/ffmpeg_video_output.cxx | 7 ++++++- arrows/ffmpeg/ffmpeg_video_settings.cxx | 14 ++++++++++---- arrows/ffmpeg/ffmpeg_video_settings.h | 2 ++ doc/release-notes/master.txt | 2 ++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index 25dfbe1909..4793f2c355 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -623,7 +623,12 @@ ::try_codec( ffmpeg_video_settings const& settings ) video_stream->codecpar->height = codec_context->height; video_stream->codecpar->format = codec_context->pix_fmt; - auto const err = avcodec_open2( codec_context.get(), codec, nullptr ); + AVDictionary* codec_options = nullptr; + for( auto const& entry : settings.codec_options ) + { + av_dict_set( &codec_options, entry.first.c_str(), entry.second.c_str(), 0 ); + } + auto const err = avcodec_open2( codec_context.get(), codec, &codec_options ); if( err < 0 ) { LOG_WARN( diff --git a/arrows/ffmpeg/ffmpeg_video_settings.cxx b/arrows/ffmpeg/ffmpeg_video_settings.cxx index 2046f0ef17..23727f0455 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.cxx +++ b/arrows/ffmpeg/ffmpeg_video_settings.cxx @@ -21,7 +21,8 @@ ::ffmpeg_video_settings() : frame_rate{ 0, 1 }, parameters{ avcodec_parameters_alloc() }, klv_stream_count{ 0 }, - start_timestamp{ AV_NOPTS_VALUE } + start_timestamp{ AV_NOPTS_VALUE }, + codec_options{} { if( !parameters ) { @@ -36,7 +37,8 @@ ::ffmpeg_video_settings( ffmpeg_video_settings const& other ) : frame_rate{ other.frame_rate }, parameters{ avcodec_parameters_alloc() }, klv_stream_count{ other.klv_stream_count }, - start_timestamp{ other.start_timestamp } + start_timestamp{ other.start_timestamp }, + codec_options{ other.codec_options } { throw_error_code( avcodec_parameters_copy( parameters.get(), other.parameters.get() ), @@ -49,7 +51,8 @@ ::ffmpeg_video_settings( ffmpeg_video_settings&& other ) : frame_rate{ std::move( other.frame_rate ) }, parameters{ std::move( other.parameters ) }, klv_stream_count{ std::move( other.klv_stream_count ) }, - start_timestamp{ other.start_timestamp } + start_timestamp{ other.start_timestamp }, + codec_options{ std::move( other.codec_options ) } {} // ---------------------------------------------------------------------------- @@ -61,7 +64,8 @@ ::ffmpeg_video_settings( : frame_rate( frame_rate ), parameters{ avcodec_parameters_alloc() }, klv_stream_count{ klv_stream_count }, - start_timestamp{ AV_NOPTS_VALUE } + start_timestamp{ AV_NOPTS_VALUE }, + codec_options{} { if( !parameters ) { @@ -89,6 +93,7 @@ ::operator=( ffmpeg_video_settings const& other ) "Could not copy codec parameters" ); klv_stream_count = other.klv_stream_count; start_timestamp = other.start_timestamp; + codec_options = other.codec_options; return *this; } @@ -101,6 +106,7 @@ ::operator=( ffmpeg_video_settings&& other ) parameters = std::move( other.parameters ); klv_stream_count = std::move( other.klv_stream_count ); start_timestamp = std::move( other.start_timestamp ); + codec_options = std::move( other.codec_options ); return *this; } diff --git a/arrows/ffmpeg/ffmpeg_video_settings.h b/arrows/ffmpeg/ffmpeg_video_settings.h index 5dc596ed45..2c9f04ec4b 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.h +++ b/arrows/ffmpeg/ffmpeg_video_settings.h @@ -17,6 +17,7 @@ extern "C" { #include } +#include #include namespace kwiver { @@ -48,6 +49,7 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings codec_parameters_uptr parameters; size_t klv_stream_count; int64_t start_timestamp; // In AV_TIME_BASE units + std::map< std::string, std::string > codec_options; }; using ffmpeg_video_settings_uptr = std::unique_ptr< ffmpeg_video_settings >; diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 2f8e0f1b70..264dd9fb41 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -30,6 +30,8 @@ Arrows: FFmpeg * Ensured proper clearing of video state even when seek attempts fail. +* Exposed codec options in ffmpeg_video_settings. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From cbb8c61ed27bcc930fa5bb19ad6a439eb0ea87f7 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 26 Sep 2023 13:17:42 -0500 Subject: [PATCH 22/51] Add .readthedocs.yaml --- .readthedocs.yaml | 13 +++++++++++++ doc/manuals/rtd_requirement.txt | 1 + 2 files changed, 14 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..4207900484 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +python: + install: + - requirements: doc/manuals/rtd_requirement.txt + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: doc/manuals/conf.py diff --git a/doc/manuals/rtd_requirement.txt b/doc/manuals/rtd_requirement.txt index cd6467ed82..dfb819190b 100644 --- a/doc/manuals/rtd_requirement.txt +++ b/doc/manuals/rtd_requirement.txt @@ -1 +1,2 @@ breathe +sphinx<7.0.0 From b56c179e05ad496f4c8f6695133ec06a1954c79c Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 26 Sep 2023 18:11:09 -0500 Subject: [PATCH 23/51] Allow access to set tag traits --- arrows/klv/klv_set.cxx | 9 +++++++++ arrows/klv/klv_set.h | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/arrows/klv/klv_set.cxx b/arrows/klv/klv_set.cxx index 5d927e6e8b..cfc1e0c8ec 100644 --- a/arrows/klv/klv_set.cxx +++ b/arrows/klv/klv_set.cxx @@ -373,6 +373,15 @@ klv_set_format< Key > ::~klv_set_format() {} +// ---------------------------------------------------------------------------- +template < class Key > +klv_tag_traits_lookup const& +klv_set_format< Key > +::traits() const +{ + return m_traits; +} + // ---------------------------------------------------------------------------- template < class Key > klv_set< Key > diff --git a/arrows/klv/klv_set.h b/arrows/klv/klv_set.h index e847e62aed..79c8705270 100644 --- a/arrows/klv/klv_set.h +++ b/arrows/klv/klv_set.h @@ -164,7 +164,9 @@ class KWIVER_ALGO_KLV_EXPORT klv_set_format virtual ~klv_set_format(); -protected: + klv_tag_traits_lookup const& + traits() const; + klv_set< Key > read_typed( klv_read_iter_t& data, size_t length ) const override; @@ -178,6 +180,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_set_format std::ostream& print_typed( std::ostream& os, klv_set< Key > const& value ) const; +protected: // Print warnings if tags appear too few or too many times in the given set. void check_tag_counts( klv_set< Key > const& klv ) const; @@ -185,6 +188,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_set_format virtual void check_set( klv_set< Key > const& klv ) const; +private: klv_tag_traits_lookup const& m_traits; }; From 258f065a03e4c813bb0fe5bca27323bc8387ac3a Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 26 Sep 2023 18:13:11 -0500 Subject: [PATCH 24/51] Add more constructors for klv_tag_traits --- arrows/klv/klv_tag_traits.cxx | 33 ++++++++++++++++++++++++++++----- arrows/klv/klv_tag_traits.h | 7 +++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/arrows/klv/klv_tag_traits.cxx b/arrows/klv/klv_tag_traits.cxx index 3a803c5608..55ece84d80 100644 --- a/arrows/klv/klv_tag_traits.cxx +++ b/arrows/klv/klv_tag_traits.cxx @@ -139,6 +139,12 @@ klv_tag_traits_lookup const* klv_tag_traits ::subtag_lookup() const { return m_subtag_lookup; } +// ---------------------------------------------------------------------------- +klv_tag_traits_lookup +::klv_tag_traits_lookup() + : m_traits{} +{} + // ---------------------------------------------------------------------------- klv_tag_traits_lookup ::klv_tag_traits_lookup( std::initializer_list< klv_tag_traits > const& traits ) @@ -155,6 +161,28 @@ ::klv_tag_traits_lookup( std::vector< klv_tag_traits > const& traits ) initialize(); } +// ---------------------------------------------------------------------------- +klv_tag_traits_lookup +::klv_tag_traits_lookup( klv_tag_traits_lookup const& other ) + : m_traits( other.m_traits ) +{ + initialize(); +} + +// ---------------------------------------------------------------------------- +klv_tag_traits_lookup& +klv_tag_traits_lookup +::operator=( klv_tag_traits_lookup const& other ) +{ + m_tag_to_traits.clear(); + m_uds_key_to_traits.clear(); + m_name_to_traits.clear(); + m_enum_name_to_traits.clear(); + m_traits = other.m_traits; + initialize(); + return *this; +} + // ---------------------------------------------------------------------------- typename klv_tag_traits_lookup::iterator klv_tag_traits_lookup @@ -220,11 +248,6 @@ void klv_tag_traits_lookup ::initialize() { - if( m_traits.empty() ) - { - throw std::logic_error( "tag traits cannot be empty" ); - } - for( auto const& trait : m_traits ) { if( trait.tag() && !m_tag_to_traits.emplace( trait.tag(), &trait ).second ) diff --git a/arrows/klv/klv_tag_traits.h b/arrows/klv/klv_tag_traits.h index 4408ba5ac2..bd7b0e1ebe 100644 --- a/arrows/klv/klv_tag_traits.h +++ b/arrows/klv/klv_tag_traits.h @@ -127,6 +127,8 @@ class KWIVER_ALGO_KLV_EXPORT klv_tag_traits_lookup public: using iterator = typename std::vector< klv_tag_traits >::const_iterator; + klv_tag_traits_lookup(); + /// Create lookup tables for the tag, uds_key, name, and enum name of /// \p traits. /// @@ -145,6 +147,11 @@ class KWIVER_ALGO_KLV_EXPORT klv_tag_traits_lookup /// klv_tag_traits_lookup( std::initializer_list< klv_tag_traits > const& ) klv_tag_traits_lookup( std::vector< klv_tag_traits > const& traits ); + klv_tag_traits_lookup( klv_tag_traits_lookup const& other ); + + klv_tag_traits_lookup& + operator=( klv_tag_traits_lookup const& other ); + iterator begin() const; From 1a01a1c3618e5c5d35e49cbaee387a5361f81614 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Wed, 27 Sep 2023 10:35:52 -0500 Subject: [PATCH 25/51] Enable FFmpeg threading by default --- arrows/ffmpeg/ffmpeg_video_input.cxx | 3 +++ arrows/ffmpeg/ffmpeg_video_output.cxx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index a9645f861b..a0a692e5fe 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -1003,6 +1003,9 @@ ::try_codec() av_buffer_ref( parent->hardware_device_context.get() ); } + codec_context->thread_count = 0; + codec_context->thread_type = FF_THREAD_FRAME; + // Open codec auto const err = avcodec_open2( codec_context.get(), codec, NULL ); if( err < 0 ) diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index 25dfbe1909..75bf83efe0 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -557,6 +557,9 @@ ::try_codec( ffmpeg_video_settings const& settings ) throw_error_null( avcodec_alloc_context3( codec ), "Could not allocate codec context" ) ); + codec_context->thread_count = 0; + codec_context->thread_type = FF_THREAD_FRAME; + // Fill in fields from given settings if( codec->id == settings.parameters->codec_id ) { From 6028695078342e10e2874fc8c7ed1eb3ecb5cbc0 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 17 Jul 2023 10:09:30 -0500 Subject: [PATCH 26/51] Cleanup Dockerfile --- .dockerignore | 2 + Dockerfile | 121 ++++++++++++++++++++++++++++---------------------- 2 files changed, 69 insertions(+), 54 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..6e19512a0e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.dockerignore +Dockerfile diff --git a/Dockerfile b/Dockerfile index 065307bf1e..712e9c9b19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,66 +1,79 @@ # Install KWIVER to /opt/kitware/kwiver -# Use latest Fletch as base image (Ubuntu 18.04) +# Use latest Fletch as base image (Ubuntu 20.04) -FROM kitware/fletch:latest-ubuntu18.04-py3-cuda10.0-cudnn7-devel +ARG BASE_IMAGE=kitware/fletch:latest-ubuntu20.04-py3 +ARG ENABLE_CUDA=OFF -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - python3-dev \ - python3-pip \ - xvfb \ - && pip3 install setuptools \ - scipy \ - six +FROM ${BASE_IMAGE} -# -# Build KWIVER -# +# Install system dependencies +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update +RUN apt-get upgrade -y +RUN apt-get install -y --no-install-recommends xvfb +# Remove unnecessary files +RUN apt-get clean +RUN rm -rf /var/lib/apt/lists/* + +# Setup build environment COPY . /kwiver -RUN cd /kwiver \ - && mkdir build \ - && cd build \ - && cmake ../ -DCMAKE_BUILD_TYPE=Release \ - -Dfletch_DIR:PATH=/opt/kitware/fletch/share/cmake \ - -DKWIVER_ENABLE_ARROWS=ON \ - -DKWIVER_ENABLE_C_BINDINGS=ON \ - -DKWIVER_ENABLE_CERES=ON \ - -DKWIVER_ENABLE_CUDA=ON \ - -DKWIVER_ENABLE_EXTRAS=ON \ - -DKWIVER_ENABLE_LOG4CPLUS=ON \ - -DKWIVER_ENABLE_OPENCV=ON \ - -DKWIVER_ENABLE_FFMPEG=ON \ - -DKWIVER_ENABLE_KLV=ON \ - -DKWIVER_ENABLE_MVG=ON \ - -DKWIVER_ENABLE_PROCESSES=ON \ - -DKWIVER_ENABLE_PROJ=ON \ - -DKWIVER_ENABLE_PYTHON=ON \ - -DKWIVER_ENABLE_SERIALIZE_JSON=ON \ - -DKWIVER_ENABLE_SPROKIT=ON \ - -DKWIVER_ENABLE_TESTS=ON \ - -DKWIVER_ENABLE_TOOLS=ON \ - -DKWIVER_ENABLE_VXL=ON \ - -DKWIVER_ENABLE_DOCS=ON \ - -DKWIVER_INSTALL_DOCS=ON \ - -DKWIVER_PYTHON_MAJOR_VERSION=3 \ - -DKWIVER_USE_BUILD_TREE=ON \ - && make -j$(nproc) -k \ - && make install \ - && chmod +x setup_KWIVER.sh -# Optionally install python build requirements if it was generated. -RUN PYTHON_REQS="python/requirements.txt" \ - && cd /kwiver/build \ - && ( ( [ ! -f "$PYTHON_REQS" ] \ - && echo "!!! No build requirements generated, nothing to install." ) \ - || pip3 install -r "$PYTHON_REQS" ) +RUN cd /kwiver && mkdir build + +# Configure +RUN cd /kwiver/build && \ + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/opt/kitware/kwiver \ + -Dfletch_DIR:PATH=/opt/kitware/fletch/share/cmake \ + -DKWIVER_ENABLE_ARROWS=ON \ + -DKWIVER_ENABLE_C_BINDINGS=ON \ + -DKWIVER_ENABLE_CERES=ON \ + -DKWIVER_ENABLE_CUDA=${ENABLE_CUDA} \ + -DKWIVER_ENABLE_EXTRAS=ON \ + -DKWIVER_ENABLE_LOG4CPLUS=ON \ + -DKWIVER_ENABLE_OPENCV=ON \ + -DKWIVER_ENABLE_FFMPEG=ON \ + -DKWIVER_ENABLE_KLV=ON \ + -DKWIVER_ENABLE_MVG=ON \ + -DKWIVER_ENABLE_PROCESSES=ON \ + -DKWIVER_ENABLE_PROJ=ON \ + -DKWIVER_ENABLE_PYTHON=ON \ + -DKWIVER_ENABLE_SERIALIZE_JSON=ON \ + -DKWIVER_ENABLE_SPROKIT=ON \ + -DKWIVER_ENABLE_TESTS=ON \ + -DKWIVER_ENABLE_TOOLS=ON \ + -DKWIVER_ENABLE_VXL=ON \ + -DKWIVER_ENABLE_DOCS=ON \ + -DKWIVER_INSTALL_DOCS=ON \ + -DKWIVER_PYTHON_MAJOR_VERSION=3 \ + -DKWIVER_USE_BUILD_TREE=ON \ + -DKWIVER_INSTALL_SET_UP_SCRIPT=ON + +# Build +RUN cd /kwiver/build && \ + . ./setup_KWIVER.sh && \ + make -j$(nproc) -k && \ + make install + +# Install python build requirements if they exist +ENV PYTHON_REQS="/kwiver/build/python/requirements.txt" +RUN [ ! -f "$PYTHON_REQS" ] || pip3 install -r "$PYTHON_REQS" + +# Remove source +RUN rm -rf /kwiver # Configure entrypoint -RUN bash -c 'echo -e "source /kwiver/build/setup_KWIVER.sh\n\ -\n# Set up X virtual framebuffer (Xvfb) to support running VTK headless\n\ +RUN bash -c '\ +#!/bin/bash\n\ +echo -e "source /opt/kitware/kwiver/setup_KWIVER.sh\n\ +\n\ +# Set up X virtual framebuffer (Xvfb) to support running VTK headless\n\ Xvfb :1 -screen 0 1024x768x16 -nolisten tcp > xvfb.log &\n\ -export DISPLAY=:1.0\n\n\ -kwiver \$@" >> entrypoint.sh' \ - && chmod +x entrypoint.sh +export DISPLAY=:1.0\n\ +\n\ +/opt/kitware/kwiver/bin/kwiver \$@" > /entrypoint.sh' && \ + chmod +x /entrypoint.sh ENTRYPOINT [ "bash", "/entrypoint.sh" ] CMD [ "help" ] From 46b8f8ecee1c1cc2c88d9629c41248311c8f5da0 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Wed, 15 Mar 2023 15:57:31 -0400 Subject: [PATCH 27/51] Handle prefix- and payload-only checksums --- arrows/klv/klv_0601.cxx | 2 +- arrows/klv/klv_0601.h | 2 +- arrows/klv/klv_0806.cxx | 2 +- arrows/klv/klv_0806.h | 2 +- arrows/klv/klv_0903.cxx | 2 +- arrows/klv/klv_0903.h | 2 +- arrows/klv/klv_1002.cxx | 2 +- arrows/klv/klv_1002.h | 2 +- arrows/klv/klv_1107.cxx | 2 +- arrows/klv/klv_1107.h | 2 +- arrows/klv/klv_1108.cxx | 2 +- arrows/klv/klv_1108.h | 2 +- arrows/klv/klv_data_format.cxx | 18 ++- arrows/klv/klv_data_format.h | 13 +- arrows/klv/klv_packet.cxx | 262 ++++++++++++++++++++++----------- arrows/klv/klv_util.h | 39 ++++- 16 files changed, 251 insertions(+), 105 deletions(-) diff --git a/arrows/klv/klv_0601.cxx b/arrows/klv/klv_0601.cxx index 0b5dd90293..1c16e4ed07 100644 --- a/arrows/klv/klv_0601.cxx +++ b/arrows/klv/klv_0601.cxx @@ -2820,7 +2820,7 @@ ::length_of_typed( klv_0601_wavelength_record const& item ) const // ---------------------------------------------------------------------------- klv_checksum_packet_format const* klv_0601_local_set_format -::checksum_format() const +::packet_checksum_format() const { return &m_checksum_format; } diff --git a/arrows/klv/klv_0601.h b/arrows/klv/klv_0601.h index e3d0609d1f..aa49de1d45 100644 --- a/arrows/klv/klv_0601.h +++ b/arrows/klv/klv_0601.h @@ -1042,7 +1042,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_0601_local_set_format description_() const override; klv_checksum_packet_format const* - checksum_format() const override; + packet_checksum_format() const override; private: klv_running_sum_16_packet_format m_checksum_format; diff --git a/arrows/klv/klv_0806.cxx b/arrows/klv/klv_0806.cxx index 0c155f32a6..b0dd441989 100644 --- a/arrows/klv/klv_0806.cxx +++ b/arrows/klv/klv_0806.cxx @@ -199,7 +199,7 @@ ::description_() const // ---------------------------------------------------------------------------- klv_checksum_packet_format const* klv_0806_local_set_format -::checksum_format() const +::packet_checksum_format() const { return &m_checksum_format; } diff --git a/arrows/klv/klv_0806.h b/arrows/klv/klv_0806.h index 4022dda879..c63793194f 100644 --- a/arrows/klv/klv_0806.h +++ b/arrows/klv/klv_0806.h @@ -66,7 +66,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_0806_local_set_format description_() const override; klv_checksum_packet_format const* - checksum_format() const override; + packet_checksum_format() const override; private: klv_crc_32_mpeg_packet_format m_checksum_format; diff --git a/arrows/klv/klv_0903.cxx b/arrows/klv/klv_0903.cxx index a455cfd799..ae9c9cdafd 100644 --- a/arrows/klv/klv_0903.cxx +++ b/arrows/klv/klv_0903.cxx @@ -174,7 +174,7 @@ ::description_() const // ---------------------------------------------------------------------------- klv_checksum_packet_format const* klv_0903_local_set_format -::checksum_format() const +::packet_checksum_format() const { return &m_checksum_format; } diff --git a/arrows/klv/klv_0903.h b/arrows/klv/klv_0903.h index 6c7fd6a44a..d2e1d17ef5 100644 --- a/arrows/klv/klv_0903.h +++ b/arrows/klv/klv_0903.h @@ -73,7 +73,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_0903_local_set_format description_() const override; klv_checksum_packet_format const* - checksum_format() const override; + packet_checksum_format() const override; private: klv_running_sum_16_packet_format m_checksum_format; diff --git a/arrows/klv/klv_1002.cxx b/arrows/klv/klv_1002.cxx index ea0f57a933..6484fe8cef 100644 --- a/arrows/klv/klv_1002.cxx +++ b/arrows/klv/klv_1002.cxx @@ -269,7 +269,7 @@ ::description_() const // ---------------------------------------------------------------------------- klv_checksum_packet_format const* klv_1002_local_set_format -::checksum_format() const +::packet_checksum_format() const { return &m_checksum_format; } diff --git a/arrows/klv/klv_1002.h b/arrows/klv/klv_1002.h index 9c232407da..aeb193f069 100644 --- a/arrows/klv/klv_1002.h +++ b/arrows/klv/klv_1002.h @@ -180,7 +180,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_1002_local_set_format description_() const override; klv_checksum_packet_format const* - checksum_format() const override; + packet_checksum_format() const override; private: klv_crc_16_ccitt_packet_format m_checksum_format; diff --git a/arrows/klv/klv_1107.cxx b/arrows/klv/klv_1107.cxx index ba36c13e1f..2a875068bf 100644 --- a/arrows/klv/klv_1107.cxx +++ b/arrows/klv/klv_1107.cxx @@ -113,7 +113,7 @@ ::klv_1107_local_set_format() // ---------------------------------------------------------------------------- klv_checksum_packet_format const* klv_1107_local_set_format -::checksum_format() const +::packet_checksum_format() const { return &m_checksum_format; } diff --git a/arrows/klv/klv_1107.h b/arrows/klv/klv_1107.h index 2c9f142416..922c6b1b34 100644 --- a/arrows/klv/klv_1107.h +++ b/arrows/klv/klv_1107.h @@ -109,7 +109,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_1107_local_set_format description_() const override; klv_checksum_packet_format const* - checksum_format() const override; + packet_checksum_format() const override; private: klv_crc_16_ccitt_packet_format m_checksum_format; diff --git a/arrows/klv/klv_1108.cxx b/arrows/klv/klv_1108.cxx index e2a6ecc874..7c9b3e4031 100644 --- a/arrows/klv/klv_1108.cxx +++ b/arrows/klv/klv_1108.cxx @@ -223,7 +223,7 @@ ::klv_1108_local_set_format() // ---------------------------------------------------------------------------- klv_checksum_packet_format const* klv_1108_local_set_format -::checksum_format() const +::packet_checksum_format() const { return &m_checksum_format; } diff --git a/arrows/klv/klv_1108.h b/arrows/klv/klv_1108.h index 603921dcac..29f4cf06c6 100644 --- a/arrows/klv/klv_1108.h +++ b/arrows/klv/klv_1108.h @@ -204,7 +204,7 @@ class KWIVER_ALGO_KLV_EXPORT klv_1108_local_set_format description_() const override; klv_checksum_packet_format const* - checksum_format() const override; + packet_checksum_format() const override; private: klv_crc_16_ccitt_packet_format m_checksum_format; diff --git a/arrows/klv/klv_data_format.cxx b/arrows/klv/klv_data_format.cxx index a320418ca5..6aeffffdbf 100644 --- a/arrows/klv/klv_data_format.cxx +++ b/arrows/klv/klv_data_format.cxx @@ -61,7 +61,23 @@ ::to_string( klv_value const& value ) const // ---------------------------------------------------------------------------- klv_checksum_packet_format const* klv_data_format -::checksum_format() const +::prefix_checksum_format() const +{ + return nullptr; +} + +// ---------------------------------------------------------------------------- +klv_checksum_packet_format const* +klv_data_format +::payload_checksum_format() const +{ + return nullptr; +} + +// ---------------------------------------------------------------------------- +klv_checksum_packet_format const* +klv_data_format +::packet_checksum_format() const { return nullptr; } diff --git a/arrows/klv/klv_data_format.h b/arrows/klv/klv_data_format.h index e58dedde35..419fd11727 100644 --- a/arrows/klv/klv_data_format.h +++ b/arrows/klv/klv_data_format.h @@ -86,9 +86,18 @@ class KWIVER_ALGO_KLV_EXPORT klv_data_format std::string description() const; - /// Optionally the checksum format for this data format. + /// Return the checksum format for the packet key and length only, or + /// `nullptr`. virtual klv_checksum_packet_format const* - checksum_format() const; + prefix_checksum_format() const; + + /// Return the checksum format for the packet payload only, or `nullptr`. + virtual klv_checksum_packet_format const* + payload_checksum_format() const; + + /// Return the checksum format for the entire packet, or `nullptr`. + virtual klv_checksum_packet_format const* + packet_checksum_format() const; /// Return the constraints on the length of this format. klv_length_constraints const& diff --git a/arrows/klv/klv_packet.cxx b/arrows/klv/klv_packet.cxx index 3317dc7487..cf48b66246 100644 --- a/arrows/klv/klv_packet.cxx +++ b/arrows/klv/klv_packet.cxx @@ -80,87 +80,189 @@ operator<<( std::ostream& os, klv_packet const& packet ) namespace { // ---------------------------------------------------------------------------- -size_t +using data_range_t = vital::range::iterator_range< klv_read_iter_t >; + +// ---------------------------------------------------------------------------- +data_range_t verify_checksum( klv_tag_traits const& traits, - iterator_tracker< klv_read_iter_t > const& tracker, - size_t value_size ) + klv_checksum_packet_format const& format, + data_range_t data_range, + std::vector< data_range_t > wrong_ranges ) { - auto const format = traits.format().checksum_format(); - if( !format ) + auto const logger = kv::get_logger( "klv" ); + + // Get the checksum written at the end of the range + auto const checksum_size = *format.length_constraints().fixed(); + if( data_range.size() < 0 || + checksum_size > static_cast< size_t >( data_range.size() ) ) + { + LOG_ERROR( + logger, + traits.name() << ": checksum not present (data range too small)" ); + return data_range; + } + uint64_t written_checksum = 0; + try { - // This format doesn't have a checksum - return 0; + auto it = data_range.end() - checksum_size; + written_checksum = format.read_( it, checksum_size ); + } + catch( vital::metadata_exception const& e ) + { + LOG_ERROR( + logger, + traits.name() << ": could not read checksum: " << e.what() ); + return data_range; } - // Get the checksum written at the end of the packet - auto const packet_size = tracker.traversed() + value_size; - auto const checksum_size = *format->length_constraints().fixed(); - auto it = tracker.begin() + packet_size - checksum_size; - auto const written_checksum = format->read_( it, checksum_size ); - - // Calculate our own checksum over the packet - using range_t = vital::range::iterator_range< klv_read_iter_t >; - auto const header_size = format->header().size(); - range_t const checked_range{ - tracker.begin(), - tracker.begin() + packet_size - checksum_size + header_size }; + // Calculate our own checksum over the range + auto const header_size = format.header().size(); + auto const checked_size = data_range.size() - checksum_size + header_size; auto const actual_checksum = - format->evaluate( checked_range.begin(), checked_range.size() ); + format.evaluate( data_range.begin(), checked_size ); // Check for match + data_range_t const result{ + data_range.begin(), data_range.end() - checksum_size }; if( written_checksum == actual_checksum ) { - return checksum_size; + return result; } // Then check that the mismatch isn't the result of computing the checksum // over the wrong range of data. If so, this doesn't merit a full ERROR log. - for( auto const alt_begin_offset : - { int64_t{ 0 }, static_cast< int64_t >( packet_size - value_size ) } ) + for( auto const& wrong_range : wrong_ranges ) { - for( auto const alt_end_offset : - { int64_t{ 0 }, -static_cast< int64_t >( header_size ) } ) - { - if( !alt_begin_offset && !alt_end_offset ) - { - // Correct algorithm - continue; - } + auto const wrong_checksum = + format.evaluate( wrong_range.begin(), wrong_range.size() ); - // Compute the checksum of the alternate data range - range_t const alt_range{ - checked_range.begin() + alt_begin_offset, - checked_range.end() + alt_end_offset }; - auto const alt_checksum = - format->evaluate( alt_range.begin(), alt_range.size() ); - - // Check if they implemented it wrong - if( written_checksum == alt_checksum ) - { - LOG_DEBUG( - kv::get_logger( "klv" ), - traits.name() << ": " - << "the producer of this data implemented the checksum incorrectly" - ); - return checksum_size; - } + if( written_checksum == wrong_checksum ) + { + LOG_DEBUG( + logger, + traits.name() << ": " + << "the producer of this data implemented the checksum incorrectly" + ); + return result; } } // Checksum is incorrect for some unknown reason. // Possibly actual packet corruption or a different misimplementation LOG_ERROR( - kv::get_logger( "klv" ), + logger, traits.name() << ": " << "calculated checksum " - << "(" << format->to_string( actual_checksum ) << ") " + << "(" << format.to_string( actual_checksum ) << ") " << "does not equal checksum contained in packet " - << "(" << format->to_string( written_checksum ) << ")" ); + << "(" << format.to_string( written_checksum ) << ")" ); - return checksum_size; + return result; } +// ---------------------------------------------------------------------------- +data_range_t +verify_checksum( + klv_tag_traits const& traits, + iterator_tracker< klv_read_iter_t > const& tracker, + size_t value_size ) +{ + auto const key_it = tracker.begin(); + auto value_it = tracker.it(); + auto end_it = value_it + value_size; + + // Prefix first, to ensure we have the right value_size + if( auto const format = traits.format().prefix_checksum_format() ) + { + auto const checksum_size = *format->length_constraints().fixed(); + data_range_t data_range{ key_it, value_it + checksum_size }; + auto const result_range = + verify_checksum( traits, *format, data_range, { { key_it, value_it } } ); + if( result_range.end() == data_range.end() ) + { + // Failure. Since the value size could be wrong, we have to abort here. + return { value_it, value_it }; + } + value_it = data_range.end(); + } + + // Only after we check that the prefix is intact should we actually try to + // read the next value_size bytes + tracker.verify( value_size ); + + // Then entire packet + if( auto const format = traits.format().packet_checksum_format() ) + { + auto const checksum_size = *format->length_constraints().fixed(); + auto const header_size = format->header().size(); + auto const right_end_it = end_it - ( checksum_size - header_size ); + auto const wrong_end_it = end_it - checksum_size; + data_range_t data_range{ key_it, end_it }; + auto const result_range = + verify_checksum( + traits, *format, data_range, + { { key_it, wrong_end_it }, + { value_it, right_end_it }, + { value_it, wrong_end_it } } ); + end_it = result_range.end(); + } + + // Then payload (value) + if( auto const format = traits.format().payload_checksum_format() ) + { + auto const checksum_size = *format->length_constraints().fixed(); + data_range_t data_range{ value_it, end_it }; + auto const result_range = + verify_checksum( + traits, *format, data_range, + { { value_it, end_it - checksum_size } } ); + end_it = result_range.end(); + } + + // Return value range with checksums removed + return { value_it, end_it }; +} + +// ---------------------------------------------------------------------------- +size_t +all_checksums_length( klv_data_format const& format ) +{ + size_t result = 0; + for( auto const checksum_format : + { format.prefix_checksum_format(), + format.payload_checksum_format(), + format.packet_checksum_format() } ) + { + if( checksum_format ) + { + result += *checksum_format->length_constraints().fixed(); + } + } + + return result; +} + +// ---------------------------------------------------------------------------- +void +write_checksum( + klv_checksum_packet_format const* format, + klv_write_iter_t begin, klv_write_iter_t& data ) +{ + if( !format ) + { + return; + } + + auto const header = format->header(); + std::copy( header.begin(), header.end(), data ); + auto const checksum = + format->evaluate( + begin, static_cast< size_t >( data - begin ) + header.size() ); + format->write_( checksum, data, *format->length_constraints().fixed() ); +} + + } // namespace // ---------------------------------------------------------------------------- @@ -195,26 +297,27 @@ klv_read_packet( klv_read_iter_t& data, size_t max_length ) { // This might be an encoding error, or maybe we falsely detected a prefix // in the data between the packets - VITAL_THROW( kwiver::vital::metadata_exception, "invalid universal key" ); + VITAL_THROW( kv::metadata_exception, "invalid universal key" ); } // Read length auto const length_of_value = klv_read_ber< size_t >( data, tracker.remaining() ); - tracker.verify( length_of_value ); + auto const value_begin = data; // Verify checksum auto const& traits = klv_lookup_packet_traits().by_uds_key( key ); auto const& format = traits.format(); - auto const checksum_length = + auto const value_range = verify_checksum( traits, tracker, length_of_value ); // Read value + data = value_range.begin(); auto const value = - format.read( data, tracker.verify( length_of_value - checksum_length ) ); + format.read( data, tracker.verify( value_range.size() ) ); // Ensure iterator ends correctly - data += checksum_length; + data = value_begin + length_of_value; return { key, value }; } @@ -228,32 +331,23 @@ klv_write_packet( klv_packet const& packet, klv_write_iter_t& data, auto const& format = klv_lookup_packet_traits().by_uds_key( packet.key ).format(); - auto const checksum_format = format.checksum_format(); - auto const length = format.length_of( packet.value ); + auto const value_length = format.length_of( packet.value ); + auto const checksums_length = all_checksums_length( format ); auto const packet_length = klv_packet_length( packet ); - auto const checksum_length = - checksum_format ? *checksum_format->length_constraints().fixed() : 0; - if( max_length < length + checksum_length ) - { - VITAL_THROW( kwiver::vital::metadata_buffer_overflow, - "writing klv packet overflows buffer" ); - } + tracker.verify( packet_length ); + // Write prefix klv_write_uds_key( packet.key, data, tracker.remaining() ); - klv_write_ber( length + checksum_length, data, tracker.remaining() ); - format.write( packet.value, data, length ); + klv_write_ber( value_length + checksums_length, data, tracker.remaining() ); - if( checksum_format ) - { - tracker.verify( checksum_length ); - auto const header = checksum_format->header(); - std::copy( header.begin(), header.end(), data ); - auto const checksum = - checksum_format->evaluate( - tracker.begin(), - packet_length - checksum_length + header.size() ); - checksum_format->write_( checksum, data, checksum_length ); - } + write_checksum( format.prefix_checksum_format(), tracker.begin(), data ); + auto const value_begin = data; + + // Write value + format.write( packet.value, data, value_length ); + + write_checksum( format.payload_checksum_format(), value_begin, data ); + write_checksum( format.packet_checksum_format(), tracker.begin(), data ); } // ---------------------------------------------------------------------------- @@ -262,15 +356,15 @@ klv_packet_length( klv_packet const& packet ) { auto const& format = klv_lookup_packet_traits().by_uds_key( packet.key ).format(); - auto const checksum_format = format.checksum_format(); + auto const length_of_key = packet.key.length; auto const length_of_value = format.length_of( packet.value ); - auto const length_of_checksum = - checksum_format ? *checksum_format->length_constraints().fixed() : 0; + auto const length_of_checksums = all_checksums_length( format ); auto const length_of_length = - klv_ber_length( length_of_value + length_of_checksum ); + klv_ber_length( length_of_value + length_of_checksums ); + return length_of_key + length_of_length + length_of_value + - length_of_checksum; + length_of_checksums; } // ---------------------------------------------------------------------------- diff --git a/arrows/klv/klv_util.h b/arrows/klv/klv_util.h index 55c4fa2eed..71d7e60314 100644 --- a/arrows/klv/klv_util.h +++ b/arrows/klv/klv_util.h @@ -247,27 +247,54 @@ class iterator_tracker { : m_begin( it ), m_length{ length }, m_it( it ) { static_assert( - std::is_same< typename std::decay< decltype( *it ) >::type, - uint8_t >::value, "iterator must point to uint8_t" ); + std::is_same_v< std::decay_t< decltype( *it ) >, uint8_t >, + "iterator must point to uint8_t" ); } - size_t verify( size_t count ) const + template< + class Int, + std::enable_if_t< std::is_unsigned_v< std::decay_t< Int > >, bool > = true > + size_t verify( Int count ) const { if( count > remaining() ) { m_it = m_begin; - VITAL_THROW( kwiver::vital::metadata_buffer_overflow, + VITAL_THROW( vital::metadata_buffer_overflow, "tried to read or write past end of data buffer" ); } return count; } + template< + class Int, + std::enable_if_t< std::is_signed_v< std::decay_t< Int > >, bool > = true > + size_t verify( Int count ) const + { + if( count < 0 ) + { + m_it = m_begin; + VITAL_THROW( vital::metadata_buffer_overflow, + "tried to read or write a value of negative length" ); + } + + return verify( static_cast< size_t >( count ) ); + } + size_t traversed() const { auto const distance = std::distance( m_begin, m_it ); - if( distance > m_length ) + + if( distance < 0 ) + { + m_it = m_begin; + VITAL_THROW( vital::metadata_buffer_overflow, + "read or written before beginning of data buffer" ); + } + + if( static_cast< size_t >( distance ) > m_length ) { - VITAL_THROW( kwiver::vital::metadata_buffer_overflow, + m_it = m_begin; + VITAL_THROW( vital::metadata_buffer_overflow, "read or written past end of data buffer" ); } From fbb18e6041a95699e81a253c2bff3a8e481600a8 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 10 Oct 2023 13:15:28 -0500 Subject: [PATCH 28/51] Delay discovery of GTest tests --- CMake/utils/kwiver-utils-tests.cmake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMake/utils/kwiver-utils-tests.cmake b/CMake/utils/kwiver-utils-tests.cmake index 411a099380..e57cc54a7e 100644 --- a/CMake/utils/kwiver-utils-tests.cmake +++ b/CMake/utils/kwiver-utils-tests.cmake @@ -63,6 +63,9 @@ include(GoogleTest) +set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST CACHE STRING + "When to run test discovery: POST_BUILD or PRE_TEST") + option(KWIVER_TEST_ADD_TARGETS "Add targets for tests to the build system" OFF) mark_as_advanced(KWIVER_TEST_ADD_TARGETS) if (KWIVER_TEST_ADD_TARGETS) From e3ef143cbf3fe72f984eabdf7872c59194a7a680 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 25 Sep 2023 16:38:03 -0500 Subject: [PATCH 29/51] Optimize image copying when writing video --- arrows/ffmpeg/ffmpeg_video_output.cxx | 39 ++++++++++++++++++++------- doc/release-notes/master.txt | 2 ++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index 887d7b3357..35b1583c99 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -678,24 +678,43 @@ ::add_image( kv::image_container_sptr const& image, // Give the frame the raw pixel data { - size_t index = 0; - auto ptr = static_cast< uint8_t* >( image->get_image().first_pixel() ); + auto ptr = + static_cast< uint8_t const* >( image->get_image().first_pixel() ); auto const i_step = image->get_image().h_step(); auto const j_step = image->get_image().w_step(); auto const k_step = image->get_image().d_step(); - for( size_t i = 0; i < image->height(); ++i ) + if( j_step == static_cast< ptrdiff_t >( image->depth() ) && + k_step == static_cast< ptrdiff_t >( 1 ) ) { - for( size_t j = 0; j < image->width(); ++j ) + for( size_t i = 0; i < image->height(); ++i ) { - for( size_t k = 0; k < image->depth(); ++k ) + std::memcpy( + frame->data[ 0 ] + i * frame->linesize[ 0 ], ptr + i * i_step, + image->width() * image->depth() ); + } + } + else + { + auto const i_step_ptr = i_step - j_step * image->width(); + auto const j_step_ptr = j_step - k_step * image->depth(); + auto const k_step_ptr = k_step; + auto const i_step_index = + frame->linesize[ 0 ] - image->width() * image->depth(); + size_t index = 0; + for( size_t i = 0; i < image->height(); ++i ) + { + for( size_t j = 0; j < image->width(); ++j ) { - frame->data[ 0 ][ index++ ] = *ptr; - ptr += k_step; + for( size_t k = 0; k < image->depth(); ++k ) + { + frame->data[ 0 ][ index++ ] = *ptr; + ptr += k_step_ptr; + } + ptr += j_step_ptr; } - ptr += j_step - k_step * image->depth(); + ptr += i_step_ptr; + index += i_step_index; } - ptr += i_step - j_step * image->width(); - index += frame->linesize[ 0 ] - image->width() * image->depth(); } } diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 264dd9fb41..bade6b42fd 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -32,6 +32,8 @@ Arrows: FFmpeg * Exposed codec options in ffmpeg_video_settings. +* Optimized copying behavior when encoding frames. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From 3afa883a2b4042a410d081cc77eba3d2e8a73774 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 10 Oct 2023 14:29:04 -0500 Subject: [PATCH 30/51] Add sphinx_rtd_theme as an rtd dependency --- doc/manuals/rtd_requirement.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/manuals/rtd_requirement.txt b/doc/manuals/rtd_requirement.txt index dfb819190b..4ed25c6e03 100644 --- a/doc/manuals/rtd_requirement.txt +++ b/doc/manuals/rtd_requirement.txt @@ -1,2 +1,3 @@ breathe sphinx<7.0.0 +sphinx_rtd_theme From d37a6040766a7c5c6dded227786e4e01b99f0584 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 3 Oct 2023 10:51:45 -0500 Subject: [PATCH 31/51] Address CMake warnings --- CMake/kwiver-setup-python.cmake | 13 ++++++++----- CMakeLists.txt | 5 +++++ vital/kwiversys/CMakeLists.txt | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CMake/kwiver-setup-python.cmake b/CMake/kwiver-setup-python.cmake index 979fec5205..3d19c4d676 100644 --- a/CMake/kwiver-setup-python.cmake +++ b/CMake/kwiver-setup-python.cmake @@ -134,14 +134,17 @@ set(__prev_kwiver_pyversion "${KWIVER_PYTHON_MAJOR_VERSION}" CACHE INTERNAL # if (KWIVER_PYTHON_MAJOR_VERSION STREQUAL "3") # note, 3.4 is a minimum version - find_package(PythonInterp 3.4 REQUIRED) - find_package(PythonLibs 3.4 REQUIRED) + find_package(Python 3.4 COMPONENTS Interpreter Development REQUIRED) else() - find_package(PythonInterp 2.7 REQUIRED) - find_package(PythonLibs 2.7 REQUIRED) + find_package(Python 2.7 COMPONENTS Interpreter Development REQUIRED) endif() -include_directories(SYSTEM ${PYTHON_INCLUDE_DIR}) +set(PYTHON_EXECUTABLE ${Python_EXECUTABLE} CACHE FILEPATH "Path to Python executable") +set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIRS} CACHE STRING "Paths to Python include directories") +set(PYTHON_LIBRARY ${Python_LIBRARIES} CACHE STRING "Paths to Python libraries") +set(PYTHON_LIBRARY_DEBUG PYTHON_LIBRARY_DEBUG_NOT_FOUND CACHE FILEPATH "Path to Python debug libraries") + +include_directories(SYSTEM ${PYTHON_INCLUDE_DIR}) ### # Python site-packages diff --git a/CMakeLists.txt b/CMakeLists.txt index ec60bbdc7b..c93d2e2163 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,11 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) # Set default visibility set(CMAKE_CXX_VISIBILITY_PRESET hidden) +# Allow capitalized _ROOT variables +if(POLICY_CMP0144) + cmake_policy(SET CMP0144 NEW) +endif() + # Organize target into folders for IDEs that support it set_property(GLOBAL PROPERTY USE_FOLDERS ON) diff --git a/vital/kwiversys/CMakeLists.txt b/vital/kwiversys/CMakeLists.txt index a47a76a41f..edd9da6fcc 100644 --- a/vital/kwiversys/CMakeLists.txt +++ b/vital/kwiversys/CMakeLists.txt @@ -75,7 +75,7 @@ # any outside mailing list and no documentation of the change will be # written. -CMAKE_MINIMUM_REQUIRED(VERSION 2.6.3 FATAL_ERROR) +CMAKE_MINIMUM_REQUIRED(VERSION 2.6.3...3.27 FATAL_ERROR) IF(POLICY CMP0025) CMAKE_POLICY(SET CMP0025 NEW) ENDIF() From 210b971b9c387e41afbe53b3ea66a7c874223250 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 9 Oct 2023 11:23:17 -0500 Subject: [PATCH 32/51] Update uncrustify configuration --- .uncrustify.cfg | 81 ++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/.uncrustify.cfg b/.uncrustify.cfg index ae81276ddc..896aac72ee 100644 --- a/.uncrustify.cfg +++ b/.uncrustify.cfg @@ -3,7 +3,7 @@ using 0.69 # General options newlines = auto -input_tab_size = 8 +input_tab_size = 2 string_escape_char = 92 string_escape_char2 = 0 @@ -70,6 +70,7 @@ sp_permit_cpp11_shift = true sp_before_sparen = remove sp_inside_sparen = force sp_after_sparen = force +sp_inside_for = force sp_sparen_brace = force sp_special_semi = ignore sp_before_semi = remove @@ -77,7 +78,7 @@ sp_before_semi_for = remove sp_before_semi_for_empty = remove sp_after_semi = add sp_after_semi_for = force -sp_after_semi_for_empty = remove +sp_after_semi_for_empty = add sp_before_square = remove sp_before_squares = remove sp_cpp_before_struct_binding = add @@ -100,7 +101,7 @@ sp_after_operator = remove sp_after_operator_sym = remove sp_after_operator_sym_empty = remove sp_after_cast = add -sp_inside_paren_cast = remove +sp_inside_paren_cast = add sp_cpp_cast_paren = remove sp_sizeof_paren = remove sp_sizeof_ellipsis = remove @@ -128,6 +129,7 @@ sp_fparen_brace = add sp_fparen_brace_initializer = remove sp_func_call_paren = remove sp_func_call_paren_empty = remove +sp_func_call_user_paren = remove sp_func_class_paren = remove sp_func_class_paren_empty = remove sp_return_paren = force @@ -136,7 +138,7 @@ sp_attribute_paren = remove sp_defined_paren = remove sp_throw_paren = force sp_after_throw = force -sp_catch_paren = force +sp_catch_paren = remove sp_macro = force sp_macro_func = force sp_else_brace = force @@ -174,8 +176,8 @@ sp_after_new = force sp_between_new_paren = force sp_after_newop_paren = force sp_inside_newop_paren = force -sp_before_tr_emb_cmt = add -sp_num_before_tr_emb_cmt = 1 +sp_before_tr_cmt = add +sp_num_before_tr_cmt = 1 sp_skip_vbrace_tokens = false sp_after_noexcept = remove @@ -205,21 +207,21 @@ indent_ctor_init_leading = 2 indent_ctor_init = 0 indent_else_if = false indent_var_def_blk = 0 -indent_var_def_cont = false -indent_shift = false +indent_var_def_cont = true +indent_shift = 0 indent_func_def_force_col1 = false -indent_func_call_param = false -indent_func_def_param = false -indent_func_proto_param = false -indent_func_class_param = false -indent_func_ctor_var_param = false -indent_template_param = false +indent_func_call_param = true +indent_func_def_param = true +indent_func_proto_param = true +indent_func_class_param = true +indent_func_ctor_var_param = true +indent_template_param = true indent_func_param_double = false indent_func_const = 0 indent_func_throw = 0 indent_member = 2 indent_member_single = true -indent_sing_line_comments = 0 +indent_single_line_comments_before = 0 indent_relative_single_line_comments = false indent_switch_case = 2 indent_switch_pp = true @@ -230,8 +232,8 @@ indent_label = 0 indent_access_spec = -2 indent_access_spec_body = false indent_paren_nl = false -indent_paren_close = 0 -indent_bool_paren = false +indent_paren_close = 2 +indent_bool_paren = 0 indent_square_nl = false indent_align_assign = true indent_align_paren = true @@ -245,7 +247,8 @@ indent_single_after_return = false indent_ignore_asm_block = false # Newline adding and removing options -nl_collapse_empty_body = false +nl_collapse_empty_body = true +nl_collapse_empty_body_functions = true nl_assign_leave_one_liners = true nl_class_leave_one_liners = true nl_enum_leave_one_liners = true @@ -261,11 +264,11 @@ nl_end_of_file = force nl_end_of_file_min = 1 nl_assign_brace = ignore nl_tsquare_brace = remove # ...or ignore -nl_func_var_def_blk = 0 +nl_var_def_blk_end_func_top = 0 nl_typedef_blk_start = 0 nl_typedef_blk_end = 0 nl_typedef_blk_in = 0 -nl_var_def_blk_start = 2 +nl_var_def_blk_start = 1 nl_var_def_blk_end = 0 nl_var_def_blk_in = 0 nl_fcall_brace = force @@ -297,7 +300,7 @@ nl_brace_while = remove # ...or ??? nl_switch_brace = force nl_synchronized_brace = force nl_multi_line_cond = false -nl_multi_line_define = false +nl_multi_line_define = true nl_before_case = false nl_after_case = false nl_case_colon_brace = force @@ -309,8 +312,8 @@ nl_template_func_def = force nl_template_func_decl = ignore nl_template_var = remove nl_class_brace = force -nl_class_init_args = ignore -nl_constr_init_args = ignore +nl_class_init_args = force +nl_constr_init_args = force nl_enum_own_lines = ignore nl_func_type_name = force nl_func_type_name_class = ignore @@ -322,8 +325,8 @@ nl_func_def_paren = remove nl_func_call_paren = remove nl_func_decl_start = ignore nl_func_def_start = ignore -nl_func_decl_start_multi_line = false -nl_func_def_start_multi_line = false +nl_func_decl_start_multi_line = true +nl_func_def_start_multi_line = true nl_func_decl_args = ignore nl_func_def_args = ignore nl_func_decl_args_multi_line = false @@ -335,7 +338,7 @@ nl_func_def_end_multi_line = false nl_func_decl_empty = remove nl_func_def_empty = remove nl_func_call_empty = remove -nl_func_call_start_multi_line = false +nl_func_call_start_multi_line = true nl_func_call_args_multi_line = false nl_func_call_end_multi_line = false nl_fdef_brace = force @@ -352,9 +355,9 @@ nl_after_vbrace_open_empty = false nl_after_brace_close = true nl_after_vbrace_close = true nl_brace_struct_var = remove -nl_define_macro = false +nl_define_macro = true nl_squeeze_paren_close = true # check me -nl_squeeze_ifdef = false +nl_squeeze_ifdef = true nl_squeeze_ifdef_top_level = false nl_ds_struct_enum_cmt = false nl_ds_struct_enum_close_brace = false @@ -414,7 +417,7 @@ pos_class_colon = lead pos_constr_colon = lead # Line Splitting options -code_width = 79 +code_width = 80 ls_for_split_full = false ls_func_split_full = false ls_code_width = false @@ -422,7 +425,7 @@ ls_code_width = false # Code alignment (not left column spaces/tabs) align_keep_tabs = false align_with_tabs = false -align_on_tabstop = true +align_on_tabstop = false align_number_right = false align_keep_extra_space = false align_func_params = false @@ -468,7 +471,7 @@ align_mix_var_proto = false align_single_line_func = false align_single_line_brace = false align_single_line_brace_gap = 0 -align_nl_cont = false +align_nl_cont = 1 align_pp_define_together = false align_pp_define_gap = 0 align_pp_define_span = 0 @@ -476,7 +479,7 @@ align_left_shift = true align_asm_colon = false # Comment modifications -cmt_width = 79 +cmt_width = 80 cmt_reflow_mode = 0 cmt_convert_tab_to_spaces = true cmt_indent_multi = true @@ -508,7 +511,9 @@ mod_add_long_switch_closebrace_comment = 0 mod_add_long_ifdef_endif_comment = 0 mod_add_long_ifdef_else_comment = 0 mod_sort_include = true +mod_remove_duplicate_include = true mod_move_case_break = true +mod_move_case_return = true mod_case_brace = ignore mod_remove_empty_return = true mod_enum_last_comma = add @@ -517,19 +522,20 @@ mod_enum_last_comma = add pp_indent = remove pp_indent_at_level = false pp_indent_count = 0 -pp_space = force -pp_space_count = 1 +pp_space_after = remove +pp_space_count = 0 pp_indent_region = 0 pp_region_indent_code = false pp_indent_if = 0 pp_if_indent_code = false pp_define_at_level = false pp_ignore_define_body = false +pp_multiline_define_body_indent = 0 # TODO next four default to true, but is that right? pp_indent_case = true pp_indent_func_def = true pp_indent_extern = true -pp_indent_brace = true +pp_indent_brace = 1 # Use or Do not Use options use_indent_func_call_param = true @@ -539,3 +545,8 @@ use_options_overriding_for_qt_macros = false # Project-specific parsing options set FUNC_WRAP TEST TEST_F TEST_P TYPED_TEST +set func_call_user LOG_TRACE +set func_call_user LOG_INFO +set func_call_user LOG_DEBUG +set func_call_user LOG_WARN +set func_call_user LOG_ERROR From e2df751057408a9235389637b7ba3c48c180a623 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 26 Jun 2023 13:18:53 -0500 Subject: [PATCH 33/51] Implement lookahead --- arrows/ffmpeg/ffmpeg_video_input.cxx | 384 +++++++++++++++++--- arrows/ffmpeg/ffmpeg_video_output.cxx | 2 +- arrows/ffmpeg/ffmpeg_video_raw_image.cxx | 6 +- arrows/ffmpeg/ffmpeg_video_raw_image.h | 3 + arrows/ffmpeg/ffmpeg_video_raw_metadata.cxx | 6 + arrows/ffmpeg/ffmpeg_video_raw_metadata.h | 12 +- arrows/ffmpeg/ffmpeg_video_settings.cxx | 14 +- arrows/ffmpeg/ffmpeg_video_settings.h | 7 +- arrows/klv/CMakeLists.txt | 2 + arrows/klv/klv_stream_settings.cxx | 36 ++ arrows/klv/klv_stream_settings.h | 46 +++ doc/release-notes/master.txt | 2 + 12 files changed, 448 insertions(+), 72 deletions(-) create mode 100644 arrows/klv/klv_stream_settings.cxx create mode 100644 arrows/klv/klv_stream_settings.h diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index a0a692e5fe..d2b28a50c2 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -73,6 +73,8 @@ struct ffmpeg_klv_stream ffmpeg_klv_stream( ffmpeg_klv_stream const& ) = delete; ffmpeg_klv_stream( ffmpeg_klv_stream&& ) = delete; + klv::klv_stream_settings settings() const; + void send_packet( AVPacket* packet ); void advance( std::optional< uint64_t > backup_timestamp, @@ -84,6 +86,7 @@ struct ffmpeg_klv_stream AVStream* stream; std::vector< packet_uptr > buffer; + std::vector< packet_uptr > this_frame_buffer; std::vector< uint8_t > bytes; std::vector< klv::klv_packet > packets; klv::klv_timeline timeline; @@ -112,6 +115,34 @@ ::ffmpeg_klv_stream( AVStream* stream ) } } +// ---------------------------------------------------------------------------- +klv::klv_stream_settings +ffmpeg_klv_stream +::settings() const +{ + klv::klv_stream_settings result; + result.index = stream->index; + switch( stream->codecpar->profile ) + { +#if LIBAVCODEC_VERSION_MAJOR > 57 + case FF_PROFILE_KLVA_SYNC: + result.type = klv::KLV_STREAM_TYPE_SYNC; + break; + case FF_PROFILE_KLVA_ASYNC: + result.type = klv::KLV_STREAM_TYPE_ASYNC; + break; +#endif + default: + LOG_DEBUG( + kv::get_logger( "klv" ), + "Could not determine synchronicity of KLV stream " << stream->index + << "; reporting as asynchronous" ); + result.type = klv::KLV_STREAM_TYPE_ASYNC; + break; + } + return result; +} + // ---------------------------------------------------------------------------- void ffmpeg_klv_stream @@ -165,6 +196,7 @@ ffmpeg_klv_stream ::advance( std::optional< uint64_t > backup_timestamp, int64_t max_pts, int64_t max_pos ) { + this_frame_buffer.clear(); packets.clear(); for( auto it = buffer.begin(); it != buffer.end(); ) @@ -174,6 +206,7 @@ ::advance( ( packet.pts == AV_NOPTS_VALUE && packet.pos <= max_pos ) ) { bytes.insert( bytes.end(), packet.data, packet.data + packet.size ); + this_frame_buffer.emplace_back( std::move( *it ) ); it = buffer.erase( it ); } else @@ -291,6 +324,9 @@ class ffmpeg_video_input::priv kv::image_container_sptr convert_image(); kv::metadata_vector const& convert_metadata(); + ffmpeg_video_raw_image& get_raw_image(); + ffmpeg_video_raw_metadata& get_raw_metadata(); + open_video_state* parent; kv::logger_handle_t logger; @@ -299,10 +335,10 @@ class ffmpeg_video_input::priv kv::image_memory_sptr image_memory; kv::image_container_sptr image; - ffmpeg_video_raw_image_sptr raw_image; + kv::video_raw_image_sptr raw_image; std::optional< kv::metadata_vector > metadata; - ffmpeg_video_raw_metadata_sptr raw_metadata; + kv::video_raw_metadata_sptr raw_metadata; bool is_draining; }; @@ -357,12 +393,17 @@ class ffmpeg_video_input::priv int64_t start_ts; std::map< int64_t, klv::misp_timestamp > pts_to_misp_ts; + std::map< int64_t, int64_t > packet_pos_to_dts; + int64_t prev_frame_dts; + int64_t prev_video_dts; + std::multimap< int64_t, packet_uptr > lookahead; std::list< ffmpeg_klv_stream > klv_streams; kv::metadata_map_sptr all_metadata; std::optional< frame_state > frame; + bool lookahead_at_eof; bool at_eof; }; @@ -736,6 +777,22 @@ ::convert_metadata() return *metadata; } +// ---------------------------------------------------------------------------- +ffmpeg_video_raw_image& +ffmpeg_video_input::priv::frame_state +::get_raw_image() +{ + return dynamic_cast< ffmpeg_video_raw_image& >( *raw_image ); +} + +// ---------------------------------------------------------------------------- +ffmpeg_video_raw_metadata& +ffmpeg_video_input::priv::frame_state +::get_raw_metadata() +{ + return dynamic_cast< ffmpeg_video_raw_metadata& >( *raw_metadata ); +} + // ---------------------------------------------------------------------------- ffmpeg_video_input::priv::open_video_state::filter_parameters ::filter_parameters( AVCodecContext const& codec_context ) @@ -794,9 +851,14 @@ ::open_video_state( priv& parent, std::string const& path ) image_conversion_context{ nullptr }, start_ts{ 0 }, pts_to_misp_ts{}, + packet_pos_to_dts{}, + prev_frame_dts{ AV_NOPTS_VALUE }, + prev_video_dts{ AV_NOPTS_VALUE }, + lookahead{}, klv_streams{}, all_metadata{ nullptr }, frame{}, + lookahead_at_eof{ false }, at_eof{ false } { // Try to probe the file for stream information @@ -1178,94 +1240,245 @@ ::advance() frame.reset(); // Run through video until we can assemble a frame image - packet_uptr packet{ - throw_error_null( av_packet_alloc(), "Could not allocate packet" ) }; - while( !at_eof && !frame.has_value() ) + while( !frame.has_value() && !at_eof ) { - if( !new_frame.is_draining ) + // We need at least one video packet before we could expect another frame + auto first_video_it = lookahead.end(); + + // We want to make sure each stream has caught up before actually using the + // next video packet + std::vector< int64_t > most_recent_dts( + format_context->nb_streams, AV_NOPTS_VALUE ); + + // Take stock of packets we have in storage + for( auto it = lookahead.begin(); it != lookahead.end(); ++it ) + { + most_recent_dts.at( it->second->stream_index ) = + std::max( + most_recent_dts.at( it->second->stream_index ), it->first ); + if( first_video_it == lookahead.end() && + it->second->stream_index == video_stream->index ) + { + first_video_it = it; + } + } + + // Functor determining if we need to parse more of other streams before + // continuing with decoding the video + auto const looked_ahead_enough = + [ this, &most_recent_dts, &first_video_it ]() -> bool + { + if( first_video_it == lookahead.end() ) + { + return false; + } + + for( auto const& stream : klv_streams ) + { + auto const dts = most_recent_dts.at( stream.stream->index ); + if( dts == AV_NOPTS_VALUE || + dts <= first_video_it->first || + dts <= first_video_it->second->pts ) + { + return false; + } + } + + return true; + }; + + // Read until all streams are up-to-date with the video stream + packet_uptr packet{ + throw_error_null( av_packet_alloc(), "Could not allocate packet" ) }; + while( !lookahead_at_eof && !looked_ahead_enough() ) { - // Read next packet av_packet_unref( packet.get() ); + auto const read_err = av_read_frame( format_context.get(), packet.get() ); if( read_err == AVERROR_EOF ) { - // End of input. Tell this to decoder - avcodec_send_packet( codec_context.get(), nullptr ); - new_frame.is_draining = true; + // End of input + lookahead_at_eof = true; + break; } - else + throw_error_code( read_err, "Could not read next packet from file" ); + + // Sanity check for stream index + if( packet->stream_index < 0 || + static_cast< unsigned int >( packet->stream_index ) >= + most_recent_dts.size() ) { - throw_error_code( read_err, "Could not read next packet from file" ); + continue; + } + + // Process video packet + if( packet->stream_index == video_stream->index ) + { + // Need pts + if( packet->pts == AV_NOPTS_VALUE ) + { + LOG_ERROR( + parent->logger, + "Dropping video packet with invalid pts" ); + continue; + } - // Video packet - if( packet->stream_index == video_stream->index ) + // Replace any weird dts with a guess + if( packet->dts == AV_NOPTS_VALUE ) { - // Record packet as raw image - new_frame.raw_image->packets.emplace_back( - throw_error_null( - av_packet_alloc(), "Could not allocate packet" ) ); - throw_error_code( - av_packet_ref( - new_frame.raw_image->packets.back().get(), packet.get() ), - "Could not give packet to raw image cache" ); - - // Find MISP timestamp - for( auto const tag_type : { klv::MISP_TIMESTAMP_TAG_STRING, - klv::MISP_TIMESTAMP_TAG_UUID } ) + packet->dts = + ( prev_video_dts == AV_NOPTS_VALUE ) + ? 0 + : ( prev_video_dts + 1 ); + } + else if( + packet->dts == AV_NOPTS_VALUE || + packet->dts < prev_video_dts || + packet->dts > packet->pts ) + { + if( prev_video_dts != AV_NOPTS_VALUE && packet->duration > 0 ) { - auto it = - klv::find_misp_timestamp( - packet->data, packet->data + packet->size, tag_type ); - if( it != packet->data + packet->size ) - { - auto const timestamp = klv::read_misp_timestamp( it ); - pts_to_misp_ts.emplace( packet->pts, timestamp ); - break; - } + packet->dts = prev_video_dts + packet->duration; + packet->dts = std::min( packet->dts, packet->pts ); } - - // Send packet to decoder - auto const send_err = - avcodec_send_packet( codec_context.get(), packet.get() ); - if( send_err != AVERROR_INVALIDDATA ) + else { - throw_error_code( send_err, "Decoder rejected packet" ); + packet->dts = packet->pts; } } + prev_video_dts = packet->dts; + } - // KLV packet - for( auto& stream : klv_streams ) + // Guess the missing DTS field for asynchronous KLV + auto packet_dts = packet->dts; + for( auto& stream : klv_streams ) + { + if( packet->stream_index != stream.stream->index || + stream.settings().type != klv::KLV_STREAM_TYPE_ASYNC ) { - if( packet->stream_index != stream.stream->index ) - { - continue; - } + continue; + } + + packet_dts = + lookahead.empty() ? 0 : std::prev( lookahead.end() )->first; + break; + } + + // Put the packet in the lookahead buffer + auto new_packet = + throw_error_null( av_packet_alloc(), "Could not allocate packet" ); + auto const it = lookahead.emplace( packet_dts, std::move( new_packet ) ); + av_packet_move_ref( it->second.get(), packet.get() ); + + // Update stats for this stream + most_recent_dts.at( it->second->stream_index ) = + std::max( + packet_dts, most_recent_dts.at( it->second->stream_index ) ); + if( first_video_it == lookahead.end() && + it->second->stream_index == video_stream->index ) + { + first_video_it = it; + } + } + + // Couldn't find next video packet? Tell the decoder to flush any remaining + // buffered frames + if( first_video_it == lookahead.end() && + lookahead_at_eof && !new_frame.is_draining ) + { + avcodec_send_packet( codec_context.get(), nullptr ); + new_frame.is_draining = true; + } - // Record packet as raw KLV - new_frame.raw_metadata->packets.emplace_back( - throw_error_null( - av_packet_alloc(), "Could not allocate packet" ) ); - throw_error_code( - av_packet_ref( - new_frame.raw_metadata->packets.back().get(), packet.get() ), - "Could not give packet to raw metadata cache" ); - - // Decode packet - stream.send_packet( packet.get() ); + // Process next video packet, if there is one + if( first_video_it != lookahead.end() ) + { + packet = std::move( first_video_it->second ); + lookahead.erase( first_video_it ); + first_video_it = lookahead.end(); + + // Record packet as raw image + new_frame.get_raw_image().packets.emplace_back( + throw_error_null( + av_packet_alloc(), "Could not allocate packet" ) ); + throw_error_code( + av_packet_ref( + new_frame.get_raw_image().packets.back().get(), packet.get() ), + "Could not give packet to raw image cache" ); + packet_pos_to_dts.emplace( packet->pos, packet->dts ); + + // Find MISP timestamp + for( auto const tag_type : { klv::MISP_TIMESTAMP_TAG_STRING, + klv::MISP_TIMESTAMP_TAG_UUID } ) + { + auto it = + klv::find_misp_timestamp( + packet->data, packet->data + packet->size, tag_type ); + if( it != packet->data + packet->size ) + { + auto const timestamp = klv::read_misp_timestamp( it ); + pts_to_misp_ts.emplace( packet->pts, timestamp ); break; } } + + // Send packet to decoder + auto const send_err = + avcodec_send_packet( codec_context.get(), packet.get() ); + if( send_err != AVERROR_INVALIDDATA ) + { + throw_error_code( send_err, "Decoder rejected packet" ); + } } // Receive decoded frame auto const recv_err = avcodec_receive_frame( codec_context.get(), new_frame.frame.get() ); + auto dts_lookup_failed = false; switch( recv_err ) { case 0: // Success frame = std::move( new_frame ); + + // Look up the dts of the packet that contained this frame + if( auto const it = packet_pos_to_dts.find( frame->frame->pkt_pos ); + it != packet_pos_to_dts.end() ) + { + frame->get_raw_image().frame_dts = it->second; + prev_frame_dts = it->second; + packet_pos_to_dts.erase( it ); + } + else + { + LOG_DEBUG( + parent->logger, + "Raw frame dts lookup failed, likely due to corruption" ); + dts_lookup_failed = true; + frame->get_raw_image().frame_dts = prev_frame_dts; + + // We can't erase an entry from packet_pos_to_dts, since we don't + // know for sure which entry to erase + } + frame->get_raw_image().frame_pts = frame->frame->best_effort_timestamp; + frame->get_raw_image().is_keyframe = frame->frame->key_frame; + + // Clean up + if( frame->frame->key_frame && !dts_lookup_failed ) + { + auto const it = + packet_pos_to_dts.lower_bound( frame->frame->pkt_pos ); + if( it != packet_pos_to_dts.begin() ) + { + auto const cleanup_count = + std::distance( packet_pos_to_dts.begin(), it ); + LOG_DEBUG( + parent->logger, + "Cleaning up " << cleanup_count << " dts lookup entries" ); + packet_pos_to_dts.erase( packet_pos_to_dts.begin(), it ); + } + } break; case AVERROR_EOF: // End of file @@ -1282,6 +1495,38 @@ ::advance() } } + if( frame.has_value() ) + { + // Give the KLV stream processors all packets up to this new frame image + for( auto it = lookahead.begin(); it != lookahead.end(); ) + { + if( it->second->pts > frame->frame->best_effort_timestamp ) + { + ++it; + continue; + } + + auto found = false; + for( auto& stream : klv_streams ) + { + if( it->second->stream_index != stream.stream->index ) + { + continue; + } + + found = true; + stream.send_packet( it->second.get() ); + it = lookahead.erase( it ); + break; + } + + if( !found ) + { + ++it; + } + } + } + // Advance KLV for( auto& stream : klv_streams ) { @@ -1324,6 +1569,10 @@ ::seek( kv::frame_id_t frame_number ) for( size_t i = 0; i < maximum_attempts; ++i ) { // Clear current state + prev_frame_dts = AV_NOPTS_VALUE; + prev_video_dts = AV_NOPTS_VALUE; + lookahead.clear(); + lookahead_at_eof = false; at_eof = false; frame.reset(); for( auto& stream : klv_streams ) @@ -1596,7 +1845,10 @@ ::implementation_settings() const { ffmpeg_video_settings_uptr result{ new ffmpeg_video_settings{} }; result->frame_rate = frame_rate(); - result->klv_stream_count = klv_streams.size(); + for( auto const& stream : klv_streams ) + { + result->klv_streams.emplace_back( stream.settings() ); + } result->start_timestamp = format_context->start_time; if( codec_context ) @@ -1632,7 +1884,7 @@ ::ffmpeg_video_input() set_capability( kva::video_input::HAS_TIMEOUT, false ); set_capability( kva::video_input::IS_SEEKABLE, true ); set_capability( kva::video_input::HAS_RAW_IMAGE, true ); - set_capability( kva::video_input::HAS_RAW_METADATA, false ); + set_capability( kva::video_input::HAS_RAW_METADATA, true ); ffmpeg_init(); } @@ -1924,6 +2176,18 @@ ::raw_frame_metadata() return nullptr; } + for( auto& stream : d->video->klv_streams ) + { + for( auto& packet : stream.this_frame_buffer ) + { + ffmpeg_video_raw_metadata::packet_info info; + info.packet = std::move( packet ); + info.stream_settings = stream.settings(); + d->video->frame->get_raw_metadata().packets + .emplace_back( std::move( info ) ); + } + stream.this_frame_buffer.clear(); + } return d->video->frame->raw_metadata; } diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index 35b1583c99..85d8f71aef 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -397,7 +397,7 @@ ::implementation_settings() const result->frame_rate = d->video->video_stream->avg_frame_rate; avcodec_parameters_from_context( result->parameters.get(), d->video->codec_context.get() ); - result->klv_stream_count = 0; // TODO + result->klv_streams = {}; result->start_timestamp = d->video->format_context->start_time; return kwiver::vital::video_settings_uptr{ result }; } diff --git a/arrows/ffmpeg/ffmpeg_video_raw_image.cxx b/arrows/ffmpeg/ffmpeg_video_raw_image.cxx index 5ce6644d7c..25680101c9 100644 --- a/arrows/ffmpeg/ffmpeg_video_raw_image.cxx +++ b/arrows/ffmpeg/ffmpeg_video_raw_image.cxx @@ -14,7 +14,11 @@ namespace arrows { namespace ffmpeg { ffmpeg_video_raw_image -::ffmpeg_video_raw_image() : packets{} +::ffmpeg_video_raw_image() + : packets{}, + frame_dts{ AV_NOPTS_VALUE }, + frame_pts{ AV_NOPTS_VALUE }, + is_keyframe{ true } {} } // namespace ffmpeg diff --git a/arrows/ffmpeg/ffmpeg_video_raw_image.h b/arrows/ffmpeg/ffmpeg_video_raw_image.h index 5734f3c876..f087ac0fd8 100644 --- a/arrows/ffmpeg/ffmpeg_video_raw_image.h +++ b/arrows/ffmpeg/ffmpeg_video_raw_image.h @@ -33,6 +33,9 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_raw_image operator=( ffmpeg_video_raw_image const& ) = delete; std::list< packet_uptr > packets; + int64_t frame_dts; + int64_t frame_pts; + bool is_keyframe; }; using ffmpeg_video_raw_image_sptr = std::shared_ptr< ffmpeg_video_raw_image >; diff --git a/arrows/ffmpeg/ffmpeg_video_raw_metadata.cxx b/arrows/ffmpeg/ffmpeg_video_raw_metadata.cxx index 92777b5d13..266f797573 100644 --- a/arrows/ffmpeg/ffmpeg_video_raw_metadata.cxx +++ b/arrows/ffmpeg/ffmpeg_video_raw_metadata.cxx @@ -13,10 +13,16 @@ namespace arrows { namespace ffmpeg { +// ---------------------------------------------------------------------------- ffmpeg_video_raw_metadata ::ffmpeg_video_raw_metadata() : packets{} {} +// ---------------------------------------------------------------------------- +ffmpeg_video_raw_metadata::packet_info +::packet_info() : packet{}, stream_settings{} +{} + } // namespace ffmpeg } // namespace arrows diff --git a/arrows/ffmpeg/ffmpeg_video_raw_metadata.h b/arrows/ffmpeg/ffmpeg_video_raw_metadata.h index b3ba3fee5c..e900e1c30f 100644 --- a/arrows/ffmpeg/ffmpeg_video_raw_metadata.h +++ b/arrows/ffmpeg/ffmpeg_video_raw_metadata.h @@ -11,6 +11,8 @@ #include #include +#include + #include #include @@ -31,7 +33,15 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_raw_metadata ffmpeg_video_raw_metadata& operator=( ffmpeg_video_raw_metadata const& ) = delete; - std::list< packet_uptr > packets; + struct packet_info + { + packet_info(); + + packet_uptr packet; + klv::klv_stream_settings stream_settings; + }; + + std::list< packet_info > packets; }; using ffmpeg_video_raw_metadata_sptr = std::shared_ptr< ffmpeg_video_raw_metadata >; diff --git a/arrows/ffmpeg/ffmpeg_video_settings.cxx b/arrows/ffmpeg/ffmpeg_video_settings.cxx index 23727f0455..33a0538d5c 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.cxx +++ b/arrows/ffmpeg/ffmpeg_video_settings.cxx @@ -20,7 +20,7 @@ ffmpeg_video_settings ::ffmpeg_video_settings() : frame_rate{ 0, 1 }, parameters{ avcodec_parameters_alloc() }, - klv_stream_count{ 0 }, + klv_streams{}, start_timestamp{ AV_NOPTS_VALUE }, codec_options{} { @@ -36,7 +36,7 @@ ffmpeg_video_settings ::ffmpeg_video_settings( ffmpeg_video_settings const& other ) : frame_rate{ other.frame_rate }, parameters{ avcodec_parameters_alloc() }, - klv_stream_count{ other.klv_stream_count }, + klv_streams{ other.klv_streams }, start_timestamp{ other.start_timestamp }, codec_options{ other.codec_options } { @@ -50,7 +50,7 @@ ffmpeg_video_settings ::ffmpeg_video_settings( ffmpeg_video_settings&& other ) : frame_rate{ std::move( other.frame_rate ) }, parameters{ std::move( other.parameters ) }, - klv_stream_count{ std::move( other.klv_stream_count ) }, + klv_streams{ std::move( other.klv_streams ) }, start_timestamp{ other.start_timestamp }, codec_options{ std::move( other.codec_options ) } {} @@ -60,10 +60,10 @@ ffmpeg_video_settings ::ffmpeg_video_settings( size_t width, size_t height, AVRational frame_rate, - size_t klv_stream_count ) + std::vector< klv::klv_stream_settings > const& klv_streams ) : frame_rate( frame_rate ), parameters{ avcodec_parameters_alloc() }, - klv_stream_count{ klv_stream_count }, + klv_streams{ klv_streams }, start_timestamp{ AV_NOPTS_VALUE }, codec_options{} { @@ -91,7 +91,7 @@ ::operator=( ffmpeg_video_settings const& other ) throw_error_code( avcodec_parameters_copy( parameters.get(), other.parameters.get() ), "Could not copy codec parameters" ); - klv_stream_count = other.klv_stream_count; + klv_streams = other.klv_streams; start_timestamp = other.start_timestamp; codec_options = other.codec_options; return *this; @@ -104,7 +104,7 @@ ::operator=( ffmpeg_video_settings&& other ) { frame_rate = std::move( other.frame_rate ); parameters = std::move( other.parameters ); - klv_stream_count = std::move( other.klv_stream_count ); + klv_streams = std::move( other.klv_streams ); start_timestamp = std::move( other.start_timestamp ); codec_options = std::move( other.codec_options ); return *this; diff --git a/arrows/ffmpeg/ffmpeg_video_settings.h b/arrows/ffmpeg/ffmpeg_video_settings.h index 2c9f04ec4b..ebb551c88f 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.h +++ b/arrows/ffmpeg/ffmpeg_video_settings.h @@ -11,6 +11,8 @@ #include #include +#include + #include extern "C" { @@ -19,6 +21,7 @@ extern "C" { #include #include +#include namespace kwiver { @@ -36,7 +39,7 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings ffmpeg_video_settings( size_t width, size_t height, AVRational frame_rate, - size_t klv_stream_count ); + std::vector< klv::klv_stream_settings > const& klv_streams = {} ); ~ffmpeg_video_settings(); @@ -47,7 +50,7 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings AVRational frame_rate; codec_parameters_uptr parameters; - size_t klv_stream_count; + std::vector< klv::klv_stream_settings > klv_streams; int64_t start_timestamp; // In AV_TIME_BASE units std::map< std::string, std::string > codec_options; }; diff --git a/arrows/klv/CMakeLists.txt b/arrows/klv/CMakeLists.txt index 4acaadc04b..faf55a3517 100644 --- a/arrows/klv/CMakeLists.txt +++ b/arrows/klv/CMakeLists.txt @@ -17,6 +17,7 @@ set( sources klv_packet.cxx klv_read_write.cxx klv_set.cxx + klv_stream_settings.cxx klv_string.cxx klv_tag_traits.cxx klv_timeline.cxx @@ -77,6 +78,7 @@ set( public_headers klv_read_write.h klv_series.h klv_set.h + klv_stream_settings.h klv_string.h klv_timeline.h klv_types.h diff --git a/arrows/klv/klv_stream_settings.cxx b/arrows/klv/klv_stream_settings.cxx new file mode 100644 index 0000000000..4c26b21ef3 --- /dev/null +++ b/arrows/klv/klv_stream_settings.cxx @@ -0,0 +1,36 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Implementation of settings structure for the creation of a klv stream. + +#include "klv_stream_settings.h" + +#include + +namespace kwiver { + +namespace arrows { + +namespace klv { + +// ---------------------------------------------------------------------------- +klv_stream_settings +::klv_stream_settings() + : type{ KLV_STREAM_TYPE_ASYNC }, + index{ INT_MIN } +{} + +// ---------------------------------------------------------------------------- +bool operator==( klv_stream_settings const& lhs, + klv_stream_settings const& rhs ) +{ + return lhs.type == rhs.type && lhs.index == rhs.index; +} + +} // namespace klv + +} // namespace arrows + +} // namespace kwiver diff --git a/arrows/klv/klv_stream_settings.h b/arrows/klv/klv_stream_settings.h new file mode 100644 index 0000000000..3ea652b967 --- /dev/null +++ b/arrows/klv/klv_stream_settings.h @@ -0,0 +1,46 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Declaration of settings structure for the creation of a klv stream. + +#ifndef KWIVER_ARROWS_KLV_KLV_STREAM_SETTINGS_H_ +#define KWIVER_ARROWS_KLV_KLV_STREAM_SETTINGS_H_ + +#include + +namespace kwiver { + +namespace arrows { + +namespace klv { + +// ---------------------------------------------------------------------------- +/// Synchronicity of a KLV stream. +enum klv_stream_type { + KLV_STREAM_TYPE_SYNC, + KLV_STREAM_TYPE_ASYNC +}; + +// ---------------------------------------------------------------------------- +/// Parameters describing the general characteristics of a KLV stream. +struct KWIVER_ALGO_KLV_EXPORT klv_stream_settings { + klv_stream_settings(); + + klv_stream_type type; + int index; +}; + +// ---------------------------------------------------------------------------- +KWIVER_ALGO_KLV_EXPORT +bool operator==( klv_stream_settings const& lhs, + klv_stream_settings const& rhs ); + +} // namespace klv + +} // namespace arrows + +} // namespace kwiver + +#endif diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index bade6b42fd..f2e8d7335d 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -34,6 +34,8 @@ Arrows: FFmpeg * Optimized copying behavior when encoding frames. +* Implemented packet lookahead to ensure stream synchronization. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. From 06f133808ee2157d14e2b6190e5b40428083c359 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Tue, 17 Oct 2023 14:36:19 -0400 Subject: [PATCH 34/51] Don't rely on implicit inclusion of cstdint Explicitly include in headers using uint8_t. Expecting it to be indirectly included isn't reliable and can lead to build failures. --- vital/types/color.h | 2 ++ vital/util/text_codec_utf_8.cxx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/vital/types/color.h b/vital/types/color.h index d61b9b9cdb..04b802d210 100644 --- a/vital/types/color.h +++ b/vital/types/color.h @@ -10,6 +10,8 @@ #include +#include + namespace kwiver { namespace vital { diff --git a/vital/util/text_codec_utf_8.cxx b/vital/util/text_codec_utf_8.cxx index 3f5c504050..69a4249851 100644 --- a/vital/util/text_codec_utf_8.cxx +++ b/vital/util/text_codec_utf_8.cxx @@ -9,6 +9,8 @@ #include +#include + namespace kwiver { namespace vital { From 14b0dc8c26ec934ca37ebe6050db09bfc6069072 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Tue, 17 Oct 2023 14:38:17 -0400 Subject: [PATCH 35/51] Don't use value uninitialized Add explicit initialization of local variable in order to avoid a compiler warning that the value may be used uninitialized. --- arrows/klv/klv_1303.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrows/klv/klv_1303.hpp b/arrows/klv/klv_1303.hpp index 557b0690e1..69baed1092 100644 --- a/arrows/klv/klv_1303.hpp +++ b/arrows/klv/klv_1303.hpp @@ -434,7 +434,7 @@ ::read_typed( klv_read_iter_t& data, size_t length ) const // Array processing algorithm parameters std::unique_ptr< klv_data_format > format; uint64_t uint_bias; - element_t rle_default_element; + element_t rle_default_element = {}; switch( result.apa ) { case KLV_1303_APA_IMAP: From 05c593e888a98e567619cd999b7509f675213e57 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Tue, 17 Oct 2023 15:42:34 -0400 Subject: [PATCH 36/51] Add missing include to class_map Explicitly include in class_map.h. Expecting it to be indirectly included / relying on forward declarations of std::string is unreliable and in some instances results in extremely obscure error messages regarding partial specialization of standard-library hash entities. --- vital/types/class_map.h | 1 + 1 file changed, 1 insertion(+) diff --git a/vital/types/class_map.h b/vital/types/class_map.h index 0d0101b8ac..9b6bedf322 100644 --- a/vital/types/class_map.h +++ b/vital/types/class_map.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include From 630db302757fedc5de0c04dfc936c7a4f93d2394 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 16 Oct 2023 16:51:58 -0500 Subject: [PATCH 37/51] Add interfacing for copying uninterpreted data streams --- arrows/core/applets/transcode.cxx | 7 ++++ .../video_input_buffered_metadata_filter.cxx | 20 +++++++++-- .../video_input_buffered_metadata_filter.h | 1 + arrows/core/video_input_metadata_filter.cxx | 14 ++++++++ arrows/core/video_input_metadata_filter.h | 1 + arrows/ffmpeg/ffmpeg_video_input.cxx | 1 + doc/release-notes/master.txt | 7 ++++ vital/CMakeLists.txt | 2 ++ vital/algo/video_input.cxx | 9 +++++ vital/algo/video_input.h | 12 +++++++ vital/algo/video_output.cxx | 13 +++++++ vital/algo/video_output.h | 17 +++++++-- vital/types/video_uninterpreted_data.cxx | 21 +++++++++++ vital/types/video_uninterpreted_data.h | 35 +++++++++++++++++++ 14 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 vital/types/video_uninterpreted_data.cxx create mode 100644 vital/types/video_uninterpreted_data.h diff --git a/arrows/core/applets/transcode.cxx b/arrows/core/applets/transcode.cxx index 16bc9f569b..103d0aa530 100644 --- a/arrows/core/applets/transcode.cxx +++ b/arrows/core/applets/transcode.cxx @@ -190,6 +190,13 @@ ::run() } } + // Transcode uninterpreted data + auto const misc_data = input->uninterpreted_frame_data(); + if( misc_data ) + { + output->add_uninterpreted_data( *misc_data ); + } + // Transcode image if( cmd_args.count( "copy-video" ) ) { diff --git a/arrows/core/video_input_buffered_metadata_filter.cxx b/arrows/core/video_input_buffered_metadata_filter.cxx index 91b331e6b2..fce9cd4562 100644 --- a/arrows/core/video_input_buffered_metadata_filter.cxx +++ b/arrows/core/video_input_buffered_metadata_filter.cxx @@ -35,6 +35,7 @@ class video_input_buffered_metadata_filter::impl kv::timestamp timestamp; kv::image_container_sptr image; kv::video_raw_image_sptr raw_image; + kv::video_uninterpreted_data_sptr uninterpreted_data; }; kv::algo::video_input_sptr video_input; @@ -50,7 +51,8 @@ video_input_buffered_metadata_filter::impl::frame_info ::frame_info( kv::algo::video_input& input ) : timestamp{ input.frame_timestamp() }, image{ input.frame_image() }, - raw_image{ input.raw_frame_image() } + raw_image{ input.raw_frame_image() }, + uninterpreted_data{ input.uninterpreted_frame_data() } {} // ---------------------------------------------------------------------------- @@ -148,7 +150,8 @@ ::open( std::string name ) vi::HAS_ABSOLUTE_FRAME_TIME, vi::HAS_TIMEOUT, vi::HAS_RAW_IMAGE, - vi::HAS_RAW_METADATA, } ) + vi::HAS_RAW_METADATA, + vi::HAS_UNINTERPRETED_DATA, } ) { set_capability( capability, capabilities.capability( capability ) ); } @@ -345,6 +348,19 @@ ::frame_metadata() return d->frame_metadata; } +// ---------------------------------------------------------------------------- +vital::video_uninterpreted_data_sptr +video_input_buffered_metadata_filter +::uninterpreted_frame_data() +{ + if( end_of_video() || d->frames.empty() ) + { + return nullptr; + } + + return d->frames.front().uninterpreted_data; +} + // ---------------------------------------------------------------------------- kv::metadata_map_sptr video_input_buffered_metadata_filter diff --git a/arrows/core/video_input_buffered_metadata_filter.h b/arrows/core/video_input_buffered_metadata_filter.h index ef4ed365b4..43c0a0d355 100644 --- a/arrows/core/video_input_buffered_metadata_filter.h +++ b/arrows/core/video_input_buffered_metadata_filter.h @@ -57,6 +57,7 @@ class KWIVER_ALGO_CORE_EXPORT video_input_buffered_metadata_filter vital::image_container_sptr frame_image() override; vital::video_raw_image_sptr raw_frame_image() override; vital::metadata_vector frame_metadata() override; + vital::video_uninterpreted_data_sptr uninterpreted_frame_data() override; vital::metadata_map_sptr metadata_map() override; vital::video_settings_uptr implementation_settings() const override; diff --git a/arrows/core/video_input_metadata_filter.cxx b/arrows/core/video_input_metadata_filter.cxx index f60b649d79..aba4fa623b 100644 --- a/arrows/core/video_input_metadata_filter.cxx +++ b/arrows/core/video_input_metadata_filter.cxx @@ -164,6 +164,7 @@ ::open( std::string name ) copy_capability( vi::IS_SEEKABLE ); copy_capability( vi::HAS_RAW_IMAGE ); copy_capability( vi::HAS_RAW_METADATA ); + copy_capability( vi::HAS_UNINTERPRETED_DATA ); } // ---------------------------------------------------------------------------- @@ -244,6 +245,19 @@ ::raw_frame_image() return m_d->video_input->raw_frame_image(); } +// ---------------------------------------------------------------------------- +kv::video_uninterpreted_data_sptr +video_input_metadata_filter +::uninterpreted_frame_data() +{ + if( !m_d->video_input ) + { + return nullptr; + } + + return m_d->video_input->uninterpreted_frame_data(); +} + // ---------------------------------------------------------------------------- kv::metadata_map_sptr video_input_metadata_filter diff --git a/arrows/core/video_input_metadata_filter.h b/arrows/core/video_input_metadata_filter.h index fb07e32254..ade51e898a 100644 --- a/arrows/core/video_input_metadata_filter.h +++ b/arrows/core/video_input_metadata_filter.h @@ -53,6 +53,7 @@ class KWIVER_ALGO_CORE_EXPORT video_input_metadata_filter kwiver::vital::image_container_sptr frame_image() override; kwiver::vital::video_raw_image_sptr raw_frame_image() override; kwiver::vital::metadata_vector frame_metadata() override; + kwiver::vital::video_uninterpreted_data_sptr uninterpreted_frame_data() override; kwiver::vital::metadata_map_sptr metadata_map() override; kwiver::vital::video_settings_uptr implementation_settings() const override; diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index d2b28a50c2..33a7052e56 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -1885,6 +1885,7 @@ ::ffmpeg_video_input() set_capability( kva::video_input::IS_SEEKABLE, true ); set_capability( kva::video_input::HAS_RAW_IMAGE, true ); set_capability( kva::video_input::HAS_RAW_METADATA, true ); + set_capability( kva::video_input::HAS_UNINTERPRETED_DATA, false ); ffmpeg_init(); } diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index f2e8d7335d..594d816de7 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -7,6 +7,10 @@ over the previous v1.8.0 release. Updates ------- +Vital: Types + +* Added interfaces for copying uninterpreted video data. + Arrows: Core * Implemented the csv_reader reading std::optional fields. @@ -16,6 +20,9 @@ Arrows: Core * Made the transcode applet ask for video settings slightly later, when they might be more accurate. +* Added pass-throughs for uninterpreted video data in the metadata_filter and + buffered_metadata_filter video inputs. + Arrows: FFmpeg * Added check for incoming raw video packets' timestamps and stream indices. diff --git a/vital/CMakeLists.txt b/vital/CMakeLists.txt index c8960bc77a..54646cc2ed 100644 --- a/vital/CMakeLists.txt +++ b/vital/CMakeLists.txt @@ -166,6 +166,7 @@ set( vital_public_headers types/video_raw_image.h types/video_raw_metadata.h types/video_settings.h + types/video_uninterpreted_data.h ) # ---------------------- @@ -236,6 +237,7 @@ set( vital_sources types/video_raw_image.cxx types/video_raw_metadata.cxx types/video_settings.cxx + types/video_uninterpreted_data.cxx types/uid.cxx ) diff --git a/vital/algo/video_input.cxx b/vital/algo/video_input.cxx index 8856f7889b..613302ff93 100644 --- a/vital/algo/video_input.cxx +++ b/vital/algo/video_input.cxx @@ -27,6 +27,7 @@ algorithm_capabilities::capability_name_t const video_input::HAS_TIMEOUT( "has-t algorithm_capabilities::capability_name_t const video_input::IS_SEEKABLE( "is-seekable" ); algorithm_capabilities::capability_name_t const video_input::HAS_RAW_IMAGE( "has-raw-image" ); algorithm_capabilities::capability_name_t const video_input::HAS_RAW_METADATA( "has-raw-metadata" ); +algorithm_capabilities::capability_name_t const video_input::HAS_UNINTERPRETED_DATA( "has-uninterpreted-data" ); // ---------------------------------------------------------------------------- @@ -68,6 +69,14 @@ ::raw_frame_metadata() return nullptr; } +// ---------------------------------------------------------------------------- +video_uninterpreted_data_sptr +video_input +::uninterpreted_frame_data() +{ + return nullptr; +} + // ---------------------------------------------------------------------------- video_settings_uptr video_input diff --git a/vital/algo/video_input.h b/vital/algo/video_input.h index 01079ca970..1c7a1b6ca9 100644 --- a/vital/algo/video_input.h +++ b/vital/algo/video_input.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -123,6 +124,7 @@ class VITAL_ALGO_EXPORT video_input static const algorithm_capabilities::capability_name_t IS_SEEKABLE; static const algorithm_capabilities::capability_name_t HAS_RAW_IMAGE; static const algorithm_capabilities::capability_name_t HAS_RAW_METADATA; + static const algorithm_capabilities::capability_name_t HAS_UNINTERPRETED_DATA; virtual ~video_input(); @@ -344,6 +346,16 @@ class VITAL_ALGO_EXPORT video_input /// \return Pointer to raw metadata. virtual video_raw_metadata_sptr raw_frame_metadata(); + /// Return an implementation-defined representation of uninterpreted data in + /// this frame. + /// + /// This method enables passage of miscellaneous data - such as audio, + /// unrecognized metadata, or secondary image streams - to a video output when + /// transcoding. + /// + /// \return Pointer to uninterpreted data. + virtual video_uninterpreted_data_sptr uninterpreted_frame_data(); + /// \brief Get metadata map for video. /// /// This method returns a metadata map for the video assuming the video is diff --git a/vital/algo/video_output.cxx b/vital/algo/video_output.cxx index 784386e3f5..682048c34d 100644 --- a/vital/algo/video_output.cxx +++ b/vital/algo/video_output.cxx @@ -27,6 +27,10 @@ video_output::SUPPORTS_FRAME_TIME( "supports-frame-time" ); const algorithm_capabilities::capability_name_t video_output::SUPPORTS_METADATA( "supports-metadata" ); +// ---------------------------------------------------------------------------- +const algorithm_capabilities::capability_name_t +video_output::SUPPORTS_UNINTERPRETED_DATA( "supports-uninterpreted-data" ); + // ---------------------------------------------------------------------------- video_output ::video_output() @@ -58,6 +62,15 @@ ::add_metadata( video_raw_metadata const& md ) "video_output: This implementation does not support raw metadata" }; } +// ---------------------------------------------------------------------------- +void +video_output +::add_uninterpreted_data( video_uninterpreted_data const& misc_data ) +{ + throw std::logic_error{ + "video_output: This implementation does not support uninterpreted data" }; +} + // ---------------------------------------------------------------------------- vital::video_settings_uptr video_output diff --git a/vital/algo/video_output.h b/vital/algo/video_output.h index 40126d7b94..b9571249ca 100644 --- a/vital/algo/video_output.h +++ b/vital/algo/video_output.h @@ -21,6 +21,7 @@ #include #include #include +#include #include @@ -61,6 +62,13 @@ class VITAL_ALGO_EXPORT video_output /// metadata. static const algorithm_capabilities::capability_name_t SUPPORTS_METADATA; + /// Writer can write uninterpreted data. + /// + /// This capability indicates if the implementation can take data which a + /// video input did not interpret and put it back in the video stream. + static const algorithm_capabilities::capability_name_t + SUPPORTS_UNINTERPRETED_DATA; + virtual ~video_output(); /// Return the name of this algorithm. @@ -132,11 +140,14 @@ class VITAL_ALGO_EXPORT video_output /// This method writes the raw metadata to the video stream. There is no /// guarantee that this functions correctly when intermixed with non-raw /// metadata. - /// - /// For implementations that do not support metadata, this method does - /// nothing. virtual void add_metadata( video_raw_metadata const& md ); + /// Add a frame of uninterpreted data to the video stream. + /// + /// This method writes the uninterpreted data to the video stream. + virtual void add_uninterpreted_data( + video_uninterpreted_data const& misc_data ); + /// Extract implementation-specific video encoding settings. /// /// The returned structure is intended to be passed to a video encoder of diff --git a/vital/types/video_uninterpreted_data.cxx b/vital/types/video_uninterpreted_data.cxx new file mode 100644 index 0000000000..5164de9644 --- /dev/null +++ b/vital/types/video_uninterpreted_data.cxx @@ -0,0 +1,21 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Implementation of base video uninterpreted data type. + +#include + +namespace kwiver { + +namespace vital { + +// ---------------------------------------------------------------------------- +video_uninterpreted_data +::~video_uninterpreted_data() +{} + +} // namespace vital + +} // namespace kwiver diff --git a/vital/types/video_uninterpreted_data.h b/vital/types/video_uninterpreted_data.h new file mode 100644 index 0000000000..76d94f3550 --- /dev/null +++ b/vital/types/video_uninterpreted_data.h @@ -0,0 +1,35 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Declaration of base video uninterpreted data type. + +#ifndef VITAL_VIDEO_UNINTERPRETED_DATA_H_ +#define VITAL_VIDEO_UNINTERPRETED_DATA_H_ + +#include + +#include + +namespace kwiver { + +namespace vital { + +// ---------------------------------------------------------------------------- +/// Base class for holding a single frame of uninterpreted data. +struct VITAL_EXPORT video_uninterpreted_data +{ + virtual ~video_uninterpreted_data(); +}; + +using video_uninterpreted_data_sptr = + std::shared_ptr< video_uninterpreted_data >; +using video_uninterpreted_data_uptr = + std::unique_ptr< video_uninterpreted_data >; + +} // namespace vital + +} // namespace kwiver + +#endif From 9cba327834564b1f8805d32f11cd85e6e7f2a868 Mon Sep 17 00:00:00 2001 From: Sedrak Date: Wed, 1 Nov 2023 20:26:49 +0400 Subject: [PATCH 38/51] Fixed issues in cpp examples For windows created by opencv waitkey is used for delay Default color scheme for opencv mat is BGR, fixed conversion issues from RGB to BGR accordingly --- examples/cpp/how_to_part_01_images.cpp | 20 ++++++++------------ examples/cpp/how_to_part_02_detections.cpp | 6 ++---- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/examples/cpp/how_to_part_01_images.cpp b/examples/cpp/how_to_part_01_images.cpp index 1ea62f3b92..d669c76d83 100644 --- a/examples/cpp/how_to_part_01_images.cpp +++ b/examples/cpp/how_to_part_01_images.cpp @@ -72,19 +72,17 @@ void how_to_part_01_images() // And that we tell our application CMake targets about OpenCV (See the CMakeLists.txt for this file) cv::Mat mat; // First, convert the image to an OpenCV image object - mat = kwiver::arrows::ocv::image_container::vital_to_ocv(ocv_img->get_image(), kwiver::arrows::ocv::image_container::RGB_COLOR ); + mat = kwiver::arrows::ocv::image_container::vital_to_ocv(ocv_img->get_image(), kwiver::arrows::ocv::image_container::BGR_COLOR ); cv::namedWindow("Image loaded by OpenCV", cv::WINDOW_AUTOSIZE);// Create a window for display. cv::imshow("Image loaded by OpenCV", mat); // Show our image inside it. - cv::waitKey(5); - kwiversys::SystemTools::Delay(2000); // Wait for 2s + cv::waitKey(2000); // Wait for 2s cv::destroyWindow("Image loaded by OpenCV"); // We can do the same, even if the image was originally loaded with VXL - mat = kwiver::arrows::ocv::image_container::vital_to_ocv(vxl_img->get_image(), kwiver::arrows::ocv::image_container::RGB_COLOR); + mat = kwiver::arrows::ocv::image_container::vital_to_ocv(vxl_img->get_image(), kwiver::arrows::ocv::image_container::BGR_COLOR); cv::namedWindow("Image loaded by VXL", cv::WINDOW_AUTOSIZE);// Create a window for display. cv::imshow("Image loaded by VXL", mat); // Show our image inside it. - cv::waitKey(5); - kwiversys::SystemTools::Delay(2000); // Wait for 2s + cv::waitKey(2000); // Wait for 2s cv::destroyWindow("Image loaded by VXL"); ////////////////// @@ -105,22 +103,20 @@ void how_to_part_01_images() std::vector ocv_imgs = ocv_split->split(vxl_img); for (kwiver::vital::image_container_sptr i : ocv_imgs) { - mat = kwiver::arrows::ocv::image_container::vital_to_ocv(i->get_image(), kwiver::arrows::ocv::image_container::RGB_COLOR); + mat = kwiver::arrows::ocv::image_container::vital_to_ocv(i->get_image(), kwiver::arrows::ocv::image_container::BGR_COLOR); cv::namedWindow("OpenCV Split Image", cv::WINDOW_AUTOSIZE);// Create a window for display. cv::imshow("OpenCV Split Image", mat); // Show our image inside it. - cv::waitKey(5); - kwiversys::SystemTools::Delay(2000); // Wait for 2s + cv::waitKey(2000); // Wait for 2s cv::destroyWindow("OpenCV Split Image"); } std::vector vxl_imgs = ocv_split->split(ocv_img); for (kwiver::vital::image_container_sptr i : vxl_imgs) { - mat = kwiver::arrows::ocv::image_container::vital_to_ocv(i->get_image(), kwiver::arrows::ocv::image_container::RGB_COLOR); + mat = kwiver::arrows::ocv::image_container::vital_to_ocv(i->get_image(), kwiver::arrows::ocv::image_container::BGR_COLOR); cv::namedWindow("VXL Split Image", cv::WINDOW_AUTOSIZE);// Create a window for display. cv::imshow("VXL Split Image", mat); // Show our image inside it. - cv::waitKey(5); - kwiversys::SystemTools::Delay(2000); // Wait for 2s + cv::waitKey(2000); // Wait for 2s cv::destroyWindow("VXL Split Image"); } diff --git a/examples/cpp/how_to_part_02_detections.cpp b/examples/cpp/how_to_part_02_detections.cpp index 29dce33426..f4171dadba 100644 --- a/examples/cpp/how_to_part_02_detections.cpp +++ b/examples/cpp/how_to_part_02_detections.cpp @@ -50,8 +50,7 @@ void how_to_part_02_detections() cv::Mat hough_mat = kwiver::arrows::ocv::image_container::vital_to_ocv(hough_img->get_image(), kwiver::arrows::ocv::image_container::RGB_COLOR); cv::namedWindow("Hough Detections", cv::WINDOW_AUTOSIZE);// Create a window for display. cv::imshow("Hough Detections", hough_mat); // Show our image inside it. - cv::waitKey(5); - kwiversys::SystemTools::Delay(2000); // Wait for 2s + cv::waitKey(2000); // Wait for 2s cv::destroyWindow("Hough Detections"); // Next, let's look at the detection data structures and we can make them @@ -116,8 +115,7 @@ void how_to_part_02_detections() cv::Mat mat = kwiver::arrows::ocv::image_container::vital_to_ocv(img_detections->get_image(), kwiver::arrows::ocv::image_container::RGB_COLOR); cv::namedWindow("Detections", cv::WINDOW_AUTOSIZE);// Create a window for display. cv::imshow("Detections", mat); // Show our image inside it. - cv::waitKey(5); - kwiversys::SystemTools::Delay(2000); // Wait for 2s + cv::waitKey(2000); // Wait for 2s cv::destroyWindow("Detections"); kwiver::vital::algo::detected_object_set_output_sptr kpf_writer = kwiver::vital::algo::detected_object_set_output::create("kpf_output"); From 95c1ab71f271adaf999b5456657be0e2f5518391 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 17 Oct 2023 16:05:24 -0500 Subject: [PATCH 39/51] Add audio stream copying --- arrows/ffmpeg/CMakeLists.txt | 4 + .../ffmpeg/ffmpeg_audio_stream_settings.cxx | 88 +++++++ arrows/ffmpeg/ffmpeg_audio_stream_settings.h | 49 ++++ arrows/ffmpeg/ffmpeg_util.cxx | 8 +- arrows/ffmpeg/ffmpeg_util.h | 1 + arrows/ffmpeg/ffmpeg_video_input.cxx | 174 +++++++++++++- arrows/ffmpeg/ffmpeg_video_input.h | 1 + arrows/ffmpeg/ffmpeg_video_output.cxx | 224 ++++++++++++++++-- arrows/ffmpeg/ffmpeg_video_output.h | 3 + arrows/ffmpeg/ffmpeg_video_settings.cxx | 11 +- arrows/ffmpeg/ffmpeg_video_settings.h | 3 + .../ffmpeg_video_uninterpreted_data.cxx | 26 ++ .../ffmpeg/ffmpeg_video_uninterpreted_data.h | 47 ++++ .../ffmpeg/tests/test_video_output_ffmpeg.cxx | 141 +++++++++++ doc/release-notes/master.txt | 4 + test_data/videos/h264_audio.ts | Bin 0 -> 114116 bytes 16 files changed, 754 insertions(+), 30 deletions(-) create mode 100644 arrows/ffmpeg/ffmpeg_audio_stream_settings.cxx create mode 100644 arrows/ffmpeg/ffmpeg_audio_stream_settings.h create mode 100644 arrows/ffmpeg/ffmpeg_video_uninterpreted_data.cxx create mode 100644 arrows/ffmpeg/ffmpeg_video_uninterpreted_data.h create mode 100644 test_data/videos/h264_audio.ts diff --git a/arrows/ffmpeg/CMakeLists.txt b/arrows/ffmpeg/CMakeLists.txt index e72f848869..de3b5ed3b3 100644 --- a/arrows/ffmpeg/CMakeLists.txt +++ b/arrows/ffmpeg/CMakeLists.txt @@ -18,6 +18,7 @@ if (NOT FFMPEG_FOUND_SEVERAL) endif() set(ffmpeg_headers_public + ffmpeg_audio_stream_settings.h ffmpeg_cuda.h ffmpeg_init.h ffmpeg_util.h @@ -26,6 +27,7 @@ set(ffmpeg_headers_public ffmpeg_video_raw_image.cxx ffmpeg_video_raw_metadata.cxx ffmpeg_video_settings.h + ffmpeg_video_uninterpreted_data.h ) kwiver_install_headers( @@ -39,6 +41,7 @@ kwiver_install_headers( ) set(ffmpeg_sources + ffmpeg_audio_stream_settings.cxx ffmpeg_cuda.cxx ffmpeg_init.cxx ffmpeg_util.cxx @@ -47,6 +50,7 @@ set(ffmpeg_sources ffmpeg_video_raw_image.cxx ffmpeg_video_raw_metadata.cxx ffmpeg_video_settings.cxx + ffmpeg_video_uninterpreted_data.cxx ) if(KWIVER_ENABLE_CUDA) diff --git a/arrows/ffmpeg/ffmpeg_audio_stream_settings.cxx b/arrows/ffmpeg/ffmpeg_audio_stream_settings.cxx new file mode 100644 index 0000000000..399f6761e7 --- /dev/null +++ b/arrows/ffmpeg/ffmpeg_audio_stream_settings.cxx @@ -0,0 +1,88 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Definition of FFmpeg audio stream settings. + +#include + +namespace kwiver { + +namespace arrows { + +namespace ffmpeg { + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream_settings +::ffmpeg_audio_stream_settings() + : index{ -1 }, + parameters{ avcodec_parameters_alloc() }, + time_base{ 0, 1 } +{ + if( !parameters ) + { + throw std::runtime_error{ "Could not allocate AVCodecParameters" }; + } + parameters->codec_type = AVMEDIA_TYPE_AUDIO; +} + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream_settings +::ffmpeg_audio_stream_settings( ffmpeg_audio_stream_settings const& other ) + : parameters{} +{ + *this = other; +} + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream_settings +::ffmpeg_audio_stream_settings( ffmpeg_audio_stream_settings&& other ) + : parameters{} +{ + *this = std::move( other ); +} + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream_settings +::~ffmpeg_audio_stream_settings() +{} + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream_settings& +ffmpeg_audio_stream_settings +::operator=( ffmpeg_audio_stream_settings const& other ) +{ + index = other.index; + + parameters.reset( + throw_error_null( + avcodec_parameters_alloc(), + "Could not allocate AVCodecParameters" ) ); + + throw_error_code( + avcodec_parameters_copy( parameters.get(), other.parameters.get() ), + "Could not copy codec parameters" ); + + time_base = other.time_base; + + return *this; +} + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream_settings& +ffmpeg_audio_stream_settings +::operator=( ffmpeg_audio_stream_settings&& other ) +{ + index = std::move( other.index ); + parameters = std::move( other.parameters ); + time_base = std::move( other.time_base ); + + return *this; +} + +} // namespace ffmpeg + +} // namespace arrows + +} // namespace kwiver diff --git a/arrows/ffmpeg/ffmpeg_audio_stream_settings.h b/arrows/ffmpeg/ffmpeg_audio_stream_settings.h new file mode 100644 index 0000000000..541d69be47 --- /dev/null +++ b/arrows/ffmpeg/ffmpeg_audio_stream_settings.h @@ -0,0 +1,49 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Declaration of FFmpeg audio stream settings. + +#ifndef KWIVER_ARROWS_FFMPEG_FFMPEG_AUDIO_STREAM_SETTINGS_H_ +#define KWIVER_ARROWS_FFMPEG_FFMPEG_AUDIO_STREAM_SETTINGS_H_ + +#include +#include + +extern "C" { +#include +} + +namespace kwiver { + +namespace arrows { + +namespace ffmpeg { + +// ---------------------------------------------------------------------------- +struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_audio_stream_settings +{ + ffmpeg_audio_stream_settings(); + ffmpeg_audio_stream_settings( ffmpeg_audio_stream_settings const& other ); + ffmpeg_audio_stream_settings( ffmpeg_audio_stream_settings&& other ); + + ~ffmpeg_audio_stream_settings(); + + ffmpeg_audio_stream_settings& + operator=( ffmpeg_audio_stream_settings const& other ); + ffmpeg_audio_stream_settings& + operator=( ffmpeg_audio_stream_settings&& other ); + + int index; + codec_parameters_uptr parameters; + AVRational time_base; +}; + +} // namespace ffmpeg + +} // namespace arrows + +} // namespace kwiver + +#endif diff --git a/arrows/ffmpeg/ffmpeg_util.cxx b/arrows/ffmpeg/ffmpeg_util.cxx index 53ebc5c74d..f680439a12 100644 --- a/arrows/ffmpeg/ffmpeg_util.cxx +++ b/arrows/ffmpeg/ffmpeg_util.cxx @@ -127,7 +127,7 @@ DEFINE_DELETER( format_context, AVFormatContext ) // ---------------------------------------------------------------------------- DEFINE_DELETER( codec_context, AVCodecContext ) { - if( ptr->codec ) + if( ptr->codec && ptr->codec_type == AVMEDIA_TYPE_VIDEO ) { avcodec_flush_buffers( ptr ); } @@ -176,6 +176,12 @@ DEFINE_DELETER( hardware_device_context, AVBufferRef ) av_buffer_unref( &ptr ); } +// ---------------------------------------------------------------------------- +DEFINE_DELETER( bsf_context, AVBSFContext ) +{ + av_bsf_free( &ptr ); +} + #undef DEFINE_DELETER } // namespace ffmpeg diff --git a/arrows/ffmpeg/ffmpeg_util.h b/arrows/ffmpeg/ffmpeg_util.h index a4bc211a99..548570a6d8 100644 --- a/arrows/ffmpeg/ffmpeg_util.h +++ b/arrows/ffmpeg/ffmpeg_util.h @@ -92,6 +92,7 @@ DECLARE_PTRS( filter_graph, AVFilterGraph ) DECLARE_PTRS( filter_in_out, AVFilterInOut ) DECLARE_PTRS( sws_context, SwsContext ) DECLARE_PTRS( hardware_device_context, AVBufferRef ) +DECLARE_PTRS( bsf_context, AVBSFContext ) #undef DECLARE_PTRS diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index 33a7052e56..0761df0a37 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -12,6 +12,7 @@ #include "ffmpeg_video_raw_image.h" #include "ffmpeg_video_raw_metadata.h" #include "ffmpeg_video_settings.h" +#include "ffmpeg_video_uninterpreted_data.h" #include #include @@ -302,6 +303,54 @@ ::vital_metadata( uint64_t timestamp, bool smooth_packets ) return result; } +// ---------------------------------------------------------------------------- +struct ffmpeg_audio_stream +{ + ffmpeg_audio_stream( AVStream* stream ); + + ffmpeg_audio_stream( ffmpeg_audio_stream const& ) = delete; + ffmpeg_audio_stream( ffmpeg_audio_stream&& ) = delete; + + ffmpeg_audio_stream_settings settings() const; + + AVStream* stream; + codec_context_uptr codec_context; +}; + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream +::ffmpeg_audio_stream( AVStream* stream ) : stream{ stream } +{ + if( !stream ) + { + throw std::logic_error( "ffmpeg_audio_stream given null stream" ); + } + + auto const codec = + throw_error_null( + avcodec_find_decoder( stream->codecpar->codec_id ), + "Could not find audio decoder" ); + + codec_context.reset( + throw_error_null( avcodec_alloc_context3( codec ), + "Could not allocate codec context" ) ); +} + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream_settings +ffmpeg_audio_stream +::settings() const +{ + ffmpeg_audio_stream_settings result; + result.index = stream->index; + throw_error_code( + avcodec_parameters_copy( + result.parameters.get(), stream->codecpar ), + "Could not copy codec parameters" ); + result.time_base = stream->time_base; + return result; +} + } // namespace // ---------------------------------------------------------------------------- @@ -326,6 +375,7 @@ class ffmpeg_video_input::priv ffmpeg_video_raw_image& get_raw_image(); ffmpeg_video_raw_metadata& get_raw_metadata(); + ffmpeg_video_uninterpreted_data& get_uninterpreted_data(); open_video_state* parent; kv::logger_handle_t logger; @@ -340,6 +390,8 @@ class ffmpeg_video_input::priv std::optional< kv::metadata_vector > metadata; kv::video_raw_metadata_sptr raw_metadata; + kv::video_uninterpreted_data_sptr uninterpreted_data; + bool is_draining; }; @@ -401,6 +453,8 @@ class ffmpeg_video_input::priv std::list< ffmpeg_klv_stream > klv_streams; kv::metadata_map_sptr all_metadata; + std::list< ffmpeg_audio_stream > audio_streams; + std::optional< frame_state > frame; bool lookahead_at_eof; @@ -413,6 +467,7 @@ class ffmpeg_video_input::priv hardware_device_context_uptr hardware_device_context; bool klv_enabled; + bool audio_enabled; bool use_misp_timestamps; bool smooth_klv_packets; std::string unknown_stream_behavior; @@ -448,6 +503,7 @@ ::priv( ffmpeg_video_input& parent ) logger{ kv::get_logger( "ffmpeg_video_input" ) }, hardware_device_context{ nullptr }, klv_enabled{ true }, + audio_enabled{ true }, use_misp_timestamps{ false }, smooth_klv_packets{ false }, unknown_stream_behavior{ "klv" }, @@ -586,6 +642,7 @@ ::frame_state( open_video_state& parent ) raw_image{}, metadata{}, raw_metadata{}, + uninterpreted_data{}, is_draining{ false } { // Allocate frame containers @@ -597,6 +654,7 @@ ::frame_state( open_video_state& parent ) // Allocate raw data containers raw_image.reset( new ffmpeg_video_raw_image{} ); raw_metadata.reset( new ffmpeg_video_raw_metadata{} ); + uninterpreted_data.reset( new ffmpeg_video_uninterpreted_data{} ); } // ---------------------------------------------------------------------------- @@ -793,6 +851,15 @@ ::get_raw_metadata() return dynamic_cast< ffmpeg_video_raw_metadata& >( *raw_metadata ); } +// ---------------------------------------------------------------------------- +ffmpeg_video_uninterpreted_data& +ffmpeg_video_input::priv::frame_state +::get_uninterpreted_data() +{ + return + dynamic_cast< ffmpeg_video_uninterpreted_data& >( *uninterpreted_data ); +} + // ---------------------------------------------------------------------------- ffmpeg_video_input::priv::open_video_state::filter_parameters ::filter_parameters( AVCodecContext const& codec_context ) @@ -857,6 +924,7 @@ ::open_video_state( priv& parent, std::string const& path ) lookahead{}, klv_streams{}, all_metadata{ nullptr }, + audio_streams{}, frame{}, lookahead_at_eof{ false }, at_eof{ false } @@ -884,7 +952,7 @@ ::open_video_state( priv& parent, std::string const& path ) avformat_find_stream_info( format_context.get(), NULL ), "Could not read stream information" ); - // Find a video stream, and optionally any data streams + // Find a video stream, and optionally any data or audio streams for( auto const j : kvr::iota( format_context->nb_streams ) ) { auto const stream = format_context->streams[ j ]; @@ -930,6 +998,12 @@ ::open_video_state( priv& parent, std::string const& path ) LOG_INFO( logger, "Ignoring unknown stream " << stream->index ); } } + else if( + parent.audio_enabled && + params->codec_type == AVMEDIA_TYPE_AUDIO ) + { + audio_streams.emplace_back( stream ); + } } if( video_stream ) @@ -1273,12 +1347,37 @@ ::advance() return false; } + auto const first_video_pts = + av_rescale_q( + first_video_it->second->pts, + video_stream->time_base, + AVRational{ 1, AV_TIME_BASE } ); + + auto const first_video_end = + ( first_video_it->second->duration <= 0 ) + ? first_video_pts + : av_rescale_q( + first_video_it->second->pts + first_video_it->second->duration, + video_stream->time_base, + AVRational{ 1, AV_TIME_BASE } ); + for( auto const& stream : klv_streams ) { auto const dts = most_recent_dts.at( stream.stream->index ); if( dts == AV_NOPTS_VALUE || dts <= first_video_it->first || - dts <= first_video_it->second->pts ) + dts <= first_video_pts ) + { + return false; + } + } + + for( auto const& stream : audio_streams ) + { + auto const dts = most_recent_dts.at( stream.stream->index ); + if( dts == AV_NOPTS_VALUE || + dts <= first_video_it->first || + dts < first_video_end ) { return false; } @@ -1351,7 +1450,11 @@ ::advance() } // Guess the missing DTS field for asynchronous KLV - auto packet_dts = packet->dts; + auto packet_dts = + av_rescale_q( + packet->dts, + format_context->streams[ packet->stream_index ]->time_base, + AVRational{ 1, AV_TIME_BASE } ); for( auto& stream : klv_streams ) { if( packet->stream_index != stream.stream->index || @@ -1497,10 +1600,17 @@ ::advance() if( frame.has_value() ) { - // Give the KLV stream processors all packets up to this new frame image + // Give the non-video streams all packets up to this new frame image for( auto it = lookahead.begin(); it != lookahead.end(); ) { - if( it->second->pts > frame->frame->best_effort_timestamp ) + if( av_rescale_q( + it->second->pts, + format_context->streams[ it->second->stream_index ]->time_base, + AVRational{ 1, AV_TIME_BASE } ) > + av_rescale_q( + frame->frame->best_effort_timestamp, + video_stream->time_base, + AVRational{ 1, AV_TIME_BASE } ) ) { ++it; continue; @@ -1520,6 +1630,25 @@ ::advance() break; } + if( found ) + { + continue; + } + + for( auto& stream : audio_streams ) + { + if( it->second->stream_index != stream.stream->index ) + { + continue; + } + + found = true; + frame->get_uninterpreted_data().audio_packets.emplace_back( + std::move( it->second ) ); + it = lookahead.erase( it ); + break; + } + if( !found ) { ++it; @@ -1538,7 +1667,10 @@ ::advance() auto max_pos = INT64_MAX; if( frame.has_value() ) { - max_pts = frame->frame->best_effort_timestamp; + max_pts = av_rescale_q( + frame->frame->best_effort_timestamp, + video_stream->time_base, + stream.stream->time_base ); max_pos = frame->frame->pkt_pos; } @@ -1849,6 +1981,11 @@ ::implementation_settings() const { result->klv_streams.emplace_back( stream.settings() ); } + for( auto const& stream: audio_streams ) + { + result->audio_streams.emplace_back( stream.settings() ); + } + result->time_base = video_stream->time_base; result->start_timestamp = format_context->start_time; if( codec_context ) @@ -1885,7 +2022,7 @@ ::ffmpeg_video_input() set_capability( kva::video_input::IS_SEEKABLE, true ); set_capability( kva::video_input::HAS_RAW_IMAGE, true ); set_capability( kva::video_input::HAS_RAW_METADATA, true ); - set_capability( kva::video_input::HAS_UNINTERPRETED_DATA, false ); + set_capability( kva::video_input::HAS_UNINTERPRETED_DATA, true ); ffmpeg_init(); } @@ -1920,6 +2057,13 @@ ::get_configuration() const "the video file. This may be useful if only processing imagery." ); + config->set_value( + "audio_enabled", d->audio_enabled, + "When set to false, will not attempt to pass along any audio streams found " + "in the video file. This may be useful if no transcoding is to be done, or " + "if audio is to be dropped." + ); + config->set_value( "use_misp_timestamps", d->use_misp_timestamps, "When set to true, will attempt to use correlate KLV packet data to " @@ -1985,6 +2129,9 @@ ::set_configuration( kv::config_block_sptr in_config ) d->klv_enabled = config->get_value< bool >( "klv_enabled", d->klv_enabled ); + d->audio_enabled = + config->get_value< bool >( "audio_enabled", d->audio_enabled ); + d->use_misp_timestamps = config->get_value< bool >( "use_misp_timestamps", d->use_misp_timestamps ); @@ -2192,6 +2339,19 @@ ::raw_frame_metadata() return d->video->frame->raw_metadata; } +// ---------------------------------------------------------------------------- +kv::video_uninterpreted_data_sptr +ffmpeg_video_input +::uninterpreted_frame_data() +{ + if( !d->is_valid() ) + { + return nullptr; + } + + return d->video->frame->uninterpreted_data; +} + // ---------------------------------------------------------------------------- kv::metadata_map_sptr ffmpeg_video_input diff --git a/arrows/ffmpeg/ffmpeg_video_input.h b/arrows/ffmpeg/ffmpeg_video_input.h index ffc59f9816..b0d76f5456 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.h +++ b/arrows/ffmpeg/ffmpeg_video_input.h @@ -64,6 +64,7 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input ::kwiver::vital::video_raw_image_sptr raw_frame_image() override; ::kwiver::vital::metadata_vector frame_metadata() override; ::kwiver::vital::video_raw_metadata_sptr raw_frame_metadata() override; + ::kwiver::vital::video_uninterpreted_data_sptr uninterpreted_frame_data() override; ::kwiver::vital::metadata_map_sptr metadata_map() override; ::kwiver::vital::video_settings_uptr implementation_settings() const override; diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index 85d8f71aef..a417cf06cf 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -10,6 +10,7 @@ #include "arrows/ffmpeg/ffmpeg_video_output.h" #include "arrows/ffmpeg/ffmpeg_video_raw_image.h" #include "arrows/ffmpeg/ffmpeg_video_settings.h" +#include "arrows/ffmpeg/ffmpeg_video_uninterpreted_data.h" extern "C" { #include @@ -18,6 +19,7 @@ extern "C" { #include } +#include #include namespace kv = kwiver::vital; @@ -28,6 +30,65 @@ namespace arrows { namespace ffmpeg { +namespace { + +// ---------------------------------------------------------------------------- +struct ffmpeg_audio_stream +{ + ffmpeg_audio_stream( + AVFormatContext* format_context, + ffmpeg_audio_stream_settings const& settings ); + + ffmpeg_audio_stream( ffmpeg_audio_stream const& ) = delete; + ffmpeg_audio_stream( ffmpeg_audio_stream&& ) = delete; + + ffmpeg_audio_stream_settings settings; + AVStream* stream; +}; + +// ---------------------------------------------------------------------------- +ffmpeg_audio_stream +::ffmpeg_audio_stream( + AVFormatContext* format_context, + ffmpeg_audio_stream_settings const& settings ) + : settings{ settings }, + stream{ nullptr } +{ + auto const codec = + throw_error_null( + avcodec_find_encoder( settings.parameters->codec_id ), + "Could not find audio codec for stream ", settings.index ); + + codec_context_uptr codec_context{ + throw_error_null( + avcodec_alloc_context3( codec ), "Could not allocate codec context" ) }; + + throw_error_code( + avcodec_parameters_to_context( + codec_context.get(), settings.parameters.get() ) ); + + codec_context->time_base = settings.time_base; + + throw_error_code( + avcodec_open2( codec_context.get(), codec, nullptr ), + "Could not open audio codec" + ); + + stream = + throw_error_null( + avformat_new_stream( format_context, codec ), + "Could not allocate audio stream" ); + + throw_error_code( + avcodec_parameters_copy( stream->codecpar, settings.parameters.get() ), + "Could not copy codec parameters" + ); + + stream->time_base = codec_context->time_base; +} + +} // namespace + // ---------------------------------------------------------------------------- class ffmpeg_video_output::impl { @@ -45,11 +106,13 @@ class ffmpeg_video_output::impl open_video_state& operator=( open_video_state&& ) = default; - bool try_codec( ffmpeg_video_settings const& settings ); + bool try_codec(); void add_image( kv::image_container_sptr const& image, kv::timestamp const& ts ); void add_image( vital::video_raw_image const& image ); + void add_uninterpreted_data( + vital::video_uninterpreted_data const& misc_data ); bool write_next_packet(); void write_remaining_packets(); @@ -65,12 +128,16 @@ class ffmpeg_video_output::impl #else AVOutputFormat* output_format; #endif + ffmpeg_video_settings video_settings; AVStream* video_stream; AVStream* metadata_stream; codec_context_uptr codec_context; AVCodec const* codec; sws_context_uptr image_conversion_context; + bsf_context_uptr annex_b_bsf; int64_t prev_video_dts; + + std::list< ffmpeg_audio_stream > audio_streams; }; impl(); @@ -383,6 +450,15 @@ ::add_metadata( VITAL_UNUSED kwiver::vital::metadata const& md ) // TODO } +// ---------------------------------------------------------------------------- +void +ffmpeg_video_output +::add_uninterpreted_data( vital::video_uninterpreted_data const& misc_data ) +{ + d->assert_open( "add_image()" ); + d->video->add_uninterpreted_data( misc_data ); +} + // ---------------------------------------------------------------------------- vital::video_settings_uptr ffmpeg_video_output @@ -398,6 +474,11 @@ ::implementation_settings() const avcodec_parameters_from_context( result->parameters.get(), d->video->codec_context.get() ); result->klv_streams = {}; + for( auto const& stream : d->video->audio_streams ) + { + result->audio_streams.emplace_back( stream.settings ); + } + result->time_base = d->video->video_stream->time_base; result->start_timestamp = d->video->format_context->start_time; return kwiver::vital::video_settings_uptr{ result }; } @@ -411,6 +492,7 @@ ::open_video_state( frame_count{ 0 }, format_context{ nullptr }, output_format{ nullptr }, + video_settings{ settings }, video_stream{ nullptr }, metadata_stream{ nullptr }, codec_context{ nullptr }, @@ -431,6 +513,8 @@ ::open_video_state( // Set timestamp value to start at format_context->output_ts_offset = settings.start_timestamp; + format_context->flags |= AVFMT_FLAG_AUTO_BSF; + format_context->flags |= AVFMT_FLAG_GENPTS; // Prioritization scheme for codecs: // (1) Match ffmpeg settings passed to constructor if present @@ -489,7 +573,7 @@ ::open_video_state( for( auto const possible_codec : possible_codecs ) { codec = possible_codec; - if( try_codec( settings ) ) + if( try_codec() ) { break; } @@ -509,20 +593,26 @@ ::open_video_state( av_dump_format( format_context.get(), video_stream->index, video_name.c_str(), 1 ); + for( auto const& stream_settings : settings.audio_streams ) + { + audio_streams.emplace_back( format_context.get(), stream_settings ); + av_dump_format( + format_context.get(), + audio_streams.back().stream->index, video_name.c_str(), 1 ); + } + // Open streams throw_error_code( avio_open( &format_context->pb, video_name.c_str(), AVIO_FLAG_WRITE ), "Could not open `", video_name, "` for writing" ); - auto const output_status = - avformat_init_output( format_context.get(), nullptr ); - if( output_status == AVSTREAM_INIT_IN_WRITE_HEADER ) - { - throw_error_code( - avformat_write_header( format_context.get(), nullptr ), - "Could not write video header" ); - } - throw_error_code( output_status, "Could not initialize output stream" ); + throw_error_code( + avformat_write_header( format_context.get(), nullptr ), + "Could not write video header" ); + + throw_error_code( + avformat_init_output( format_context.get(), nullptr ), + "Could not initialize output stream" ); } // ---------------------------------------------------------------------------- @@ -547,7 +637,7 @@ ffmpeg_video_output::impl::open_video_state // ---------------------------------------------------------------------------- bool ffmpeg_video_output::impl::open_video_state -::try_codec( ffmpeg_video_settings const& settings ) +::try_codec() { LOG_DEBUG( parent->logger, "Trying output codec: " << pretty_codec_name( codec ) ); @@ -561,19 +651,19 @@ ::try_codec( ffmpeg_video_settings const& settings ) codec_context->thread_type = FF_THREAD_FRAME; // Fill in fields from given settings - if( codec->id == settings.parameters->codec_id ) + if( codec->id == video_settings.parameters->codec_id ) { throw_error_code( avcodec_parameters_to_context( - codec_context.get(), settings.parameters.get() ) ); + codec_context.get(), video_settings.parameters.get() ) ); } else { - codec_context->width = settings.parameters->width; - codec_context->height = settings.parameters->height; + codec_context->width = video_settings.parameters->width; + codec_context->height = video_settings.parameters->height; } - codec_context->time_base = av_inv_q( settings.frame_rate ); - codec_context->framerate = settings.frame_rate; + codec_context->time_base = av_inv_q( video_settings.frame_rate ); + codec_context->framerate = video_settings.frame_rate; // Fill in backup parameters from config if( codec_context->pix_fmt < 0 ) @@ -627,7 +717,7 @@ ::try_codec( ffmpeg_video_settings const& settings ) video_stream->codecpar->format = codec_context->pix_fmt; AVDictionary* codec_options = nullptr; - for( auto const& entry : settings.codec_options ) + for( auto const& entry : video_settings.codec_options ) { av_dict_set( &codec_options, entry.first.c_str(), entry.second.c_str(), 0 ); } @@ -771,6 +861,41 @@ ::add_image( kv::video_raw_image const& image ) { auto const& ffmpeg_image = dynamic_cast< ffmpeg_video_raw_image const& >( image ); + + // Initialize bitstream filters + if( !annex_b_bsf && + ( codec_context->codec_id == AV_CODEC_ID_H264 || + codec_context->codec_id == AV_CODEC_ID_H265 ) ) + { + // Find filter + auto const bsf_name = + ( codec_context->codec_id == AV_CODEC_ID_H264 ) + ? "h264_mp4toannexb" + : "hevc_mp4toannexb"; + auto const bsf = av_bsf_get_by_name( bsf_name ); + + if( bsf ) + { + // Allocate filter context + AVBSFContext* bsf_context = nullptr; + av_bsf_alloc( bsf, &bsf_context ); + annex_b_bsf.reset( + throw_error_null( bsf_context, "Could not allocate BSF context" ) ); + + // Fill in filter parameters + throw_error_code( + avcodec_parameters_copy( + annex_b_bsf->par_in, video_settings.parameters.get() ), + "Could not copy codec parameters" ); + annex_b_bsf->time_base_in = video_settings.time_base; + + // Initialize filter + throw_error_code( + av_bsf_init( annex_b_bsf.get() ), + "Could not initialize Annex B filter" ); + } + } + for( auto const& packet : ffmpeg_image.packets ) { // Ensure this packet has sensible timestamps or FFmpeg will complain @@ -785,14 +910,26 @@ ::add_image( kv::video_raw_image const& image ) continue; } + // Record this DTS for next time + prev_video_dts = packet->dts; + // Copy the packet so we can switch the video stream index packet_uptr tmp_packet{ throw_error_null( av_packet_clone( packet.get() ), "Could not copy video packet" ) }; tmp_packet->stream_index = video_stream->index; - // Record this DTS for next time - prev_video_dts = packet->dts; + // Convert MP4-compatible H.264/H.265 to TS-compatible + if( annex_b_bsf ) + { + throw_error_code( + av_bsf_send_packet( annex_b_bsf.get(), tmp_packet.get() ) ); + throw_error_code( + av_bsf_receive_packet( annex_b_bsf.get(), tmp_packet.get() ) ); + } + + av_packet_rescale_ts( + tmp_packet.get(), video_settings.time_base, video_stream->time_base ); // Write the packet throw_error_code( @@ -802,6 +939,51 @@ ::add_image( kv::video_raw_image const& image ) ++frame_count; } +// ---------------------------------------------------------------------------- +void +ffmpeg_video_output::impl::open_video_state +::add_uninterpreted_data( vital::video_uninterpreted_data const& misc_data ) +{ + auto const& ffmpeg_data = + dynamic_cast< ffmpeg_video_uninterpreted_data const& >( misc_data ); + + for( auto const& packet : ffmpeg_data.audio_packets ) + { + for( auto const& stream : audio_streams ) + { + if( stream.settings.index != packet->stream_index ) + { + continue; + } + + // Copy the packet to switch the stream index + packet_uptr tmp_packet{ + throw_error_null( + av_packet_clone( packet.get() ), "Could not copy audio packet" ) }; + tmp_packet->stream_index = stream.stream->index; + + av_packet_rescale_ts( + tmp_packet.get(), stream.settings.time_base, stream.stream->time_base ); + + // Adjust for any global timestamp offset + auto const counter_offset = + av_rescale_q( + format_context->output_ts_offset, + AVRational{ 1, AV_TIME_BASE }, + stream.stream->time_base ); + tmp_packet->dts -= counter_offset; + tmp_packet->pts -= counter_offset; + + // Write the packet + throw_error_code( + av_interleaved_write_frame( format_context.get(), tmp_packet.get() ), + "Could not write frame to file" ); + + break; + } + } +} + // ---------------------------------------------------------------------------- bool ffmpeg_video_output::impl::open_video_state diff --git a/arrows/ffmpeg/ffmpeg_video_output.h b/arrows/ffmpeg/ffmpeg_video_output.h index 9f4ff3a239..e3716a5175 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.h +++ b/arrows/ffmpeg/ffmpeg_video_output.h @@ -57,6 +57,9 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_output void add_metadata( vital::metadata const& md ) override; + void add_uninterpreted_data( + vital::video_uninterpreted_data const& misc_data ) override; + vital::video_settings_uptr implementation_settings() const override; private: diff --git a/arrows/ffmpeg/ffmpeg_video_settings.cxx b/arrows/ffmpeg/ffmpeg_video_settings.cxx index 33a0538d5c..a43934af31 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.cxx +++ b/arrows/ffmpeg/ffmpeg_video_settings.cxx @@ -21,6 +21,8 @@ ::ffmpeg_video_settings() : frame_rate{ 0, 1 }, parameters{ avcodec_parameters_alloc() }, klv_streams{}, + audio_streams{}, + time_base{ 0, 1 }, start_timestamp{ AV_NOPTS_VALUE }, codec_options{} { @@ -37,6 +39,8 @@ ::ffmpeg_video_settings( ffmpeg_video_settings const& other ) : frame_rate{ other.frame_rate }, parameters{ avcodec_parameters_alloc() }, klv_streams{ other.klv_streams }, + audio_streams{ other.audio_streams }, + time_base{ other.time_base }, start_timestamp{ other.start_timestamp }, codec_options{ other.codec_options } { @@ -51,7 +55,9 @@ ::ffmpeg_video_settings( ffmpeg_video_settings&& other ) : frame_rate{ std::move( other.frame_rate ) }, parameters{ std::move( other.parameters ) }, klv_streams{ std::move( other.klv_streams ) }, - start_timestamp{ other.start_timestamp }, + audio_streams{ std::move( other.audio_streams ) }, + time_base{ std::move( other.time_base ) }, + start_timestamp{ std::move( other.start_timestamp ) }, codec_options{ std::move( other.codec_options ) } {} @@ -64,6 +70,7 @@ ::ffmpeg_video_settings( : frame_rate( frame_rate ), parameters{ avcodec_parameters_alloc() }, klv_streams{ klv_streams }, + time_base{ 0, 1 }, start_timestamp{ AV_NOPTS_VALUE }, codec_options{} { @@ -92,6 +99,7 @@ ::operator=( ffmpeg_video_settings const& other ) avcodec_parameters_copy( parameters.get(), other.parameters.get() ), "Could not copy codec parameters" ); klv_streams = other.klv_streams; + time_base = other.time_base; start_timestamp = other.start_timestamp; codec_options = other.codec_options; return *this; @@ -105,6 +113,7 @@ ::operator=( ffmpeg_video_settings&& other ) frame_rate = std::move( other.frame_rate ); parameters = std::move( other.parameters ); klv_streams = std::move( other.klv_streams ); + time_base = std::move( other.time_base ); start_timestamp = std::move( other.start_timestamp ); codec_options = std::move( other.codec_options ); return *this; diff --git a/arrows/ffmpeg/ffmpeg_video_settings.h b/arrows/ffmpeg/ffmpeg_video_settings.h index ebb551c88f..b5616a735c 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.h +++ b/arrows/ffmpeg/ffmpeg_video_settings.h @@ -9,6 +9,7 @@ #define KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_SETTINGS_H_ #include +#include #include #include @@ -51,6 +52,8 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings AVRational frame_rate; codec_parameters_uptr parameters; std::vector< klv::klv_stream_settings > klv_streams; + std::vector< ffmpeg_audio_stream_settings > audio_streams; + AVRational time_base; int64_t start_timestamp; // In AV_TIME_BASE units std::map< std::string, std::string > codec_options; }; diff --git a/arrows/ffmpeg/ffmpeg_video_uninterpreted_data.cxx b/arrows/ffmpeg/ffmpeg_video_uninterpreted_data.cxx new file mode 100644 index 0000000000..4accfe6f78 --- /dev/null +++ b/arrows/ffmpeg/ffmpeg_video_uninterpreted_data.cxx @@ -0,0 +1,26 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Implementation of FFmpeg video uninterpreted data. + +#include + +namespace kwiver { + +namespace arrows { + +namespace ffmpeg { + +// ---------------------------------------------------------------------------- +ffmpeg_video_uninterpreted_data +::ffmpeg_video_uninterpreted_data() + : audio_packets{} +{} + +} // namespace ffmpeg + +} // namespace arrows + +} // namespace kwiver diff --git a/arrows/ffmpeg/ffmpeg_video_uninterpreted_data.h b/arrows/ffmpeg/ffmpeg_video_uninterpreted_data.h new file mode 100644 index 0000000000..15fc46058b --- /dev/null +++ b/arrows/ffmpeg/ffmpeg_video_uninterpreted_data.h @@ -0,0 +1,47 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Declaration of FFmpeg video uninterpreted data. + +#ifndef KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_UNINTERPRETED_DATA_H_ +#define KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_UNINTERPRETED_DATA_H_ + +#include +#include + +#include + +#include +#include + +namespace kwiver { + +namespace arrows { + +namespace ffmpeg { + +// ---------------------------------------------------------------------------- +struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_uninterpreted_data + : public vital::video_uninterpreted_data +{ + ffmpeg_video_uninterpreted_data(); + + ffmpeg_video_uninterpreted_data( + ffmpeg_video_uninterpreted_data const& ) = delete; + ffmpeg_video_uninterpreted_data& + operator=( ffmpeg_video_uninterpreted_data const& ) = delete; + + std::list< packet_uptr > audio_packets; +}; +using ffmpeg_video_uninterpreted_data_sptr = + std::shared_ptr< ffmpeg_video_uninterpreted_data >; + +} // namespace ffmpeg + +} // namespace arrows + +} // namespace kwiver + +#endif diff --git a/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx b/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx index f9fcf04997..3ccc4ebc12 100644 --- a/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx +++ b/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -22,6 +23,7 @@ namespace kvr = kwiver::vital::range; kv::path_t g_data_dir; static std::string short_video_name = "videos/aphill_short.ts"; +static std::string audio_video_name = "videos/h264_audio.ts"; // ---------------------------------------------------------------------------- int @@ -141,6 +143,38 @@ expect_eq_images( kv::image const& src_image, EXPECT_LE( error, epsilon ); } +// ---------------------------------------------------------------------------- +void +expect_eq_audio( kv::video_uninterpreted_data_sptr const& src_data, + kv::video_uninterpreted_data_sptr const& tmp_data ) +{ + ASSERT_EQ( src_data == nullptr, tmp_data == nullptr ); + if( !src_data ) + { + return; + } + + auto const& src_packets = + dynamic_cast< ffmpeg::ffmpeg_video_uninterpreted_data const& >( *src_data ) + .audio_packets; + auto const& tmp_packets = + dynamic_cast< ffmpeg::ffmpeg_video_uninterpreted_data const& >( *tmp_data ) + .audio_packets; + ASSERT_EQ( src_packets.size(), tmp_packets.size() ); + + auto src_it = src_packets.begin(); + auto tmp_it = tmp_packets.begin(); + while( src_it != src_packets.begin() && tmp_it != tmp_packets.begin() ) + { + ASSERT_EQ( ( *src_it )->size, ( *tmp_it )->size ); + EXPECT_TRUE( + std::equal( ( *src_it )->data, ( *src_it )->data + ( *src_it )->size, + ( *tmp_it )->data ) ); + ++src_it; + ++tmp_it; + } +} + // ---------------------------------------------------------------------------- void expect_eq_videos( std::string const& src_path, std::string const& tmp_path, @@ -161,6 +195,10 @@ expect_eq_videos( std::string const& src_path, std::string const& tmp_path, EXPECT_EQ( src_ts.get_frame(), tmp_ts.get_frame() ); EXPECT_EQ( src_ts.get_time_usec(), tmp_ts.get_time_usec() ); + auto const src_data = src_is.uninterpreted_frame_data(); + auto const tmp_data = tmp_is.uninterpreted_frame_data(); + expect_eq_audio( src_data, tmp_data ); + auto const src_image = src_is.frame_image()->get_image(); auto const tmp_image = tmp_is.frame_image()->get_image(); expect_eq_images( src_image, tmp_image, image_epsilon ); @@ -226,6 +264,8 @@ TEST_F ( ffmpeg_video_output, round_trip ) expect_eq_videos( src_path, tmp_path, image_epsilon ); } +// ---------------------------------------------------------------------------- +// Similar to round_trip, but copying the video stream instead of re-encoding. TEST_F ( ffmpeg_video_output, round_trip_direct ) { auto const src_path = data_dir + "/" + short_video_name; @@ -276,6 +316,107 @@ TEST_F ( ffmpeg_video_output, round_trip_direct ) expect_eq_videos( src_path, tmp_path, image_epsilon ); } +// ---------------------------------------------------------------------------- +// Similar to round_trip, but for a test video with an audio stream. +TEST_F ( ffmpeg_video_output, round_trip_audio ) +{ + auto const src_path = data_dir + "/" + audio_video_name; + auto const tmp_path = + kwiver::testing::temp_file_name( "test-ffmpeg-output-", ".ts" ); + + kv::timestamp ts; + ffmpeg::ffmpeg_video_input is; + is.open( src_path ); + + ffmpeg::ffmpeg_video_output os; + os.open( tmp_path, is.implementation_settings().get() ); + _tmp_file_deleter tmp_file_deleter{ tmp_path }; + + // Write to a temporary file + for( is.next_frame( ts ); !is.end_of_video(); is.next_frame( ts ) ) + { + auto const image = is.frame_image(); + auto const uninterpreted_data = is.uninterpreted_frame_data(); + if( uninterpreted_data ) + { + os.add_uninterpreted_data( *uninterpreted_data ); + } + os.add_image( image, ts ); + } + os.close(); + is.close(); + + // Determined experimentally. 6.5 / 256 is non-negligable compression, but + // you can still see what the image is supposed to be + auto image_epsilon = 6.5; + + // Hardware decoding produces a lower-quality image + if( is.get_configuration()->get_value< bool >( "cuda_enabled", false ) ) + { + image_epsilon = 10.5; + } + + // Read the temporary file back in + expect_eq_videos( src_path, tmp_path, image_epsilon ); +} + +// ---------------------------------------------------------------------------- +// Similar to round_trip_direct, but for a test video with an audio stream. +TEST_F ( ffmpeg_video_output, round_trip_audio_direct ) +{ + auto const src_path = data_dir + "/" + short_video_name; + auto const tmp_path = + kwiver::testing::temp_file_name( "test-ffmpeg-output-", ".ts" ); + + kv::timestamp ts; + ffmpeg::ffmpeg_video_input is; + is.open( src_path ); + + ffmpeg::ffmpeg_video_output os; + os.open( tmp_path, is.implementation_settings().get() ); + _tmp_file_deleter tmp_file_deleter{ tmp_path }; + + // Skip this test if we can't write the output video in the same format as + // the input video + { + auto const src_generic_settings = is.implementation_settings(); + auto const src_settings = + dynamic_cast< ffmpeg::ffmpeg_video_settings const* >( + src_generic_settings.get() ); + auto const tmp_generic_settings = os.implementation_settings(); + auto const tmp_settings = + dynamic_cast< ffmpeg::ffmpeg_video_settings const* >( + tmp_generic_settings.get() ); + if( !src_settings || !tmp_settings || + src_settings->parameters->codec_id != + tmp_settings->parameters->codec_id ) + { + return; + } + } + + // Write to a temporary file + for( is.next_frame( ts ); !is.end_of_video(); is.next_frame( ts ) ) + { + auto const image = is.raw_frame_image(); + ASSERT_TRUE( image ); + auto const uninterpreted_data = is.uninterpreted_frame_data(); + if( uninterpreted_data ) + { + os.add_uninterpreted_data( *uninterpreted_data ); + } + os.add_image( *image ); + } + os.close(); + is.close(); + + // Images should be identical + auto const image_epsilon = 0.0; + + // Read the temporary file back in + expect_eq_videos( src_path, tmp_path, image_epsilon ); +} + // ---------------------------------------------------------------------------- // Ensure we can open a video output without knowing the implementation type. TEST_F ( ffmpeg_video_output, generic_open ) diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 594d816de7..abf1ae9c32 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -43,6 +43,10 @@ Arrows: FFmpeg * Implemented packet lookahead to ensure stream synchronization. +* Implemented copy of audio streams from input to output. + +* Fixed bugs causing invalid output when transcoding between .mp4 and .ts files. + Arrows: KLV * Ensured that NaN comparisons happen consistently across all data structures. diff --git a/test_data/videos/h264_audio.ts b/test_data/videos/h264_audio.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7d586449944d761ba8988e50b82ac18fd3af8c7 GIT binary patch literal 114116 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 E9}r_gqyPW_ literal 0 HcmV?d00001 From 6bae5100385fd317de38e2751d98fab58014e230 Mon Sep 17 00:00:00 2001 From: Aram Harutyunyan Date: Tue, 7 Nov 2023 17:46:11 +0400 Subject: [PATCH 40/51] Refactor plugin loader pointer to reference --- vital/plugin_loader/plugin_loader.cxx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vital/plugin_loader/plugin_loader.cxx b/vital/plugin_loader/plugin_loader.cxx index 5b37d57c2c..28d446e821 100644 --- a/vital/plugin_loader/plugin_loader.cxx +++ b/vital/plugin_loader/plugin_loader.cxx @@ -368,11 +368,15 @@ ::load_from_module( path_t const& path ) // Save currently opened library in map m_library_map[path] = lib_handle; - typedef void (* reg_fp_t)( plugin_loader* ); + typedef void (* reg_fp_t)( plugin_loader& ); + reg_fp_t reg_fp = reinterpret_cast< reg_fp_t > ( fp ); - ( *reg_fp )( m_parent ); // register plugins + if(m_parent) + { + ( *reg_fp )( *m_parent ); // register plugins + } } // ---------------------------------------------------------------------------- From ff52a5d7755da51934b91fb7eb13e7d7c9109161 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 7 Nov 2023 10:44:11 -0600 Subject: [PATCH 41/51] Increase output precision of floats when writing CSVs --- arrows/core/csv_io.cxx | 9 +++++++++ doc/release-notes/master.txt | 2 ++ 2 files changed, 11 insertions(+) diff --git a/arrows/core/csv_io.cxx b/arrows/core/csv_io.cxx index e8c6b51f5d..8bd5522e4d 100644 --- a/arrows/core/csv_io.cxx +++ b/arrows/core/csv_io.cxx @@ -16,6 +16,7 @@ #include #include +#include namespace kwiver { @@ -122,6 +123,14 @@ ::write( T const& value ) { m_ss << static_cast< uint64_t >( value ); } + else if constexpr( std::is_same_v< T, float > ) + { + m_ss << std::setprecision( FLT_DIG + 1 ) << value; + } + else if constexpr( std::is_same_v< T, double > ) + { + m_ss << std::setprecision( DBL_DIG + 1 ) << value; + } else { m_ss << value; diff --git a/doc/release-notes/master.txt b/doc/release-notes/master.txt index 594d816de7..8f161621fc 100644 --- a/doc/release-notes/master.txt +++ b/doc/release-notes/master.txt @@ -15,6 +15,8 @@ Arrows: Core * Implemented the csv_reader reading std::optional fields. +* Increased the precision of written floats and doubles in the csv_writer. + * Made the transcode applet's failure to open a video result in a more graceful exit. * Made the transcode applet ask for video settings slightly later, when they From 84c5109b47c5018b8b73decd5aa740ead0a2778e Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 7 Nov 2023 11:35:29 -0600 Subject: [PATCH 42/51] Document public members of settings structs --- arrows/ffmpeg/ffmpeg_audio_stream_settings.h | 12 ++++++++++ arrows/ffmpeg/ffmpeg_video_settings.h | 24 +++++++++++++++++++- arrows/klv/klv_stream_settings.h | 6 +++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/arrows/ffmpeg/ffmpeg_audio_stream_settings.h b/arrows/ffmpeg/ffmpeg_audio_stream_settings.h index 541d69be47..ff372aef45 100644 --- a/arrows/ffmpeg/ffmpeg_audio_stream_settings.h +++ b/arrows/ffmpeg/ffmpeg_audio_stream_settings.h @@ -22,6 +22,11 @@ namespace arrows { namespace ffmpeg { // ---------------------------------------------------------------------------- +/// Parameters describing the general characteristics of an audio stream. +/// +/// This struct will be filled in by ffmpeg_video_input, to be used by +/// ffmpeg_video_output when creating an audio stream. Members have been left +/// public so users may modify them before passing to ffmpeg_video_output. struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_audio_stream_settings { ffmpeg_audio_stream_settings(); @@ -35,8 +40,15 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_audio_stream_settings ffmpeg_audio_stream_settings& operator=( ffmpeg_audio_stream_settings&& other ); + /// Index of this stream in the input video. Does not determine the index in + /// the output video. int index; + + /// FFmpeg's parameters determining how the audio codec is set up. codec_parameters_uptr parameters; + + /// Time base of this stream in the input video. Not guaranteed to determine + /// the time base in the output video. AVRational time_base; }; diff --git a/arrows/ffmpeg/ffmpeg_video_settings.h b/arrows/ffmpeg/ffmpeg_video_settings.h index b5616a735c..f16c7cb2ee 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.h +++ b/arrows/ffmpeg/ffmpeg_video_settings.h @@ -31,6 +31,11 @@ namespace arrows { namespace ffmpeg { // ---------------------------------------------------------------------------- +/// Parameters defining the desired characteristics of a video file. +/// +/// This struct will be filled in by ffmpeg_video_input when transcoding, or +/// by the user when created a new video from scratch. Members have been left +/// public so the user may modify them before passing to ffmpeg_video_output. struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings : public vital::video_settings { @@ -49,12 +54,29 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings ffmpeg_video_settings& operator=( ffmpeg_video_settings&& other ); + /// Desired frame rate of the video. Must be set in most cases. AVRational frame_rate; + + /// FFmpeg's parameters determining how the video codec is set up. Notably, + /// height and width must be set before opening a video. codec_parameters_uptr parameters; + + /// Settings for each KLV stream to be inserted. std::vector< klv::klv_stream_settings > klv_streams; + + /// Settings for each audio stream to be inserted. std::vector< ffmpeg_audio_stream_settings > audio_streams; + + /// Time base of the video stream in the input video, if transcoding. Not + /// guaranteed to determine the time base in the output video. AVRational time_base; - int64_t start_timestamp; // In AV_TIME_BASE units + + /// Desired PTS of the first video frame, in AV_TIME_BASE units + /// (microseconds). For some formats, the actual first PTS may exceed this + /// value to ensure non-negative PTS or DTS. + int64_t start_timestamp; + + /// FFmpeg-defined string options passed to the video codec. std::map< std::string, std::string > codec_options; }; using ffmpeg_video_settings_uptr = std::unique_ptr< ffmpeg_video_settings >; diff --git a/arrows/klv/klv_stream_settings.h b/arrows/klv/klv_stream_settings.h index 3ea652b967..41349160ad 100644 --- a/arrows/klv/klv_stream_settings.h +++ b/arrows/klv/klv_stream_settings.h @@ -25,10 +25,16 @@ enum klv_stream_type { // ---------------------------------------------------------------------------- /// Parameters describing the general characteristics of a KLV stream. +/// +/// Members have been left public so users may modify them at their disgression. struct KWIVER_ALGO_KLV_EXPORT klv_stream_settings { klv_stream_settings(); + /// Whether this stream is synchronous or asynchronous. klv_stream_type type; + + /// Index of this stream in the input file. May not determine the index in + /// an output file. int index; }; From 366b80222062203c4614d01a6b6e0f4d1f24db36 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Tue, 7 Nov 2023 18:03:46 -0600 Subject: [PATCH 43/51] Fix packet synchronization bug --- arrows/ffmpeg/ffmpeg_video_output.cxx | 23 ++++++++++--------- arrows/ffmpeg/ffmpeg_video_settings.h | 6 ++--- .../ffmpeg/tests/test_video_output_ffmpeg.cxx | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_output.cxx b/arrows/ffmpeg/ffmpeg_video_output.cxx index a417cf06cf..35954f947e 100644 --- a/arrows/ffmpeg/ffmpeg_video_output.cxx +++ b/arrows/ffmpeg/ffmpeg_video_output.cxx @@ -511,8 +511,6 @@ ::open_video_state( } output_format = format_context->oformat; - // Set timestamp value to start at - format_context->output_ts_offset = settings.start_timestamp; format_context->flags |= AVFMT_FLAG_AUTO_BSF; format_context->flags |= AVFMT_FLAG_GENPTS; @@ -965,15 +963,6 @@ ::add_uninterpreted_data( vital::video_uninterpreted_data const& misc_data ) av_packet_rescale_ts( tmp_packet.get(), stream.settings.time_base, stream.stream->time_base ); - // Adjust for any global timestamp offset - auto const counter_offset = - av_rescale_q( - format_context->output_ts_offset, - AVRational{ 1, AV_TIME_BASE }, - stream.stream->time_base ); - tmp_packet->dts -= counter_offset; - tmp_packet->pts -= counter_offset; - // Write the packet throw_error_code( av_interleaved_write_frame( format_context.get(), tmp_packet.get() ), @@ -1002,6 +991,18 @@ ::write_next_packet() } throw_error_code( err, "Could not get next packet from encoder" ); + // Adjust for any global timestamp offset + if( video_settings.start_timestamp != AV_NOPTS_VALUE ) + { + auto const offset = + av_rescale_q( + video_settings.start_timestamp, + AVRational{ 1, AV_TIME_BASE }, + video_stream->time_base ); + packet->dts += offset; + packet->pts += offset; + } + // Succeeded; write to file throw_error_code( av_interleaved_write_frame( format_context.get(), packet.get() ), diff --git a/arrows/ffmpeg/ffmpeg_video_settings.h b/arrows/ffmpeg/ffmpeg_video_settings.h index f16c7cb2ee..06075bebb8 100644 --- a/arrows/ffmpeg/ffmpeg_video_settings.h +++ b/arrows/ffmpeg/ffmpeg_video_settings.h @@ -71,9 +71,9 @@ struct KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_settings /// guaranteed to determine the time base in the output video. AVRational time_base; - /// Desired PTS of the first video frame, in AV_TIME_BASE units - /// (microseconds). For some formats, the actual first PTS may exceed this - /// value to ensure non-negative PTS or DTS. + /// Start time of the input video, in AV_TIME_BASE units (microseconds). + /// This information is necessary for copied and newly-encoded packets to sync + /// correctly. int64_t start_timestamp; /// FFmpeg-defined string options passed to the video codec. diff --git a/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx b/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx index 3ccc4ebc12..42c5b8eeb2 100644 --- a/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx +++ b/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx @@ -364,7 +364,7 @@ TEST_F ( ffmpeg_video_output, round_trip_audio ) // Similar to round_trip_direct, but for a test video with an audio stream. TEST_F ( ffmpeg_video_output, round_trip_audio_direct ) { - auto const src_path = data_dir + "/" + short_video_name; + auto const src_path = data_dir + "/" + audio_video_name; auto const tmp_path = kwiver::testing::temp_file_name( "test-ffmpeg-output-", ".ts" ); From f7d6e2446b86e7e20e6a47462291f730349c53e9 Mon Sep 17 00:00:00 2001 From: Aram <148330443+aramSofthenge@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:07:43 +0400 Subject: [PATCH 44/51] Update coding style. Co-authored-by: Daniel Riehm --- vital/plugin_loader/plugin_loader.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vital/plugin_loader/plugin_loader.cxx b/vital/plugin_loader/plugin_loader.cxx index 28d446e821..bda05d20e2 100644 --- a/vital/plugin_loader/plugin_loader.cxx +++ b/vital/plugin_loader/plugin_loader.cxx @@ -373,7 +373,7 @@ ::load_from_module( path_t const& path ) reg_fp_t reg_fp = reinterpret_cast< reg_fp_t > ( fp ); - if(m_parent) + if( m_parent ) { ( *reg_fp )( *m_parent ); // register plugins } From f5e19959abe251a8eb6a726d1a15c37822b6444c Mon Sep 17 00:00:00 2001 From: aramSofthenge Date: Thu, 16 Nov 2023 18:10:20 +0400 Subject: [PATCH 45/51] Add error handling to mesh_io Introduced exceptions in mesh read functions for better error management. Updated tests to validate new behavior. --- vital/io/mesh_io.cxx | 10 +++++++--- vital/tests/test_mesh_io.cxx | 7 +++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/vital/io/mesh_io.cxx b/vital/io/mesh_io.cxx index 10dc674e77..362795c849 100644 --- a/vital/io/mesh_io.cxx +++ b/vital/io/mesh_io.cxx @@ -95,7 +95,8 @@ read_mesh(const std::string& filename) return read_obj(input_stream); } - return mesh_sptr(); + VITAL_THROW( invalid_file, filename, + "Unrecognized file extension for mesh file"); } /// Read a mesh from a PLY2 file @@ -104,6 +105,7 @@ read_ply2(const std::string& filename) { check_input_file(filename); std::ifstream input_stream(filename.c_str()); + return read_ply2(input_stream); } @@ -341,7 +343,8 @@ read_obj(std::istream& is) else { LOG_ERROR(logger, "improperly formed face line in OBJ: "< Date: Thu, 30 Nov 2023 16:15:40 +0400 Subject: [PATCH 46/51] Refined mesh file IO: Streamlined write ops, enhanced read_uv2 handling --- vital/io/mesh_io.cxx | 82 ++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/vital/io/mesh_io.cxx b/vital/io/mesh_io.cxx index 362795c849..9b3300c5e1 100644 --- a/vital/io/mesh_io.cxx +++ b/vital/io/mesh_io.cxx @@ -20,19 +20,18 @@ namespace vital { namespace { -/// Helper function to check that the output file name can be used -void -check_output_file(const std::string& filename) +/// Helper function to open the output file and return its stream +std::ofstream +open_output_file(const std::string& filename) { - // If the given path is a directory, we obviously can't write to it. + // Check if the given path is a directory if ( kwiversys::SystemTools::FileIsDirectory( filename ) ) { VITAL_THROW( file_write_exception, filename, - "Path given is a directory, can not write file." ); + "Path is a directory, cannot write file." ); } - // Check that the directory of the given filepath exists, creating necessary - // directories where needed. + // Ensure the directory of the given filepath exists, create if necessary std::string parent_dir = kwiversys::SystemTools::GetFilenamePath( kwiversys::SystemTools::CollapseFullPath( filename )); if ( ! kwiversys::SystemTools::FileIsDirectory( parent_dir ) ) @@ -40,37 +39,46 @@ check_output_file(const std::string& filename) if ( ! kwiversys::SystemTools::MakeDirectory( parent_dir ) ) { VITAL_THROW( file_write_exception, parent_dir, - "Attempted directory creation, but no directory created!" ); + "Failed to create directory." ); } } - // Open the output stream - std::ofstream output_stream(filename.c_str()); + // Open the output file stream + std::ofstream output_stream(filename.c_str(), std::fstream::out); + if (!output_stream) + { + VITAL_THROW( file_write_exception, filename, + "Could not open file for writing." ); + } + + return output_stream; } -/// Helper function to check that the input file name can be used -void -check_input_file(const std::string& filename) +/// Helper function to open the input file and return its stream +std::ifstream +open_input_file(const std::string& filename) { - // Check that file exists + // Check that file exists and is not a directory if ( ! kwiversys::SystemTools::FileExists( filename ) ) { VITAL_THROW( file_not_found_exception, filename, "File does not exist." ); } - else if ( kwiversys::SystemTools::FileIsDirectory( filename ) ) + else if ( kwiversys::SystemTools::FileIsDirectory( filename ) ) { VITAL_THROW( file_not_found_exception, filename, "Path given doesn't point to a regular file!" ); } - // Reading in input file data + // Open the input file stream std::ifstream input_stream( filename.c_str(), std::fstream::in ); if ( ! input_stream ) { VITAL_THROW( file_not_read_exception, filename, "Could not open file at given path." ); } + + return input_stream; } } @@ -79,9 +87,9 @@ check_input_file(const std::string& filename) mesh_sptr read_mesh(const std::string& filename) { - check_input_file(filename); - std::ifstream input_stream(filename.c_str()); + std::ifstream input_stream = open_input_file(filename); const std::string ext = kwiversys::SystemTools::GetFilenameLastExtension(filename); + if (ext == ".ply2") { return read_ply2(input_stream); @@ -103,8 +111,7 @@ read_mesh(const std::string& filename) mesh_sptr read_ply2(const std::string& filename) { - check_input_file(filename); - std::ifstream input_stream(filename.c_str()); + std::ifstream input_stream = open_input_file(filename); return read_ply2(input_stream); } @@ -141,8 +148,8 @@ read_ply2(std::istream& is) mesh_sptr read_ply(const std::string& filename) { - check_input_file(filename); - std::ifstream input_stream(filename.c_str()); + std::ifstream input_stream = open_input_file(filename); + return read_ply(input_stream); } @@ -200,8 +207,8 @@ mesh_sptr read_ply(std::istream& is) void write_ply2(const std::string& filename, const mesh& mesh) { - check_output_file(filename); - std::ofstream output_stream(filename.c_str()); + std::ofstream output_stream = open_output_file(filename); + write_ply2(output_stream, mesh); } @@ -232,10 +239,9 @@ void write_ply2(std::ostream& os, const mesh& mesh) /// Read texture coordinates from a UV2 file bool read_uv2(const std::string& filename, mesh& mesh) { - std::ifstream fh(filename.c_str()); - bool retval = read_uv2(fh,mesh); - fh.close(); - return retval; + std::ifstream input_stream = open_input_file(filename); + + return read_uv2(input_stream, mesh); } /// Read texture coordinates from a UV2 stream @@ -263,8 +269,8 @@ bool read_uv2(std::istream& is, mesh& mesh) mesh_sptr read_obj(const std::string& filename) { - check_input_file(filename); - std::ifstream input_stream(filename.c_str()); + std::ifstream input_stream = open_input_file(filename); + return read_obj(input_stream); } @@ -401,8 +407,8 @@ read_obj(std::istream& is) void write_obj(const std::string& filename, const mesh& mesh) { - check_output_file(filename); - std::ofstream output_stream(filename.c_str()); + std::ofstream output_stream = open_output_file(filename); + write_obj(output_stream, mesh); } @@ -499,8 +505,8 @@ write_obj(std::ostream& os, const mesh& mesh) void write_kml(const std::string& filename, const mesh& mesh) { - check_output_file(filename); - std::ofstream output_stream(filename.c_str()); + std::ofstream output_stream = open_output_file(filename); + write_kml(output_stream, mesh); } @@ -555,8 +561,8 @@ write_kml(std::ostream& os, const mesh& mesh) void write_kml_collada(const std::string& filename, const mesh& mesh) { - check_output_file(filename); - std::ofstream output_stream(filename.c_str()); + std::ofstream output_stream = open_output_file(filename); + write_kml_collada(output_stream, mesh); } @@ -715,8 +721,8 @@ write_kml_collada(std::ostream& os, const mesh& mesh) void write_vrml(const std::string& filename, const mesh& mesh) { - check_output_file(filename); - std::ofstream output_stream(filename.c_str()); + std::ofstream output_stream = open_output_file(filename); + write_vrml(output_stream, mesh); } From b1744e1f8e890c54d2a4aed0e39ee066929c8ce0 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Thu, 7 Dec 2023 11:23:40 -0600 Subject: [PATCH 47/51] Fix typos in metadata CSV column names --- arrows/core/metadata_map_io_csv.cxx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/arrows/core/metadata_map_io_csv.cxx b/arrows/core/metadata_map_io_csv.cxx index 1a280ea0d8..d54034ca07 100644 --- a/arrows/core/metadata_map_io_csv.cxx +++ b/arrows/core/metadata_map_io_csv.cxx @@ -163,13 +163,20 @@ special_column_names() { { kv::VITAL_META_SENSOR_LOCATION, 2 }, "Sensor Geodetic Altitude (meters)" }, - { { kv::VITAL_META_FRAME_CENTER, 0 }, + { { kv::VITAL_META_TARGET_LOCATION, 0 }, "Target Geodetic Location Longitude (EPSG:4326)" }, - { { kv::VITAL_META_FRAME_CENTER, 1 }, + { { kv::VITAL_META_TARGET_LOCATION, 1 }, "Target Geodetic Location Latitude (EPSG:4326)" }, - { { kv::VITAL_META_FRAME_CENTER, 2 }, + { { kv::VITAL_META_TARGET_LOCATION, 2 }, "Target Geodetic Location Altitude (meters)" }, + { { kv::VITAL_META_FRAME_CENTER, 0 }, + "Geodetic Frame Center Longitude (EPSG:4326)" }, + { { kv::VITAL_META_FRAME_CENTER, 1 }, + "Geodetic Frame Center Longitude (EPSG:4326)" }, + { { kv::VITAL_META_FRAME_CENTER, 2 }, + "Geodetic Frame Center Altitude (meters)" }, + { { kv::VITAL_META_CORNER_POINTS, 0 }, "Upper Left Corner Longitude (EPSG:4326)" }, { { kv::VITAL_META_CORNER_POINTS, 1 }, From e0ecea2b683d2a3d02afe221710ffb5c4e50f369 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Thu, 7 Dec 2023 13:19:05 -0600 Subject: [PATCH 48/51] Include avcodec/bsf.h --- arrows/ffmpeg/ffmpeg_util.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arrows/ffmpeg/ffmpeg_util.h b/arrows/ffmpeg/ffmpeg_util.h index 548570a6d8..07b45f1f56 100644 --- a/arrows/ffmpeg/ffmpeg_util.h +++ b/arrows/ffmpeg/ffmpeg_util.h @@ -14,6 +14,9 @@ extern "C" { #include +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 87, 100) +#include +#endif #include #include #include From 68badeec4098dc7a52266b205fdb7198ef3d8f29 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Fri, 10 Nov 2023 17:39:31 -0600 Subject: [PATCH 49/51] Add ffmpeg_video_input_clip --- arrows/ffmpeg/CMakeLists.txt | 2 + arrows/ffmpeg/ffmpeg_video_input.cxx | 446 ++++++++++++++---- arrows/ffmpeg/ffmpeg_video_input.h | 10 + arrows/ffmpeg/ffmpeg_video_input_clip.cxx | 438 +++++++++++++++++ arrows/ffmpeg/ffmpeg_video_input_clip.h | 76 +++ arrows/ffmpeg/register_algorithms.cxx | 2 + arrows/ffmpeg/tests/CMakeLists.txt | 5 +- arrows/ffmpeg/tests/common.h | 145 ++++++ .../tests/test_video_input_ffmpeg_clip.cxx | 234 +++++++++ .../ffmpeg/tests/test_video_output_ffmpeg.cxx | 114 +---- 10 files changed, 1272 insertions(+), 200 deletions(-) create mode 100644 arrows/ffmpeg/ffmpeg_video_input_clip.cxx create mode 100644 arrows/ffmpeg/ffmpeg_video_input_clip.h create mode 100644 arrows/ffmpeg/tests/common.h create mode 100644 arrows/ffmpeg/tests/test_video_input_ffmpeg_clip.cxx diff --git a/arrows/ffmpeg/CMakeLists.txt b/arrows/ffmpeg/CMakeLists.txt index de3b5ed3b3..5428e102dc 100644 --- a/arrows/ffmpeg/CMakeLists.txt +++ b/arrows/ffmpeg/CMakeLists.txt @@ -23,6 +23,7 @@ set(ffmpeg_headers_public ffmpeg_init.h ffmpeg_util.h ffmpeg_video_input.h + ffmpeg_video_input_clip.h ffmpeg_video_output.h ffmpeg_video_raw_image.cxx ffmpeg_video_raw_metadata.cxx @@ -46,6 +47,7 @@ set(ffmpeg_sources ffmpeg_init.cxx ffmpeg_util.cxx ffmpeg_video_input.cxx + ffmpeg_video_input_clip.cxx ffmpeg_video_output.cxx ffmpeg_video_raw_image.cxx ffmpeg_video_raw_metadata.cxx diff --git a/arrows/ffmpeg/ffmpeg_video_input.cxx b/arrows/ffmpeg/ffmpeg_video_input.cxx index 0761df0a37..acd4a08d30 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.cxx +++ b/arrows/ffmpeg/ffmpeg_video_input.cxx @@ -291,14 +291,9 @@ ::vital_metadata( uint64_t timestamp, bool smooth_packets ) klv_result.add< kv::VITAL_META_METADATA_ORIGIN >( "KLV" ); klv_result.add< kv::VITAL_META_VIDEO_DATA_STREAM_INDEX >( stream->index ); -#if LIBAVCODEC_VERSION_MAJOR > 57 - if( stream->codecpar->profile >= 0 ) - { - klv_result.add< kv::VITAL_META_VIDEO_DATA_STREAM_SYNCHRONOUS >( - stream->codecpar->profile == FF_PROFILE_KLVA_SYNC - ); - } -#endif + klv_result.add< kv::VITAL_META_VIDEO_DATA_STREAM_SYNCHRONOUS >( + settings().type == klv::KLV_STREAM_TYPE_SYNC + ); return result; } @@ -414,8 +409,10 @@ class ffmpeg_video_input::priv bool try_codec(); void init_filters( filter_parameters const& parameters ); - bool advance(); - void seek( kv::frame_id_t frame_number ); + bool advance( bool is_first_frame_of_seek = false ); + void clear_state_for_seek(); + void seek_to_start(); + void seek( kv::frame_id_t frame_number, seek_mode mode ); void set_video_metadata( kv::metadata& md ); double curr_time() const; double duration() const; @@ -443,12 +440,15 @@ class ffmpeg_video_input::priv sws_context_uptr image_conversion_context; + std::optional< kv::frame_id_t > frame_count; int64_t start_ts; + AVRational maybe_frame_rate; std::map< int64_t, klv::misp_timestamp > pts_to_misp_ts; std::map< int64_t, int64_t > packet_pos_to_dts; int64_t prev_frame_dts; int64_t prev_video_dts; std::multimap< int64_t, packet_uptr > lookahead; + std::list< packet_uptr > raw_image_buffer; std::list< ffmpeg_klv_stream > klv_streams; kv::metadata_map_sptr all_metadata; @@ -916,12 +916,15 @@ ::open_video_state( priv& parent, std::string const& path ) filter_sink_context{ nullptr }, filter_source_context{ nullptr }, image_conversion_context{ nullptr }, + frame_count{}, start_ts{ 0 }, + maybe_frame_rate{ 0, 0 }, pts_to_misp_ts{}, packet_pos_to_dts{}, prev_frame_dts{ AV_NOPTS_VALUE }, prev_video_dts{ AV_NOPTS_VALUE }, lookahead{}, + raw_image_buffer{}, klv_streams{}, all_metadata{ nullptr }, audio_streams{}, @@ -1189,24 +1192,14 @@ ::try_codec() } av_packet_unref( tmp_packet.get() ); } while( send_err || recv_err ); + auto const duration_q = + AVRational{ static_cast< int >( tmp_frame->pkt_duration ), 1 }; + maybe_frame_rate = + av_inv_q( av_mul_q( duration_q, video_stream->time_base ) ); start_ts = tmp_frame->best_effort_timestamp; } - // Seek back to start - auto seek_err = - av_seek_frame( - format_context.get(), -1, INT64_MIN, - AVSEEK_FLAG_BYTE | AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_ANY ); - if( seek_err < 0 ) - { - // Sometimes seeking by byte position is not allowed, so try by timestamp - throw_error_code( - av_seek_frame( - format_context.get(), -1, INT64_MIN, - AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_ANY ), - "Could not seek to beginning of video" ); - } - avcodec_flush_buffers( codec_context.get() ); + seek_to_start(); return true; } @@ -1297,7 +1290,7 @@ ::init_filters( filter_parameters const& parameters ) // ---------------------------------------------------------------------------- bool ffmpeg_video_input::priv::open_video_state -::advance() +::advance( bool is_first_frame_of_seek ) { if( at_eof ) { @@ -1314,6 +1307,7 @@ ::advance() frame.reset(); // Run through video until we can assemble a frame image + std::vector< int64_t > video_pos_list; while( !frame.has_value() && !at_eof ) { // We need at least one video packet before we could expect another frame @@ -1500,14 +1494,14 @@ ::advance() packet = std::move( first_video_it->second ); lookahead.erase( first_video_it ); first_video_it = lookahead.end(); + video_pos_list.emplace_back( packet->pos ); // Record packet as raw image - new_frame.get_raw_image().packets.emplace_back( + raw_image_buffer.emplace_back( throw_error_null( av_packet_alloc(), "Could not allocate packet" ) ); throw_error_code( - av_packet_ref( - new_frame.get_raw_image().packets.back().get(), packet.get() ), + av_packet_ref( raw_image_buffer.back().get(), packet.get() ), "Could not give packet to raw image cache" ); packet_pos_to_dts.emplace( packet->pos, packet->dts ); @@ -1544,11 +1538,31 @@ ::advance() case 0: // Success frame = std::move( new_frame ); + if( frame_count ) + { + ++( *frame_count ); + } // Look up the dts of the packet that contained this frame if( auto const it = packet_pos_to_dts.find( frame->frame->pkt_pos ); it != packet_pos_to_dts.end() ) { + for( auto jt = raw_image_buffer.begin(); + jt != raw_image_buffer.end(); ) + { + if( ( *jt )->dts <= it->second || + ( *jt )->dts <= frame->frame->pkt_dts ) + { + auto const next_jt = std::next( jt ); + frame->get_raw_image().packets.splice( + frame->get_raw_image().packets.end(), raw_image_buffer, jt ); + jt = next_jt; + } + else + { + ++jt; + } + } frame->get_raw_image().frame_dts = it->second; prev_frame_dts = it->second; packet_pos_to_dts.erase( it ); @@ -1603,17 +1617,35 @@ ::advance() // Give the non-video streams all packets up to this new frame image for( auto it = lookahead.begin(); it != lookahead.end(); ) { - if( av_rescale_q( - it->second->pts, - format_context->streams[ it->second->stream_index ]->time_base, - AVRational{ 1, AV_TIME_BASE } ) > - av_rescale_q( - frame->frame->best_effort_timestamp, - video_stream->time_base, - AVRational{ 1, AV_TIME_BASE } ) ) + auto const packet_pts = + av_rescale_q( + it->second->pts, + format_context->streams[ it->second->stream_index ]->time_base, + AVRational{ 1, AV_TIME_BASE } ); + auto const frame_pts = + av_rescale_q( + frame->frame->best_effort_timestamp, + video_stream->time_base, + AVRational{ 1, AV_TIME_BASE } ); + auto const frame_minus_one_pts = + av_rescale_q( + frame->frame->best_effort_timestamp - frame->frame->pkt_duration, + video_stream->time_base, + AVRational{ 1, AV_TIME_BASE } ); + auto const frame_plus_one_pts = + av_rescale_q( + frame->frame->best_effort_timestamp + frame->frame->pkt_duration, + video_stream->time_base, + AVRational{ 1, AV_TIME_BASE } ); + + int64_t min_pos = AV_NOPTS_VALUE; + if( auto const pos_it = + std::lower_bound( + video_pos_list.begin(), video_pos_list.end(), + frame->frame->pkt_pos ); + pos_it != video_pos_list.end() && pos_it != video_pos_list.begin() ) { - ++it; - continue; + min_pos = *std::prev( pos_it ); } auto found = false; @@ -1625,7 +1657,19 @@ ::advance() } found = true; - stream.send_packet( it->second.get() ); + if( packet_pts > frame_pts ) + { + ++it; + break; + } + + if( !is_first_frame_of_seek || this->frame_number() == 0 || + packet_pts >= frame_minus_one_pts || + ( it->second->pts == AV_NOPTS_VALUE && + it->second->pos >= min_pos ) ) + { + stream.send_packet( it->second.get() ); + } it = lookahead.erase( it ); break; } @@ -1643,8 +1687,17 @@ ::advance() } found = true; - frame->get_uninterpreted_data().audio_packets.emplace_back( - std::move( it->second ) ); + if( packet_pts > frame_plus_one_pts ) + { + ++it; + break; + } + + if( !is_first_frame_of_seek || this->frame_number() == 0 ) + { + frame->get_uninterpreted_data().audio_packets.emplace_back( + std::move( it->second ) ); + } it = lookahead.erase( it ); break; } @@ -1683,7 +1736,55 @@ ::advance() // ---------------------------------------------------------------------------- void ffmpeg_video_input::priv::open_video_state -::seek( kv::frame_id_t frame_number ) +::clear_state_for_seek() +{ + // Clear current state + frame_count.reset(); + prev_frame_dts = AV_NOPTS_VALUE; + prev_video_dts = AV_NOPTS_VALUE; + lookahead.clear(); + raw_image_buffer.clear(); + lookahead_at_eof = false; + at_eof = false; + frame.reset(); + for( auto& stream : klv_streams ) + { + stream.reset(); + } +} + +// ---------------------------------------------------------------------------- +void +ffmpeg_video_input::priv::open_video_state +::seek_to_start() +{ + clear_state_for_seek(); + frame_count.emplace( -1 ); + + auto const err = + av_seek_frame( + format_context.get(), -1, INT64_MIN, + AVSEEK_FLAG_BYTE | AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_ANY ); + if( err < 0 ) + { + // Sometimes seeking by byte position is not allowed, so try by timestamp + throw_error_code( + av_seek_frame( + format_context.get(), -1, INT64_MIN, + AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_ANY ), + "Could not seek to beginning of video" ); + } + + if( codec_context ) + { + avcodec_flush_buffers( codec_context.get() ); + } +} + +// ---------------------------------------------------------------------------- +void +ffmpeg_video_input::priv::open_video_state +::seek( kv::frame_id_t frame_number, seek_mode mode ) { if( frame_number == this->frame_number() ) { @@ -1697,35 +1798,20 @@ ::seek( kv::frame_id_t frame_number ) // more frames than that. auto const backstep_size = codec_context->gop_size ? codec_context->gop_size : 12; - constexpr size_t maximum_attempts = 5; - for( size_t i = 0; i < maximum_attempts; ++i ) - { - // Clear current state - prev_frame_dts = AV_NOPTS_VALUE; - prev_video_dts = AV_NOPTS_VALUE; - lookahead.clear(); - lookahead_at_eof = false; - at_eof = false; - frame.reset(); - for( auto& stream : klv_streams ) - { - stream.reset(); - } - + constexpr size_t maximum_attempts = 7; + for( size_t i = 0; i < maximum_attempts && frame_rate().num > 0; ++i ) + { // Increasing backstep intervals on further tries - size_t const backstep = i ? ( ( 1 << ( i - 1 ) ) * backstep_size ) : 1; + size_t const backstep = i ? ( ( 1 << ( i - 1 ) ) * backstep_size ) : 0; // Determine timestamp from frame number - int64_t converted_timestamp = frame_number - backstep; - converted_timestamp = - av_rescale( converted_timestamp, frame_rate().den, frame_rate().num ); - converted_timestamp = - av_rescale( - converted_timestamp, - video_stream->time_base.den, video_stream->time_base.num ); - converted_timestamp += start_ts; + auto converted_timestamp = + av_rescale_q( + frame_number - backstep, av_inv_q( frame_rate() ), + video_stream->time_base ) + start_ts; // Do the seek + clear_state_for_seek(); throw_error_code( av_seek_frame( format_context.get(), video_stream->index, converted_timestamp, @@ -1737,31 +1823,156 @@ ::seek( kv::frame_id_t frame_number ) } // Move forward through frames until we get to the desired frame + size_t advance_count = 0; do { - advance(); + advance( advance_count == 0 ); + ++advance_count; if( at_eof ) { throw_error( "Could not seek to frame ", frame_number + 1, ": " "End of file reached" ); } - } while( this->frame_number() < frame_number ); + } while( mode == SEEK_MODE_EXACT && this->frame_number() < frame_number ); // Check for success - if( this->frame_number() == frame_number ) + if( ( mode == SEEK_MODE_EXACT && this->frame_number() == frame_number ) || + ( mode != SEEK_MODE_EXACT && frame && frame->frame->key_frame && + this->frame_number() <= frame_number ) ) { - break; + if( parent->klv_enabled && advance_count <= 1 && false ) + { + auto const chosen_frame_number = this->frame_number(); + converted_timestamp = + av_rescale_q( + frame_number - backstep - 1, av_inv_q( frame_rate() ), + video_stream->time_base ) + start_ts; + + clear_state_for_seek(); + throw_error_code( + av_seek_frame( + format_context.get(), video_stream->index, converted_timestamp, + AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_ANY ), + "Could not seek to frame ", frame_number ); + if( codec_context ) + { + avcodec_flush_buffers( codec_context.get() ); + } + + advance_count = 0; + do + { + advance( advance_count == 0 ); + ++advance_count; + if( at_eof || this->frame_number() > chosen_frame_number ) + { + throw_error( + "Could not seek to frame ", frame_number + 1, ": " + "KLV re-seek failed" ); + } + } while( this->frame_number() < chosen_frame_number ); + + if( advance_count <= 1 ) + { + LOG_WARN( + parent->logger, + "KLV re-seek failed; " + "KLV reported for first frame may be incomplete" ); + } + } + + if( mode != SEEK_MODE_EXACT ) + { + auto& image_packets = frame->get_raw_image().packets; + for( auto it = image_packets.begin(); it != image_packets.end(); ) + { + if( !( ( *it )->flags & AV_PKT_FLAG_KEY ) ) + { + it = image_packets.erase( it ); + } + else + { + ++it; + } + } + } + + return; } } - // Check for failure - if( this->frame_number() > frame_number ) + // Backup slow strategy + if( !frame_count || *frame_count > frame_number || + mode == SEEK_MODE_KEYFRAME_BEFORE ) { - LOG_WARN( - kv::get_logger( "klv" ), - "Could not seek exactly to frame " << ( frame_number + 1 ) << ": " - "Ended up on frame " << ( this->frame_number() + 1 ) ); + seek_to_start(); + advance(); + } + + int64_t last_keyframe_pts = AV_NOPTS_VALUE; + int64_t last_keyframe_dts = AV_NOPTS_VALUE; + for( kv::frame_id_t i = *frame_count; i < frame_number; ++i ) + { + advance(); + + if( mode == SEEK_MODE_KEYFRAME_BEFORE && frame && frame->frame->key_frame ) + { + last_keyframe_dts = frame->get_raw_image().frame_dts; + last_keyframe_pts = frame->frame->pts; + } + + if( at_eof ) + { + throw_error( + "Could not seek to frame ", frame_number + 1, ": End of file reached" ); + } + } + + if( mode == SEEK_MODE_KEYFRAME_BEFORE ) + { + auto success = false; + for( auto const last_keyframe_ts : + { last_keyframe_pts, last_keyframe_dts } ) + { + if( last_keyframe_ts == AV_NOPTS_VALUE && frame_number > 0 ) + { + continue; + } + + clear_state_for_seek(); + throw_error_code( + av_seek_frame( + format_context.get(), video_stream->index, last_keyframe_ts, + AVSEEK_FLAG_BACKWARD ), + "Could not seek to frame ", frame_number + 1 ); + + if( codec_context ) + { + avcodec_flush_buffers( codec_context.get() ); + } + + do + { + advance(); + } while( + frame_number > 0 && frame && !frame->frame->key_frame && + frame->frame->pts < last_keyframe_pts ); + + if( frame_number <= 0 || ( frame && frame->frame->key_frame ) ) + { + success = true; + break; + } + } + + if( !success ) + { + throw_error( + "Could not seek to keyframe before frame ", frame_number + 1 ); + } + + frame_count.emplace( frame_number ); } } @@ -1915,9 +2126,13 @@ double ffmpeg_video_input::priv::open_video_state ::duration() const { - return - ( video_stream->start_time + video_stream->duration - start_ts ) * - av_q2d( video_stream->time_base ); + if( video_stream->start_time != AV_NOPTS_VALUE && video_stream->duration > 0 ) + { + return + ( video_stream->start_time + video_stream->duration - start_ts ) * + av_q2d( video_stream->time_base ); + } + return 0.0; } // ---------------------------------------------------------------------------- @@ -1925,12 +2140,11 @@ AVRational ffmpeg_video_input::priv::open_video_state ::frame_rate() const { - auto result = video_stream->avg_frame_rate; - if( !result.num ) + if( video_stream->avg_frame_rate.num ) { - result = video_stream->r_frame_rate; + return video_stream->avg_frame_rate; } - return result; + return maybe_frame_rate; } // ---------------------------------------------------------------------------- @@ -1938,6 +2152,11 @@ size_t ffmpeg_video_input::priv::open_video_state ::num_frames() const { + if( video_stream->nb_frames > 0 ) + { + return static_cast< size_t >( video_stream->nb_frames ); + } + return static_cast< size_t >( duration() * av_q2d( frame_rate() ) + 0.5 ); } @@ -1946,8 +2165,18 @@ kv::frame_id_t ffmpeg_video_input::priv::open_video_state ::frame_number() const { - if( !frame.has_value() || - frame->frame->best_effort_timestamp == AV_NOPTS_VALUE ) + if( !frame.has_value() ) + { + return -1; + } + + if( frame_count ) + { + return *frame_count; + } + + if( frame->frame->best_effort_timestamp == AV_NOPTS_VALUE || + frame_rate().num <= 0 ) { return -1; } @@ -1965,9 +2194,20 @@ ::timestamp() const { return {}; } - return kv::timestamp{ - static_cast< kv::time_usec_t >( curr_time() * 1000000.0 + 0.5 ), - frame_number() + 1 }; + + kv::timestamp ts; + if( frame->frame->best_effort_timestamp != AV_NOPTS_VALUE ) + { + ts.set_time_usec( + static_cast< kv::time_usec_t >( curr_time() * 1000000.0 + 0.5 ) ); + } + + if( frame_rate().num > 0 ) + { + ts.set_frame( frame_number() + 1 ); + } + + return ts; } // ---------------------------------------------------------------------------- @@ -2232,6 +2472,16 @@ ffmpeg_video_input ::seek_frame( kv::timestamp& ts, kv::timestamp::frame_t frame_number, uint32_t timeout ) +{ + return seek_frame_( ts, frame_number, SEEK_MODE_EXACT, timeout ); +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input +::seek_frame_( + kv::timestamp& ts, kv::timestamp::frame_t frame_number, + ffmpeg_video_input::seek_mode mode, uint32_t timeout ) { d->assert_open( "seek_frame()" ); @@ -2251,7 +2501,7 @@ ::seek_frame( kv::timestamp& ts, try { - d->video->seek( frame_number - 1 ); + d->video->seek( frame_number - 1, mode ); ts = frame_timestamp(); return true; } @@ -2411,6 +2661,22 @@ ::num_frames() const return d->video->num_frames(); } +// ---------------------------------------------------------------------------- +double +ffmpeg_video_input +::frame_rate() +{ + d->assert_open( "frame_rate()" ); + + auto const result = d->video->frame_rate(); + if( result.num > 0 && result.den > 0 ) + { + return av_q2d( result ); + } + + return -1.0; +} + // ---------------------------------------------------------------------------- kv::video_settings_uptr ffmpeg_video_input diff --git a/arrows/ffmpeg/ffmpeg_video_input.h b/arrows/ffmpeg/ffmpeg_video_input.h index b0d76f5456..d17fc157a3 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.h +++ b/arrows/ffmpeg/ffmpeg_video_input.h @@ -27,6 +27,11 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input : public vital::algo::video_input { public: + enum seek_mode { + SEEK_MODE_EXACT, + SEEK_MODE_KEYFRAME_BEFORE, + }; + /// Constructor ffmpeg_video_input(); virtual ~ffmpeg_video_input(); @@ -52,6 +57,7 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input bool seekable() const override; size_t num_frames() const override; + double frame_rate() override; bool next_frame( ::kwiver::vital::timestamp& ts, uint32_t timeout = 0 ) override; @@ -59,6 +65,10 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input ::kwiver::vital::timestamp::frame_t frame_number, uint32_t timeout = 0 ) override; + bool seek_frame_( vital::timestamp& ts, + vital::timestamp::frame_t frame_number, + seek_mode mode, uint32_t timeout = 0 ); + ::kwiver::vital::timestamp frame_timestamp() const override; ::kwiver::vital::image_container_sptr frame_image() override; ::kwiver::vital::video_raw_image_sptr raw_frame_image() override; diff --git a/arrows/ffmpeg/ffmpeg_video_input_clip.cxx b/arrows/ffmpeg/ffmpeg_video_input_clip.cxx new file mode 100644 index 0000000000..aabfd70fb9 --- /dev/null +++ b/arrows/ffmpeg/ffmpeg_video_input_clip.cxx @@ -0,0 +1,438 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Definition of FFmpeg video clipping utility. + +#include + +#include +#include +#include +#include +#include + +#include + +namespace kwiver { + +namespace arrows { + +namespace ffmpeg { + +// ---------------------------------------------------------------------------- +class ffmpeg_video_input_clip::impl +{ +public: + impl(); + + void seek_to_start(); + void filter_metadata( + vital::metadata_vector& metadata, vital::timestamp const& ts ) const; + vital::frame_id_t true_frame_begin() const; + vital::frame_id_t true_frame_end() const; + + std::shared_ptr< ffmpeg_video_input > video; + vital::frame_id_t frame_begin; + vital::frame_id_t frame_end; + + vital::metadata_map_sptr all_metadata; + std::string video_name; + vital::timestamp initial_timestamp; + int64_t initial_pts; + bool start_at_keyframe; + bool before_first_frame; +}; + +// ---------------------------------------------------------------------------- +ffmpeg_video_input_clip::impl +::impl() + : video{ new ffmpeg_video_input }, + frame_begin{ 0 }, + frame_end{ 0 }, + all_metadata{ nullptr }, + video_name{}, + initial_timestamp{}, + initial_pts{ AV_NOPTS_VALUE }, + start_at_keyframe{ false }, + before_first_frame{ true } +{} + +// ---------------------------------------------------------------------------- +void +ffmpeg_video_input_clip::impl +::seek_to_start() +{ + if( !video->seek_frame_( + initial_timestamp, frame_begin, + start_at_keyframe + ? ffmpeg_video_input::SEEK_MODE_KEYFRAME_BEFORE + : ffmpeg_video_input::SEEK_MODE_EXACT ) ) + { + throw_error( "Could not start video clip" ); + } +} + +// ---------------------------------------------------------------------------- +void +ffmpeg_video_input_clip::impl +::filter_metadata( + vital::metadata_vector& metadata, vital::timestamp const& ts ) const +{ + for( auto& md : metadata ) + { + if( md ) + { + md.reset( new vital::metadata( *md ) ); + md->set_timestamp( ts ); + } + } +} + +// ---------------------------------------------------------------------------- +vital::frame_id_t +ffmpeg_video_input_clip::impl +::true_frame_begin() const +{ + return + initial_timestamp.has_valid_frame() + ? initial_timestamp.get_frame() + : frame_begin; +} + +// ---------------------------------------------------------------------------- +vital::frame_id_t +ffmpeg_video_input_clip::impl +::true_frame_end() const +{ + return + video->num_frames() + ? std::min< vital::frame_id_t >( frame_end, video->num_frames() ) + : frame_end; +} + +// ---------------------------------------------------------------------------- +ffmpeg_video_input_clip +::ffmpeg_video_input_clip() + : d{ new impl } +{ + attach_logger( "ffmpeg_video_input_clip" ); +} + +// ---------------------------------------------------------------------------- +ffmpeg_video_input_clip +::~ffmpeg_video_input_clip() +{} + +// ---------------------------------------------------------------------------- +vital::config_block_sptr +ffmpeg_video_input_clip +::get_configuration() const +{ + auto config = vital::algo::video_input::get_configuration(); + config->set_value( + "frame_begin", d->frame_begin, + "First frame to include in the clip. Indexed from 1." ); + config->set_value( + "frame_end", d->frame_end, + "First frame not to include in the clip, i.e. one past the final frame in " + "the clip. Indexed from 1." ); + config->set_value( + "start_at_keyframe", d->start_at_keyframe, + "Start at the first keyframe before frame_begin, if frame_begin is not a " + "keyframe." ); + + vital::algo::video_input:: + get_nested_algo_configuration( "video_input", config, d->video ); + return config; +} + +// ---------------------------------------------------------------------------- +void +ffmpeg_video_input_clip +::set_configuration( vital::config_block_sptr in_config ) +{ + auto config = get_configuration(); + config->merge_config( in_config ); + + d->frame_begin = + config->get_value< vital::frame_id_t >( "frame_begin", d->frame_begin ); + d->frame_end = + config->get_value< vital::frame_id_t >( "frame_end", d->frame_end ); + d->start_at_keyframe = + config->get_value< bool >( "start_at_keyframe", d->start_at_keyframe ); + + d->video->set_configuration( config->subblock_view( "video_input:ffmpeg" ) ); +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input_clip +::check_configuration( vital::config_block_sptr config ) const +{ + if( !config->has_value( "frame_begin" ) || + !config->has_value( "frame_end" ) || + !config->has_value( "video_input:type" ) ) + { + return false; + } + + auto const frame_begin = + config->get_value< vital::frame_id_t >( "frame_begin" ); + auto const frame_end = + config->get_value< vital::frame_id_t >( "frame_end" ); + + return frame_begin <= frame_end && frame_begin > 0; +} + +// ---------------------------------------------------------------------------- +void +ffmpeg_video_input_clip +::open( std::string video_name ) +{ + d->video_name = video_name; + d->before_first_frame = true; + d->video->open( video_name ); + d->seek_to_start(); + auto const raw_image = + dynamic_cast< ffmpeg_video_raw_image const* >( + d->video->raw_frame_image().get() ); + if( !raw_image || raw_image->frame_pts == AV_NOPTS_VALUE ) + { + throw std::runtime_error( "Could not acquire PTS of first frame" ); + } + d->initial_pts = raw_image->frame_pts; + + auto const& capabilities = d->video->get_implementation_capabilities(); + using vi = vital::algo::video_input; + for( auto const& capability : { + vi::HAS_EOV, + vi::HAS_FRAME_NUMBERS, + vi::HAS_FRAME_DATA, + vi::HAS_FRAME_TIME, + vi::HAS_METADATA, + vi::HAS_ABSOLUTE_FRAME_TIME, + vi::HAS_TIMEOUT, + vi::IS_SEEKABLE, + vi::HAS_RAW_IMAGE, + vi::HAS_RAW_METADATA, + vi::HAS_UNINTERPRETED_DATA, } ) + { + set_capability( capability, capabilities.capability( capability ) ); + } +} + +// ---------------------------------------------------------------------------- +void +ffmpeg_video_input_clip +::close() +{ + d->all_metadata.reset(); + d->video->close(); +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input_clip +::end_of_video() const +{ + if( d->before_first_frame ) + { + return false; + } + + return + d->video->end_of_video() || + ( d->video->frame_timestamp().get_frame() >= d->frame_end ); +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input_clip +::good() const +{ + if( d->before_first_frame || end_of_video() ) + { + return false; + } + + return d->video->good(); +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input_clip +::seekable() const +{ + return d->video->seekable(); +} + +// ---------------------------------------------------------------------------- +size_t +ffmpeg_video_input_clip +::num_frames() const +{ + return d->true_frame_end() - d->true_frame_begin(); +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input_clip +::next_frame( vital::timestamp& ts, uint32_t timeout ) +{ + if( end_of_video() ) + { + ts = vital::timestamp{}; + return false; + } + + if( d->before_first_frame ) + { + d->before_first_frame = false; + ts = frame_timestamp(); + return true; + } + + vital::timestamp tmp_ts; + auto const success = + d->video->next_frame( tmp_ts, timeout ) && !end_of_video(); + ts = success ? frame_timestamp() : vital::timestamp{}; + return success; +} + +// ---------------------------------------------------------------------------- +bool +ffmpeg_video_input_clip +::seek_frame( + vital::timestamp& ts, vital::timestamp::frame_t frame_number, + uint32_t timeout ) +{ + if( frame_number > 1 ) + { + frame_number += d->true_frame_begin(); + frame_number = std::min( frame_number, d->true_frame_end() ); + return d->video->seek_frame( ts, frame_number, timeout ); + } + else + { + d->seek_to_start(); + return good(); + } +} + +// ---------------------------------------------------------------------------- +vital::timestamp +ffmpeg_video_input_clip +::frame_timestamp() const +{ + auto video_ts = d->video->frame_timestamp(); + vital::timestamp ts; + if( video_ts.has_valid_frame() ) + { + ts.set_frame( video_ts.get_frame() - d->true_frame_begin() + 1 ); + } + if( video_ts.has_valid_time() && d->initial_timestamp.has_valid_time() ) + { + ts.set_time_usec( + video_ts.get_time_usec() - d->initial_timestamp.get_time_usec() ); + } + return ts; +} + +// ---------------------------------------------------------------------------- +vital::image_container_sptr +ffmpeg_video_input_clip +::frame_image() +{ + return d->before_first_frame ? nullptr : d->video->frame_image(); +} + +// ---------------------------------------------------------------------------- +vital::video_raw_image_sptr +ffmpeg_video_input_clip +::raw_frame_image() +{ + return d->before_first_frame ? nullptr : d->video->raw_frame_image(); +} + +// ---------------------------------------------------------------------------- +vital::metadata_vector +ffmpeg_video_input_clip +::frame_metadata() +{ + if( d->before_first_frame ) + { + return {}; + } + + auto result = d->video->frame_metadata(); + d->filter_metadata( result, frame_timestamp() ); + return result; +} + +// ---------------------------------------------------------------------------- +vital::video_raw_metadata_sptr +ffmpeg_video_input_clip +::raw_frame_metadata() +{ + return d->before_first_frame ? nullptr : d->video->raw_frame_metadata(); +} + +// ---------------------------------------------------------------------------- +vital::video_uninterpreted_data_sptr +ffmpeg_video_input_clip +::uninterpreted_frame_data() +{ + return d->before_first_frame ? nullptr : d->video->uninterpreted_frame_data(); +} + +// ---------------------------------------------------------------------------- +vital::metadata_map_sptr +ffmpeg_video_input_clip +::metadata_map() +{ + if( d->all_metadata ) + { + return d->all_metadata; + } + + ffmpeg_video_input_clip tmp_video; + tmp_video.set_configuration( get_configuration() ); + tmp_video.open( d->video_name ); + + vital::metadata_map::map_metadata_t result; + vital::timestamp ts; + while( tmp_video.next_frame( ts ) ) + { + result.emplace( ts.get_frame(), tmp_video.frame_metadata() ); + } + + d->all_metadata.reset( + new vital::simple_metadata_map{ std::move( result ) } ); + return d->all_metadata; +} + +// ---------------------------------------------------------------------------- +vital::video_settings_uptr +ffmpeg_video_input_clip +::implementation_settings() const +{ + auto const settings = d->video->implementation_settings(); + auto const ffmpeg_settings = + dynamic_cast< ffmpeg_video_settings const* >( settings.get() ); + if( !ffmpeg_settings ) + { + return nullptr; + } + + auto result = *ffmpeg_settings; + result.start_timestamp = d->initial_pts; + return std::make_unique< ffmpeg_video_settings >( std::move( result ) ); +} + +} // namespace ffmpeg + +} // namespace arrows + +} // end namespace diff --git a/arrows/ffmpeg/ffmpeg_video_input_clip.h b/arrows/ffmpeg/ffmpeg_video_input_clip.h new file mode 100644 index 0000000000..426cfdc446 --- /dev/null +++ b/arrows/ffmpeg/ffmpeg_video_input_clip.h @@ -0,0 +1,76 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Declaration of FFmpeg video clipping utility. + +#ifndef KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_INPUT_CLIP_H +#define KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_INPUT_CLIP_H + +#include +#include + +#include + +namespace kwiver { + +namespace arrows { + +namespace ffmpeg { + +// ---------------------------------------------------------------------------- +/// Video input which temporally clips an FFmpeg-sourced video. +/// +/// This implementation must have access to FFmpeg-level detailed information in +/// order to properly clip raw streams. +class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input_clip + : public vital::algo::video_input +{ +public: + ffmpeg_video_input_clip(); + virtual ~ffmpeg_video_input_clip(); + + PLUGIN_INFO( "ffmpeg_clip", "Clip an FFmpeg-sourced video." ) + + vital::config_block_sptr get_configuration() const override; + void set_configuration( vital::config_block_sptr config ) override; + bool check_configuration( vital::config_block_sptr config ) const override; + + void open( std::string video_name ) override; + void close() override; + + bool end_of_video() const override; + bool good() const override; + + bool seekable() const override; + size_t num_frames() const override; + + bool next_frame( vital::timestamp& ts, uint32_t timeout = 0 ) override; + bool seek_frame( + vital::timestamp& ts, vital::timestamp::frame_t frame_number, + uint32_t timeout = 0 ) override; + + vital::timestamp frame_timestamp() const override; + vital::image_container_sptr frame_image() override; + vital::video_raw_image_sptr raw_frame_image() override; + vital::metadata_vector frame_metadata() override; + vital::video_raw_metadata_sptr raw_frame_metadata() override; + vital::video_uninterpreted_data_sptr uninterpreted_frame_data() override; + vital::metadata_map_sptr metadata_map() override; + + vital::video_settings_uptr implementation_settings() const override; + +private: + class impl; + + std::unique_ptr< impl > const d; +}; + +} // namespace ffmpeg + +} // namespace arrows + +} // end namespace + +#endif diff --git a/arrows/ffmpeg/register_algorithms.cxx b/arrows/ffmpeg/register_algorithms.cxx index bf4d84d8b8..bb7dd83ddf 100644 --- a/arrows/ffmpeg/register_algorithms.cxx +++ b/arrows/ffmpeg/register_algorithms.cxx @@ -9,6 +9,7 @@ #include #include +#include #include namespace kwiver { @@ -30,6 +31,7 @@ register_factories( kwiver::vital::plugin_loader& vpm ) } reg.register_algorithm< ::kwiver::arrows::ffmpeg::ffmpeg_video_input >(); + reg.register_algorithm< ::kwiver::arrows::ffmpeg::ffmpeg_video_input_clip >(); reg.register_algorithm< ::kwiver::arrows::ffmpeg::ffmpeg_video_output >(); reg.mark_module_as_loaded(); diff --git a/arrows/ffmpeg/tests/CMakeLists.txt b/arrows/ffmpeg/tests/CMakeLists.txt index e9c82f530c..2f1f41c001 100644 --- a/arrows/ffmpeg/tests/CMakeLists.txt +++ b/arrows/ffmpeg/tests/CMakeLists.txt @@ -9,8 +9,9 @@ set(test_libraries kwiver_algo_ffmpeg kwiver_algo_core) ############################## # Algorithms ffmpeg tests ############################## -kwiver_discover_gtests(ffmpeg video_input_ffmpeg LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") -kwiver_discover_gtests(ffmpeg video_output_ffmpeg LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") +kwiver_discover_gtests(ffmpeg video_input_ffmpeg LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") +kwiver_discover_gtests(ffmpeg video_input_ffmpeg_clip LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") +kwiver_discover_gtests(ffmpeg video_output_ffmpeg LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") if( KWIVER_ENABLE_SERIALIZE_JSON ) kwiver_discover_gtests(ffmpeg video_input_ffmpeg_klv diff --git a/arrows/ffmpeg/tests/common.h b/arrows/ffmpeg/tests/common.h new file mode 100644 index 0000000000..e8d13fe415 --- /dev/null +++ b/arrows/ffmpeg/tests/common.h @@ -0,0 +1,145 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +#include + +#include +#include + +#include + +namespace ffmpeg = kwiver::arrows::ffmpeg; +namespace klv = kwiver::arrows::klv; +namespace kv = kwiver::vital; +namespace kvr = kwiver::vital::range; + +// ---------------------------------------------------------------------------- +// Verify the average difference between pixels is not too high. Some +// difference is expected due to compression artifacts, but we need to make +// sure the frame images we get out are generally the same as what we put in. +void +expect_eq_images( kv::image const& src_image, + kv::image const& tmp_image, + double epsilon ) +{ + auto error = 0.0; + + ASSERT_TRUE( src_image.width() == tmp_image.width() ); + ASSERT_TRUE( src_image.height() == tmp_image.height() ); + ASSERT_TRUE( src_image.depth() == tmp_image.depth() ); + + for( auto const i : kvr::iota( src_image.width() ) ) + { + for( auto const j : kvr::iota( src_image.height() ) ) + { + for( auto const k : kvr::iota( src_image.depth() ) ) + { + error += std::abs( + static_cast< double >( src_image.at< uint8_t >( i, j, k ) ) - + static_cast< double >( tmp_image.at< uint8_t >( i, j, k ) ) ); + } + } + } + error /= src_image.width() * src_image.height() * src_image.depth(); + + EXPECT_LE( error, epsilon ); +} + +// ---------------------------------------------------------------------------- +void +expect_eq_audio( kv::video_uninterpreted_data_sptr const& src_data, + kv::video_uninterpreted_data_sptr const& tmp_data ) +{ + ASSERT_EQ( src_data == nullptr, tmp_data == nullptr ); + if( !src_data ) + { + return; + } + + auto const& src_packets = + dynamic_cast< ffmpeg::ffmpeg_video_uninterpreted_data const& >( *src_data ) + .audio_packets; + auto const& tmp_packets = + dynamic_cast< ffmpeg::ffmpeg_video_uninterpreted_data const& >( *tmp_data ) + .audio_packets; + ASSERT_EQ( src_packets.size(), tmp_packets.size() ); + + auto src_it = src_packets.begin(); + auto tmp_it = tmp_packets.begin(); + while( src_it != src_packets.begin() && tmp_it != tmp_packets.begin() ) + { + ASSERT_EQ( ( *src_it )->size, ( *tmp_it )->size ); + EXPECT_TRUE( + std::equal( ( *src_it )->data, ( *src_it )->data + ( *src_it )->size, + ( *tmp_it )->data ) ); + ++src_it; + ++tmp_it; + } +} + +// ---------------------------------------------------------------------------- +void +expect_eq_videos( + kv::algo::video_input& src_is, kv::algo::video_input& tmp_is, + double image_epsilon = 0.0, kv::frame_id_t frame_offset = 0, + kv::time_usec_t usec_offset = 0, bool allow_different_lengths = false ) +{ + kv::timestamp src_ts; + kv::timestamp tmp_ts; + + // Check each pair of frames for equality + for( src_is.next_frame( src_ts ), tmp_is.next_frame( tmp_ts ); + !src_is.end_of_video() && !tmp_is.end_of_video(); + src_is.next_frame( src_ts ), tmp_is.next_frame( tmp_ts ) ) + { + SCOPED_TRACE( + std::string{ "Frame: " } + + std::to_string( src_ts.get_frame() ) + " | " + + std::to_string( tmp_ts.get_frame() ) ); + + EXPECT_EQ( src_ts.get_frame() + frame_offset, tmp_ts.get_frame() ); + EXPECT_NEAR( + src_ts.get_time_usec() + usec_offset, tmp_ts.get_time_usec(), 1 ); + + auto const src_data = src_is.uninterpreted_frame_data(); + auto const tmp_data = tmp_is.uninterpreted_frame_data(); + expect_eq_audio( src_data, tmp_data ); + + auto const src_image = src_is.frame_image()->get_image(); + auto const tmp_image = tmp_is.frame_image()->get_image(); + expect_eq_images( src_image, tmp_image, image_epsilon ); + } + if( !allow_different_lengths ) + { + EXPECT_TRUE( src_is.end_of_video() ); + EXPECT_TRUE( tmp_is.end_of_video() ); + } +} + +// ---------------------------------------------------------------------------- +void +expect_eq_videos( + std::string const& src_path, std::string const& tmp_path, + double image_epsilon = 0.0, kv::frame_id_t frame_offset = 0, + kv::time_usec_t usec_offset = 0, bool allow_different_lengths = false ) +{ + ASSERT_GE( frame_offset, 0 ); + ASSERT_GE( usec_offset, 0 ); + + ffmpeg::ffmpeg_video_input src_is; + ffmpeg::ffmpeg_video_input tmp_is; + src_is.open( src_path ); + tmp_is.open( tmp_path ); + + kv::timestamp ts; + for( kv::frame_id_t i = 0; i < frame_offset; ++i ) + { + src_is.next_frame( ts ); + } + + expect_eq_videos( src_is, tmp_is, image_epsilon ); + + src_is.close(); + tmp_is.close(); +} diff --git a/arrows/ffmpeg/tests/test_video_input_ffmpeg_clip.cxx b/arrows/ffmpeg/tests/test_video_input_ffmpeg_clip.cxx new file mode 100644 index 0000000000..ae7129a484 --- /dev/null +++ b/arrows/ffmpeg/tests/test_video_input_ffmpeg_clip.cxx @@ -0,0 +1,234 @@ +// This file is part of KWIVER, and is distributed under the +// OSI-approved BSD 3-Clause License. See top-level LICENSE file or +// https://github.com/Kitware/kwiver/blob/master/LICENSE for details. + +/// \file +/// Test the FFmpeg video clip input. + +#include + +#include + +#include +#include + +#include + +#include + +namespace ffmpeg = kwiver::arrows::ffmpeg; +namespace kv = kwiver::vital; + +std::filesystem::path g_data_dir; + +namespace { + +// ---------------------------------------------------------------------------- +void +configure_input( + ffmpeg::ffmpeg_video_input_clip& input, + kv::frame_id_t frame_begin, kv::frame_id_t frame_end, + bool start_at_keyframe ) +{ + auto config = input.get_configuration(); + config->set_value( "frame_begin", frame_begin ); + config->set_value( "frame_end", frame_end ); + config->set_value( "start_at_keyframe", start_at_keyframe ); + EXPECT_TRUE( input.check_configuration( config ) ); + input.set_configuration( config ); +} + +// ---------------------------------------------------------------------------- +void test_clipped( + ffmpeg::ffmpeg_video_input_clip& input, + std::filesystem::path const& filepath, + kv::frame_id_t frame_begin, kv::frame_id_t frame_end, + kv::time_usec_t usec_begin ) +{ + ffmpeg::ffmpeg_video_input unclipped_input; + unclipped_input.open( filepath.string() ); + kv::timestamp ts; + for( kv::frame_id_t i = 1; i < frame_begin; ++i ) + { + unclipped_input.next_frame( ts ); + } + + input.open( filepath.string() ); + EXPECT_FALSE( input.good() ); + EXPECT_FALSE( input.end_of_video() ); + ts = input.frame_timestamp(); + EXPECT_EQ( 1, ts.get_frame() ); + + CALL_TEST( + expect_eq_videos, + unclipped_input, input, 0.0, -frame_begin + 1, -usec_begin, true ); + + EXPECT_FALSE( input.good() ); + EXPECT_TRUE( input.end_of_video() ); + + if( !unclipped_input.end_of_video() ) + { + ts = unclipped_input.frame_timestamp(); + EXPECT_EQ( frame_end, ts.get_frame() ); + } + + unclipped_input.close(); + input.close(); + + EXPECT_FALSE( input.good() ); +} + +} // namespace + +// ---------------------------------------------------------------------------- +int +main( int argc, char* argv[] ) +{ + ::testing::InitGoogleTest( &argc, argv ); + TEST_LOAD_PLUGINS(); + + GET_ARG( 1, g_data_dir ); + + return RUN_ALL_TESTS(); +} + +// ---------------------------------------------------------------------------- +class ffmpeg_video_input_clip : public ::testing::Test +{ +public: + void SetUp() override + { + ffmpeg_video_path = data_dir / "videos/ffmpeg_video.mp4"; + aphill_video_path = data_dir / "videos/aphill_short.ts"; + } + + std::filesystem::path ffmpeg_video_path; + std::filesystem::path aphill_video_path; + TEST_ARG( data_dir ); +}; + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, create ) +{ + EXPECT_NE( nullptr, kv::algo::video_input::create( "ffmpeg_clip" ) ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, entire_video_exact_aphill ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 1, 49, false ); + CALL_TEST( test_clipped, input, aphill_video_path, 1, 49, 0 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, entire_video_keyframe_aphill ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 1, 49, true ); + CALL_TEST( test_clipped, input, aphill_video_path, 1, 49, 0 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, entire_video_exact_ffmpeg ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 1, 51, false ); + CALL_TEST( test_clipped, input, ffmpeg_video_path, 1, 51, 0 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, entire_video_keyframe_ffmpeg ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 1, 51, true ); + CALL_TEST( test_clipped, input, ffmpeg_video_path, 1, 51, 0 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, end_past_end ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 1, 100, false ); + CALL_TEST( test_clipped, input, aphill_video_path, 1, 49, 0 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, begin_past_end ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 100, 200, false ); + EXPECT_THROW( input.open( aphill_video_path.string() ), std::runtime_error ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, single_frame ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 20, 21, false ); + CALL_TEST( test_clipped, input, aphill_video_path, 20, 21, 633'966 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, non_keyframe_exact_aphill ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 7, 23, false ); + CALL_TEST( test_clipped, input, aphill_video_path, 7, 23, 200'200 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, non_keyframe_keyframe_aphill ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 7, 23, true ); + CALL_TEST( test_clipped, input, aphill_video_path, 1, 23, 0 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, non_keyframe_exact_ffmpeg ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 7, 23, false ); + CALL_TEST( test_clipped, input, ffmpeg_video_path, 7, 23, 1'200'000 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, non_keyframe_keyframe_ffmpeg ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 7, 23, true ); + CALL_TEST( test_clipped, input, ffmpeg_video_path, 6, 23, 1'000'000 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, keyframe_exact_aphill ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 17, 33, false ); + CALL_TEST( test_clipped, input, aphill_video_path, 17, 33, 533'866 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, keyframe_keyframe_aphill ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 17, 33, true ); + CALL_TEST( test_clipped, input, aphill_video_path, 17, 33, 533'866 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, keyframe_exact_ffmpeg ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 11, 33, false ); + CALL_TEST( test_clipped, input, ffmpeg_video_path, 11, 33, 2'000'000 ); +} + +// ---------------------------------------------------------------------------- +TEST_F ( ffmpeg_video_input_clip, keyframe_keyframe_ffmpeg ) +{ + ffmpeg::ffmpeg_video_input_clip input; + configure_input( input, 11, 33, true ); + CALL_TEST( test_clipped, input, ffmpeg_video_path, 11, 33, 2'000'000 ); +} diff --git a/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx b/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx index 42c5b8eeb2..5b5c1a720d 100644 --- a/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx +++ b/arrows/ffmpeg/tests/test_video_output_ffmpeg.cxx @@ -5,21 +5,17 @@ #include #include +#include + #include #include #include #include #include -#include #include -namespace ffmpeg = kwiver::arrows::ffmpeg; -namespace klv = kwiver::arrows::klv; -namespace kv = kwiver::vital; -namespace kvr = kwiver::vital::range; - kv::path_t g_data_dir; static std::string short_video_name = "videos/aphill_short.ts"; @@ -111,104 +107,6 @@ class ffmpeg_video_output : public ::testing::Test TEST_ARG( data_dir ); }; -// ---------------------------------------------------------------------------- -// Verify the average difference between pixels is not too high. Some -// difference is expected due to compression artifacts, but we need to make -// sure the frame images we get out are generally the same as what we put in. -void -expect_eq_images( kv::image const& src_image, - kv::image const& tmp_image, - double epsilon ) -{ - auto error = 0.0; - - ASSERT_TRUE( src_image.width() == tmp_image.width() ); - ASSERT_TRUE( src_image.height() == tmp_image.height() ); - ASSERT_TRUE( src_image.depth() == tmp_image.depth() ); - - for( auto const i : kvr::iota( src_image.width() ) ) - { - for( auto const j : kvr::iota( src_image.height() ) ) - { - for( auto const k : kvr::iota( src_image.depth() ) ) - { - error += std::abs( - static_cast< double >( src_image.at< uint8_t >( i, j, k ) ) - - static_cast< double >( tmp_image.at< uint8_t >( i, j, k ) ) ); - } - } - } - error /= src_image.width() * src_image.height() * src_image.depth(); - - EXPECT_LE( error, epsilon ); -} - -// ---------------------------------------------------------------------------- -void -expect_eq_audio( kv::video_uninterpreted_data_sptr const& src_data, - kv::video_uninterpreted_data_sptr const& tmp_data ) -{ - ASSERT_EQ( src_data == nullptr, tmp_data == nullptr ); - if( !src_data ) - { - return; - } - - auto const& src_packets = - dynamic_cast< ffmpeg::ffmpeg_video_uninterpreted_data const& >( *src_data ) - .audio_packets; - auto const& tmp_packets = - dynamic_cast< ffmpeg::ffmpeg_video_uninterpreted_data const& >( *tmp_data ) - .audio_packets; - ASSERT_EQ( src_packets.size(), tmp_packets.size() ); - - auto src_it = src_packets.begin(); - auto tmp_it = tmp_packets.begin(); - while( src_it != src_packets.begin() && tmp_it != tmp_packets.begin() ) - { - ASSERT_EQ( ( *src_it )->size, ( *tmp_it )->size ); - EXPECT_TRUE( - std::equal( ( *src_it )->data, ( *src_it )->data + ( *src_it )->size, - ( *tmp_it )->data ) ); - ++src_it; - ++tmp_it; - } -} - -// ---------------------------------------------------------------------------- -void -expect_eq_videos( std::string const& src_path, std::string const& tmp_path, - double image_epsilon ) -{ - ffmpeg::ffmpeg_video_input src_is; - ffmpeg::ffmpeg_video_input tmp_is; - kv::timestamp src_ts; - kv::timestamp tmp_ts; - src_is.open( src_path ); - tmp_is.open( tmp_path ); - - // Check each pair of frames for equality - for( src_is.next_frame( src_ts ), tmp_is.next_frame( tmp_ts ); - !src_is.end_of_video() && !tmp_is.end_of_video(); - src_is.next_frame( src_ts ), tmp_is.next_frame( tmp_ts ) ) - { - EXPECT_EQ( src_ts.get_frame(), tmp_ts.get_frame() ); - EXPECT_EQ( src_ts.get_time_usec(), tmp_ts.get_time_usec() ); - - auto const src_data = src_is.uninterpreted_frame_data(); - auto const tmp_data = tmp_is.uninterpreted_frame_data(); - expect_eq_audio( src_data, tmp_data ); - - auto const src_image = src_is.frame_image()->get_image(); - auto const tmp_image = tmp_is.frame_image()->get_image(); - expect_eq_images( src_image, tmp_image, image_epsilon ); - } - EXPECT_TRUE( src_is.end_of_video() ); - EXPECT_TRUE( tmp_is.end_of_video() ); - src_is.close(); - tmp_is.close(); -} - namespace { // This will delete the temporary file even if an exception is thrown @@ -261,7 +159,7 @@ TEST_F ( ffmpeg_video_output, round_trip ) } // Read the temporary file back in - expect_eq_videos( src_path, tmp_path, image_epsilon ); + CALL_TEST( expect_eq_videos, src_path, tmp_path, image_epsilon ); } // ---------------------------------------------------------------------------- @@ -313,7 +211,7 @@ TEST_F ( ffmpeg_video_output, round_trip_direct ) auto const image_epsilon = 0.0; // Read the temporary file back in - expect_eq_videos( src_path, tmp_path, image_epsilon ); + CALL_TEST( expect_eq_videos, src_path, tmp_path, image_epsilon ); } // ---------------------------------------------------------------------------- @@ -357,7 +255,7 @@ TEST_F ( ffmpeg_video_output, round_trip_audio ) } // Read the temporary file back in - expect_eq_videos( src_path, tmp_path, image_epsilon ); + CALL_TEST( expect_eq_videos, src_path, tmp_path, image_epsilon ); } // ---------------------------------------------------------------------------- @@ -414,7 +312,7 @@ TEST_F ( ffmpeg_video_output, round_trip_audio_direct ) auto const image_epsilon = 0.0; // Read the temporary file back in - expect_eq_videos( src_path, tmp_path, image_epsilon ); + CALL_TEST( expect_eq_videos, src_path, tmp_path, image_epsilon ); } // ---------------------------------------------------------------------------- From b93a7a2ddb53b736def2fa5fa9295f0022b674a5 Mon Sep 17 00:00:00 2001 From: Daniel Riehm Date: Mon, 18 Dec 2023 10:23:59 -0600 Subject: [PATCH 50/51] Clean up ffmpeg_video_input.h --- arrows/ffmpeg/ffmpeg_video_input.h | 64 +++++++++++++----------------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/arrows/ffmpeg/ffmpeg_video_input.h b/arrows/ffmpeg/ffmpeg_video_input.h index d17fc157a3..cfa3bf954b 100644 --- a/arrows/ffmpeg/ffmpeg_video_input.h +++ b/arrows/ffmpeg/ffmpeg_video_input.h @@ -3,26 +3,24 @@ // https://github.com/Kitware/kwiver/blob/master/LICENSE for details. /// \file -/// \brief \todo +/// Declaration of FFmpeg-based video input. -#ifndef KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_INPUT_H -#define KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_INPUT_H - -#include +#ifndef KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_INPUT_H_ +#define KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_INPUT_H_ #include #include + +#include + namespace kwiver { namespace arrows { namespace ffmpeg { -/// Video input using ffmpeg services. // ---------------------------------------------------------------------------- - -/// This class implements a video input algorithm using ffmpeg video services. -/// +/// Video input using FFmpeg (libav). class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input : public vital::algo::video_input { @@ -32,21 +30,14 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input SEEK_MODE_KEYFRAME_BEFORE, }; - /// Constructor ffmpeg_video_input(); virtual ~ffmpeg_video_input(); - PLUGIN_INFO( "ffmpeg", - "Use FFMPEG to read video files as a sequence of images." ) + PLUGIN_INFO( + "ffmpeg", "Use FFmpeg to read video files as a sequence of images." ) - /// Get this algorithm's \link vital::config_block configuration block - /// \endlink vital::config_block_sptr get_configuration() const override; - - /// Set this algorithm's properties via a config block void set_configuration( vital::config_block_sptr config ) override; - - /// Check that the algorithm's currently configuration is valid bool check_configuration( vital::config_block_sptr config ) const override; void open( std::string video_name ) override; @@ -59,28 +50,27 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input size_t num_frames() const override; double frame_rate() override; - bool next_frame( ::kwiver::vital::timestamp& ts, - uint32_t timeout = 0 ) override; - bool seek_frame( ::kwiver::vital::timestamp& ts, - ::kwiver::vital::timestamp::frame_t frame_number, - uint32_t timeout = 0 ) override; + bool next_frame( vital::timestamp& ts, uint32_t timeout = 0 ) override; + bool seek_frame( + vital::timestamp& ts, vital::timestamp::frame_t frame_number, + uint32_t timeout = 0 ) override; - bool seek_frame_( vital::timestamp& ts, - vital::timestamp::frame_t frame_number, - seek_mode mode, uint32_t timeout = 0 ); + bool seek_frame_( + vital::timestamp& ts, vital::timestamp::frame_t frame_number, + seek_mode mode, uint32_t timeout = 0 ); - ::kwiver::vital::timestamp frame_timestamp() const override; - ::kwiver::vital::image_container_sptr frame_image() override; - ::kwiver::vital::video_raw_image_sptr raw_frame_image() override; - ::kwiver::vital::metadata_vector frame_metadata() override; - ::kwiver::vital::video_raw_metadata_sptr raw_frame_metadata() override; - ::kwiver::vital::video_uninterpreted_data_sptr uninterpreted_frame_data() override; - ::kwiver::vital::metadata_map_sptr metadata_map() override; + vital::timestamp frame_timestamp() const override; + vital::image_container_sptr frame_image() override; + vital::video_raw_image_sptr raw_frame_image() override; + vital::metadata_vector frame_metadata() override; + vital::video_raw_metadata_sptr raw_frame_metadata() override; + vital::video_uninterpreted_data_sptr uninterpreted_frame_data() override; + vital::metadata_map_sptr metadata_map() override; - ::kwiver::vital::video_settings_uptr implementation_settings() const override; + vital::video_settings_uptr implementation_settings() const override; private: - /// private implementation class + /// Private implementation class class priv; const std::unique_ptr< priv > d; @@ -90,6 +80,6 @@ class KWIVER_ALGO_FFMPEG_EXPORT ffmpeg_video_input } // namespace arrows -} // end namespace +} // namespace kwiver -#endif // KWIVER_ARROWS_FFMPEG_FFMPEG_VIDEO_INPUT_H +#endif From b01884bf8380ddb9f5cf48df5f44b46fe209cafd Mon Sep 17 00:00:00 2001 From: pbeasly <128631135+pbeasly@users.noreply.github.com> Date: Wed, 3 Jan 2024 20:53:25 -0500 Subject: [PATCH 51/51] Add a unit test for match_matrix function --- arrows/core/tests/CMakeLists.txt | 1 + arrows/core/tests/test_match_matrix.cxx | 376 ++++++++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 arrows/core/tests/test_match_matrix.cxx diff --git a/arrows/core/tests/CMakeLists.txt b/arrows/core/tests/CMakeLists.txt index 419679b9ab..95da52ebf9 100644 --- a/arrows/core/tests/CMakeLists.txt +++ b/arrows/core/tests/CMakeLists.txt @@ -32,3 +32,4 @@ kwiver_discover_gtests(core video_input_image_list LIBRARIES ${test_libraries kwiver_discover_gtests(core video_input_pos LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") kwiver_discover_gtests(core video_input_splice LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") kwiver_discover_gtests(core video_input_split LIBRARIES ${test_libraries} ARGUMENTS "${kwiver_test_data_directory}") +kwiver_discover_gtests(core match_matrix LIBRARIES ${test_libraries}) \ No newline at end of file diff --git a/arrows/core/tests/test_match_matrix.cxx b/arrows/core/tests/test_match_matrix.cxx new file mode 100644 index 0000000000..1293708f85 --- /dev/null +++ b/arrows/core/tests/test_match_matrix.cxx @@ -0,0 +1,376 @@ +/*ckwg +29 + * Copyright 2014-2017 by Kitware, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither name of Kitware, Inc. nor the names of any contributors may be used + * to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include +#include + +using namespace kwiver::vital; + +// This can be removed prior to merging with source code +void view_set_matrix(); + +// ---------------------------------------------------------------------------- +int main( int argc, char** argv ) +{ + // This can be removed prior to merging with source code + view_set_matrix(); + + ::testing::InitGoogleTest( &argc, argv ); + return RUN_ALL_TESTS(); +} + +namespace { + +// ---------------------------------------------------------------------------- +// Helper function to generate deterministic track set +kwiver::vital::track_set_sptr +gen_set_tracks( unsigned frames=100, + unsigned max_tracks_per_frame=1000, + unsigned min_tracks_per_frame=500, + double termination_fraction = 0.01, + double skip_fraction = 0.01, + double frame_drop_fraction = 0.01 ) +{ + + // Manually terminate tracks on frames 0, 1 and 3 + track_id_t track_id=0; + std::vector< track_sptr > all_tracks, active_tracks; + for( unsigned f=0; fset_id(track_id++); + active_tracks.push_back(t); + all_tracks.push_back(t); + } + + // Add a state for each track to this frame + for( auto t : active_tracks ) + { + t->append( std::make_shared( f ) ); + } + + if(f==0) + { + // Terminate tracks 0 and 3 on frame 0 + std::vector< track_sptr > next_tracks; + for( auto t : active_tracks ) + { + if( t->id() != 0 && t->id() != 3 ) + { + next_tracks.push_back( t ); + } + } + active_tracks.swap( next_tracks ); + } + + if(f==1) + { + // Terminate tracks 2 and 7 on frame 1 + std::vector< track_sptr > next_tracks; + for( auto t : active_tracks ) + { + if( t->id() != 2 && t->id() != 7 ) + { + next_tracks.push_back( t ); + } + } + active_tracks.swap( next_tracks ); + } + + if(f==3) + { + // Terminate tracks 5 and 9 on frame 3 + std::vector< track_sptr > next_tracks; + for( auto t : active_tracks ) + { + if( t->id() != 5 && t->id() != 9 ) + { + next_tracks.push_back( t ); + } + } + active_tracks.swap( next_tracks ); + } + } + return std::make_shared( all_tracks ); +} + +// ---------------------------------------------------------------------------- +// Function to generate match matrix with known values +Eigen::SparseMatrix gen_test_matrix() +{ + Eigen::Matrix dense_matrix; + + // Manually calculated matrix from gen_set_tracks() + dense_matrix << 8, 6, 4, 4, 3, + 6, 8, 6, 6, 4, + 4, 6, 8, 8, 6, + 4, 6, 8, 8, 6, + 3, 4, 6, 6, 8; + + // Convert the dense matrix to a sparse matrix for unit test comparison + Eigen::SparseMatrix test_matrix = dense_matrix.sparseView(); + + return test_matrix; +} + +// ---------------------------------------------------------------------------- +// Function to calculate the max possible importance score +double gen_max_score(Eigen::SparseMatrix matrix) +{ + double sum = 0.0; + + for (int row = 0; row < matrix.rows(); ++row) + { + for (int col = 0; col <= row; ++col) { + unsigned int value = matrix.coeff(row, col); + if (value != 0) { + sum += 1.0 / static_cast(value); + } + } + } + return sum; +} + +// ---------------------------------------------------------------------------- +// Function to generate importance scores from known values for comparison +std::vector gen_set_scores() +{ + std::vector set_scores; + + // Manually calculated for the 'set_tracks' and 'set_matrix' + set_scores = {1.0/8, 8.0/3, 5.0/12, 1.0/8, 8.0/3, 1.625, 8.0/3, + 5.0/12, 37.0/24, 5.0/6, 5.0/6, 5.0/6, 1.0/8, 1.0/8}; + + return set_scores; +} + +// ---------------------------------------------------------------------------- +// Function to check range of elements in match matrix +bool matrix_values(const Eigen::SparseMatrix& matrix, + unsigned int max_tracks) +{ + for (int i = 0; i < matrix.rows(); ++i) { + for (int k = 0; k < matrix.cols(); ++k) { + unsigned int value = matrix.coeff(i, k); + if (value < 0 || value > max_tracks) { + return false; + } + } + } + return true; +} + +// ---------------------------------------------------------------------------- +// Establish constants and create variables for test_tracks + +// These parameters can be varied for further testing +const unsigned int num_frames = 100; +const unsigned int max_tracks = 1000; + +track_set_sptr test_tracks = + kwiver::testing::generate_tracks(num_frames, max_tracks); + +const auto trks = test_tracks->tracks(); + +std::set frame_ids = test_tracks->all_frame_ids(); +std::vector frames = + std::vector(frame_ids.begin(), frame_ids.end()); + +// Frames might dropped in track set generation +int actual_num_frames = test_tracks->all_frame_ids().size(); + +Eigen::SparseMatrix matched_matrix = + kwiver::arrows::match_matrix(test_tracks, frames); + +// ---------------------------------------------------------------------------- +// Establish constants and create variables for set_tracks + +// DO NOT EDIT these two constants, might cause unit tests to fail +const unsigned int set_num_frames = 5; +const unsigned int set_max_tracks = 8; + +track_set_sptr set_tracks = + gen_set_tracks(set_num_frames, set_max_tracks); + +const auto set_trks = set_tracks->tracks(); + +std::set set_frame_ids = set_tracks->all_frame_ids(); +std::vector set_frames = + std::vector(set_frame_ids.begin(), set_frame_ids.end()); + +Eigen::SparseMatrix set_matrix = + kwiver::arrows::match_matrix(set_tracks, set_frames); + +std::map set_importance_scores = + kwiver::arrows::match_matrix_track_importance(set_tracks, + set_frames, set_matrix); + +} // end namespace anonymous + +// ---------------------------------------------------------------------------- +TEST(match_matrix, matrix_dimensions) +{ + int num_rows = matched_matrix.rows(); + int num_cols = matched_matrix.cols(); + + ASSERT_EQ(num_rows, actual_num_frames); + ASSERT_EQ(num_cols, actual_num_frames); +} + +// ---------------------------------------------------------------------------- +// Test range of matrix values and symmetry +TEST(match_matrix, matrix_values) +{ + EXPECT_TRUE(matrix_values(matched_matrix, max_tracks)); + EXPECT_TRUE(matched_matrix.isApprox(matched_matrix.transpose())); +} + +// ---------------------------------------------------------------------------- +// Test matrix diagonal values match the number of tracks in each frame +TEST(match_matrix, diagonal_values) +{ + std::vector tracks_in_frame(actual_num_frames, 0); + + for (const auto& t : trks) { + std::set t_frames = t->all_frame_ids(); + for (const auto& fid : t_frames) { + tracks_in_frame[fid]++; + } + } + + std::vector diag_elements(actual_num_frames, 0); + + for (Eigen::Index i = 0; i < matched_matrix.rows(); ++i) { + diag_elements[i] =(matched_matrix.coeff(i, i)); + } + + EXPECT_EQ(diag_elements, tracks_in_frame); +} + +// ---------------------------------------------------------------------------- +// Test that match_matrix() function is equivalent to calculated matrix +TEST(match_matrix, test_matrix) +{ + Eigen::SparseMatrix test_matrix = gen_test_matrix(); + + ASSERT_TRUE(set_matrix.isApprox(test_matrix)); +} + +// ---------------------------------------------------------------------------- +TEST(importance_score, vector_size) +{ + std::map importance_scores = + kwiver::arrows::match_matrix_track_importance(test_tracks, + frames, matched_matrix); + + double max_score = gen_max_score(matched_matrix); + + std::vector score_values; + for (const auto& entry : importance_scores) + { + score_values.push_back(entry.second); + } + + double largest_score = *std::max_element(score_values.begin(), + score_values.end()); + + EXPECT_EQ(test_tracks->size(), importance_scores.size()); + EXPECT_LE(largest_score, max_score); +} + +// ---------------------------------------------------------------------------- +// Test importance score function against pre-determined result +TEST(importance_score, score_values) +{ + // invoke the importance scores that were manually calculated + auto set_scores = gen_set_scores(); + + std::vector score_values; + for (const auto& entry : set_importance_scores) + { + score_values.push_back(entry.second); + } + + const double tolerance = 1e-5; + + for (size_t i = 0; i < set_scores.size(); ++i) + { + EXPECT_NEAR(set_scores[i], score_values[i], tolerance); + } +} + +// ---------------------------------------------------------------------------- +// Function to view results for a small track set +// Used for visual inspection, manual calculations and de-bugging +// Can be removed before merging with source code +void view_set_matrix() +{ + std::cout << "Deterministic track set"<all_frame_ids().size(); + + // View each frame and associated tracks + for (frame_id_t f_id = 0; f_id < actual_set_num_frames; ++f_id) { + + std::cout << "Frame " << f_id << " - Tracks: "; + + for (const auto& t : set_trks) { + // Get all frames covered by this track + std::set t_frames = t->all_frame_ids(); + + // Check if the desired frame is in the set of + // frames covered by the track + if (t_frames.find(f_id) != t_frames.end()) { + std::cout << t->id() << " "; + } + } + + std::cout << std::endl; + } + + std::cout << '\n'; + + std::cout << "Deterministic matched matrix\n" << set_matrix << std::endl; + + std::cout << "Track Importance Scores:\n"; + for (const auto& entry : set_importance_scores) { + std::cout <<"Track ID: "<