├── .gitignore ├── banner.png ├── .gitmodules ├── source ├── size.cpp ├── font.hpp ├── evt2_to_es.cpp ├── evt3_to_es.cpp ├── es_to_csv.cpp ├── dat_to_es.cpp ├── timecode.hpp ├── cut.cpp ├── crop.cpp ├── es_to_ply.cpp ├── rainbow.cpp ├── dat.hpp ├── evt.hpp ├── synth.cpp ├── statistics.cpp ├── rainmaker.cpp └── html.hpp ├── .clang-format ├── premake4.lua ├── render.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .DS_STORE 3 | *.mex* 4 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neuromorphic-paris/command_line_tools/HEAD/banner.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/pontella"] 2 | path = third_party/pontella 3 | url = https://github.com/neuromorphic-paris/pontella.git 4 | [submodule "third_party/sepia"] 5 | path = third_party/sepia 6 | url = https://github.com/neuromorphic-paris/sepia.git 7 | [submodule "third_party/tarsier"] 8 | path = third_party/tarsier 9 | url = https://github.com/neuromorphic-paris/tarsier.git 10 | [submodule "third_party/lodepng"] 11 | path = third_party/lodepng 12 | url = https://github.com/lvandeve/lodepng.git 13 | -------------------------------------------------------------------------------- /source/size.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | 4 | int main(int argc, char* argv[]) { 5 | return pontella::main( 6 | { 7 | "size prints the spatial dimensions of the given Event Stream file.", 8 | "Syntax: ./size [options] /path/to/input.es", 9 | "Available options:", 10 | " -h, --help shows this help message", 11 | }, 12 | argc, 13 | argv, 14 | 1, 15 | {}, 16 | {}, 17 | [](pontella::command command) { 18 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 19 | switch (header.event_stream_type) { 20 | case sepia::type::generic: 21 | throw std::runtime_error("generic events do not have spatial dimensions"); 22 | case sepia::type::atis: 23 | std::cout << (header.width * 2) << "x" << header.height; 24 | std::cout.flush(); 25 | break; 26 | default: 27 | std::cout << header.width << "x" << header.height; 28 | std::cout.flush(); 29 | break; 30 | } 31 | }); 32 | return 0; 33 | } 34 | -------------------------------------------------------------------------------- /source/font.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | static const std::array base64_index{ 6 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 63, 62, 62, 63, 52, 53, 8 | 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9 | 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 10 | 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51}; 11 | 12 | std::vector base64_decode(const std::string& encoded_data) { 13 | const auto pad = 14 | !encoded_data.empty() && (encoded_data.size() % 4 > 0 || encoded_data[encoded_data.size() - 1] == '='); 15 | const auto unpadded_size = ((encoded_data.size() + 3) / 4 - pad) * 4; 16 | std::vector result(unpadded_size / 4 * 3 + pad); 17 | for (std::size_t input_index = 0, output_index = 0; input_index < unpadded_size; input_index += 4) { 18 | const uint32_t block = 19 | ((base64_index[encoded_data[input_index]] << 18) | (base64_index[encoded_data[input_index + 1]] << 12) 20 | | (base64_index[encoded_data[input_index + 2]] << 6) | (base64_index[encoded_data[input_index + 3]])); 21 | result[output_index++] = block >> 16; 22 | result[output_index++] = (block >> 8) & 0xFF; 23 | result[output_index++] = block & 0xFF; 24 | } 25 | if (pad) { 26 | uint32_t block = 27 | (base64_index[encoded_data[unpadded_size]] << 18) | (base64_index[encoded_data[unpadded_size + 1]] << 12); 28 | result[result.size() - 1] = block >> 16; 29 | if (encoded_data.size() > unpadded_size + 2 && encoded_data[unpadded_size + 2] != '=') { 30 | block |= (base64_index[encoded_data[unpadded_size + 2]] << 6); 31 | result.push_back((block >> 8) & 0xFF); 32 | } 33 | } 34 | return result; 35 | } 36 | 37 | const auto monaco_bytes = base64_decode( 38 | #include "../third_party/monaco.ttf.base64.hpp" 39 | ); 40 | -------------------------------------------------------------------------------- /source/evt2_to_es.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "evt.hpp" 3 | 4 | int main(int argc, char* argv[]) { 5 | return pontella::main( 6 | {"evt2_to_es converts a raw file (EVT2) into an Event Stream file", 7 | "Syntax: ./evt3_to_es [options] /path/to/input.raw /path/to/output.es", 8 | "Available options:", 9 | " -x size, --width size sets the sensor width in pixels if not specified in the header", 10 | " defaults to 640", 11 | " -y size, --height size sets the sensor height in pixels if not specified in the header", 12 | " defaults to 480", 13 | " -n, --normalize offsets the timestamps so that the first one is zero", 14 | " -h, --help shows this help message"}, 15 | argc, 16 | argv, 17 | 2, 18 | { 19 | {"width", {"x"}}, 20 | {"height", {"y"}}, 21 | }, 22 | { 23 | {"normalize", {"n"}}, 24 | }, 25 | [](pontella::command command) { 26 | if (command.arguments[0] == command.arguments[1]) { 27 | throw std::runtime_error("The raw input and the Event Stream output must be different files"); 28 | } 29 | if (command.arguments[0] == "none" && command.arguments[1] == "none") { 30 | throw std::runtime_error("none cannot be used for both the td file and aps file"); 31 | } 32 | evt::header default_header{640, 480}; 33 | { 34 | const auto name_and_argument = command.options.find("width"); 35 | if (name_and_argument != command.options.end()) { 36 | default_header.width = static_cast(std::stoull(name_and_argument->second)); 37 | } 38 | } 39 | { 40 | const auto name_and_argument = command.options.find("height"); 41 | if (name_and_argument != command.options.end()) { 42 | default_header.height = static_cast(std::stoull(name_and_argument->second)); 43 | } 44 | } 45 | auto stream = sepia::filename_to_ifstream(command.arguments[0]); 46 | const auto header = evt::read_header(*stream, std::move(default_header)); 47 | evt::observable_2( 48 | *stream, 49 | header, 50 | command.flags.find("normalize") != command.flags.end(), 51 | sepia::write( 52 | sepia::filename_to_ofstream(command.arguments[1]), header.width, header.height)); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /source/evt3_to_es.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "evt.hpp" 3 | 4 | int main(int argc, char* argv[]) { 5 | return pontella::main( 6 | {"evt3_to_es converts a raw file (EVT3) into an Event Stream file", 7 | "Syntax: ./evt3_to_es [options] /path/to/input.raw /path/to/output.es", 8 | "Available options:", 9 | " -x size, --width size sets the sensor width in pixels if not specified in the header", 10 | " defaults to 1280", 11 | " -y size, --height size sets the sensor height in pixels if not specified in the header", 12 | " defaults to 720", 13 | " -n, --normalize offsets the timestamps so that the first one is zero", 14 | " -h, --help shows this help message"}, 15 | argc, 16 | argv, 17 | 2, 18 | { 19 | {"width", {"x"}}, 20 | {"height", {"y"}}, 21 | }, 22 | { 23 | {"normalize", {"n"}}, 24 | }, 25 | [](pontella::command command) { 26 | if (command.arguments[0] == command.arguments[1]) { 27 | throw std::runtime_error("The raw input and the Event Stream output must be different files"); 28 | } 29 | if (command.arguments[0] == "none" && command.arguments[1] == "none") { 30 | throw std::runtime_error("none cannot be used for both the td file and aps file"); 31 | } 32 | evt::header default_header{1280, 720}; 33 | { 34 | const auto name_and_argument = command.options.find("width"); 35 | if (name_and_argument != command.options.end()) { 36 | default_header.width = static_cast(std::stoull(name_and_argument->second)); 37 | } 38 | } 39 | { 40 | const auto name_and_argument = command.options.find("height"); 41 | if (name_and_argument != command.options.end()) { 42 | default_header.height = static_cast(std::stoull(name_and_argument->second)); 43 | } 44 | } 45 | auto stream = sepia::filename_to_ifstream(command.arguments[0]); 46 | const auto header = evt::read_header(*stream, std::move(default_header)); 47 | evt::observable_3( 48 | *stream, 49 | header, 50 | command.flags.find("normalize") != command.flags.end(), 51 | sepia::write( 52 | sepia::filename_to_ofstream(command.arguments[1]), header.width, header.height)); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /source/es_to_csv.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | 4 | int main(int argc, char* argv[]) { 5 | return pontella::main( 6 | {"es_to_csv converts an Event Stream file into a csv file (compatible with Excel and Matlab)\n" 7 | "Syntax: ./es_to_csv [options] /path/to/input.es /path/to/output.csv\n", 8 | "Available options:", 9 | " -h, --help shows this help message"}, 10 | argc, 11 | argv, 12 | 2, 13 | {}, 14 | {}, 15 | [](pontella::command command) { 16 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 17 | auto input = sepia::filename_to_ifstream(command.arguments[0]); 18 | auto output = sepia::filename_to_ofstream(command.arguments[1]); 19 | switch (header.event_stream_type) { 20 | case sepia::type::generic: 21 | *output << "t,bytes\n"; 22 | sepia::join_observable( 23 | std::move(input), [&](sepia::generic_event generic_event) { 24 | *output << generic_event.t << ","; 25 | *output << std::hex; 26 | for (std::size_t index = 0; index < generic_event.bytes.size(); ++index) { 27 | *output << static_cast(generic_event.bytes[index]); 28 | if (index == generic_event.bytes.size() - 1) { 29 | *output << "\n"; 30 | } else { 31 | *output << " "; 32 | } 33 | } 34 | *output << std::dec; 35 | }); 36 | break; 37 | case sepia::type::dvs: 38 | *output << "t,x,y,is_increase\n"; 39 | sepia::join_observable(std::move(input), [&](sepia::dvs_event dvs_event) { 40 | *output << dvs_event.t << "," << dvs_event.x << "," << dvs_event.y << "," 41 | << dvs_event.is_increase << "\n"; 42 | }); 43 | break; 44 | case sepia::type::atis: 45 | *output << "t,x,y,is_threshold_crossing,polarity\n"; 46 | sepia::join_observable(std::move(input), [&](sepia::atis_event atis_event) { 47 | *output << atis_event.t << "," << atis_event.x << "," << atis_event.y << "," 48 | << atis_event.is_threshold_crossing << "," << atis_event.polarity << "\n"; 49 | }); 50 | break; 51 | case sepia::type::color: 52 | *output << "t,x,y,r,g,b\n"; 53 | sepia::join_observable(std::move(input), [&](sepia::color_event color_event) { 54 | *output << color_event.t << "," << color_event.x << "," << color_event.y << "," 55 | << static_cast(color_event.r) << "," << static_cast(color_event.g) 56 | << "," << static_cast(color_event.b) << "\n"; 57 | }); 58 | break; 59 | } 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: 0 4 | AlignAfterOpenBracket: AlwaysBreak 5 | AlignConsecutiveAssignments: false 6 | AlignConsecutiveDeclarations: false 7 | AlignEscapedNewlines: Right 8 | AlignOperands: true 9 | AlignTrailingComments: true 10 | AllowAllParametersOfDeclarationOnNextLine: false 11 | AllowShortBlocksOnASingleLine: false 12 | AllowShortCaseLabelsOnASingleLine: false 13 | AllowShortFunctionsOnASingleLine: Empty 14 | AllowShortIfStatementsOnASingleLine: false 15 | AllowShortLoopsOnASingleLine: true 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: true 20 | BinPackArguments: false 21 | BinPackParameters: false 22 | BraceWrapping: 23 | AfterClass: false 24 | AfterControlStatement: false 25 | AfterEnum: false 26 | AfterFunction: false 27 | AfterNamespace: false 28 | AfterObjCDeclaration: false 29 | AfterStruct: false 30 | AfterUnion: false 31 | BeforeCatch: false 32 | BeforeElse: false 33 | IndentBraces: false 34 | SplitEmptyFunction: true 35 | SplitEmptyRecord: true 36 | SplitEmptyNamespace: true 37 | BreakBeforeBinaryOperators: NonAssignment 38 | BreakBeforeBraces: Attach 39 | BreakBeforeInheritanceComma: false 40 | BreakBeforeTernaryOperators: false 41 | BreakConstructorInitializersBeforeComma: false 42 | BreakConstructorInitializers: AfterColon 43 | BreakAfterJavaFieldAnnotations: false 44 | BreakStringLiterals: true 45 | ColumnLimit: 120 46 | CommentPragmas: '^ IWYU pragma:' 47 | CompactNamespaces: false 48 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 49 | ConstructorInitializerIndentWidth: 4 50 | ContinuationIndentWidth: 4 51 | Cpp11BracedListStyle: true 52 | DerivePointerAlignment: false 53 | DisableFormat: false 54 | ExperimentalAutoDetectBinPacking: false 55 | FixNamespaceComments: false 56 | ForEachMacros: 57 | - foreach 58 | - Q_FOREACH 59 | - BOOST_FOREACH 60 | IncludeCategories: 61 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 62 | Priority: 2 63 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 64 | Priority: 3 65 | - Regex: '.*' 66 | Priority: 1 67 | IncludeIsMainRegex: '(Test)?$' 68 | IndentCaseLabels: true 69 | IndentWidth: 4 70 | IndentWrappedFunctionNames: false 71 | JavaScriptQuotes: Single 72 | JavaScriptWrapImports: true 73 | KeepEmptyLinesAtTheStartOfBlocks: false 74 | MacroBlockBegin: '' 75 | MacroBlockEnd: '' 76 | MaxEmptyLinesToKeep: 1 77 | NamespaceIndentation: All 78 | ObjCBlockIndentWidth: 4 79 | ObjCSpaceAfterProperty: false 80 | ObjCSpaceBeforeProtocolList: true 81 | PenaltyBreakAssignment: 2 82 | PenaltyBreakBeforeFirstCallParameter: 19 83 | PenaltyBreakComment: 300 84 | PenaltyBreakFirstLessLess: 120 85 | PenaltyBreakString: 1000 86 | PenaltyExcessCharacter: 1000000 87 | PenaltyReturnTypeOnItsOwnLine: 60 88 | PointerAlignment: Left 89 | ReflowComments: true 90 | SortIncludes: true 91 | SortUsingDeclarations: true 92 | SpaceAfterCStyleCast: false 93 | SpaceAfterTemplateKeyword: true 94 | SpaceBeforeAssignmentOperators: true 95 | SpaceBeforeParens: ControlStatements 96 | SpaceInEmptyParentheses: false 97 | SpacesBeforeTrailingComments: 1 98 | SpacesInAngles: false 99 | SpacesInContainerLiterals: false 100 | SpacesInCStyleCastParentheses: false 101 | SpacesInParentheses: false 102 | SpacesInSquareBrackets: false 103 | Standard: Cpp11 104 | TabWidth: 8 105 | UseTab: Never 106 | ... 107 | -------------------------------------------------------------------------------- /source/dat_to_es.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "dat.hpp" 3 | 4 | int main(int argc, char* argv[]) { 5 | return pontella::main( 6 | {"dat_to_es converts a td file and an aps file into an Event Stream file", 7 | "Syntax: ./dat_to_es [options] /path/to/input_td.dat /path/to/input_aps.dat /path/to/output.es", 8 | " If the string 'none' (without quotes) is used for the td (respectively, aps) file,", 9 | " the Event Stream file is build from the aps (respectively, td) file only", 10 | "Available options:", 11 | " -h, --help shows this help message"}, 12 | argc, 13 | argv, 14 | 3, 15 | {}, 16 | {}, 17 | [](pontella::command command) { 18 | if (command.arguments[0] == command.arguments[1]) { 19 | throw std::runtime_error("The td and aps inputs must be different files, and cannot be both none"); 20 | } 21 | if (command.arguments[0] == command.arguments[2]) { 22 | throw std::runtime_error("The td input and the Event Stream output must be different files"); 23 | } 24 | if (command.arguments[1] == command.arguments[2]) { 25 | throw std::runtime_error("The aps input and the Event Stream output must be different files"); 26 | } 27 | if (command.arguments[0] == "none" && command.arguments[1] == "none") { 28 | throw std::runtime_error("none cannot be used for both the td file and aps file"); 29 | } 30 | if (command.arguments[1] == "none") { 31 | auto stream = sepia::filename_to_ifstream(command.arguments[0]); 32 | const auto header = dat::read_header(*stream); 33 | dat::td_observable( 34 | *stream, 35 | header, 36 | sepia::write( 37 | sepia::filename_to_ofstream(command.arguments[2]), header.width, header.height)); 38 | } else if (command.arguments[0] == "none") { 39 | auto stream = sepia::filename_to_ifstream(command.arguments[1]); 40 | const auto header = dat::read_header(*stream); 41 | dat::aps_observable( 42 | *stream, 43 | header, 44 | sepia::write( 45 | sepia::filename_to_ofstream(command.arguments[2]), header.width, header.height)); 46 | } else { 47 | auto td_stream = sepia::filename_to_ifstream(command.arguments[0]); 48 | auto aps_stream = sepia::filename_to_ifstream(command.arguments[1]); 49 | const auto header = dat::read_header(*td_stream); 50 | { 51 | const auto aps_header = dat::read_header(*aps_stream); 52 | if (header.version != aps_header.version || header.width != aps_header.width 53 | || header.height != aps_header.height) { 54 | throw std::runtime_error("the td and aps file have incompatible headers"); 55 | } 56 | } 57 | dat::td_aps_observable( 58 | *td_stream, 59 | *aps_stream, 60 | header, 61 | sepia::write( 62 | sepia::filename_to_ofstream(command.arguments[2]), header.width, header.height)); 63 | } 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /source/timecode.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class timecode { 12 | public: 13 | timecode(uint64_t value) : _value(value) {} 14 | timecode(const std::string& representation) : _value(0) { 15 | if (std::all_of(representation.begin(), representation.end(), [](uint8_t character) { 16 | return std::isdigit(character); 17 | })) { 18 | _value = std::stoull(representation); 19 | } else { 20 | std::regex timecode_pattern("^(\\d+):(\\d+):(\\d+)(?:\\.(\\d+))?$"); 21 | std::smatch match; 22 | if (std::regex_match(representation, match, timecode_pattern)) { 23 | _value = 24 | (std::stoull(std::string(match[1].first, match[1].second)) * 3600000000ull 25 | + std::stoull(std::string(match[2].first, match[2].second)) * 60000000ull 26 | + std::stoull(std::string(match[3].first, match[3].second)) * 1000000ull); 27 | if (match[4].matched) { 28 | const auto fraction_string = std::string(match[4].first, match[4].second); 29 | if (fraction_string.size() == 6) { 30 | _value += std::stoull(fraction_string); 31 | } else if (fraction_string.size() < 6) { 32 | _value += std::stoull(fraction_string + std::string(6 - fraction_string.size(), '0')); 33 | } else { 34 | _value = 35 | static_cast(std::llround(std::stod(std::string("0.") + fraction_string) * 1e6)); 36 | } 37 | } 38 | } else { 39 | throw std::runtime_error(std::string("parsing the timecode '") + representation + "' failed"); 40 | } 41 | } 42 | } 43 | timecode(const timecode&) = default; 44 | timecode(timecode&&) = default; 45 | timecode& operator=(const timecode&) = default; 46 | timecode& operator=(timecode&&) = default; 47 | virtual ~timecode() {} 48 | 49 | /// to_string converts the timecode to a human-friendly string. 50 | virtual std::string to_string() const { 51 | const auto hours = _value / 3600000000ull; 52 | auto remainder = _value - hours * 3600000000ull; 53 | const auto minutes = remainder / 60000000ull; 54 | remainder -= minutes * 60000000ull; 55 | const auto seconds = remainder / 1000000ull; 56 | remainder -= seconds * 1000000ull; 57 | std::stringstream stream; 58 | stream.fill('0'); 59 | stream << _value << " µs (" << std::setw(2) << hours << ':' << std::setw(2) << minutes << ':' << std::setw(2) 60 | << seconds << '.' << std::setw(6) << remainder << ")"; 61 | return stream.str(); 62 | } 63 | 64 | /// to_timecode_string converts the timecode to a human-friendly string without microseconds. 65 | virtual std::string to_timecode_string() const { 66 | const auto hours = _value / 3600000000ull; 67 | auto remainder = _value - hours * 3600000000ull; 68 | const auto minutes = remainder / 60000000ull; 69 | remainder -= minutes * 60000000ull; 70 | const auto seconds = remainder / 1000000ull; 71 | remainder -= seconds * 1000000ull; 72 | std::stringstream stream; 73 | stream.fill('0'); 74 | stream << std::setw(2) << hours << ':' << std::setw(2) << minutes << ':' << std::setw(2) << seconds << '.' 75 | << std::setw(6) << remainder; 76 | return stream.str(); 77 | } 78 | 79 | /// value returns a number of microseconds. 80 | virtual uint64_t value() const { 81 | return _value; 82 | } 83 | 84 | protected: 85 | uint64_t _value; 86 | }; 87 | -------------------------------------------------------------------------------- /source/cut.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | #include "timecode.hpp" 4 | 5 | enum class timestamp { preserve, relative, zero }; 6 | 7 | /// cut creates a new Event Stream file with only events from the given time range. 8 | template 9 | void cut(sepia::header header, const pontella::command& command) { 10 | const auto begin_t = timecode(command.arguments[2]).value(); 11 | const auto end_t = timecode(command.arguments[3]).value(); 12 | auto first_t = std::numeric_limits::max(); 13 | auto timestamp_strategy = timestamp::preserve; 14 | { 15 | const auto name_and_argument = command.options.find("timestamp"); 16 | if (name_and_argument != command.options.end()) { 17 | if (name_and_argument->second == "relative") { 18 | timestamp_strategy = timestamp::relative; 19 | } else if (name_and_argument->second == "zero") { 20 | timestamp_strategy = timestamp::zero; 21 | } else if (name_and_argument->second != "preserve") { 22 | throw std::runtime_error("timestamp must be one of {preserve, relative, zero}"); 23 | } 24 | } 25 | } 26 | sepia::write write( 27 | sepia::filename_to_ofstream(command.arguments[1]), header.width, header.height); 28 | sepia::join_observable( 29 | sepia::filename_to_ifstream(command.arguments[0]), [&](sepia::event event) { 30 | if (event.t < begin_t) { 31 | return; 32 | } 33 | if (event.t >= end_t) { 34 | throw sepia::end_of_file(); 35 | } 36 | switch (timestamp_strategy) { 37 | case timestamp::preserve: { 38 | break; 39 | } 40 | case timestamp::relative: { 41 | event.t -= begin_t; 42 | break; 43 | } 44 | case timestamp::zero: { 45 | if (first_t == std::numeric_limits::max()) { 46 | first_t = event.t; 47 | } 48 | event.t -= first_t; 49 | break; 50 | } 51 | } 52 | write(event); 53 | }); 54 | } 55 | 56 | int main(int argc, char* argv[]) { 57 | return pontella::main( 58 | { 59 | "cut generates a new Event Stream file with only events from the given time range.", 60 | "Syntax: ./cut [options] /path/to/input.es /path/to/output.es begin end", 61 | "Available options:", 62 | " -t [strategy], --timestamp [strategy] selects the timestamp conversion strategy", 63 | " one of preserve (default), relative, zero", 64 | " preserve uses the same timestamps as the original", 65 | " relative calculate timestamps relatively to begin", 66 | " zero sets the first generated timestamp to zero", 67 | " -h, --help shows this help message", 68 | }, 69 | argc, 70 | argv, 71 | 4, 72 | {{"timestamp", {"t"}}}, 73 | {}, 74 | [](pontella::command command) { 75 | if (command.arguments[0] == command.arguments[1]) { 76 | throw std::runtime_error("The Event Stream input and output must be different files"); 77 | } 78 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 79 | switch (header.event_stream_type) { 80 | case sepia::type::generic: { 81 | cut(header, command); 82 | break; 83 | } 84 | case sepia::type::dvs: { 85 | cut(header, command); 86 | break; 87 | } 88 | case sepia::type::atis: { 89 | cut(header, command); 90 | break; 91 | } 92 | case sepia::type::color: { 93 | cut(header, command); 94 | break; 95 | } 96 | } 97 | }); 98 | return 0; 99 | } 100 | -------------------------------------------------------------------------------- /source/crop.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | 4 | /// crop creates a new Event Stream file with only events from the given region. 5 | template 6 | void crop( 7 | const sepia::header& header, 8 | std::unique_ptr input_stream, 9 | std::unique_ptr output_stream, 10 | uint16_t left, 11 | uint16_t bottom, 12 | uint16_t width, 13 | uint16_t height, 14 | bool preserve_offset) { 15 | const auto right = left + width; 16 | const auto top = bottom + height; 17 | if (preserve_offset) { 18 | sepia::write write(std::move(output_stream), header.width, header.height); 19 | sepia::join_observable(std::move(input_stream), [&](sepia::event event) { 20 | if (event.x >= left && event.x < right && event.y >= bottom && event.y < top) { 21 | write(event); 22 | } 23 | }); 24 | } else { 25 | sepia::write write(std::move(output_stream), width, height); 26 | sepia::join_observable(std::move(input_stream), [&](sepia::event event) { 27 | if (event.x >= left && event.x < right && event.y >= bottom && event.y < top) { 28 | event.x -= left; 29 | event.y -= bottom; 30 | write(event); 31 | } 32 | }); 33 | } 34 | } 35 | 36 | int main(int argc, char* argv[]) { 37 | return pontella::main( 38 | { 39 | "crop generates a new Event Stream file with only events from the given region.", 40 | "Syntax: ./crop [options] /path/to/input.es /path/to/output.es left bottom width height", 41 | "Available options:", 42 | " -p, --preserve-offset prevents the coordinates of the cropped area from being normalized", 43 | " -h, --help shows this help message", 44 | }, 45 | argc, 46 | argv, 47 | 6, 48 | {}, 49 | {{"preserve-offset", {"p"}}}, 50 | [](pontella::command command) { 51 | if (command.arguments[0] == command.arguments[1]) { 52 | throw std::runtime_error("The Event Stream input and output must be different files"); 53 | } 54 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 55 | const auto left = std::stoull(command.arguments[2]); 56 | const auto bottom = std::stoull(command.arguments[3]); 57 | const auto width = std::stoull(command.arguments[4]); 58 | const auto height = std::stoull(command.arguments[5]); 59 | if (left + width > header.width || bottom + height > header.height) { 60 | throw std::runtime_error("The selected region is out of scope"); 61 | } 62 | const auto preserve_offset = command.flags.find("preserve-offset") != command.flags.end(); 63 | auto input_stream = sepia::filename_to_ifstream(command.arguments[0]); 64 | auto output_stream = sepia::filename_to_ofstream(command.arguments[1]); 65 | switch (header.event_stream_type) { 66 | case sepia::type::generic: { 67 | throw std::runtime_error("Unsupported event type: generic"); 68 | break; 69 | } 70 | case sepia::type::dvs: { 71 | crop( 72 | header, 73 | std::move(input_stream), 74 | std::move(output_stream), 75 | left, 76 | bottom, 77 | width, 78 | height, 79 | preserve_offset); 80 | break; 81 | } 82 | case sepia::type::atis: { 83 | crop( 84 | header, 85 | std::move(input_stream), 86 | std::move(output_stream), 87 | left, 88 | bottom, 89 | width, 90 | height, 91 | preserve_offset); 92 | break; 93 | } 94 | case sepia::type::color: { 95 | crop( 96 | header, 97 | std::move(input_stream), 98 | std::move(output_stream), 99 | left, 100 | bottom, 101 | width, 102 | height, 103 | preserve_offset); 104 | break; 105 | } 106 | } 107 | }); 108 | return 0; 109 | } 110 | -------------------------------------------------------------------------------- /source/es_to_ply.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | #include "timecode.hpp" 4 | 5 | int main(int argc, char* argv[]) { 6 | return pontella::main( 7 | {"es_to_ply converts an Event Stream file to a PLY file (Polygon File Format, compatible with Blender)\n" 8 | "Syntax: ./es_to_ply [options] /path/to/input.es /path/to/output_on.ply /path/to/output_off.ply\n", 9 | "Available options:", 10 | " -t timestamp, --timestamp timestamp sets the initial timestamp for the point cloud (timecode)", 11 | " defaults to 00:00:00", 12 | " -d duration, --duration duration sets the duration for the point cloud (timecode)", 13 | " defaults to 00:00:01", 14 | " -h, --help shows this help message"}, 15 | argc, 16 | argv, 17 | 3, 18 | { 19 | {"timestamp", {"t"}}, 20 | {"duration", {"d"}}, 21 | }, 22 | {}, 23 | [](pontella::command command) { 24 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 25 | uint64_t begin_t = 0; 26 | { 27 | const auto name_and_argument = command.options.find("timestamp"); 28 | if (name_and_argument != command.options.end()) { 29 | begin_t = timecode(name_and_argument->second).value(); 30 | } 31 | } 32 | auto target_end_t = std::numeric_limits::max(); 33 | { 34 | const auto name_and_argument = command.options.find("duration"); 35 | if (name_and_argument != command.options.end()) { 36 | const auto duration = timecode(name_and_argument->second).value(); 37 | target_end_t = begin_t + duration; 38 | } 39 | } 40 | auto input = sepia::filename_to_ifstream(command.arguments[0]); 41 | std::size_t on_count = 0; 42 | std::size_t off_count = 0; 43 | auto end_t = target_end_t; 44 | switch (header.event_stream_type) { 45 | case sepia::type::generic: 46 | throw std::runtime_error("generic events are not compatible with this application"); 47 | break; 48 | case sepia::type::dvs: 49 | sepia::join_observable(std::move(input), [&](sepia::dvs_event dvs_event) { 50 | if (dvs_event.t >= begin_t) { 51 | if (dvs_event.t >= target_end_t) { 52 | throw sepia::end_of_file(); 53 | } 54 | if (dvs_event.is_increase) { 55 | ++on_count; 56 | } else { 57 | ++off_count; 58 | } 59 | if (target_end_t == std::numeric_limits::max()) { 60 | end_t = dvs_event.t + 1; 61 | } 62 | } 63 | }); 64 | break; 65 | case sepia::type::atis: 66 | sepia::join_observable(std::move(input), [&](sepia::atis_event atis_event) { 67 | if (!atis_event.is_threshold_crossing && atis_event.t >= begin_t) { 68 | if (atis_event.t >= end_t) { 69 | return; 70 | } 71 | if (atis_event.polarity) { 72 | ++on_count; 73 | } else { 74 | ++off_count; 75 | } 76 | if (target_end_t == std::numeric_limits::max()) { 77 | end_t = atis_event.t + 1; 78 | } 79 | } 80 | }); 81 | break; 82 | case sepia::type::color: 83 | sepia::join_observable(std::move(input), [&](sepia::color_event color_event) { 84 | if (color_event.t >= begin_t) { 85 | if (color_event.t >= end_t) { 86 | return; 87 | } 88 | if (color_event.r + color_event.b + color_event.g >= 128 * 3) { 89 | ++on_count; 90 | } else { 91 | ++off_count; 92 | } 93 | if (target_end_t == std::numeric_limits::max()) { 94 | end_t = color_event.t + 1; 95 | } 96 | } 97 | }); 98 | break; 99 | } 100 | if (on_count == 0 && off_count == 0) { 101 | throw std::runtime_error("there are no DVS events in the given file and range"); 102 | } 103 | auto output_on = sepia::filename_to_ofstream(command.arguments[1]); 104 | auto output_off = sepia::filename_to_ofstream(command.arguments[2]); 105 | *output_on << "ply\nformat binary_little_endian 1.0\nelement vertex " << on_count 106 | << "\nproperty double x\nproperty double y\nproperty double z\nend_header\n"; 107 | *output_off << "ply\nformat binary_little_endian 1.0\nelement vertex " << off_count 108 | << "\nproperty double x\nproperty double y\nproperty double z\nend_header\n"; 109 | input = sepia::filename_to_ifstream(command.arguments[0]); 110 | const auto spatial_scale = 1.0 / static_cast(std::max(header.width, header.height)); 111 | const auto temporal_scale = begin_t == end_t ? 1.0 : 1.0 / (end_t - begin_t); 112 | switch (header.event_stream_type) { 113 | case sepia::type::generic: 114 | throw std::runtime_error("generic events are not compatible with this application"); 115 | break; 116 | case sepia::type::dvs: 117 | sepia::join_observable(std::move(input), [&](sepia::dvs_event dvs_event) { 118 | if (dvs_event.t >= begin_t) { 119 | if (dvs_event.t >= end_t) { 120 | return; 121 | } 122 | std::array vertex{ 123 | (dvs_event.x - header.width / 2.0) * spatial_scale, 124 | (dvs_event.y - header.height / 2.0) * spatial_scale, 125 | (dvs_event.t - begin_t) * temporal_scale, 126 | }; 127 | if (dvs_event.is_increase) { 128 | output_on->write( 129 | reinterpret_cast(vertex.data()), vertex.size() * sizeof(double)); 130 | } else { 131 | output_off->write( 132 | reinterpret_cast(vertex.data()), vertex.size() * sizeof(double)); 133 | } 134 | } 135 | }); 136 | break; 137 | case sepia::type::atis: 138 | sepia::join_observable(std::move(input), [&](sepia::atis_event atis_event) { 139 | if (!atis_event.is_threshold_crossing && atis_event.t >= begin_t) { 140 | if (atis_event.t >= end_t) { 141 | return; 142 | } 143 | std::array vertex{ 144 | (atis_event.x - header.width / 2.0) * spatial_scale, 145 | (atis_event.y - header.height / 2.0) * spatial_scale, 146 | (atis_event.t - begin_t) * temporal_scale, 147 | }; 148 | if (atis_event.polarity) { 149 | output_on->write( 150 | reinterpret_cast(vertex.data()), vertex.size() * sizeof(double)); 151 | } else { 152 | output_off->write( 153 | reinterpret_cast(vertex.data()), vertex.size() * sizeof(double)); 154 | } 155 | } 156 | }); 157 | break; 158 | case sepia::type::color: 159 | sepia::join_observable(std::move(input), [&](sepia::color_event color_event) { 160 | if (color_event.t >= begin_t) { 161 | if (color_event.t >= end_t) { 162 | return; 163 | } 164 | std::array vertex{ 165 | (color_event.x - header.width / 2.0) * spatial_scale, 166 | (color_event.y - header.height / 2.0) * spatial_scale, 167 | (color_event.t - begin_t) * temporal_scale, 168 | }; 169 | if (color_event.r + color_event.b + color_event.g >= 128 * 3) { 170 | output_on->write( 171 | reinterpret_cast(vertex.data()), vertex.size() * sizeof(double)); 172 | } else { 173 | output_off->write( 174 | reinterpret_cast(vertex.data()), vertex.size() * sizeof(double)); 175 | } 176 | } 177 | }); 178 | break; 179 | } 180 | }); 181 | } 182 | -------------------------------------------------------------------------------- /source/rainbow.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | #include "../third_party/tarsier/source/stitch.hpp" 4 | #include "html.hpp" 5 | #include "timecode.hpp" 6 | #include 7 | 8 | struct color { 9 | uint8_t r; 10 | uint8_t g; 11 | uint8_t b; 12 | color(uint8_t default_r, uint8_t default_g, uint8_t default_b) : r(default_r), g(default_g), b(default_b) {} 13 | color(const std::string& hexadecimal_string) { 14 | if (hexadecimal_string.size() != 7 || hexadecimal_string.front() != '#' 15 | || std::any_of(std::next(hexadecimal_string.begin()), hexadecimal_string.end(), [](char character) { 16 | return !std::isxdigit(character); 17 | })) { 18 | throw std::runtime_error("color must be formatted as #hhhhhh, where h is an hexadecimal digit"); 19 | } 20 | r = static_cast(std::stoul(hexadecimal_string.substr(1, 2), nullptr, 16)); 21 | g = static_cast(std::stoul(hexadecimal_string.substr(3, 2), nullptr, 16)); 22 | b = static_cast(std::stoul(hexadecimal_string.substr(5, 2), nullptr, 16)); 23 | } 24 | }; 25 | 26 | std::array parula_colors = { 27 | {{53, 42, 135}, {54, 48, 147}, {54, 55, 160}, {53, 61, 173}, {50, 67, 186}, {44, 74, 199}, 28 | {32, 83, 212}, {15, 92, 221}, {3, 99, 225}, {2, 104, 225}, {4, 109, 224}, {8, 113, 222}, 29 | {13, 117, 220}, {16, 121, 218}, {18, 125, 216}, {20, 129, 214}, {20, 133, 212}, {19, 137, 211}, 30 | {16, 142, 210}, {12, 147, 210}, {9, 152, 209}, {7, 156, 207}, {6, 160, 205}, {6, 164, 202}, 31 | {6, 167, 198}, {7, 169, 194}, {10, 172, 190}, {15, 174, 185}, {21, 177, 180}, {29, 179, 175}, 32 | {37, 181, 169}, {46, 183, 164}, {56, 185, 158}, {66, 187, 152}, {77, 188, 146}, {89, 189, 140}, 33 | {101, 190, 134}, {113, 191, 128}, {124, 191, 123}, {135, 191, 119}, {146, 191, 115}, {156, 191, 111}, 34 | {165, 190, 107}, {174, 190, 103}, {183, 189, 100}, {192, 188, 96}, {200, 188, 93}, {209, 187, 89}, 35 | {217, 186, 86}, {225, 185, 82}, {233, 185, 78}, {241, 185, 74}, {248, 187, 68}, {253, 190, 61}, 36 | {255, 195, 55}, {254, 200, 50}, {252, 206, 46}, {250, 211, 42}, {247, 216, 38}, {245, 222, 33}, 37 | {245, 228, 29}, {245, 235, 24}, {246, 243, 19}, {249, 251, 14}}}; 38 | 39 | template 40 | std::pair read_begin_t_and_end_t(std::unique_ptr stream) { 41 | std::pair result = {std::numeric_limits::max(), 1}; 42 | sepia::join_observable(std::move(stream), [&](sepia::event event) { 43 | if (result.first == std::numeric_limits::max()) { 44 | result.first = event.t; 45 | } 46 | result.second = event.t; 47 | }); 48 | if (result.first == std::numeric_limits::max()) { 49 | result.first = 0; 50 | result.second = 1; 51 | } else if (result.first == result.second) { 52 | ++result.second; 53 | } 54 | return result; 55 | } 56 | 57 | template 58 | std::vector rainbow( 59 | const sepia::header& header, 60 | std::unique_ptr stream, 61 | color initial_color, 62 | float alpha, 63 | std::pair begin_t_and_end_t) { 64 | const auto t_scale = parula_colors.size() / static_cast(begin_t_and_end_t.second - begin_t_and_end_t.first); 65 | auto t_to_color = [=](uint64_t t) { 66 | const auto theta = (t - begin_t_and_end_t.first) * t_scale; 67 | const auto theta_integer = static_cast(std::floor(theta)); 68 | if (theta_integer >= parula_colors.size() - 1) { 69 | return parula_colors[parula_colors.size() - 1]; 70 | } 71 | const auto ratio = theta - theta_integer; 72 | return color{ 73 | static_cast( 74 | parula_colors[theta_integer + 1].r * ratio + parula_colors[theta_integer].r * (1.0f - ratio)), 75 | static_cast( 76 | parula_colors[theta_integer + 1].g * ratio + parula_colors[theta_integer].g * (1.0f - ratio)), 77 | static_cast( 78 | parula_colors[theta_integer + 1].b * ratio + parula_colors[theta_integer].b * (1.0f - ratio)), 79 | }; 80 | }; 81 | std::vector frame(header.width * header.height * 3, initial_color.r); 82 | for (std::size_t index = 0; index < header.width * header.height; ++index) { 83 | frame[index * 3 + 1] = initial_color.g; 84 | frame[index * 3 + 2] = initial_color.b; 85 | } 86 | sepia::join_observable(std::move(stream), [&](sepia::event event) { 87 | const auto color = t_to_color(event.t); 88 | const auto index = event.x + (header.height - 1 - event.y) * header.width; 89 | frame[3 * index + 0] = static_cast((1.0f - alpha) * frame[3 * index + 0] + alpha * color.r); 90 | frame[3 * index + 1] = static_cast((1.0f - alpha) * frame[3 * index + 1] + alpha * color.g); 91 | frame[3 * index + 2] = static_cast((1.0f - alpha) * frame[3 * index + 2] + alpha * color.b); 92 | }); 93 | return frame; 94 | } 95 | 96 | int main(int argc, char* argv[]) { 97 | return pontella::main( 98 | {"rainbow represents events by mapping time to colors", 99 | "Syntax: ./rainbow [options] /path/to/input.es /path/to/output.ppm", 100 | "Available options:", 101 | " -a alpha, --alpha alpha sets the transparency level for each event", 102 | " must be in the range ]0, 1]", 103 | " defaults to 0.1", 104 | " -l color, --idlecolor color sets the background color", 105 | " color must be formatted as #hhhhhh,", 106 | " where h is an hexadecimal digit", 107 | " defaults to #191919", 108 | " -h, --help shows this help message"}, 109 | argc, 110 | argv, 111 | 2, 112 | { 113 | {"alpha", {"a"}}, 114 | {"idlecolor", {"l"}}, 115 | }, 116 | {}, 117 | [](pontella::command command) { 118 | float alpha = 0.1; 119 | { 120 | const auto name_and_argument = command.options.find("alpha"); 121 | if (name_and_argument != command.options.end()) { 122 | alpha = std::stof(name_and_argument->second); 123 | if (alpha <= 0.0 || alpha > 1.0) { 124 | throw std::runtime_error("alpha must be in the range ]0, 1]"); 125 | } 126 | } 127 | } 128 | color idle_color(0x29, 0x29, 0x29); 129 | { 130 | const auto name_and_argument = command.options.find("idlecolor"); 131 | if (name_and_argument != command.options.end()) { 132 | idle_color = color(name_and_argument->second); 133 | } 134 | } 135 | std::pair begin_t_and_end_t; 136 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 137 | switch (header.event_stream_type) { 138 | case sepia::type::generic: { 139 | throw std::runtime_error("generic events are not compatible with this application"); 140 | break; 141 | } 142 | case sepia::type::dvs: { 143 | begin_t_and_end_t = 144 | read_begin_t_and_end_t(sepia::filename_to_ifstream(command.arguments[0])); 145 | break; 146 | } 147 | case sepia::type::atis: { 148 | begin_t_and_end_t = 149 | read_begin_t_and_end_t(sepia::filename_to_ifstream(command.arguments[0])); 150 | break; 151 | } 152 | case sepia::type::color: { 153 | begin_t_and_end_t = 154 | read_begin_t_and_end_t(sepia::filename_to_ifstream(command.arguments[0])); 155 | break; 156 | } 157 | } 158 | std::vector frame; 159 | switch (header.event_stream_type) { 160 | case sepia::type::generic: { 161 | throw std::runtime_error("generic events are not compatible with this application"); 162 | break; 163 | } 164 | case sepia::type::dvs: { 165 | frame = rainbow( 166 | header, 167 | sepia::filename_to_ifstream(command.arguments[0]), 168 | idle_color, 169 | alpha, 170 | begin_t_and_end_t); 171 | break; 172 | } 173 | case sepia::type::atis: { 174 | frame = rainbow( 175 | header, 176 | sepia::filename_to_ifstream(command.arguments[0]), 177 | idle_color, 178 | alpha, 179 | begin_t_and_end_t); 180 | break; 181 | } 182 | case sepia::type::color: { 183 | frame = rainbow( 184 | header, 185 | sepia::filename_to_ifstream(command.arguments[0]), 186 | idle_color, 187 | alpha, 188 | begin_t_and_end_t); 189 | break; 190 | } 191 | } 192 | auto output = sepia::filename_to_ofstream(command.arguments[1]); 193 | *output << "P6\n" << header.width << " " << header.height << "\n255\n"; 194 | output->write(reinterpret_cast(frame.data()), frame.size()); 195 | }); 196 | } 197 | -------------------------------------------------------------------------------- /source/dat.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../third_party/sepia/source/sepia.hpp" 4 | #include 5 | 6 | namespace dat { 7 | /// header bundles a .dat file header's information. 8 | struct header { 9 | uint8_t version; 10 | uint16_t width; 11 | uint16_t height; 12 | }; 13 | 14 | /// read_header retrieves header information from a .dat file. 15 | inline header read_header(std::istream& stream) { 16 | std::vector header_lines; 17 | for (;;) { 18 | if (stream.peek() != '%' || stream.eof()) { 19 | break; 20 | } 21 | stream.ignore(); 22 | header_lines.emplace_back(); 23 | for (;;) { 24 | const auto character = stream.get(); 25 | if (stream.eof()) { 26 | break; 27 | } 28 | if (character == '\n') { 29 | break; 30 | } 31 | header_lines.back().push_back(character); 32 | } 33 | } 34 | if (header_lines.empty() 35 | || std::any_of(header_lines.begin(), header_lines.end(), [](const std::string& header_line) { 36 | return std::any_of(header_line.begin(), header_line.end(), [](char character) { 37 | return !std::isprint(character) && !std::isspace(character); 38 | }); 39 | })) { 40 | stream.seekg(0, std::istream::beg); 41 | return {0, 304, 240}; 42 | } 43 | stream.ignore(2); 44 | header stream_header = {}; 45 | for (const auto& header_line : header_lines) { 46 | std::vector words; 47 | for (auto character : header_line) { 48 | if (std::isspace(character)) { 49 | if (!words.empty() && !words.back().empty()) { 50 | words.emplace_back(); 51 | } 52 | } else { 53 | if (words.empty()) { 54 | words.emplace_back(); 55 | } 56 | words.back().push_back(character); 57 | } 58 | } 59 | if (words.size() > 1) { 60 | try { 61 | if (words[0] == "Version") { 62 | stream_header.version = static_cast(stoul(words[1])); 63 | } else if (words[0] == "Width") { 64 | stream_header.width = static_cast(stoul(words[1])); 65 | } else if (words[0] == "Height") { 66 | stream_header.height = static_cast(stoul(words[1])); 67 | } 68 | } catch (const std::invalid_argument&) { 69 | } catch (const std::out_of_range&) { 70 | } 71 | } 72 | } 73 | if (stream_header.version >= 2 && stream_header.width > 0 && stream_header.height > 0) { 74 | return stream_header; 75 | } 76 | return {stream_header.version, 304, 240}; 77 | } 78 | 79 | /// bytes_to_dvs_event converts raw bytes to a polarized event. 80 | /// The DVS event type is used for both td and aps .dat files. 81 | inline sepia::dvs_event bytes_to_dvs_event(std::array bytes, header stream_header) { 82 | if (stream_header.version < 2) { 83 | return { 84 | static_cast(bytes[0]) | (static_cast(bytes[1]) << 8) 85 | | (static_cast(bytes[2]) << 16) | (static_cast(bytes[3]) << 24), 86 | static_cast(static_cast(bytes[4]) | (static_cast(bytes[5] & 1) << 8)), 87 | static_cast( 88 | stream_header.height - 1 89 | - (static_cast(bytes[5] >> 1) | (static_cast(bytes[6] & 1) << 7))), 90 | (bytes[6] & 0b10) == 0b10, 91 | }; 92 | } 93 | return { 94 | static_cast(bytes[0]) | (static_cast(bytes[1]) << 8) 95 | | (static_cast(bytes[2]) << 16) | (static_cast(bytes[3]) << 24), 96 | static_cast(static_cast(bytes[4]) | (static_cast(bytes[5] & 0b111111) << 8)), 97 | static_cast( 98 | stream_header.height - 1 99 | - (static_cast(bytes[5] >> 6) | (static_cast(bytes[6]) << 2) 100 | | (static_cast(bytes[7] & 0b1111) << 10))), 101 | (bytes[7] & 0b10000) == 0b10000, 102 | }; 103 | } 104 | 105 | /// td_observable dispatches DVS events from a td stream. 106 | /// The header must be read from the stream before calling this function. 107 | template 108 | inline void td_observable(std::istream& stream, header stream_header, HandleEvent handle_event) { 109 | uint64_t previous_t = 0; 110 | for (;;) { 111 | std::array bytes; 112 | stream.read(reinterpret_cast(bytes.data()), bytes.size()); 113 | if (stream.eof()) { 114 | break; 115 | } 116 | const auto dvs_event = bytes_to_dvs_event(bytes, stream_header); 117 | if (dvs_event.t >= previous_t && dvs_event.x < stream_header.width && dvs_event.y < stream_header.height) { 118 | handle_event(dvs_event); 119 | previous_t = dvs_event.t; 120 | } 121 | } 122 | } 123 | 124 | /// aps_observable dispatches ATIS events from an aps stream. 125 | /// The header must be read from the stream before calling this function. 126 | template 127 | inline void aps_observable(std::istream& stream, header stream_header, HandleEvent handle_event) { 128 | uint64_t previous_t = 0; 129 | for (;;) { 130 | std::array bytes; 131 | stream.read(reinterpret_cast(bytes.data()), bytes.size()); 132 | if (stream.eof()) { 133 | break; 134 | } 135 | const auto dvs_event = bytes_to_dvs_event(bytes, stream_header); 136 | if (dvs_event.t >= previous_t && dvs_event.x < stream_header.width && dvs_event.y < stream_header.height) { 137 | handle_event(sepia::atis_event{dvs_event.t, dvs_event.x, dvs_event.y, true, dvs_event.is_increase}); 138 | previous_t = dvs_event.t; 139 | } 140 | } 141 | } 142 | 143 | /// td_aps_observable dispatches ATIS events from a td stream and an aps stream. 144 | /// The headers must be read from both streams before calling this function. 145 | template 146 | inline void td_aps_observable( 147 | std::istream& td_stream, 148 | std::istream& aps_stream, 149 | header stream_header, 150 | HandleEvent handle_event) { 151 | sepia::dvs_event td_event = {}; 152 | sepia::dvs_event aps_event = {}; 153 | uint64_t previous_t = 0; 154 | for (;;) { 155 | std::array bytes; 156 | td_stream.read(reinterpret_cast(bytes.data()), bytes.size()); 157 | if (td_stream.eof()) { 158 | break; 159 | } else { 160 | td_event = bytes_to_dvs_event(bytes, stream_header); 161 | if (td_event.x < stream_header.width && td_event.y < stream_header.height) { 162 | break; 163 | } 164 | } 165 | } 166 | for (;;) { 167 | std::array bytes; 168 | aps_stream.read(reinterpret_cast(bytes.data()), bytes.size()); 169 | if (aps_stream.eof()) { 170 | break; 171 | } else { 172 | aps_event = bytes_to_dvs_event(bytes, stream_header); 173 | if (aps_event.x < stream_header.width && aps_event.y < stream_header.height) { 174 | break; 175 | } 176 | } 177 | } 178 | while (!td_stream.eof() && !aps_stream.eof()) { 179 | if (td_event.t <= aps_event.t) { 180 | handle_event(sepia::atis_event{td_event.t, td_event.x, td_event.y, false, td_event.is_increase}); 181 | previous_t = td_event.t; 182 | for (;;) { 183 | std::array bytes; 184 | td_stream.read(reinterpret_cast(bytes.data()), bytes.size()); 185 | if (td_stream.eof()) { 186 | break; 187 | } 188 | td_event = bytes_to_dvs_event(bytes, stream_header); 189 | if (td_event.t >= previous_t && td_event.x < stream_header.width 190 | && td_event.y < stream_header.height) { 191 | break; 192 | } 193 | } 194 | } else { 195 | handle_event(sepia::atis_event{aps_event.t, aps_event.x, aps_event.y, true, aps_event.is_increase}); 196 | previous_t = aps_event.t; 197 | for (;;) { 198 | std::array bytes; 199 | aps_stream.read(reinterpret_cast(bytes.data()), bytes.size()); 200 | if (aps_stream.eof()) { 201 | break; 202 | } 203 | aps_event = bytes_to_dvs_event(bytes, stream_header); 204 | if (aps_event.t >= previous_t && aps_event.x < stream_header.width 205 | && aps_event.y < stream_header.height) { 206 | break; 207 | } 208 | } 209 | } 210 | } 211 | if (td_stream) { 212 | handle_event(sepia::atis_event{td_event.t, td_event.x, td_event.y, false, td_event.is_increase}); 213 | for (;;) { 214 | std::array bytes; 215 | td_stream.read(reinterpret_cast(bytes.data()), bytes.size()); 216 | if (td_stream.eof()) { 217 | break; 218 | } 219 | td_event = bytes_to_dvs_event(bytes, stream_header); 220 | if (td_event.t >= previous_t && td_event.x < stream_header.width && td_event.y < stream_header.height) { 221 | handle_event(sepia::atis_event{td_event.t, td_event.x, td_event.y, false, td_event.is_increase}); 222 | previous_t = td_event.t; 223 | } 224 | } 225 | } else { 226 | handle_event(sepia::atis_event{aps_event.t, aps_event.x, aps_event.y, false, aps_event.is_increase}); 227 | for (;;) { 228 | std::array bytes; 229 | aps_stream.read(reinterpret_cast(bytes.data()), bytes.size()); 230 | if (aps_stream.eof()) { 231 | break; 232 | } 233 | aps_event = bytes_to_dvs_event(bytes, stream_header); 234 | if (aps_event.t >= previous_t && aps_event.x < stream_header.width 235 | && aps_event.y < stream_header.height) { 236 | handle_event( 237 | sepia::atis_event{aps_event.t, aps_event.x, aps_event.y, false, aps_event.is_increase}); 238 | previous_t = aps_event.t; 239 | } 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /premake4.lua: -------------------------------------------------------------------------------- 1 | solution 'utilities' 2 | configurations {'release', 'debug'} 3 | location 'build' 4 | project 'crop' 5 | kind 'ConsoleApp' 6 | language 'C++' 7 | location 'build' 8 | files {'source/crop.cpp'} 9 | configuration 'release' 10 | targetdir 'build/release' 11 | defines {'NDEBUG'} 12 | flags {'OptimizeSpeed'} 13 | configuration 'debug' 14 | targetdir 'build/debug' 15 | defines {'DEBUG'} 16 | flags {'Symbols'} 17 | configuration 'linux' 18 | links {'pthread'} 19 | buildoptions {'-std=c++11'} 20 | linkoptions {'-std=c++11'} 21 | configuration 'macosx' 22 | buildoptions {'-std=c++11'} 23 | linkoptions {'-std=c++11'} 24 | configuration 'windows' 25 | files {'.clang-format'} 26 | project 'cut' 27 | kind 'ConsoleApp' 28 | language 'C++' 29 | location 'build' 30 | files {'source/timecode.hpp', 'source/cut.cpp'} 31 | configuration 'release' 32 | targetdir 'build/release' 33 | defines {'NDEBUG'} 34 | flags {'OptimizeSpeed'} 35 | configuration 'debug' 36 | targetdir 'build/debug' 37 | defines {'DEBUG'} 38 | flags {'Symbols'} 39 | configuration 'linux' 40 | links {'pthread'} 41 | buildoptions {'-std=c++11'} 42 | linkoptions {'-std=c++11'} 43 | configuration 'macosx' 44 | buildoptions {'-std=c++11'} 45 | linkoptions {'-std=c++11'} 46 | configuration 'windows' 47 | files {'.clang-format'} 48 | project 'dat_to_es' 49 | kind 'ConsoleApp' 50 | language 'C++' 51 | location 'build' 52 | files {'source/dat.hpp', 'source/dat_to_es.cpp'} 53 | configuration 'release' 54 | targetdir 'build/release' 55 | defines {'NDEBUG'} 56 | flags {'OptimizeSpeed'} 57 | configuration 'debug' 58 | targetdir 'build/debug' 59 | defines {'DEBUG'} 60 | flags {'Symbols'} 61 | configuration 'linux' 62 | links {'pthread'} 63 | buildoptions {'-std=c++11'} 64 | linkoptions {'-std=c++11'} 65 | configuration 'macosx' 66 | buildoptions {'-std=c++11'} 67 | linkoptions {'-std=c++11'} 68 | configuration 'windows' 69 | files {'.clang-format'} 70 | project 'es_to_csv' 71 | kind 'ConsoleApp' 72 | language 'C++' 73 | location 'build' 74 | files {'source/es_to_csv.cpp'} 75 | configuration 'release' 76 | targetdir 'build/release' 77 | defines {'NDEBUG'} 78 | flags {'OptimizeSpeed'} 79 | configuration 'debug' 80 | targetdir 'build/debug' 81 | defines {'DEBUG'} 82 | flags {'Symbols'} 83 | configuration 'linux' 84 | links {'pthread'} 85 | buildoptions {'-std=c++11'} 86 | linkoptions {'-std=c++11'} 87 | configuration 'macosx' 88 | buildoptions {'-std=c++11'} 89 | linkoptions {'-std=c++11'} 90 | configuration 'windows' 91 | files {'.clang-format'} 92 | project 'es_to_frames' 93 | kind 'ConsoleApp' 94 | language 'C++' 95 | location 'build' 96 | files {'source/font.hpp', 'source/es_to_frames.cpp'} 97 | configuration 'release' 98 | targetdir 'build/release' 99 | defines {'NDEBUG'} 100 | flags {'OptimizeSpeed'} 101 | configuration 'debug' 102 | targetdir 'build/debug' 103 | defines {'DEBUG'} 104 | flags {'Symbols'} 105 | configuration 'linux' 106 | links {'pthread'} 107 | buildoptions {'-std=c++11'} 108 | linkoptions {'-std=c++11'} 109 | configuration 'macosx' 110 | buildoptions {'-std=c++11'} 111 | linkoptions {'-std=c++11'} 112 | configuration 'windows' 113 | files {'.clang-format'} 114 | project 'es_to_ply' 115 | kind 'ConsoleApp' 116 | language 'C++' 117 | location 'build' 118 | files {'source/es_to_ply.cpp'} 119 | configuration 'release' 120 | targetdir 'build/release' 121 | defines {'NDEBUG'} 122 | flags {'OptimizeSpeed'} 123 | configuration 'debug' 124 | targetdir 'build/debug' 125 | defines {'DEBUG'} 126 | flags {'Symbols'} 127 | configuration 'linux' 128 | links {'pthread'} 129 | buildoptions {'-std=c++11'} 130 | linkoptions {'-std=c++11'} 131 | configuration 'macosx' 132 | buildoptions {'-std=c++11'} 133 | linkoptions {'-std=c++11'} 134 | configuration 'windows' 135 | files {'.clang-format'} 136 | project 'event_rate' 137 | kind 'ConsoleApp' 138 | language 'C++' 139 | location 'build' 140 | files {'source/event_rate.cpp'} 141 | configuration 'release' 142 | targetdir 'build/release' 143 | defines {'NDEBUG'} 144 | flags {'OptimizeSpeed'} 145 | configuration 'debug' 146 | targetdir 'build/debug' 147 | defines {'DEBUG'} 148 | flags {'Symbols'} 149 | configuration 'linux' 150 | links {'pthread'} 151 | buildoptions {'-std=c++11'} 152 | linkoptions {'-std=c++11'} 153 | configuration 'macosx' 154 | buildoptions {'-std=c++11'} 155 | linkoptions {'-std=c++11'} 156 | configuration 'windows' 157 | files {'.clang-format'} 158 | project 'evt2_to_es' 159 | kind 'ConsoleApp' 160 | language 'C++' 161 | location 'build' 162 | files {'source/evt.hpp', 'source/evt2_to_es.cpp'} 163 | configuration 'release' 164 | targetdir 'build/release' 165 | defines {'NDEBUG'} 166 | flags {'OptimizeSpeed'} 167 | configuration 'debug' 168 | targetdir 'build/debug' 169 | defines {'DEBUG'} 170 | flags {'Symbols'} 171 | configuration 'linux' 172 | links {'pthread'} 173 | buildoptions {'-std=c++11'} 174 | linkoptions {'-std=c++11'} 175 | configuration 'macosx' 176 | buildoptions {'-std=c++11'} 177 | linkoptions {'-std=c++11'} 178 | configuration 'windows' 179 | files {'.clang-format'} 180 | project 'evt3_to_es' 181 | kind 'ConsoleApp' 182 | language 'C++' 183 | location 'build' 184 | files {'source/evt.hpp', 'source/evt3_to_es.cpp'} 185 | configuration 'release' 186 | targetdir 'build/release' 187 | defines {'NDEBUG'} 188 | flags {'OptimizeSpeed'} 189 | configuration 'debug' 190 | targetdir 'build/debug' 191 | defines {'DEBUG'} 192 | flags {'Symbols'} 193 | configuration 'linux' 194 | links {'pthread'} 195 | buildoptions {'-std=c++11'} 196 | linkoptions {'-std=c++11'} 197 | configuration 'macosx' 198 | buildoptions {'-std=c++11'} 199 | linkoptions {'-std=c++11'} 200 | configuration 'windows' 201 | files {'.clang-format'} 202 | project 'rainbow' 203 | kind 'ConsoleApp' 204 | language 'C++' 205 | location 'build' 206 | files {'source/rainbow.cpp'} 207 | configuration 'release' 208 | targetdir 'build/release' 209 | defines {'NDEBUG'} 210 | flags {'OptimizeSpeed'} 211 | configuration 'debug' 212 | targetdir 'build/debug' 213 | defines {'DEBUG'} 214 | flags {'Symbols'} 215 | configuration 'linux' 216 | links {'pthread'} 217 | buildoptions {'-std=c++11'} 218 | linkoptions {'-std=c++11'} 219 | configuration 'macosx' 220 | buildoptions {'-std=c++11'} 221 | linkoptions {'-std=c++11'} 222 | configuration 'windows' 223 | files {'.clang-format'} 224 | project 'rainmaker' 225 | kind 'ConsoleApp' 226 | language 'C++' 227 | location 'build' 228 | files {'source/html.hpp', 'third_party/lodepng/lodepng.cpp', 'source/rainmaker.cpp'} 229 | defines {'SEPIA_COMPILER_WORKING_DIRECTORY="' .. project().location .. '"'} 230 | configuration 'release' 231 | targetdir 'build/release' 232 | defines {'NDEBUG'} 233 | flags {'OptimizeSpeed'} 234 | configuration 'debug' 235 | targetdir 'build/debug' 236 | defines {'DEBUG'} 237 | flags {'Symbols'} 238 | configuration 'linux' 239 | links {'pthread'} 240 | buildoptions {'-std=c++11'} 241 | linkoptions {'-std=c++11'} 242 | configuration 'macosx' 243 | buildoptions {'-std=c++11'} 244 | linkoptions {'-std=c++11'} 245 | configuration 'windows' 246 | files {'.clang-format'} 247 | project 'size' 248 | kind 'ConsoleApp' 249 | language 'C++' 250 | location 'build' 251 | files {'source/size.cpp'} 252 | configuration 'release' 253 | targetdir 'build/release' 254 | defines {'NDEBUG'} 255 | flags {'OptimizeSpeed'} 256 | configuration 'debug' 257 | targetdir 'build/debug' 258 | defines {'DEBUG'} 259 | flags {'Symbols'} 260 | configuration 'linux' 261 | links {'pthread'} 262 | buildoptions {'-std=c++11'} 263 | linkoptions {'-std=c++11'} 264 | configuration 'macosx' 265 | buildoptions {'-std=c++11'} 266 | linkoptions {'-std=c++11'} 267 | configuration 'windows' 268 | files {'.clang-format'} 269 | project 'spatiospectrogram' 270 | kind 'ConsoleApp' 271 | language 'C++' 272 | location 'build' 273 | files {'source/spatiospectrogram.cpp'} 274 | configuration 'release' 275 | targetdir 'build/release' 276 | defines {'NDEBUG'} 277 | flags {'OptimizeSpeed'} 278 | configuration 'debug' 279 | targetdir 'build/debug' 280 | defines {'DEBUG'} 281 | flags {'Symbols'} 282 | configuration 'linux' 283 | links {'pthread'} 284 | buildoptions {'-std=c++11'} 285 | linkoptions {'-std=c++11'} 286 | configuration 'macosx' 287 | buildoptions {'-std=c++11'} 288 | linkoptions {'-std=c++11'} 289 | configuration 'windows' 290 | files {'.clang-format'} 291 | project 'spectrogram' 292 | kind 'ConsoleApp' 293 | language 'C++' 294 | location 'build' 295 | files {'source/spectrogram.cpp', 'third_party/lodepng/lodepng.cpp'} 296 | configuration 'release' 297 | targetdir 'build/release' 298 | defines {'NDEBUG'} 299 | flags {'OptimizeSpeed'} 300 | configuration 'debug' 301 | targetdir 'build/debug' 302 | defines {'DEBUG'} 303 | flags {'Symbols'} 304 | configuration 'linux' 305 | links {'pthread'} 306 | buildoptions {'-std=c++11'} 307 | linkoptions {'-std=c++11'} 308 | configuration 'macosx' 309 | buildoptions {'-std=c++11'} 310 | linkoptions {'-std=c++11'} 311 | configuration 'windows' 312 | files {'.clang-format'} 313 | project 'statistics' 314 | kind 'ConsoleApp' 315 | language 'C++' 316 | location 'build' 317 | files {'source/statistics.cpp'} 318 | configuration 'release' 319 | targetdir 'build/release' 320 | defines {'NDEBUG'} 321 | flags {'OptimizeSpeed'} 322 | configuration 'debug' 323 | targetdir 'build/debug' 324 | defines {'DEBUG'} 325 | flags {'Symbols'} 326 | configuration 'linux' 327 | links {'pthread'} 328 | buildoptions {'-std=c++11'} 329 | linkoptions {'-std=c++11'} 330 | configuration 'macosx' 331 | buildoptions {'-std=c++11'} 332 | linkoptions {'-std=c++11'} 333 | configuration 'windows' 334 | files {'.clang-format'} 335 | project 'synth' 336 | kind 'ConsoleApp' 337 | language 'C++' 338 | location 'build' 339 | files {'source/synth.cpp'} 340 | configuration 'release' 341 | targetdir 'build/release' 342 | defines {'NDEBUG'} 343 | flags {'OptimizeSpeed'} 344 | configuration 'debug' 345 | targetdir 'build/debug' 346 | defines {'DEBUG'} 347 | flags {'Symbols'} 348 | configuration 'linux' 349 | links {'pthread'} 350 | buildoptions {'-std=c++11'} 351 | linkoptions {'-std=c++11'} 352 | configuration 'macosx' 353 | buildoptions {'-std=c++11'} 354 | linkoptions {'-std=c++11'} 355 | configuration 'windows' 356 | files {'.clang-format'} 357 | -------------------------------------------------------------------------------- /source/evt.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../third_party/sepia/source/sepia.hpp" 4 | #include 5 | #include 6 | #include 7 | 8 | namespace evt { 9 | /// header bundles a .dat file header's information. 10 | struct header { 11 | uint16_t width; 12 | uint16_t height; 13 | }; 14 | 15 | /// read_header retrieves header information from a .dat file. 16 | inline header read_header(std::istream& stream, header default_header) { 17 | std::vector header_lines; 18 | for (;;) { 19 | if (stream.peek() != '%' || stream.eof()) { 20 | break; 21 | } 22 | stream.ignore(); 23 | header_lines.emplace_back(); 24 | for (;;) { 25 | const auto character = stream.get(); 26 | if (stream.eof()) { 27 | break; 28 | } 29 | if (character == '\n') { 30 | break; 31 | } 32 | header_lines.back().push_back(character); 33 | } 34 | } 35 | if (header_lines.empty() 36 | || std::any_of(header_lines.begin(), header_lines.end(), [](const std::string& header_line) { 37 | return std::any_of(header_line.begin(), header_line.end(), [](char character) { 38 | return !std::isprint(character) && !std::isspace(character); 39 | }); 40 | })) { 41 | stream.seekg(0, std::istream::beg); 42 | return {1280, 720}; 43 | } 44 | for (const auto& header_line : header_lines) { 45 | std::vector words; 46 | for (auto character : header_line) { 47 | if (std::isspace(character)) { 48 | if (!words.empty() && !words.back().empty()) { 49 | words.emplace_back(); 50 | } 51 | } else { 52 | if (words.empty()) { 53 | words.emplace_back(); 54 | } 55 | words.back().push_back(character); 56 | } 57 | } 58 | if (words.size() > 1) { 59 | try { 60 | if (words[0] == "Width") { 61 | default_header.width = static_cast(stoul(words[1])); 62 | } else if (words[0] == "Height") { 63 | default_header.height = static_cast(stoul(words[1])); 64 | } else if (words[0] == "geometry") { 65 | const auto separator_position = words[1].find("x"); 66 | if (separator_position > 0 && separator_position != std::string::npos) { 67 | default_header.width = static_cast(stoul(words[1].substr(0, separator_position))); 68 | default_header.height = 69 | static_cast(stoul(words[1].substr(separator_position + 1))); 70 | } 71 | } 72 | } catch (const std::invalid_argument&) { 73 | } catch (const std::out_of_range&) { 74 | } 75 | } 76 | } 77 | return default_header; 78 | } 79 | 80 | /// observable dispatches DVS events from a stream. 81 | /// The header must be read from the stream before calling this function. 82 | template 83 | inline void observable_2(std::istream& stream, header stream_header, bool normalize, HandleEvent handle_event) { 84 | uint64_t offset = 0; 85 | uint64_t t_without_offset = 0; 86 | uint64_t reference_t = 0; 87 | uint64_t first_t = normalize ? std::numeric_limits::max() : 0; 88 | uint64_t previous_t = 0; 89 | std::vector bytes(1 << 16); 90 | for (;;) { 91 | stream.read(reinterpret_cast(bytes.data()), bytes.size()); 92 | for (std::size_t index = 0; index < (stream.gcount() / 4) * 4; index += 4) { 93 | switch (bytes[index + 3] >> 4) { 94 | case 0b0000: // CD_OFF 95 | case 0b0001: { // CD_ON 96 | sepia::dvs_event event = {}; 97 | event.t = reference_t 98 | + ((static_cast(bytes[index + 2]) >> 6) 99 | | (static_cast(bytes[index + 3] & 0b1111) << 2)); 100 | if (first_t == std::numeric_limits::max()) { 101 | first_t = event.t; 102 | } 103 | event.t -= first_t; 104 | if (event.t < previous_t) { 105 | event.t = previous_t; 106 | } else { 107 | previous_t = event.t; 108 | } 109 | event.x = static_cast(bytes[index + 1] >> 3) 110 | | (static_cast(bytes[index + 2] & 0b111111) << 5); 111 | event.y = static_cast(bytes[index]) 112 | | (static_cast(bytes[index + 1] & 0b111) << 8); 113 | event.is_increase = (bytes[index + 3] >> 4) == 0b0001; 114 | if (event.x < stream_header.width && event.y < stream_header.height) { 115 | event.y = stream_header.height - 1 - event.y; 116 | handle_event(event); 117 | } else { 118 | std::cerr << "out of bounds event (t=" << event.t << ", x=" << event.x << ", y=" << event.y 119 | << ", on=" << (event.is_increase ? "true" : "false") << ")" << std::endl; 120 | } 121 | break; 122 | } 123 | case 0b1000: { // EVT_TIME_HIGH 124 | const auto new_t_without_offset = 125 | ((static_cast(bytes[index]) | (static_cast(bytes[index + 1]) << 8) 126 | | (static_cast(bytes[index + 2]) << 16) 127 | | (static_cast(bytes[index + 3] & 0b1111) << 24))) 128 | << 6; 129 | if (new_t_without_offset < t_without_offset) { 130 | offset += (1ull << 34); 131 | } 132 | t_without_offset = new_t_without_offset; 133 | reference_t = t_without_offset + offset; 134 | break; 135 | } 136 | case 0b1010: // EXT_TRIGGER 137 | break; 138 | case 0b1110: // OTHERS 139 | break; 140 | case 0b1111: // CONTINUED 141 | break; 142 | } 143 | } 144 | if (stream.eof()) { 145 | break; 146 | } 147 | } 148 | } 149 | 150 | /// observable dispatches DVS events from a stream. 151 | /// The header must be read from the stream before calling this function. 152 | template 153 | inline void observable_3(std::istream& stream, header stream_header, bool normalize, HandleEvent handle_event) { 154 | uint32_t previous_msb_t = 0; 155 | uint32_t previous_lsb_t = 0; 156 | uint32_t overflows = 0; 157 | uint64_t first_t = normalize ? std::numeric_limits::max() : 0; 158 | sepia::dvs_event event = {}; 159 | std::vector bytes(1 << 16); 160 | for (;;) { 161 | stream.read(reinterpret_cast(bytes.data()), bytes.size()); 162 | for (std::size_t index = 0; index < (stream.gcount() / 2) * 2; index += 2) { 163 | switch (bytes[index + 1] >> 4) { 164 | case 0b0000: // EVT_ADDR_Y 165 | event.y = static_cast( 166 | stream_header.height - 1 167 | - (bytes[index] | (static_cast(bytes[index + 1] & 0b111) << 8))); 168 | break; 169 | case 0b0001: 170 | break; 171 | case 0b0010: // EVT_ADDR_X 172 | event.x = static_cast( 173 | bytes[index] | (static_cast(bytes[index + 1] & 0b111) << 8)); 174 | event.is_increase = ((bytes[index + 1] >> 3) & 1) == 1; 175 | if (event.x < stream_header.width && event.y < stream_header.height) { 176 | handle_event(event); 177 | } else { 178 | std::cerr << "out of bounds event (t=" << event.t << ", x=" << event.x << ", y=" << event.y 179 | << ", on=" << (event.is_increase ? "true" : "false") << ")" << std::endl; 180 | } 181 | break; 182 | case 0b0011: // VECT_BASE_X 183 | event.x = static_cast( 184 | bytes[index] | (static_cast(bytes[index + 1] & 0b111) << 8)); 185 | event.is_increase = ((bytes[index + 1] >> 3) & 1) == 1; 186 | break; 187 | case 0b0100: // VECT_12 188 | for (uint8_t bit = 0; bit < 8; ++bit) { 189 | if (((bytes[index] >> bit) & 1) == 1) { 190 | if (event.x < stream_header.width && event.y < stream_header.height) { 191 | handle_event(event); 192 | } 193 | } 194 | ++event.x; 195 | } 196 | for (uint8_t bit = 0; bit < 4; ++bit) { 197 | if (((bytes[index + 1] >> bit) & 1) == 1) { 198 | if (event.x < stream_header.width && event.y < stream_header.height) { 199 | handle_event(event); 200 | } 201 | } 202 | ++event.x; 203 | } 204 | break; 205 | case 0b0101: // VECT_8 206 | for (uint8_t bit = 0; bit < 8; ++bit) { 207 | if (((bytes[index] >> bit) & 1) == 1) { 208 | if (event.x < stream_header.width && event.y < stream_header.height) { 209 | handle_event(event); 210 | } 211 | } 212 | ++event.x; 213 | } 214 | break; 215 | case 0b0110: { // EVT_TIME_LOW 216 | const auto lsb_t = static_cast( 217 | bytes[index] | (static_cast(bytes[index + 1] & 0b1111) << 8)); 218 | if (lsb_t != previous_lsb_t) { 219 | previous_lsb_t = lsb_t; 220 | auto t = static_cast(previous_lsb_t | (previous_msb_t << 12)) 221 | + (static_cast(overflows) << 24); 222 | if (first_t == std::numeric_limits::max()) { 223 | first_t = t; 224 | } 225 | t -= first_t; 226 | if (t >= event.t) { 227 | event.t = t; 228 | } 229 | } 230 | break; 231 | } 232 | case 0b0111: 233 | break; 234 | case 0b1000: { // EVT_TIME_HIGH 235 | const auto msb_t = static_cast( 236 | bytes[index] | (static_cast(bytes[index + 1] & 0b1111) << 8)); 237 | if (msb_t != previous_msb_t) { 238 | if (msb_t > previous_msb_t) { 239 | if (msb_t - previous_msb_t < static_cast((1 << 12) - 2)) { 240 | previous_lsb_t = 0; 241 | previous_msb_t = msb_t; 242 | } 243 | } else { 244 | if (previous_msb_t - msb_t > static_cast((1 << 12) - 2)) { 245 | ++overflows; 246 | previous_lsb_t = 0; 247 | previous_msb_t = msb_t; 248 | } 249 | } 250 | auto t = static_cast(previous_lsb_t | (previous_msb_t << 12)) 251 | + (static_cast(overflows) << 24); 252 | if (first_t == std::numeric_limits::max()) { 253 | first_t = t; 254 | } 255 | t -= first_t; 256 | if (t >= event.t) { 257 | event.t = t; 258 | } 259 | } 260 | } 261 | case 0b1001: 262 | break; 263 | case 0b1010: // EXT_TRIGGER 264 | break; 265 | case 0b1011: 266 | case 0b1100: 267 | case 0b1101: 268 | case 0b1110: 269 | case 0b1111: 270 | break; 271 | default: 272 | break; 273 | } 274 | } 275 | if (stream.eof()) { 276 | break; 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /render.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import atexit 3 | import pathlib 4 | import re 5 | import subprocess 6 | import sys 7 | import tempfile 8 | import typing 9 | 10 | dirname = pathlib.Path(__file__).resolve().parent 11 | timecode_pattern = re.compile(r"^(\d+):(\d+):(\d+)(?:\.(\d+))?$") 12 | 13 | 14 | def timecode(value: str) -> int: 15 | if value.isdigit(): 16 | return int(value) 17 | match = timecode_pattern.match(value) 18 | if match is None: 19 | raise argparse.ArgumentTypeError( 20 | "expected an integer or a timecode (12:34:56.789000)" 21 | ) 22 | result = ( 23 | int(match[1]) * 3600000000 + int(match[2]) * 60000000 + int(match[3]) * 1000000 24 | ) 25 | if match[4] is not None: 26 | fraction_string: str = match[4] 27 | if len(fraction_string) == 6: 28 | result += int(fraction_string) 29 | elif len(fraction_string) < 6: 30 | result += int(fraction_string + "0" * (6 - len(fraction_string))) 31 | else: 32 | result += round(float("0." + fraction_string) * 1e6) 33 | return result 34 | 35 | 36 | parser = argparse.ArgumentParser( 37 | description="Generate a frame-based video from an .es file, using es_to_frames and ffmpeg", 38 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 39 | ) 40 | parser.add_argument("input", help="input .es file or directory") 41 | parser.add_argument( 42 | "--output", 43 | "-o", 44 | help="output .mp4 file, calculated from the input file name if not provided", 45 | ) 46 | parser.add_argument( 47 | "--begin", 48 | "-b", 49 | type=timecode, 50 | help="ignore events before this timestamp (timecode)", 51 | ) 52 | parser.add_argument( 53 | "--end", "-e", type=timecode, help="ignore events after this timestamp (timecode)" 54 | ) 55 | parser.add_argument( 56 | "--frametime", 57 | "-f", 58 | type=timecode, 59 | default=20000, 60 | help="time between two frames in µs", 61 | ) 62 | parser.add_argument( 63 | "--scale", 64 | "-c", 65 | type=int, 66 | default=1, 67 | help="scale up the output by this integer factor", 68 | ) 69 | parser.add_argument( 70 | "--style", 71 | "-s", 72 | choices=["exponential", "linear", "window", "cumulative", "cumulative-shared"], 73 | default="exponential", 74 | help="decay function", 75 | ) 76 | parser.add_argument( 77 | "--tau", "-t", type=timecode, default=200000, help="decay function parameter in µs" 78 | ) 79 | parser.add_argument("--oncolor", "-j", default="#f4c20d", help="color for ON events") 80 | parser.add_argument("--offcolor", "-k", default="#1e88e5", help="color for OFF events") 81 | parser.add_argument("--idlecolor", "-l", default="#191919", help="background color") 82 | parser.add_argument( 83 | "--cumulative-ratio", 84 | "-m", 85 | type=float, 86 | default=0.01, 87 | help="ratio of pixels discarded for cumulative mapping (cumulative and cumulative-shared styles only)", 88 | ) 89 | parser.add_argument( 90 | "--lambda-max", 91 | "-n", 92 | help="cumulative mapping maximum activity, defaults to automatic discard calculation", 93 | ) 94 | parser.add_argument( 95 | "--no-timecode", "-a", action="store_true", help="do not add a timecode overlay" 96 | ) 97 | parser.add_argument( 98 | "--discard-ratio", 99 | "-r", 100 | type=float, 101 | default=0.01, 102 | help="ratio of pixels discarded for tone mapping (ATIS only)", 103 | ) 104 | parser.add_argument( 105 | "--black", 106 | "-v", 107 | help="black integration duration for tone mapping (timecode, ATIS only)", 108 | ) 109 | parser.add_argument( 110 | "--white", 111 | "-w", 112 | help="white integration duration for tone mapping (timecode, ATIS only)", 113 | ) 114 | parser.add_argument( 115 | "--atiscolor", 116 | "-x", 117 | default="#000000", 118 | help="background color for ATIS exposure measurements", 119 | ) 120 | parser.add_argument( 121 | "--ffmpeg", 122 | "-g", 123 | default="ffmpeg", 124 | help="FFmpeg executable", 125 | ) 126 | parser.add_argument( 127 | "--h264-crf", 128 | type=int, 129 | default=15, 130 | help="H264 Constant Rate Factor (CRF)", 131 | ) 132 | parser.add_argument( 133 | "--sonify", 134 | "-y", 135 | action="store_true", 136 | help="generate audio with 'synth'", 137 | ) 138 | parser.add_argument( 139 | "--no-merge", 140 | action="store_true", 141 | help="do not merge audio and video", 142 | ) 143 | parser.add_argument( 144 | "--sonify-amplitude-gain", 145 | type=float, 146 | default=0.1, 147 | help="activity to sound amplitude conversion factor", 148 | ) 149 | parser.add_argument( 150 | "--sonify-minimum-frequency", 151 | type=float, 152 | default=27.5, 153 | help="minimum frequency (bottom row) in Hertz", 154 | ) 155 | parser.add_argument( 156 | "--sonify-maximum-frequency", 157 | type=float, 158 | default=4186.009, 159 | help="maximum frequency (top row) in Hertz", 160 | ) 161 | parser.add_argument( 162 | "--sonify-sampling-rate", type=int, default=44100, help="sampling rate in Hertz" 163 | ) 164 | parser.add_argument( 165 | "--sonify-tracker-lambda", 166 | type=float, 167 | default=0.1, 168 | help="row tracker moving mean parameter", 169 | ) 170 | parser.add_argument( 171 | "--sonify-activity-tau", 172 | type=timecode, 173 | default=10000, 174 | help="row decay parameter in µs", 175 | ) 176 | parser.add_argument( 177 | "--no-mp4", 178 | action="store_true", 179 | help="do not render mp4 videos", 180 | ) 181 | parser.add_argument( 182 | "--rainbow", 183 | "-p", 184 | action="store_true", 185 | help="render rainbow plots", 186 | ) 187 | parser.add_argument( 188 | "--rainbow-alpha", 189 | type=float, 190 | default=0.1, 191 | help="transparency level for each event in the rainbow plot", 192 | ) 193 | parser.add_argument( 194 | "--rainbow-idlecolor", 195 | default="#191919", 196 | help="background color for the rainbow plot", 197 | ) 198 | args = parser.parse_args() 199 | 200 | no_merge = args.no_merge 201 | if args.no_mp4: 202 | no_merge = True 203 | if args.no_mp4 and not args.rainbow and not args.sonify: 204 | sys.stderr.write("--no-mp4 requires --rainbow or --sonify\n") 205 | sys.exit(1) 206 | 207 | active: dict[str, typing.Optional[subprocess.Popen]] = { 208 | "es_to_frames": None, 209 | "ffmpeg": None, 210 | "synth": None, 211 | } 212 | 213 | 214 | def cleanup(): 215 | for name in list(active.keys()): 216 | popen = active[name] 217 | if popen is not None: 218 | popen.kill() 219 | active[name] = None 220 | 221 | 222 | atexit.register(cleanup) 223 | 224 | 225 | def render( 226 | input_file: pathlib.Path, 227 | output_file_or_directory: typing.Optional[pathlib.Path], 228 | ): 229 | print(f"\033[1m{input_file}\033[0m") 230 | output_parent: typing.Optional[pathlib.Path] 231 | if output_file_or_directory is None: 232 | output_parent = input_file.parent 233 | elif output_file_or_directory.is_dir(): 234 | output_parent = output_file_or_directory 235 | else: 236 | output_parent = None 237 | range_string = "" 238 | if args.begin is not None or args.end is not None: 239 | range_string += f"_begin={0 if args.begin is None else args.begin}" 240 | if args.end is not None: 241 | range_string += f"_end={args.end}" 242 | if (args.frametime // 1e6) * 1e6 == args.frametime: 243 | frametime_string = f"{args.frametime // 1000000}s" 244 | elif (args.frametime // 1e3) * 1e3 == args.frametime: 245 | frametime_string = f"{args.frametime // 1000}ms" 246 | else: 247 | frametime_string = f"{args.frametime}us" 248 | if (args.tau // 1e6) * 1e6 == args.tau: 249 | tau_string = f"{args.tau // 1000000}s" 250 | elif (args.tau // 1e3) * 1e3 == args.tau: 251 | tau_string = f"{args.tau // 1000}ms" 252 | else: 253 | tau_string = f"{args.tau}us" 254 | if output_parent is None: 255 | assert output_file_or_directory is not None 256 | rainbow_output_file = output_file_or_directory.with_suffix(".png") 257 | else: 258 | rainbow_output_file = ( 259 | output_parent / f"{input_file.stem}_alpha={args.rainbow_alpha}.png" 260 | ) 261 | if args.rainbow: 262 | print(f"🌈 {rainbow_output_file}") 263 | if args.begin is None and args.end is None: 264 | subprocess.run( 265 | ( 266 | str(dirname / "build" / "release" / "rainbow"), 267 | str(input_file), 268 | str(rainbow_output_file), 269 | f"--alpha={args.rainbow_alpha}", 270 | f"--idlecolor={args.rainbow_idlecolor}", 271 | ), 272 | check=True, 273 | ) 274 | else: 275 | with tempfile.TemporaryDirectory( 276 | prefix="command-line-tools-render" 277 | ) as temporary_directory_string: 278 | temporary_directory = pathlib.Path(temporary_directory_string) 279 | subprocess.run( 280 | ( 281 | str(dirname / "build" / "release" / "cut"), 282 | str(input_file), 283 | str(temporary_directory / input_file.name), 284 | str(0 if args.begin is None else args.begin), 285 | str((2**64 - 1) if args.end is None else args.end), 286 | "--timestamp", 287 | "zero", 288 | ), 289 | check=True, 290 | ) 291 | subprocess.run( 292 | ( 293 | str(dirname / "build" / "release" / "rainbow"), 294 | str(temporary_directory / input_file.name), 295 | str(rainbow_output_file), 296 | f"--alpha={args.rainbow_alpha}", 297 | f"--idlecolor={args.rainbow_idlecolor}", 298 | ), 299 | check=True, 300 | ) 301 | sonify_suffix = "_sound" if args.sonify else "" 302 | if output_parent is None: 303 | assert output_file_or_directory is not None 304 | mp4_output = output_file_or_directory.with_suffix(".mp4") 305 | else: 306 | mp4_output = ( 307 | output_parent 308 | / f"{input_file.stem}{range_string}_{args.style}_frametime={frametime_string}_tau={tau_string}{sonify_suffix}.mp4" 309 | ) 310 | if not args.no_mp4: 311 | if args.sonify: 312 | print(f"🎬 {mp4_output} (video only)") 313 | else: 314 | print(f"🎬 {mp4_output}") 315 | width, height = ( 316 | int(value) 317 | for value in subprocess.run( 318 | [str(dirname / "build" / "release" / "size"), str(input_file)], 319 | check=True, 320 | capture_output=True, 321 | ).stdout.split(b"x") 322 | ) 323 | width *= args.scale 324 | height *= args.scale 325 | es_to_frames_arguments = [ 326 | str(dirname / "build" / "release" / "es_to_frames"), 327 | f"--input={str(input_file)}", 328 | f"--begin={0 if args.begin is None else args.begin}", 329 | f"--frametime={args.frametime}", 330 | f"--scale={args.scale}", 331 | f"--style={args.style}", 332 | f"--tau={args.tau}", 333 | f"--oncolor={args.oncolor}", 334 | f"--offcolor={args.offcolor}", 335 | f"--idlecolor={args.idlecolor}", 336 | f"--cumulative-ratio={args.cumulative_ratio}", 337 | f"--discard-ratio={args.discard_ratio}", 338 | f"--atiscolor={args.atiscolor}", 339 | ] 340 | if args.end is not None: 341 | es_to_frames_arguments.append(f"--end={args.end}") 342 | if args.lambda_max is not None: 343 | es_to_frames_arguments.append(f"--lambda-max={args.lambda_max}") 344 | if not args.no_timecode: 345 | es_to_frames_arguments.append("--add-timecode") 346 | if args.white is not None: 347 | es_to_frames_arguments.append(f"--white={args.white}") 348 | if args.black is not None: 349 | es_to_frames_arguments.append(f"--black={args.black}") 350 | active["es_to_frames"] = subprocess.Popen( 351 | es_to_frames_arguments, 352 | stdout=subprocess.PIPE, 353 | ) 354 | assert active["es_to_frames"].stdout is not None 355 | active["ffmpeg"] = subprocess.Popen( 356 | [ 357 | args.ffmpeg, 358 | "-hide_banner", 359 | "-loglevel", 360 | "warning", 361 | "-stats", 362 | "-f", 363 | "rawvideo", 364 | "-s", 365 | f"{width}x{height}", 366 | "-framerate", 367 | "50", 368 | "-pix_fmt", 369 | "rgb24", 370 | "-i", 371 | "-", 372 | "-c:v", 373 | "libx264", 374 | "-pix_fmt", 375 | "yuv420p", 376 | "-crf", 377 | str(args.h264_crf), 378 | "-f", 379 | "mp4", 380 | "-y", 381 | f"{mp4_output}.render", 382 | ], 383 | stdin=subprocess.PIPE, 384 | ) 385 | assert active["ffmpeg"].stdin is not None 386 | frame_size = width * height * 3 387 | while True: 388 | frame = active["es_to_frames"].stdout.read(frame_size) 389 | if len(frame) != frame_size: 390 | break 391 | active["ffmpeg"].stdin.write(frame) 392 | 393 | active["ffmpeg"].stdin.close() 394 | active["es_to_frames"].wait() 395 | active["es_to_frames"] = None 396 | active["ffmpeg"].wait() 397 | active["ffmpeg"] = None 398 | if not args.sonify or no_merge: 399 | mp4_output.unlink(missing_ok=True) 400 | pathlib.Path(f"{mp4_output}.render").replace(mp4_output) 401 | 402 | if args.sonify: 403 | if output_parent is None: 404 | assert output_file_or_directory is not None 405 | wav_output = output_file_or_directory.with_suffix(".wav") 406 | else: 407 | if args.no_mp4: 408 | wav_output = ( 409 | output_parent 410 | / f"{input_file.stem}{range_string}_frametime={frametime_string}.wav" 411 | ) 412 | else: 413 | wav_output = mp4_output.with_suffix(".wav") 414 | print(f"🔊 {wav_output}") 415 | synth_arguments = [ 416 | str(dirname / "build" / "release" / "synth"), 417 | str(input_file), 418 | f"{wav_output}.render", 419 | "--output-mode=1", 420 | f"--begin={0 if args.begin is None else args.begin}", 421 | f"--amplitude-gain={args.sonify_amplitude_gain}", 422 | f"--minimum-frequency={args.sonify_minimum_frequency}", 423 | f"--maximum-frequency={args.sonify_maximum_frequency}", 424 | f"--sampling-rate={args.sonify_sampling_rate}", 425 | f"--tracker-lambda={args.sonify_tracker_lambda}", 426 | f"--playback-speed={args.frametime / 20000.0}", 427 | f"--activity-tau={args.sonify_activity_tau}", 428 | ] 429 | if args.end is not None: 430 | synth_arguments.append(f"--end={args.end}") 431 | active["synth"] = subprocess.Popen( 432 | synth_arguments, 433 | stdout=subprocess.PIPE, 434 | ) 435 | assert active["synth"].stdout is not None 436 | while True: 437 | line = active["synth"].stdout.readline() 438 | if len(line) == 0: 439 | sys.stdout.write(f"\n") 440 | break 441 | sys.stdout.write(f"\r{line[:-1].decode()}") 442 | sys.stdout.flush() 443 | active["synth"].wait() 444 | active["synth"] = None 445 | if no_merge: 446 | wav_output.unlink(missing_ok=True) 447 | pathlib.Path(f"{wav_output}.render").replace(wav_output) 448 | else: 449 | print(f"🎬+🔊 {mp4_output}") 450 | active["ffmpeg"] = subprocess.Popen( 451 | [ 452 | args.ffmpeg, 453 | "-hide_banner", 454 | "-loglevel", 455 | "warning", 456 | "-stats", 457 | "-i", 458 | f"{mp4_output}.render", 459 | "-guess_layout_max", 460 | "0", 461 | "-i", 462 | f"{wav_output}.render", 463 | "-af", 464 | "pan=stereo| c0=c0 | c1=c1", 465 | "-c:v", 466 | "copy", 467 | "-c:a", 468 | "aac", 469 | "-f", 470 | "mp4", 471 | "-y", 472 | f"{mp4_output}.merge", 473 | ], 474 | ) 475 | active["ffmpeg"].wait() 476 | active["ffmpeg"] = None 477 | mp4_output.unlink(missing_ok=True) 478 | pathlib.Path(f"{mp4_output}.merge").replace(mp4_output) 479 | pathlib.Path(f"{mp4_output}.render").unlink() 480 | pathlib.Path(f"{wav_output}.render").unlink() 481 | 482 | 483 | def render_directory( 484 | input_directory: pathlib.Path, 485 | output_directory: pathlib.Path, 486 | ): 487 | children = [] 488 | for child in sorted(input_directory.iterdir()): 489 | if child.is_dir(): 490 | children.append(child) 491 | elif child.is_file() and child.suffix == ".es": 492 | output_directory.mkdir(parents=True, exist_ok=True) 493 | render(child, output_directory) 494 | for child in children: 495 | render_directory(child, output_directory / child.name) 496 | 497 | 498 | input = pathlib.Path(args.input).resolve() 499 | if input.is_dir(): 500 | if args.output is None: 501 | render_directory(input, input) 502 | else: 503 | output = pathlib.Path(args.output).resolve() 504 | if output.exists() and not output.is_dir(): 505 | sys.stderr.write( 506 | "--output must be a directory (out of tree generation) or unspecified (in tree generation) if the input is a directory\n" 507 | ) 508 | sys.exit(1) 509 | render_directory(input, output) 510 | else: 511 | if not pathlib.Path(args.input).exists(): 512 | sys.stderr.write(f"{input} does not exist\n") 513 | sys.exit(1) 514 | if not pathlib.Path(args.input).is_file(): 515 | sys.stderr.write(f"{input} is neither a file nor a directory\n") 516 | sys.exit(1) 517 | if args.output is None: 518 | render(input, None) 519 | else: 520 | output = pathlib.Path(args.output).resolve() 521 | if input == output: 522 | sys.stderr.write("input and output must be different files\n") 523 | sys.exit(1) 524 | render(input, output) 525 | -------------------------------------------------------------------------------- /source/synth.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | #include "timecode.hpp" 4 | #include 5 | #include 6 | 7 | constexpr uint32_t from_bytes(char b0, char b1, char b2, char b3) { 8 | return static_cast(b0) | (static_cast(b1) << 8) | (static_cast(b2) << 16) 9 | | (static_cast(b3) << 24); 10 | } 11 | 12 | static void write_le_uint32(std::ostream& stream, uint32_t value) { 13 | std::array bytes{ 14 | static_cast(value & 0xff), 15 | static_cast((value >> 8) & 0xff), 16 | static_cast((value >> 16) & 0xff), 17 | static_cast((value >> 24) & 0xff), 18 | }; 19 | stream.write(reinterpret_cast(bytes.data()), bytes.size()); 20 | } 21 | 22 | enum class output_mode { 23 | carriage_return, 24 | line_feed, 25 | quiet, 26 | }; 27 | 28 | /// synth converts a visual event stream to sound. 29 | template 30 | void synth( 31 | const sepia::header& header, 32 | std::unique_ptr input_stream, 33 | std::unique_ptr output_stream, 34 | uint64_t begin_t, 35 | uint64_t end_t, 36 | float amplitude_gain, 37 | float minimum_frequency, 38 | float maximum_frequency, 39 | float tracker_lambda, 40 | uint32_t sampling_rate, 41 | float playback_speed, 42 | uint64_t activity_tau, 43 | output_mode mode) { 44 | for (const auto header_value : std::initializer_list{ 45 | from_bytes('R', 'I', 'F', 'F'), 46 | 0, // chunk data size 47 | from_bytes('W', 'A', 'V', 'E'), 48 | from_bytes('f', 'm', 't', ' '), 49 | 16, // format chunk size 50 | from_bytes(1, 0, 2, 0), // audio format and channels 51 | sampling_rate, 52 | sampling_rate * 4, // bytes per second 53 | from_bytes(4, 0, 16, 0), // block align and bits per sample 54 | from_bytes('d', 'a', 't', 'a'), 55 | 0, // chunk data size 56 | }) { 57 | write_le_uint32(*output_stream.get(), header_value); 58 | } 59 | const auto inverse_tau = 1.0f / static_cast(activity_tau); 60 | const auto frequency_a = static_cast(minimum_frequency) / static_cast(sampling_rate) * 2.0 * M_PI; 61 | const auto frequency_b = 62 | (std::log(maximum_frequency) - std::log(minimum_frequency)) / static_cast(header.height - 1); 63 | uint32_t sample = 0; 64 | uint64_t sample_t = 0; 65 | auto gain = amplitude_gain * static_cast(std::numeric_limits::max()); 66 | std::vector> ts_and_activities( 67 | header.height, {std::numeric_limits::max(), 0.0f}); 68 | std::vector row_trackers(header.height, static_cast(header.width) / 2.0f); 69 | const auto sample_factor = static_cast(playback_speed) / static_cast(sampling_rate) * 1e6; 70 | const auto output_modulo = 71 | std::min(1u, static_cast(std::round(static_cast(sampling_rate) / 100.0))); 72 | sepia::join_observable(std::move(input_stream), [&](sepia::event event) { 73 | if (event.t >= begin_t) { 74 | if (event.t >= end_t) { 75 | throw sepia::end_of_file(); 76 | } 77 | event.t -= begin_t; 78 | while (event.t > sample_t) { 79 | if (sample % output_modulo == 0) { 80 | switch (mode) { 81 | case output_mode::carriage_return: 82 | case output_mode::line_feed: { 83 | std::stringstream message; 84 | if (mode == output_mode::carriage_return) { 85 | message << "\r"; 86 | } 87 | message << "synth sample " << sample << " (" << timecode(sample_t).to_timecode_string() 88 | << ")"; 89 | if (mode == output_mode::line_feed) { 90 | message << "\n"; 91 | } 92 | std::cout << message.str(); 93 | std::cout.flush(); 94 | } break; 95 | case output_mode::quiet: 96 | break; 97 | } 98 | } 99 | auto left = 0.0; 100 | auto right = 0.0; 101 | for (uint16_t y = 0; y < header.height; ++y) { 102 | const auto signal = 103 | ts_and_activities[y].first == std::numeric_limits::max() ? 104 | 0.0f : 105 | (ts_and_activities[y].second 106 | * std::exp(-static_cast(sample_t - ts_and_activities[y].first) * inverse_tau) 107 | / static_cast(header.width)) 108 | * gain 109 | * std::sin( 110 | static_cast(static_cast(sample) * frequency_a) 111 | * std::exp(frequency_b * static_cast(y))); 112 | const auto balance = row_trackers[y] / static_cast(header.width - 1); 113 | left += static_cast((1.0f - balance) * signal); 114 | right += static_cast(balance * signal); 115 | } 116 | int16_t left_sample = 0; 117 | if (left < std::numeric_limits::min()) { 118 | left_sample = std::numeric_limits::min(); 119 | } else if (left > std::numeric_limits::max()) { 120 | left_sample = std::numeric_limits::max(); 121 | } else { 122 | left_sample = static_cast(std::round(left)); 123 | } 124 | int16_t right_sample = 0; 125 | if (right < std::numeric_limits::min()) { 126 | right_sample = std::numeric_limits::min(); 127 | } else if (right > std::numeric_limits::max()) { 128 | right_sample = std::numeric_limits::max(); 129 | } else { 130 | right_sample = static_cast(std::round(right)); 131 | } 132 | std::array bytes{ 133 | static_cast(*reinterpret_cast(&left_sample) & 0xff), 134 | static_cast((*reinterpret_cast(&left_sample) >> 8) & 0xff), 135 | static_cast(*reinterpret_cast(&right_sample) & 0xff), 136 | static_cast((*reinterpret_cast(&right_sample) >> 8) & 0xff), 137 | }; 138 | output_stream->write(reinterpret_cast(bytes.data()), bytes.size()); 139 | ++sample; 140 | sample_t = static_cast(std::round(static_cast(sample) * sample_factor)); 141 | } 142 | auto& t_and_activity = ts_and_activities[event.y]; 143 | if (t_and_activity.first == std::numeric_limits::max()) { 144 | t_and_activity.second = 1.0f; 145 | } else { 146 | t_and_activity.second = 147 | t_and_activity.second * std::exp(-static_cast(event.t - t_and_activity.first) * inverse_tau) 148 | + 1.0f; 149 | } 150 | t_and_activity.first = event.t; 151 | row_trackers[event.y] = 152 | (1.0f - tracker_lambda) * row_trackers[event.y] + tracker_lambda * static_cast(event.x); 153 | } 154 | }); 155 | if (mode == output_mode::carriage_return) { 156 | std::cout << std::endl; 157 | } 158 | output_stream->seekp(4, std::ostream::beg); 159 | write_le_uint32(*output_stream.get(), sample * 4 + 36); 160 | output_stream->seekp(40, std::ostream::beg); 161 | write_le_uint32(*output_stream.get(), sample * 4); 162 | } 163 | 164 | int main(int argc, char* argv[]) { 165 | return pontella::main( 166 | { 167 | "synth converts events into sound (.wav)", 168 | "Syntax: ./synth [options] /path/to/input.es /path/to/output.wav", 169 | "Available options:", 170 | " -b timecode, --begin timecode ignore events before this timestamp (timecode)", 171 | " defaults to 00:00:00", 172 | " -e timecode, --end timecode ignore events after this timestamp (timecode)", 173 | " defaults to the end of the recording", 174 | " -a float, --amplitude-gain float activity to sound amplitude conversion factor", 175 | " defaults to 0.1", 176 | " -i float, --minimum-frequency float minimum frequency (bottom row) in Hertz", 177 | " defaults to 27.5", 178 | " -j float, --maximum-frequency float maximum frequency (top row) in Hertz", 179 | " defaults to 4186.009", 180 | " -s integer, --sampling-rate integer sampling rate in Hertz", 181 | " defaults to 44100", 182 | " -l float, --tracker-lambda float row tracker moving mean parameter", 183 | " defaults to 0.1", 184 | " -p float, --playback-speed float relative playback speed", 185 | " defaults to 1.0 (real-time)", 186 | " -t integer, --activity-tau integer row decay in µs", 187 | " defaults to 10000", 188 | " -o integer, --output-mode integer 0 prints progress without new lines", 189 | " 1 prints progress with new lines", 190 | " 2 is quiet", 191 | " defaults to 0", 192 | " -h, --help show this help message", 193 | }, 194 | argc, 195 | argv, 196 | 2, 197 | { 198 | {"begin", {"b"}}, 199 | {"end", {"e"}}, 200 | {"amplitude-gain", {"a"}}, 201 | {"minimum-frequency", {"i"}}, 202 | {"maximum-frequency", {"j"}}, 203 | {"sampling-rate", {"s"}}, 204 | {"tracker-lambda", {"l"}}, 205 | {"playback-speed", {"p"}}, 206 | {"activity-tau", {"t"}}, 207 | {"output-mode", {"o"}}, 208 | }, 209 | {}, 210 | [](pontella::command command) { 211 | if (command.arguments[0] == command.arguments[1]) { 212 | throw std::runtime_error("input and output must be different files"); 213 | } 214 | uint64_t begin_t = 0; 215 | { 216 | const auto name_and_argument = command.options.find("begin"); 217 | if (name_and_argument != command.options.end()) { 218 | begin_t = timecode(name_and_argument->second).value(); 219 | } 220 | } 221 | auto end_t = std::numeric_limits::max(); 222 | { 223 | const auto name_and_argument = command.options.find("end"); 224 | if (name_and_argument != command.options.end()) { 225 | end_t = timecode(name_and_argument->second).value(); 226 | if (end_t <= begin_t) { 227 | throw std::runtime_error("end must be strictly larger than begin"); 228 | } 229 | } 230 | } 231 | auto amplitude_gain = 0.1f; 232 | { 233 | const auto name_and_argument = command.options.find("amplitude-gain"); 234 | if (name_and_argument != command.options.end()) { 235 | amplitude_gain = std::stof(name_and_argument->second); 236 | if (amplitude_gain <= 0.0f) { 237 | throw std::runtime_error("amplitude-gain must be larger than zero"); 238 | } 239 | } 240 | } 241 | auto minimum_frequency = 27.5f; 242 | { 243 | const auto name_and_argument = command.options.find("minimum-frequency"); 244 | if (name_and_argument != command.options.end()) { 245 | minimum_frequency = std::stof(name_and_argument->second); 246 | if (minimum_frequency <= 0.0f) { 247 | throw std::runtime_error("minimum-frequency must be larger than zero"); 248 | } 249 | } 250 | } 251 | auto maximum_frequency = 4186.009f; 252 | { 253 | const auto name_and_argument = command.options.find("maximum-frequency"); 254 | if (name_and_argument != command.options.end()) { 255 | maximum_frequency = std::stof(name_and_argument->second); 256 | if (maximum_frequency <= 0.0f) { 257 | throw std::runtime_error("maximum-frequency must be larger than zero"); 258 | } 259 | if (minimum_frequency >= maximum_frequency) { 260 | throw std::runtime_error("minimum-frequency must be smaller than maximum-frequency"); 261 | } 262 | } 263 | } 264 | uint32_t sampling_rate = 44100; 265 | { 266 | const auto name_and_argument = command.options.find("sampling-rate"); 267 | if (name_and_argument != command.options.end()) { 268 | sampling_rate = static_cast(std::stoul(name_and_argument->second)); 269 | if (sampling_rate == 0) { 270 | throw std::runtime_error("sampling-rate must be larger than zero"); 271 | } 272 | } 273 | } 274 | auto tracker_lambda = 0.01f; 275 | { 276 | const auto name_and_argument = command.options.find("tracker-lambda"); 277 | if (name_and_argument != command.options.end()) { 278 | tracker_lambda = std::stof(name_and_argument->second); 279 | if (tracker_lambda < 0.0f || tracker_lambda > 1.0f) { 280 | throw std::runtime_error("tracker-lambda must be in the range [0, 1]"); 281 | } 282 | } 283 | } 284 | auto playback_speed = 1.0f; 285 | { 286 | const auto name_and_argument = command.options.find("playback-speed"); 287 | if (name_and_argument != command.options.end()) { 288 | playback_speed = std::stof(name_and_argument->second); 289 | if (playback_speed <= 0.0f) { 290 | throw std::runtime_error("playack-speed must be larger than zero"); 291 | } 292 | } 293 | } 294 | uint64_t activity_tau = 10000; 295 | { 296 | const auto name_and_argument = command.options.find("activity-tau"); 297 | if (name_and_argument != command.options.end()) { 298 | activity_tau = timecode(name_and_argument->second).value(); 299 | if (activity_tau == 0) { 300 | throw std::runtime_error("activity-tau must be larger than 0"); 301 | } 302 | } 303 | } 304 | auto mode = output_mode::carriage_return; 305 | { 306 | const auto name_and_argument = command.options.find("output-mode"); 307 | if (name_and_argument != command.options.end()) { 308 | switch (std::stoul(name_and_argument->second)) { 309 | case 0: 310 | break; 311 | case 1: 312 | mode = output_mode::line_feed; 313 | break; 314 | case 2: 315 | mode = output_mode::quiet; 316 | break; 317 | default: 318 | throw std::runtime_error("output-mode must 0, 1, or 2"); 319 | break; 320 | } 321 | } 322 | } 323 | 324 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 325 | auto input_stream = sepia::filename_to_ifstream(command.arguments[0]); 326 | auto output_stream = sepia::filename_to_ofstream(command.arguments[1]); 327 | switch (header.event_stream_type) { 328 | case sepia::type::generic: { 329 | throw std::runtime_error("Unsupported event type: generic"); 330 | break; 331 | } 332 | case sepia::type::dvs: { 333 | synth( 334 | header, 335 | std::move(input_stream), 336 | std::move(output_stream), 337 | begin_t, 338 | end_t, 339 | amplitude_gain, 340 | minimum_frequency, 341 | maximum_frequency, 342 | tracker_lambda, 343 | sampling_rate, 344 | playback_speed, 345 | activity_tau, 346 | mode); 347 | break; 348 | } 349 | case sepia::type::atis: { 350 | synth( 351 | header, 352 | std::move(input_stream), 353 | std::move(output_stream), 354 | begin_t, 355 | end_t, 356 | amplitude_gain, 357 | minimum_frequency, 358 | maximum_frequency, 359 | tracker_lambda, 360 | sampling_rate, 361 | playback_speed, 362 | activity_tau, 363 | mode); 364 | break; 365 | } 366 | case sepia::type::color: { 367 | throw std::runtime_error("Unsupported event type: color"); 368 | break; 369 | } 370 | } 371 | }); 372 | return 0; 373 | } 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](banner.png) 2 | 3 | Command-line tools bundles command-line applications to manipulate event files. 4 | 5 | - [install](#install) 6 | - [clone](#clone) 7 | - [dependencies](#dependencies) 8 | - [Debian / Ubuntu](#debian--ubuntu) 9 | - [macOS](#macos) 10 | - [Windows](#windows) 11 | - [compilation](#compilation) 12 | - [documentation](#documentation) 13 | - [timecode](#timecode) 14 | - [crop](#crop) 15 | - [cut](#cut) 16 | - [dat\_to\_es](#dat_to_es) 17 | - [es\_to\_csv](#es_to_csv) 18 | - [es\_to\_frames](#es_to_frames) 19 | - [es\_to\_ply](#es_to_ply) 20 | - [event\_rate](#event_rate) 21 | - [evt2\_to\_es](#evt2_to_es) 22 | - [evt3\_to\_es](#evt3_to_es) 23 | - [rainmaker](#rainmaker) 24 | - [rainbow](#rainbow) 25 | - [size](#size) 26 | - [spectrogram](#spectrogram) 27 | - [spatiospectrogram](#spatiospectrogram) 28 | - [statistics](#statistics) 29 | - [contribute](#contribute) 30 | - [development dependencies](#development-dependencies) 31 | - [Debian / Ubuntu](#debian--ubuntu-1) 32 | - [macOS](#macos-1) 33 | - [test](#test) 34 | - [license](#license) 35 | 36 | # install 37 | 38 | ## clone 39 | 40 | To download Command Line Tools, run: 41 | 42 | ```sh 43 | git clone --recursive https://github.com/neuromorphic-paris/command_line_tools.git 44 | ``` 45 | 46 | ## dependencies 47 | 48 | ### Debian / Ubuntu 49 | 50 | Open a terminal and run: 51 | 52 | ```sh 53 | sudo apt install premake4 54 | ``` 55 | 56 | ### macOS 57 | 58 | Open a terminal and run: 59 | 60 | ```sh 61 | brew tap tonyseek/premake 62 | brew install tonyseek/premake/premake4 63 | ``` 64 | 65 | If the command is not found, you need to install Homebrew first with the command: 66 | 67 | ```sh 68 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 69 | ``` 70 | 71 | ### Windows 72 | 73 | Install Chocolatey (https://chocolatey.org/) by running as administrator (Start > Windows Powershell > Right click > Run as Administator): 74 | 75 | ```sh 76 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) 77 | ``` 78 | 79 | Open Powershell as administator and run: 80 | 81 | ```sh 82 | choco install -y premake.portable 83 | choco install -y visualstudio2019buildtools 84 | choco install -y visualstudio2019-workload-vctools 85 | choco install -y ffmpeg 86 | ``` 87 | 88 | ## compilation 89 | 90 | Run the following commands from the _command_line_tools_ directory to compile the applications: 91 | 92 | ```sh 93 | premake4 gmake 94 | cd build 95 | make 96 | cd release 97 | ``` 98 | 99 | **Windows** users must run the following commands from non-administrator Powershell instead: 100 | 101 | ```sh 102 | premake4 vs2010 103 | cd build 104 | Get-ChildItem . -Filter *.vcxproj | Foreach-Object {C:\Program` Files` `(x86`)\Microsoft` Visual` Studio\2019\BuildTools\MSBuild\Current\Bin\MsBuild.exe /p:PlatformToolset=v142 /property:Configuration=Release $_} 105 | ``` 106 | 107 | The command-line applications are located in the _release_ directory. 108 | 109 | # documentation 110 | 111 | ## timecode 112 | 113 | Time parameters in command-line applications (timestamps, durations, decays...) support three input formats: 114 | 115 | - raw integers (`42`, `12345`...), interpreted as microseconds 116 | - hh:mm:ss timecodes (`00:00:00`, `04:20:00`, `0:0:678`, `00:1440:00`...) 117 | - hh:mm:ss.uuuuuu timecodes (`00:00:00.123456`, `04:20:00.000000`, `0:0:678.9`...), timecodes with more than 6 digits are rounded to the nearest microsecond. 118 | 119 | ## crop 120 | 121 | crop generates a new Event Stream file with only events from the given region. 122 | 123 | ```sh 124 | ./crop [options] /path/to/input.es /path/to/output.es left bottom width height 125 | ``` 126 | 127 | Available options: 128 | 129 | - `-p`, `--preserve-offset` prevents the coordinates of the cropped area from being normalized 130 | - `-h`, `--help` shows the help message 131 | 132 | ## cut 133 | 134 | cut generates a new Event Stream file with only events from the given time range. 135 | 136 | ```sh 137 | ./cut [options] /path/to/input.es /path/to/output.es begin end 138 | ``` 139 | 140 | `begin` and `end` must be timecodes. 141 | 142 | Available options: 143 | 144 | - `-h`, `--help` shows the help message 145 | 146 | ## dat_to_es 147 | 148 | dat_to_es converts a TD file (and an optional APS file) to an Event Stream file. 149 | 150 | ```sh 151 | ./dat_to_es [options] /path/to/input_td.dat /path/to/input_aps.dat /path/to/output.es 152 | ``` 153 | 154 | If the string `none` is used for the td (respectively, aps) file, the Event Stream file is build from the aps (respectively, td) file only. 155 | 156 | Available options: 157 | 158 | - `-h`, `--help` shows the help message 159 | 160 | ## es_to_csv 161 | 162 | es_to_csv converts an Event Stream file to a CSV file (compatible with Excel and Matlab): 163 | 164 | ```sh 165 | ./es_to_csv [options] /path/to/input.es /path/to/output.csv 166 | ``` 167 | 168 | Available options: 169 | 170 | - `-h`, `--help` shows the help message 171 | 172 | ## es_to_frames 173 | 174 | es_to_frames converts an Event Stream file to video frames. Frames use the P6 Netpbm format (https://en.wikipedia.org/wiki/Netpbm) if the output is a directory. Otherwise, the output consists of raw rgb24 frames. 175 | 176 | ```sh 177 | ./es_to_frames [options] 178 | ``` 179 | 180 | Available options: 181 | 182 | - `-i file`, `--input file` sets the path to the input .es file (defaults to standard input) 183 | - `-o directory`, `--output directory` sets the path to the output directory (defaults to standard output) 184 | - `-b timestamp`, `--begin timestamp` ignores events before this timestamp (timecode, defaults to `00:00:00`), 185 | - `-e timestamp`, `--end timestamp` ignores events after this timestamp (timecode, defaults to the end of the recording), 186 | - `-f frametime`, `--frametime frametime` sets the time between two frames (timecode, defaults to `00:00:00.020`) 187 | - `-s style`, `--style style` selects the decay function, one of `exponential` (default), `linear`, `window`, `cumulative`, and `cumulative_shared` 188 | - `-t tau`, `--tau tau` sets the decay function parameter (timecode, defaults to `00:00:00.200`) 189 | - if `style` is `exponential`, the decay is set to `parameter` 190 | - if `style` is `linear`, the decay is set to `parameter / 2` 191 | - if `style` is `window`, the time window is set to `parameter` 192 | - if `style` is `cumulative`, the time window is set to `parameter` 193 | - if `style` is `cumulative-shared`, the time window is set to `parameter` 194 | - `-j color`, `--oncolor color` sets the color for ON events (color must be formatted as `#hhhhhh` where `h` is an hexadecimal digit, defaults to `#f4c20d`) 195 | - `-k color`, `--offcolor color` sets the color for OFF events (color must be formatted as `#hhhhhh` where `h` is an hexadecimal digit, defaults to `#1e88e5`) 196 | - `-l color`, `--idlecolor color` sets the background color (color must be formatted as `#hhhhhh` where `h` is an hexadecimal digit, defaults to `#191919`) 197 | - `-r ratio`, `--discard-ratio ratio` sets the ratio of pixels discarded for cumulative mapping, ignored if the style is cumulative or cumulative-shared (defaults to 0.01) 198 | - `-a`, `--add-timecode` adds a timecode overlay 199 | - `-d digits`, `--digits digits` sets the number of digits in output filenames, ignored if the output is not a directory (defaults to `6`) 200 | - `-r ratio`, `--discard-ratio ratio` sets the ratio of pixels discarded for tone mapping, ignored if the stream type is not atis, used for black (resp. white) if `--black` (resp. `--white`) is not set (defaults to 0.01) 201 | - `-v duration`, `--black duration` sets the black integration duration for tone mapping (timecode, defaults to automatic discard calculation) 202 | - `-w duration`, `--white duration` sets the white integration duration for tone mapping (timecode, defaults to automatic discard calculation) 203 | - `-x color`, `--atiscolor color` sets the background color for ATIS exposure measurements (color must be formatted as #hhhhhh where h is an hexadecimal digit, defaults to `#000000`) 204 | - `-h`, `--help` shows the help message 205 | 206 | Once can use the script _render.py_ to directly generate an MP4 video instead of frames. _es_to_frames_ must be compiled before using _render.py_, and FFmpeg (https://www.ffmpeg.org) must be installed and on the system's path. Run `python3 render.py --help` for details. 207 | 208 | The commands below show how to manually pipe the generated frames into FFmpeg: 209 | 210 | ```sh 211 | cat /path/to/input.es | ./es_to_frames | ffmpeg -f rawvideo -s 1280x720 -framerate 50 -pix_fmt rgb24 -i - -c:v libx264 -pix_fmt yuv420p /path/to/output.mp4 212 | ``` 213 | 214 | You may need to change the width, height and framerate of the video depending on the `es_to_frames` options and the Event Stream dimensions. You can use `./size /path/to/input.es` to read the dimensions: 215 | 216 | ```sh 217 | cat /path/to/input.es | ./es_to_frames --frametime 10000 | ffmpeg -f rawvideo -s $(./size /path/to/input.es) -framerate 100 -pix_fmt rgb24 -i - -c:v libx264 -pix_fmt yuv420p /path/to/output.mp4 218 | ``` 219 | 220 | You can also use a lossless encoding format: 221 | 222 | ```sh 223 | cat /path/to/input.es | ./es_to_frames | ffmpeg -f rawvideo -s 1280x720 -framerate 50 -pix_fmt rgb24 -i - -c:v libx265 -x265-params lossless=1 -pix_fmt yuv444p /path/to/output.mp4 224 | ``` 225 | 226 | ## es_to_ply 227 | 228 | es_to_ply converts an Event Stream file to a PLY file (Polygon File Format, compatible with Blender). 229 | 230 | ```sh 231 | ./es_to_ply [options] /path/to/input.es /path/to/output_on.ply /path/to/output_off.ply 232 | ``` 233 | 234 | Available options: 235 | 236 | - `-t timestamp`, `--timestamp timestamp` sets the initial timestamp for the point cloud (timecode, defaults to `00:00:00`) 237 | - `-d duration`, `--duration duration` sets the duration for the point cloud (timecode, defaults to `00:00:01`) 238 | - `-h`, `--help` shows the help message 239 | 240 | ## event_rate 241 | 242 | event_rate plots the number of events per second (slidding time window). 243 | 244 | ```sh 245 | ./event_rate [options] /path/to/input.es /path/to/output.svg 246 | ``` 247 | 248 | Available options: 249 | 250 | - `-l tau`, `--long tau` sets the long (foreground curve) time window (timecode, defaults to `00:00:01`) 251 | - `-s tau`, `--short tau` sets the short (background curve) time window (timecode, defaults to `00:00:00.010`) 252 | - `-i size`, `--width size` sets the output width in pixels (defaults to `1280`) 253 | - `-e size`, `--height size` sets the output height in pixels (defaults to `720`) 254 | - `-h`, `--help` shows the help message 255 | 256 | ## evt2_to_es 257 | 258 | evt2_to_es converts a raw file (EVT2) into an Event Stream file. 259 | 260 | ```sh 261 | ./evt2_to_es [options] /path/to/input.raw /path/to/output.es 262 | ``` 263 | 264 | Available options: 265 | 266 | - `-x size`, `--width size` sets the sensor width in pixels if not specified in the header (defaults to `640`) 267 | - `-y size`, `--height size` sets the sensor height in pixels if not specified in the header (defaults to `480`) 268 | - `-n`, `--normalize` offsets the timestamps so that the first one is zero 269 | - `-h`, `--help` shows the help message 270 | 271 | ## evt3_to_es 272 | 273 | evt3_to_es converts a raw file (EVT3) into an Event Stream file. 274 | 275 | ```sh 276 | ./evt3_to_es [options] /path/to/input.raw /path/to/output.es 277 | ``` 278 | 279 | Available options: 280 | 281 | - `-x size`, `--width size` sets the sensor width in pixels if not specified in the header (defaults to `1280`) 282 | - `-y size`, `--height size` sets the sensor height in pixels if not specified in the header (defaults to `720`) 283 | - `-n`, `--normalize` offsets the timestamps so that the first one is zero 284 | - `-h`, `--help` shows the help message 285 | 286 | ## rainmaker 287 | 288 | rainmaker generates a standalone HTML file containing a 3D representation of events from an Event Stream file. 289 | 290 | ```sh 291 | ./rainmaker [options] /path/to/input.es /path/to/output.html 292 | ``` 293 | 294 | Available options: 295 | 296 | - `-t timestamp`, `--timestamp timestamp` sets the initial timestamp for the point cloud (timecode, defaults to `00:00:00`) 297 | - `-d duration`, `--duration duration` sets the duration for the point cloud (timecode, defaults to `00:00:01`) 298 | - `-r ratio`, `--ratio ratio` sets the discard ratio for logarithmic tone mapping (default to `0.05`, ignored if the file does not contain ATIS events) 299 | - `-f duration`, `--frametime duration` sets the time between two frames (defaults to `auto`), `auto` calculates the time between two frames so that there is the same amount of raw data in events and frames, a duration in microseconds can be provided instead, `none` disables the frames, ignored if the file contains DVS events 300 | - `-a`, `--dark` renders in dark mode 301 | - `-h`, `--help` shows the help message 302 | 303 | ## rainbow 304 | 305 | rainbow represents events by mapping time to colors. 306 | 307 | ```sh 308 | ./rainbow [options] /path/to/input.es /path/to/output.ppm 309 | ``` 310 | 311 | Available options: 312 | 313 | - `-a alpha`, `--alpha alpha` sets the transparency level for each event (must be in the range ]0, 1], defaults to `0.1`) 314 | - `-l color`, `--idlecolor color` sets the background color (color must be formatted as `#hhhhhh` where `h` is an hexadecimal digit, defaults to `#191919`) 315 | - `-h`, `--help` shows the help message 316 | 317 | ## size 318 | 319 | size prints the spatial dimensions of the given Event Stream file. 320 | 321 | ```sh 322 | ./size /path/to/input.es 323 | ``` 324 | 325 | Available options: 326 | 327 | - `-h`, `--help` shows the help message 328 | 329 | ## spectrogram 330 | 331 | spectrogram plots a short-time Fourier transform. 332 | 333 | ```sh 334 | ./spectrogram [options] /path/to/input.es /path/to/output.png /path/to/output.json 335 | ``` 336 | 337 | Available options: 338 | 339 | - `-b [timecode]`, `--begin [timecode]` ignores events before this timestamp (timecode, `defaults to 00:00:00`) 340 | - `-e [timecode]`, `--end [timecode]` ignores events after this timestamp (timecode, defaults to the end of the recording) 341 | - `-l [int]`, `--left [int]` input region of interest in pixels (defaults to 0) 342 | - `-o [int]`, `--bottom [int]` input region of interest in pixels (defaults to 0) 343 | - `-c [int]`, `--width [int]` input region of interest in pixels (defaults to input width) 344 | - `-d [int]`, `--height [int]` input region of interest in pixels (defaults to input height) 345 | - `-t [timecode]`, `--tau [timecode]` decay in µs (timecode, defaults to `00:00:00.100000`) 346 | - `-m [mode]`, `--mode [mode]` polarity mode, one of `on`, `off`, `all`, `abs` (defaults to `all`) 347 | - `on` only uses ON events 348 | - `off` only uses OFF events 349 | - `all` multiplies the complex activity by 1 for ON events and -1 for OFF events 350 | - `abs` multiplies the complex activity by 1 for all events 351 | - `-i [float]`, `--minimum [float]` minimum frequency in Hertz (defaults to `5e6 / (end - begin)`) 352 | - `-j [float]`, `--maximum [float]` maximum frequency in Hertz (defaults to `10000.0`) 353 | - `-f [int]`, `--frequencies [int]` number of frequencies (defaults to `100`) 354 | - `-s [int]`, `--times [int]` number of time samples (defaults to `1000`) 355 | - `-g [float]`, `--gamma [float]` gamma ramp (power) to apply to the output image (defaults to `0.5`) 356 | - `-h`, `--help` shows the help message 357 | 358 | ## spatiospectrogram 359 | 360 | - `-i [path]`, `--input [path]` sets the path to the input .es file (defaults to standard input) 361 | - `-o [path]`, `--output [path]` sets the path to the output directory (defaults to standard output) 362 | - `-b [timecode]`, `--begin [timecode]` ignores events before this timestamp (timecode, defaults to `00:00:00`) 363 | - `-e [timecode]`, `--end [timecode]` ignores events after this timestamp (timecode, defaults to the end of the recording) 364 | - `-f [timecode]`, `--frametime [timecode]` sets the time between two frames (timecode, defaults to `00:00:00.020`) 365 | - `-c [int]`, `--scale [int]` scale up the output by the given integer factor (defaults to `1`) 366 | - `-t [timecode]`, `--tau [timecode]` decay in µs (timecode, defaults to `00:00:00.100000`) 367 | - `-m [mode]`, `--mode [mode]` polarity mode, one of `on`, `off`, `all`, `abs` (defaults to `all`) 368 | - `on` only uses ON events 369 | - `off` only uses OFF events 370 | - `all` multiplies the complex activity by 1 for ON events and -1 for OFF events 371 | - `abs` multiplies the complex activity by 1 for all events 372 | - `-p [float]`, `--minimum [float]` minimum frequency in Hertz (defaults `10.0`) 373 | - `-q [float]`, `--maximum [float]` maximum frequency in Hertz (defaults to `10000.0`) 374 | - `-u [int]`, `--frequencies [int]` number of frequencies (defaults to `100`) 375 | - `-g [float]`, `--frequency-gamma [float]` gamma ramp (power) to apply to the output frequency (defaults to `0.5`) 376 | - `-k [float]`, `--amplitude-gamma [float]` gamma ramp (power) to apply to the output amplitude (defaults to `0.5`) 377 | - `-r [float]`, `--discard [float]` amplitude discard ratio for tone-mapping (defaults to `0.001`) 378 | - `-a`, `--add-timecode` adds a timecode overlay 379 | - `-d [int]`, `--digits [int]` sets the number of digits in output filenames, ignored if the output is not a directory (defaults to `6`) 380 | - `-h`, `--help` shows the help message 381 | 382 | The commands below show how to manually pipe the generated frames into FFmpeg: 383 | 384 | ```sh 385 | cat /path/to/input.es | ./spatiospectrogram | ffmpeg -f rawvideo -s 1280x720 -framerate 50 -pix_fmt rgb24 -i - -c:v libx264 -pix_fmt yuv420p /path/to/output.mp4 386 | ``` 387 | 388 | You may need to change the width, height and framerate of the video depending on the `spatiospectrogram` options and the Event Stream dimensions. You can use `./size /path/to/input.es` to read the dimensions: 389 | 390 | ```sh 391 | cat /path/to/input.es | ./spatiospectrogram --frametime 10000 | ffmpeg -f rawvideo -s $(./size /path/to/input.es) -framerate 100 -pix_fmt rgb24 -i - -c:v libx264 -pix_fmt yuv420p /path/to/output.mp4 392 | ``` 393 | 394 | You can also use a lossless encoding format: 395 | 396 | ```sh 397 | cat /path/to/input.es | ./spatiospectrogram | ffmpeg -f rawvideo -s 1280x720 -framerate 50 -pix_fmt rgb24 -i - -c:v libx265 -x265-params lossless=1 -pix_fmt yuv444p /path/to/output.mp4 398 | ``` 399 | 400 | ## statistics 401 | 402 | statistics retrieves the event stream's properties and outputs them in JSON format. 403 | 404 | ```sh 405 | ./statistics [options] /path/to/input.es 406 | ``` 407 | 408 | Available options: 409 | 410 | - `-h`, `--help` shows the help message 411 | 412 | # contribute 413 | 414 | ## development dependencies 415 | 416 | ### Debian / Ubuntu 417 | 418 | Open a terminal and run: 419 | 420 | ```sh 421 | sudo apt install clang-format # formatting tool 422 | ``` 423 | 424 | ### macOS 425 | 426 | Open a terminal and run: 427 | 428 | ```sh 429 | brew install clang-format # formatting tool 430 | ``` 431 | 432 | ## test 433 | 434 | To test the library, run from the _command_line_tools_ directory: 435 | 436 | ```sh 437 | premake4 gmake 438 | cd build 439 | make 440 | cd release 441 | ``` 442 | 443 | **Windows** users must run `premake4 vs2010` instead, and open the generated solution with Visual Studio. 444 | 445 | You can then run sequentially the executables located in the _release_ directory. 446 | 447 | After changing the code, format the source files by running from the _command_line_tools_ directory: 448 | 449 | ```sh 450 | clang-format -i source/*.hpp source/*.cpp 451 | ``` 452 | 453 | **Windows** users must run _Edit_ > _Advanced_ > _Format Document_ from the Visual Studio menu instead. 454 | 455 | # license 456 | 457 | See the [LICENSE](LICENSE.txt) file for license rights and limitations (GNU GPLv3). 458 | -------------------------------------------------------------------------------- /source/statistics.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/pontella/source/pontella.hpp" 2 | #include "../third_party/sepia/source/sepia.hpp" 3 | #include "../third_party/tarsier/source/convert.hpp" 4 | #include "../third_party/tarsier/source/hash.hpp" 5 | #include "../third_party/tarsier/source/replicate.hpp" 6 | #include "timecode.hpp" 7 | #include 8 | #include 9 | 10 | #ifdef _WIN32 11 | #include 12 | #include 13 | #endif 14 | 15 | /// type_to_string returns a text representation of the type enum. 16 | std::string type_to_string(sepia::type type) { 17 | switch (type) { 18 | case sepia::type::generic: 19 | return "generic"; 20 | case sepia::type::dvs: 21 | return "dvs"; 22 | case sepia::type::atis: 23 | return "atis"; 24 | case sepia::type::color: 25 | return "color"; 26 | } 27 | return "unknown"; 28 | } 29 | 30 | /// hash_to_string converts a 128 bits hash to a hexadecimal representation. 31 | std::string hash_to_string(std::pair hash) { 32 | std::stringstream stream; 33 | stream << '"' << std::hex << std::get<1>(hash) << std::hex << std::setfill('0') << std::setw(16) 34 | << std::get<0>(hash) << '"'; 35 | return stream.str(); 36 | } 37 | 38 | /// properties_to_json converts a list of properties to pretty-printed JSON. 39 | std::string properties_to_json(const std::vector>& properties) { 40 | std::string json("{\n"); 41 | for (std::size_t index = 0; index < properties.size(); ++index) { 42 | json += std::string(" \"") + properties[index].first + "\": " + properties[index].second 43 | + (index < properties.size() - 1 ? "," : "") + "\n"; 44 | } 45 | json += "}"; 46 | return json; 47 | } 48 | 49 | int main(int argc, char* argv[]) { 50 | return pontella::main( 51 | { 52 | "statistics retrieves the event stream's properties and outputs them in JSON format.", 53 | "Syntax: ./statistics [options] [/path/to/input.es]", 54 | "Available options:", 55 | " -i file, --input file sets the path to the input .es file", 56 | " defaults to standard input", 57 | " -h, --help shows this help message", 58 | }, 59 | argc, 60 | argv, 61 | -1, 62 | {{"input", {"i"}}}, 63 | {}, 64 | [](pontella::command command) { 65 | std::unique_ptr input; 66 | { 67 | const auto name_and_argument = command.options.find("input"); 68 | if (name_and_argument == command.options.end()) { 69 | if (command.arguments.empty()) { 70 | #ifdef _WIN32 71 | _setmode(_fileno(stdin), _O_BINARY); 72 | #endif 73 | input = sepia::make_unique(std::cin.rdbuf()); 74 | } else if (command.arguments.size() == 1) { 75 | input = sepia::filename_to_ifstream(command.arguments.front()); 76 | } else { 77 | throw std::runtime_error("too many arguments (expected 0 or 1)"); 78 | } 79 | } else { 80 | if (command.arguments.empty()) { 81 | input = sepia::filename_to_ifstream(name_and_argument->second); 82 | } else if (command.arguments.size() == 1) { 83 | throw std::runtime_error("a filename can be passed either as a positional argument or to the " 84 | "--input option, not both"); 85 | } else { 86 | throw std::runtime_error("too many arguments (expected 0 or 1)"); 87 | } 88 | } 89 | } 90 | const auto header = sepia::read_header(*input); 91 | std::vector> properties{ 92 | {"version", 93 | std::string("\"") + std::to_string(static_cast(std::get<0>(header.version))) + "." 94 | + std::to_string(static_cast(std::get<1>(header.version))) + "." 95 | + std::to_string(static_cast(std::get<2>(header.version))) + "\""}, 96 | {"type", std::string("\"") + type_to_string(header.event_stream_type) + "\""}, 97 | }; 98 | if (header.event_stream_type != sepia::type::generic) { 99 | properties.emplace_back("width", std::to_string(header.width)); 100 | properties.emplace_back("height", std::to_string(header.height)); 101 | } 102 | switch (header.event_stream_type) { 103 | case sepia::type::generic: { 104 | auto first = true; 105 | uint64_t begin_t; 106 | uint64_t end_t; 107 | std::size_t events = 0; 108 | std::string t_hash; 109 | std::string bytes_hash; 110 | { 111 | auto hash = tarsier::make_hash( 112 | [&](std::pair hash_value) { bytes_hash = hash_to_string(hash_value); }); 113 | sepia::join_observable( 114 | std::move(input), 115 | header, 116 | tarsier::make_replicate( 117 | [&](sepia::generic_event generic_event) { 118 | if (first) { 119 | first = false; 120 | begin_t = generic_event.t; 121 | } 122 | end_t = generic_event.t; 123 | ++events; 124 | for (const auto character : generic_event.bytes) { 125 | hash(character); 126 | } 127 | }, 128 | tarsier::make_convert( 129 | [](sepia::generic_event generic_event) -> uint64_t { return generic_event.t; }, 130 | tarsier::make_hash([&](std::pair hash_value) { 131 | t_hash = hash_to_string(hash_value); 132 | })))); 133 | } 134 | properties.emplace_back( 135 | "begin_representation", std::string("\"") + timecode(begin_t).to_string() + "\""); 136 | properties.emplace_back( 137 | "end_representation", std::string("\"") + timecode(end_t).to_string() + "\""); 138 | properties.emplace_back( 139 | "duration_representation", std::string("\"") + timecode(end_t - begin_t).to_string() + "\""); 140 | properties.emplace_back("begin_t", std::to_string(begin_t)); 141 | properties.emplace_back("end_t", std::to_string(end_t)); 142 | properties.emplace_back("duration", std::to_string(end_t - begin_t)); 143 | properties.emplace_back("events", std::to_string(events)); 144 | properties.emplace_back("t_hash", t_hash); 145 | properties.emplace_back("bytes_hash", bytes_hash); 146 | std::cout << properties_to_json(properties) << std::endl; 147 | break; 148 | } 149 | case sepia::type::dvs: { 150 | auto first = true; 151 | uint64_t begin_t; 152 | uint64_t end_t; 153 | std::size_t events = 0; 154 | std::size_t increase_events = 0; 155 | std::string t_hash; 156 | std::string x_hash; 157 | std::string y_hash; 158 | sepia::join_observable( 159 | std::move(input), 160 | header, 161 | tarsier::make_replicate( 162 | [&](sepia::dvs_event dvs_event) { 163 | if (first) { 164 | first = false; 165 | begin_t = dvs_event.t; 166 | } 167 | end_t = dvs_event.t; 168 | ++events; 169 | if (dvs_event.is_increase) { 170 | ++increase_events; 171 | } 172 | }, 173 | tarsier::make_convert( 174 | [](sepia::dvs_event dvs_event) -> uint64_t { return dvs_event.t; }, 175 | tarsier::make_hash([&](std::pair hash_value) { 176 | t_hash = hash_to_string(hash_value); 177 | })), 178 | tarsier::make_convert( 179 | [](sepia::dvs_event dvs_event) -> uint16_t { return dvs_event.x; }, 180 | tarsier::make_hash([&](std::pair hash_value) { 181 | x_hash = hash_to_string(hash_value); 182 | })), 183 | tarsier::make_convert( 184 | [](sepia::dvs_event dvs_event) -> uint16_t { return dvs_event.y; }, 185 | tarsier::make_hash([&](std::pair hash_value) { 186 | y_hash = hash_to_string(hash_value); 187 | })))); 188 | properties.emplace_back( 189 | "begin_representation", std::string("\"") + timecode(begin_t).to_string() + "\""); 190 | properties.emplace_back( 191 | "end_representation", std::string("\"") + timecode(end_t).to_string() + "\""); 192 | properties.emplace_back( 193 | "duration_representation", std::string("\"") + timecode(end_t - begin_t).to_string() + "\""); 194 | properties.emplace_back("begin_t", std::to_string(begin_t)); 195 | properties.emplace_back("end_t", std::to_string(end_t)); 196 | properties.emplace_back("duration", std::to_string(end_t - begin_t)); 197 | properties.emplace_back("events", std::to_string(events)); 198 | properties.emplace_back("increase_events", std::to_string(increase_events)); 199 | properties.emplace_back("t_hash", t_hash); 200 | properties.emplace_back("x_hash", x_hash); 201 | properties.emplace_back("y_hash", y_hash); 202 | std::cout << properties_to_json(properties) << std::endl; 203 | break; 204 | } 205 | case sepia::type::atis: { 206 | auto first = true; 207 | uint64_t begin_t; 208 | uint64_t end_t; 209 | std::size_t events = 0; 210 | std::size_t dvs_events = 0; 211 | std::size_t increase_events = 0; 212 | std::size_t second_events = 0; 213 | std::string t_hash; 214 | std::string x_hash; 215 | std::string y_hash; 216 | sepia::join_observable( 217 | std::move(input), 218 | header, 219 | tarsier::make_replicate( 220 | [&](sepia::atis_event atis_event) { 221 | if (first) { 222 | first = false; 223 | begin_t = atis_event.t; 224 | } 225 | end_t = atis_event.t; 226 | ++events; 227 | if (!atis_event.is_threshold_crossing) { 228 | ++dvs_events; 229 | } 230 | if (atis_event.polarity) { 231 | if (atis_event.is_threshold_crossing) { 232 | ++second_events; 233 | } else { 234 | ++increase_events; 235 | } 236 | } 237 | }, 238 | tarsier::make_convert( 239 | [](sepia::atis_event atis_event) -> uint64_t { return atis_event.t; }, 240 | tarsier::make_hash([&](std::pair hash_value) { 241 | t_hash = hash_to_string(hash_value); 242 | })), 243 | tarsier::make_convert( 244 | [](sepia::atis_event atis_event) -> uint16_t { return atis_event.x; }, 245 | tarsier::make_hash([&](std::pair hash_value) { 246 | x_hash = hash_to_string(hash_value); 247 | })), 248 | tarsier::make_convert( 249 | [](sepia::atis_event atis_event) -> uint16_t { return atis_event.y; }, 250 | tarsier::make_hash([&](std::pair hash_value) { 251 | y_hash = hash_to_string(hash_value); 252 | })))); 253 | properties.emplace_back( 254 | "begin_representation", std::string("\"") + timecode(begin_t).to_string() + "\""); 255 | properties.emplace_back( 256 | "end_representation", std::string("\"") + timecode(end_t).to_string() + "\""); 257 | properties.emplace_back( 258 | "duration_representation", std::string("\"") + timecode(end_t - begin_t).to_string() + "\""); 259 | properties.emplace_back("begin_t", std::to_string(begin_t)); 260 | properties.emplace_back("end_t", std::to_string(end_t)); 261 | properties.emplace_back("duration", std::to_string(end_t - begin_t)); 262 | properties.emplace_back("events", std::to_string(events)); 263 | properties.emplace_back("dvs_events", std::to_string(dvs_events)); 264 | properties.emplace_back("increase_events", std::to_string(increase_events)); 265 | properties.emplace_back("second_events", std::to_string(second_events)); 266 | properties.emplace_back("t_hash", t_hash); 267 | properties.emplace_back("x_hash", x_hash); 268 | properties.emplace_back("y_hash", y_hash); 269 | std::cout << properties_to_json(properties) << std::endl; 270 | break; 271 | } 272 | case sepia::type::color: { 273 | auto first = true; 274 | uint64_t begin_t; 275 | uint64_t end_t; 276 | std::size_t events = 0; 277 | std::string t_hash; 278 | std::string x_hash; 279 | std::string y_hash; 280 | std::string r_hash; 281 | std::string g_hash; 282 | std::string b_hash; 283 | sepia::join_observable( 284 | std::move(input), 285 | header, 286 | tarsier::make_replicate( 287 | [&](sepia::color_event color_event) { 288 | if (first) { 289 | first = false; 290 | begin_t = color_event.t; 291 | } 292 | end_t = color_event.t; 293 | ++events; 294 | }, 295 | tarsier::make_convert( 296 | [](sepia::color_event color_event) -> uint64_t { return color_event.t; }, 297 | tarsier::make_hash([&](std::pair hash_value) { 298 | t_hash = hash_to_string(hash_value); 299 | })), 300 | tarsier::make_convert( 301 | [](sepia::color_event color_event) -> uint16_t { return color_event.x; }, 302 | tarsier::make_hash([&](std::pair hash_value) { 303 | x_hash = hash_to_string(hash_value); 304 | })), 305 | tarsier::make_convert( 306 | [](sepia::color_event color_event) -> uint16_t { return color_event.y; }, 307 | tarsier::make_hash([&](std::pair hash_value) { 308 | y_hash = hash_to_string(hash_value); 309 | })), 310 | tarsier::make_convert( 311 | [](sepia::color_event color_event) -> uint8_t { return color_event.r; }, 312 | tarsier::make_hash([&](std::pair hash_value) { 313 | r_hash = hash_to_string(hash_value); 314 | })), 315 | tarsier::make_convert( 316 | [](sepia::color_event color_event) -> uint8_t { return color_event.g; }, 317 | tarsier::make_hash([&](std::pair hash_value) { 318 | g_hash = hash_to_string(hash_value); 319 | })), 320 | tarsier::make_convert( 321 | [](sepia::color_event color_event) -> uint8_t { return color_event.b; }, 322 | tarsier::make_hash([&](std::pair hash_value) { 323 | b_hash = hash_to_string(hash_value); 324 | })))); 325 | properties.emplace_back( 326 | "begin_representation", std::string("\"") + timecode(begin_t).to_string() + "\""); 327 | properties.emplace_back( 328 | "end_representation", std::string("\"") + timecode(end_t).to_string() + "\""); 329 | properties.emplace_back( 330 | "duration_representation", std::string("\"") + timecode(end_t - begin_t).to_string() + "\""); 331 | properties.emplace_back("begin_t", std::to_string(begin_t)); 332 | properties.emplace_back("end_t", std::to_string(end_t)); 333 | properties.emplace_back("duration", std::to_string(end_t - begin_t)); 334 | properties.emplace_back("events", std::to_string(events)); 335 | properties.emplace_back("t_hash", t_hash); 336 | properties.emplace_back("x_hash", x_hash); 337 | properties.emplace_back("y_hash", y_hash); 338 | properties.emplace_back("r_hash", r_hash); 339 | properties.emplace_back("g_hash", g_hash); 340 | properties.emplace_back("b_hash", b_hash); 341 | std::cout << properties_to_json(properties) << std::endl; 342 | break; 343 | } 344 | } 345 | }); 346 | return 0; 347 | } 348 | -------------------------------------------------------------------------------- /source/rainmaker.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/lodepng/lodepng.h" 2 | #include "../third_party/pontella/source/pontella.hpp" 3 | #include "../third_party/sepia/source/sepia.hpp" 4 | #include "../third_party/tarsier/source/stitch.hpp" 5 | #include "html.hpp" 6 | #include "timecode.hpp" 7 | #include 8 | 9 | /// exposure_measurement represents an exposure measurement as a time delta. 10 | SEPIA_PACK(struct exposure_measurement { 11 | uint64_t t; 12 | uint64_t delta_t; 13 | uint16_t x; 14 | uint16_t y; 15 | }); 16 | 17 | /// filename_to_string reads the contents of a file to a string. 18 | std::string filename_to_string(const std::string& filename) { 19 | auto stream = sepia::filename_to_ifstream(filename); 20 | return std::string(std::istreambuf_iterator(*stream), std::istreambuf_iterator()); 21 | } 22 | 23 | int main(int argc, char* argv[]) { 24 | return pontella::main( 25 | {"rainmaker generates a standalone HTML file containing a 3D representation of events", 26 | "Syntax: ./rainmaker [options] /path/to/input.es /path/to/output.html", 27 | "Available options:", 28 | " -t timestamp, --timestamp timestamp sets the initial timestamp for the point cloud (timecode)", 29 | " defaults to 00:00:00", 30 | " -d duration, --duration duration sets the duration for the point cloud (timecode)", 31 | " defaults to 00:00:01", 32 | " -r ratio, --ratio ratio sets the discard ratio for logarithmic tone mapping", 33 | " defaults to 0.05", 34 | " ignored if the file does not contain ATIS events", 35 | " -f duration, --frametime duration sets the frame duration", 36 | " defaults to 'auto'", 37 | " 'auto' calculates the time between two frames so that", 38 | " there is the same amount of raw data in events", 39 | " and frames,", 40 | " a duration in microseconds can be provided instead,", 41 | " 'none' disables the frames,", 42 | " ignored if the file contains DVS events", 43 | " -a, --dark renders in dark mode", 44 | " -h, --help shows this help message"}, 45 | argc, 46 | argv, 47 | 2, 48 | { 49 | {"timestamp", {"t"}}, 50 | {"duration", {"d"}}, 51 | {"ratio", {"r"}}, 52 | {"frametime", {"f"}}, 53 | }, 54 | { 55 | {"dark", {"a"}}, 56 | }, 57 | [](pontella::command command) { 58 | const auto nodes = html::parse(filename_to_string(sepia::join({SEPIA_DIRNAME, "rainmaker.html"}))); 59 | uint64_t begin_t = 0; 60 | { 61 | const auto name_and_argument = command.options.find("timestamp"); 62 | if (name_and_argument != command.options.end()) { 63 | begin_t = timecode(name_and_argument->second).value(); 64 | } 65 | } 66 | auto end_t = begin_t + 1000000; 67 | { 68 | const auto name_and_argument = command.options.find("duration"); 69 | if (name_and_argument != command.options.end()) { 70 | const auto duration = timecode(name_and_argument->second).value(); 71 | end_t = begin_t + duration; 72 | } 73 | } 74 | const auto dark = command.flags.find("dark") != command.flags.end(); 75 | { 76 | std::ofstream output(command.arguments[1]); 77 | if (!output.good()) { 78 | throw sepia::unwritable_file(command.arguments[1]); 79 | } 80 | } 81 | std::vector color_events; 82 | std::vector dvs_color_events; 83 | const auto header = sepia::read_header(sepia::filename_to_ifstream(command.arguments[0])); 84 | std::vector base_frame(header.width * header.height * 4, 0); 85 | switch (header.event_stream_type) { 86 | case sepia::type::generic: { 87 | throw std::runtime_error("generic events are not compatible with this application"); 88 | break; 89 | } 90 | case sepia::type::dvs: { 91 | sepia::join_observable< 92 | sepia::type:: 93 | dvs>(sepia::filename_to_ifstream(command.arguments[0]), [&](sepia::dvs_event dvs_event) { 94 | if (dvs_event.t >= end_t) { 95 | throw sepia::end_of_file(); 96 | } 97 | if (dvs_event.t >= begin_t) { 98 | if (dvs_event.is_increase) { 99 | if (dark) { 100 | color_events.push_back({dvs_event.t, dvs_event.x, dvs_event.y, 0xfb, 0xbc, 0x05}); 101 | } else { 102 | color_events.push_back({dvs_event.t, dvs_event.x, dvs_event.y, 0x00, 0x8c, 0xff}); 103 | } 104 | 105 | } else { 106 | if (dark) { 107 | color_events.push_back({dvs_event.t, dvs_event.x, dvs_event.y, 0x42, 0x85, 0xf4}); 108 | } else { 109 | color_events.push_back({dvs_event.t, dvs_event.x, dvs_event.y, 0x33, 0x4d, 0x5c}); 110 | } 111 | } 112 | } 113 | }); 114 | if (color_events.empty()) { 115 | throw std::runtime_error("there are no DVS events in the given file and range"); 116 | } 117 | break; 118 | } 119 | case sepia::type::atis: { 120 | auto ratio = 0.05; 121 | { 122 | const auto name_and_argument = command.options.find("ratio"); 123 | if (name_and_argument != command.options.end()) { 124 | ratio = std::stod(name_and_argument->second); 125 | } 126 | if (ratio < 0 || ratio >= 1) { 127 | throw std::runtime_error("ratio must be a real number in the range [0, 1["); 128 | } 129 | } 130 | std::vector exposure_measurements; 131 | std::vector delta_t_base_frame(header.width * header.height, 0); 132 | sepia::join_observable( 133 | sepia::filename_to_ifstream(command.arguments[0]), 134 | sepia::make_split( 135 | [&](sepia::dvs_event dvs_event) { 136 | if (dvs_event.t >= end_t) { 137 | throw sepia::end_of_file(); 138 | } 139 | if (dvs_event.t >= begin_t) { 140 | if (dvs_event.is_increase) { 141 | if (dark) { 142 | color_events.push_back( 143 | {dvs_event.t, dvs_event.x, dvs_event.y, 0xfb, 0xbc, 0x05}); 144 | } else { 145 | color_events.push_back( 146 | {dvs_event.t, dvs_event.x, dvs_event.y, 0x00, 0x8c, 0xff}); 147 | } 148 | 149 | } else { 150 | if (dark) { 151 | color_events.push_back( 152 | {dvs_event.t, dvs_event.x, dvs_event.y, 0x42, 0x85, 0xf4}); 153 | } else { 154 | color_events.push_back( 155 | {dvs_event.t, dvs_event.x, dvs_event.y, 0x33, 0x4d, 0x5c}); 156 | } 157 | } 158 | } 159 | }, 160 | tarsier::make_stitch( 161 | header.width, 162 | header.height, 163 | [](sepia::threshold_crossing threshold_crossing, 164 | uint64_t delta_t) -> exposure_measurement { 165 | return {threshold_crossing.t, delta_t, threshold_crossing.x, threshold_crossing.y}; 166 | }, 167 | [&](exposure_measurement exposure_measurement) { 168 | if (exposure_measurement.t >= end_t) { 169 | throw sepia::end_of_file(); 170 | } else if (exposure_measurement.t >= begin_t) { 171 | exposure_measurements.push_back(exposure_measurement); 172 | } else { 173 | delta_t_base_frame 174 | [exposure_measurement.x 175 | + header.width * (header.height - 1 - exposure_measurement.y)] = 176 | exposure_measurement.delta_t; 177 | } 178 | }))); 179 | if (exposure_measurements.empty()) { 180 | throw std::runtime_error("there are no ATIS events in the given file and range"); 181 | } 182 | auto slope = 0.0; 183 | auto intercept = 128.0; 184 | { 185 | std::vector delta_ts; 186 | delta_ts.resize(exposure_measurements.size()); 187 | std::transform( 188 | exposure_measurements.begin(), 189 | exposure_measurements.end(), 190 | delta_ts.begin(), 191 | [](exposure_measurement exposure_measurement) { return exposure_measurement.delta_t; }); 192 | 193 | delta_ts.reserve(delta_ts.size() + delta_t_base_frame.size()); 194 | for (auto delta_t : delta_t_base_frame) { 195 | if (delta_t > 0 && delta_t < std::numeric_limits::max()) { 196 | delta_ts.push_back(delta_t); 197 | } 198 | } 199 | auto discarded_delta_ts = 200 | std::vector(static_cast(delta_ts.size() * ratio)); 201 | std::partial_sort_copy( 202 | delta_ts.begin(), delta_ts.end(), discarded_delta_ts.begin(), discarded_delta_ts.end()); 203 | auto white_discard = discarded_delta_ts.back(); 204 | const auto white_discard_fallback = discarded_delta_ts.front(); 205 | std::partial_sort_copy( 206 | delta_ts.begin(), 207 | delta_ts.end(), 208 | discarded_delta_ts.begin(), 209 | discarded_delta_ts.end(), 210 | std::greater()); 211 | auto black_discard = discarded_delta_ts.back(); 212 | const auto black_discard_fallback = discarded_delta_ts.front(); 213 | if (black_discard <= white_discard) { 214 | white_discard = white_discard_fallback; 215 | black_discard = black_discard_fallback; 216 | } 217 | if (black_discard > white_discard) { 218 | const auto delta = 219 | std::log(static_cast(black_discard) / static_cast(white_discard)); 220 | slope = -255.0 / delta; 221 | intercept = 255.0 * std::log(static_cast(black_discard)) / delta; 222 | } 223 | } 224 | auto delta_t_to_exposure = [&](uint64_t delta_t) -> uint8_t { 225 | const auto exposure_candidate = slope * std::log(delta_t) + intercept; 226 | return static_cast( 227 | exposure_candidate > 255 ? 255 : (exposure_candidate < 0 ? 0 : exposure_candidate)); 228 | }; 229 | for (auto delta_t_iterator = delta_t_base_frame.begin(); 230 | delta_t_iterator != delta_t_base_frame.end(); 231 | ++delta_t_iterator) { 232 | const auto exposure = delta_t_to_exposure(*delta_t_iterator); 233 | const std::size_t index = std::distance(delta_t_base_frame.begin(), delta_t_iterator) * 4; 234 | base_frame[index] = exposure; 235 | base_frame[index + 1] = exposure; 236 | base_frame[index + 2] = exposure; 237 | base_frame[index + 3] = 255; 238 | } 239 | color_events.resize(exposure_measurements.size()); 240 | std::transform( 241 | exposure_measurements.begin(), 242 | exposure_measurements.end(), 243 | color_events.begin(), 244 | [&](exposure_measurement exposure_measurement) -> sepia::color_event { 245 | const auto exposure = delta_t_to_exposure(exposure_measurement.delta_t); 246 | return { 247 | exposure_measurement.t, 248 | exposure_measurement.x, 249 | exposure_measurement.y, 250 | exposure, 251 | exposure, 252 | exposure}; 253 | }); 254 | break; 255 | } 256 | case sepia::type::color: { 257 | sepia::join_observable( 258 | sepia::filename_to_ifstream(command.arguments[0]), [&](sepia::color_event color_event) { 259 | if (color_event.t >= end_t) { 260 | throw sepia::end_of_file(); 261 | } 262 | if (color_event.t >= begin_t) { 263 | color_events.push_back(color_event); 264 | } else { 265 | const auto index = 266 | (color_event.x + header.width * (header.height - 1 - color_event.y)) * 4; 267 | base_frame[index] = color_event.r; 268 | base_frame[index + 1] = color_event.g; 269 | base_frame[index + 2] = color_event.b; 270 | base_frame[index + 3] = 255; 271 | } 272 | }); 273 | if (color_events.empty()) { 274 | throw std::runtime_error("there are no color events in the given file and range"); 275 | } 276 | break; 277 | } 278 | } 279 | 280 | // retrieve the frametime 281 | uint64_t frametime = 0; 282 | if (header.event_stream_type != sepia::type::dvs) { 283 | const auto name_and_argument = command.options.find("frametime"); 284 | if (name_and_argument == command.options.end() || name_and_argument->second == "auto") { 285 | frametime = static_cast( 286 | (end_t - begin_t) 287 | / (static_cast(color_events.size()) / (header.height * header.width))); 288 | if (frametime == 0) { 289 | frametime = 1; 290 | } 291 | } else if (name_and_argument != command.options.end() && name_and_argument->second != "none") { 292 | frametime = std::stoull(name_and_argument->second); 293 | } 294 | } 295 | 296 | // generate the frames 297 | std::vector> frames; 298 | if (frametime > 0) { 299 | frames.push_back(base_frame); 300 | for (auto color_event : color_events) { 301 | if (color_event.t - begin_t > frametime * (frames.size() - 1)) { 302 | if (frametime * frames.size() >= end_t) { 303 | break; 304 | } else { 305 | frames.push_back(frames.back()); 306 | } 307 | } 308 | const auto index = (color_event.x + header.width * (header.height - 1 - color_event.y)) * 4; 309 | frames.back()[index] = color_event.r; 310 | frames.back()[index + 1] = color_event.g; 311 | frames.back()[index + 2] = color_event.b; 312 | frames.back()[index + 3] = 255; 313 | } 314 | } 315 | 316 | // encode the events 317 | std::vector events_bytes; 318 | { 319 | std::stringstream event_stream; 320 | sepia::write_to_reference write(event_stream, header.width, header.height); 321 | if (header.event_stream_type == sepia::type::atis) { 322 | color_events = dvs_color_events; 323 | } 324 | for (auto color_event : color_events) { 325 | write(color_event); 326 | } 327 | sepia::read_header(event_stream); 328 | const auto event_stream_as_string = event_stream.str(); 329 | events_bytes.assign( 330 | std::next( 331 | event_stream_as_string.begin(), 332 | static_cast::iterator>::difference_type>( 333 | event_stream.tellg())), 334 | event_stream_as_string.end()); 335 | } 336 | 337 | // encode the base frame 338 | std::vector png_bytes; 339 | if (lodepng::encode(png_bytes, base_frame, header.width, header.height) != 0) { 340 | throw std::logic_error("encoding the base frame failed"); 341 | } 342 | 343 | // render the HTML output 344 | html::render( 345 | sepia::filename_to_ofstream(command.arguments[1]), 346 | nodes, 347 | { 348 | {"title", html::variable("rainmaker")}, 349 | {"x_max", 350 | html::variable(std::to_string( 351 | header.width > header.height ? 1.0 : static_cast(header.width) / header.height))}, 352 | {"y_max", 353 | html::variable(std::to_string( 354 | header.width > header.height ? static_cast(header.height) / header.width : 1.0))}, 355 | {"z_max", html::variable(std::to_string(1.0))}, 356 | {"color_axis", html::variable(std::string(dark ? "#bbbbbb" : "#334d5c"))}, 357 | {"color_link", html::variable(std::string(dark ? "#005cb2" : "#334d5c"))}, 358 | {"color_active", html::variable(std::string(dark ? "#1e88e5" : "#008cff"))}, 359 | {"color_background", html::variable(std::string(dark ? "#191919" : "#ffffff"))}, 360 | {"color_separator", html::variable(std::string(dark ? "#494949" : "#e8e8e8"))}, 361 | {"color_controls_background", html::variable(std::string(dark ? "#393939" : "#fafafa"))}, 362 | {"color_nolink", html::variable(std::string(dark ? "#888888" : "#b4b4b4"))}, 363 | {"color_content", html::variable(std::string(dark ? "#ffffff" : "#555555"))}, 364 | {"width", html::variable(std::to_string(header.width))}, 365 | {"height", html::variable(std::to_string(header.height))}, 366 | {"begin_t", html::variable(std::to_string(begin_t))}, 367 | {"end_t", html::variable(std::to_string(end_t))}, 368 | {"has_frames", html::variable(!frames.empty())}, 369 | {"frametime", html::variable(std::to_string(frametime))}, 370 | {"base_frame", html::variable(html::bytes_to_encoded_characters(png_bytes))}, 371 | {"frames", 372 | html::variable(std::accumulate( 373 | frames.begin(), 374 | frames.end(), 375 | std::string(), 376 | [&](const std::string& frames_as_string, const std::vector& frame) { 377 | std::vector png_bytes; 378 | if (lodepng::encode(png_bytes, frame, header.width, header.height) != 0) { 379 | throw std::logic_error("encoding a PNG frame failed"); 380 | } 381 | return (frames_as_string.empty() ? std::string() : frames_as_string + ", ") + "'" 382 | + html::bytes_to_encoded_characters(png_bytes) + "'"; 383 | }))}, 384 | {"events", html::variable(html::bytes_to_encoded_characters(events_bytes))}, 385 | {"x3dom", 386 | html::variable( 387 | filename_to_string(sepia::join({sepia::dirname(SEPIA_DIRNAME), "third_party", "x3dom.js"})))}, 388 | }); 389 | }); 390 | } 391 | -------------------------------------------------------------------------------- /source/html.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace html { 13 | /// bytes_to_encoded_characters converts bytes to a URL-encoded string. 14 | /// It is equivalent to JavaScript's btoa function. 15 | inline std::string bytes_to_encoded_characters(const std::vector& bytes) { 16 | std::string output; 17 | output.reserve(bytes.size() * 4); 18 | const std::string characters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); 19 | std::size_t data = 0; 20 | auto length = bytes.size(); 21 | for (; length > 2; length -= 3) { 22 | data = 23 | ((static_cast(bytes[bytes.size() - length]) << 16) 24 | | (static_cast(bytes[bytes.size() - length + 1]) << 8) 25 | | bytes[bytes.size() - length + 2]); 26 | output.push_back(characters[(data & (63 << 18)) >> 18]); 27 | output.push_back(characters[(data & (63 << 12)) >> 12]); 28 | output.push_back(characters[(data & (63 << 6)) >> 6]); 29 | output.push_back(characters[data & 63]); 30 | } 31 | if (length == 2) { 32 | data = (static_cast(bytes[bytes.size() - length]) << 16) 33 | | (static_cast(bytes[bytes.size() - length + 1]) << 8); 34 | output.push_back(characters[(data & (63 << 18)) >> 18]); 35 | output.push_back(characters[(data & (63 << 12)) >> 12]); 36 | output.push_back(characters[(data & (63 << 6)) >> 6]); 37 | output.push_back('='); 38 | } else if (length == 1) { 39 | data = (static_cast(bytes[bytes.size() - length]) << 16); 40 | output.push_back(characters[(data & (63 << 18)) >> 18]); 41 | output.push_back(characters[(data & (63 << 12)) >> 12]); 42 | output.push_back('='); 43 | output.push_back('='); 44 | } 45 | return output; 46 | } 47 | 48 | /// variable stores either text or a boolean. 49 | class variable { 50 | public: 51 | variable(bool boolean) : _boolean(boolean), _is_boolean(true) {} 52 | variable(const char* text) : _boolean(false), _text(text), _is_boolean(false) {} 53 | variable(const std::string& text) : _boolean(false), _text(text), _is_boolean(false) {} 54 | variable(const variable&) = default; 55 | variable(variable&&) = default; 56 | variable& operator=(const variable&) = default; 57 | variable& operator=(variable&&) = default; 58 | virtual ~variable() {} 59 | 60 | /// to_boolean returns a boolean if the variable is a boolean, and throws an exception otherwise. 61 | virtual bool to_boolean() const { 62 | if (!_is_boolean) { 63 | throw std::logic_error("the variable is not a conditional"); 64 | } 65 | return _boolean; 66 | } 67 | 68 | /// to_text returns a string if the variable is a text, and throws an exception otherwise. 69 | virtual const std::string& to_text() const { 70 | if (_is_boolean) { 71 | throw std::logic_error("the variable is not a text"); 72 | } 73 | return _text; 74 | } 75 | 76 | /// is_boolean returns false if the variable is a string. 77 | virtual bool is_boolean() const { 78 | return _is_boolean; 79 | } 80 | 81 | protected: 82 | bool _boolean; 83 | std::string _text; 84 | bool _is_boolean; 85 | }; 86 | 87 | /// node represents a part of an HTML template. 88 | class node { 89 | public: 90 | node(node* parent_node) : parent_node(parent_node) {} 91 | node(const node&) = delete; 92 | node(node&&) = default; 93 | node& operator=(const node&) = delete; 94 | node& operator=(node&&) = default; 95 | virtual ~node() {} 96 | 97 | /// parent_node is the node's enclosing node. 98 | node* parent_node; 99 | }; 100 | 101 | /// text_node is a raw HTML text node. 102 | class text_node : public node { 103 | public: 104 | text_node(const std::string& content, node* parent_node) : node(parent_node), content(content) {} 105 | text_node(const text_node&) = delete; 106 | text_node(text_node&&) = delete; 107 | text_node& operator=(const text_node&) = delete; 108 | text_node& operator=(text_node&&) = delete; 109 | virtual ~text_node() {} 110 | 111 | /// content contains the node's text. 112 | const std::string content; 113 | }; 114 | 115 | /// variable_node is a tag to be replaced. 116 | class variable_node : public node { 117 | public: 118 | variable_node(const std::string& name, node* parent_node) : node(parent_node), name(name) {} 119 | variable_node(const variable_node&) = delete; 120 | variable_node(variable_node&&) = delete; 121 | variable_node& operator=(const variable_node&) = delete; 122 | variable_node& operator=(variable_node&&) = delete; 123 | virtual ~variable_node() {} 124 | 125 | /// name is the variable's name. 126 | const std::string name; 127 | }; 128 | 129 | /// conditional_node is a wrapper node which content is rendered conditionally. 130 | class conditional_node : public node { 131 | public: 132 | conditional_node(const std::string& name, node* parent_node, bool created_from_else_if, bool is_inline) : 133 | node(parent_node), 134 | name(name), 135 | created_from_else_if(created_from_else_if), 136 | is_inline(is_inline), 137 | in_else_block(false) {} 138 | conditional_node(const conditional_node&) = delete; 139 | conditional_node(conditional_node&&) = delete; 140 | conditional_node& operator=(const conditional_node&) = delete; 141 | conditional_node& operator=(conditional_node&&) = delete; 142 | virtual ~conditional_node() {} 143 | 144 | /// name is the variable's name. 145 | const std::string name; 146 | 147 | /// created_from_else_if is true if the node was created to represent an else-if statement. 148 | const bool created_from_else_if; 149 | 150 | /// is_inline is true if the node tags are on the same line, which impacts rendering. 151 | const bool is_inline; 152 | 153 | /// in_else_block is the current parser state. 154 | bool in_else_block; 155 | 156 | /// true_nodes returns the inner true nodes. 157 | std::vector>& true_nodes() { 158 | return _true_nodes; 159 | } 160 | 161 | /// ctrue_nodes returns a constant reference to the inner true nodes. 162 | const std::vector>& ctrue_nodes() const { 163 | return _true_nodes; 164 | } 165 | 166 | /// false_nodes returns the inner false nodes. 167 | std::vector>& false_nodes() { 168 | return _false_nodes; 169 | } 170 | 171 | /// cfalse_nodes returns a constant reference to the inner false nodes. 172 | const std::vector>& cfalse_nodes() const { 173 | return _false_nodes; 174 | } 175 | 176 | protected: 177 | std::vector> _true_nodes; 178 | std::vector> _false_nodes; 179 | }; 180 | 181 | /// state holds the parser character state machine. 182 | enum class state_c { 183 | content, 184 | opening_brace, 185 | expression, 186 | closing_percent, 187 | }; 188 | 189 | /// part represents either an expression or raw text. 190 | struct part { 191 | std::string content; 192 | bool is_expression; 193 | bool is_complete; 194 | }; 195 | 196 | /// parse creates nodes from an HTML template. 197 | inline std::vector> parse(const std::string& html_template) { 198 | std::vector> nodes; 199 | conditional_node* parent_node = nullptr; 200 | auto current_nodes = std::ref(nodes); 201 | part previous_part{"", false, false}; 202 | std::size_t line_count = 1; 203 | std::size_t character_count = 1; 204 | auto parse_error = [&](const std::string& message) { 205 | throw std::logic_error( 206 | "parse error: " + message + " (line " + std::to_string(line_count) + ":" 207 | + std::to_string(character_count) + ")"); 208 | }; 209 | std::istringstream stream(html_template); 210 | std::string line; 211 | while (std::getline(stream, line)) { 212 | auto state = state_c::content; 213 | std::vector parts; 214 | if (!previous_part.content.empty() && previous_part.is_expression) { 215 | state = state_c::expression; 216 | parts.push_back(previous_part); 217 | previous_part.content.clear(); 218 | } 219 | for (auto character_iterator = line.cbegin(); character_iterator != line.cend(); ++character_iterator) { 220 | switch (state) { 221 | case state_c::content: 222 | if (*character_iterator == '{') { 223 | state = state_c::opening_brace; 224 | } else { 225 | if (parts.empty() || parts.back().is_expression) { 226 | parts.push_back({"", false, false}); 227 | } 228 | parts.back().content.push_back(*character_iterator); 229 | } 230 | break; 231 | case state_c::opening_brace: 232 | if (*character_iterator == '%') { 233 | if (!parts.empty()) { 234 | parts.back().is_complete = true; 235 | } 236 | parts.push_back(part{"", true, false}); 237 | state = state_c::expression; 238 | } else { 239 | if (parts.empty() || parts.back().is_expression) { 240 | parts.push_back({"", false, false}); 241 | } 242 | parts.back().content.push_back('{'); 243 | parts.back().content.push_back(*character_iterator); 244 | state = state_c::content; 245 | } 246 | break; 247 | case state_c::expression: 248 | if (*character_iterator == '%') { 249 | state = state_c::closing_percent; 250 | } else if ( 251 | std::isspace(*character_iterator) || std::isalnum(*character_iterator) 252 | || *character_iterator == '_') { 253 | parts.back().content.push_back(*character_iterator); 254 | } else { 255 | parse_error("expected a word character or a percent sign"); 256 | } 257 | break; 258 | case state_c::closing_percent: 259 | if (*character_iterator == '}') { 260 | parts.back().is_complete = true; 261 | state = state_c::content; 262 | } else { 263 | parse_error("expected a closing brace"); 264 | } 265 | break; 266 | } 267 | ++character_count; 268 | } 269 | switch (state) { 270 | case state_c::content: 271 | if (parts.empty() || parts.back().is_expression) { 272 | parts.push_back({"", false, false}); 273 | } 274 | parts.back().content.push_back('\n'); 275 | break; 276 | case state_c::opening_brace: 277 | if (parts.empty() || parts.back().is_expression) { 278 | parts.push_back({"", false, false}); 279 | } 280 | parts.back().content.push_back('{'); 281 | parts.back().content.push_back('\n'); 282 | break; 283 | case state_c::expression: 284 | parts.back().content.push_back('\n'); 285 | break; 286 | case state_c::closing_percent: 287 | parse_error("expected a closing brace"); 288 | break; 289 | } 290 | auto is_inline = true; 291 | if (parts.size() == 1 && parts[0].is_expression && parts[0].is_complete) { 292 | is_inline = false; 293 | } else if ( 294 | (parts.size() == 2 || parts.size() == 3) && !parts[0].is_expression 295 | && std::all_of( 296 | parts[0].content.begin(), 297 | parts[0].content.end(), 298 | [](unsigned char character) { return std::isspace(character); }) 299 | && parts[1].is_expression && parts[1].is_complete 300 | && (parts.size() == 2 301 | || std::all_of(parts[2].content.begin(), parts[2].content.end(), [](unsigned char character) { 302 | return std::isspace(character); 303 | }))) { 304 | is_inline = false; 305 | parts = std::vector{parts[1]}; 306 | } 307 | if (!previous_part.content.empty()) { 308 | if (parts.front().is_expression) { 309 | current_nodes.get().emplace_back(new text_node(previous_part.content, parent_node)); 310 | } else { 311 | previous_part.content.append(parts.front().content); 312 | parts.front().content.swap(previous_part.content); 313 | } 314 | previous_part.content.clear(); 315 | } 316 | if (!parts.back().is_complete) { 317 | previous_part = parts.back(); 318 | parts.pop_back(); 319 | } 320 | for (auto part : parts) { 321 | if (part.is_expression) { 322 | std::vector words; 323 | { 324 | auto next_word = true; 325 | for (auto character : part.content) { 326 | if (std::isspace(character)) { 327 | next_word = true; 328 | } else { 329 | if (next_word) { 330 | next_word = false; 331 | words.emplace_back(1, character); 332 | } else { 333 | words.back().push_back(character); 334 | } 335 | } 336 | } 337 | } 338 | if (words.empty()) { 339 | parse_error("empty tag"); 340 | } else if (words.size() == 1) { 341 | if (words[0] == "else") { 342 | if (parent_node == nullptr || parent_node->in_else_block) { 343 | parse_error("unexpected 'else' tag"); 344 | } else { 345 | if (!parent_node->is_inline && is_inline) { 346 | parse_error("the associated 'if' tag is not inline"); 347 | } 348 | parent_node->in_else_block = true; 349 | current_nodes = parent_node->false_nodes(); 350 | } 351 | } else if (words[0] == "end") { 352 | if (parent_node == nullptr) { 353 | parse_error("unexpected 'end' tag"); 354 | } else { 355 | for (;;) { 356 | const auto created_from_else_if = parent_node->created_from_else_if; 357 | parent_node = static_cast(parent_node->parent_node); 358 | if (parent_node == nullptr) { 359 | current_nodes = nodes; 360 | break; 361 | } 362 | if (!created_from_else_if) { 363 | if (parent_node->in_else_block) { 364 | current_nodes = parent_node->false_nodes(); 365 | } else { 366 | current_nodes = parent_node->true_nodes(); 367 | } 368 | break; 369 | } 370 | if (!parent_node->is_inline && is_inline) { 371 | parse_error("the associated 'if' tag is not inline"); 372 | } 373 | } 374 | } 375 | } else { 376 | current_nodes.get().emplace_back(new variable_node(words[0], parent_node)); 377 | } 378 | } else if (words.size() == 2) { 379 | if (words[0] == "if") { 380 | current_nodes.get().emplace_back( 381 | new conditional_node(words[1], parent_node, false, is_inline)); 382 | parent_node = static_cast(current_nodes.get().back().get()); 383 | current_nodes = static_cast(parent_node)->true_nodes(); 384 | } else { 385 | parse_error("expected 'if' as first word of a two-words tag"); 386 | } 387 | } else if (words.size() == 3) { 388 | if (words[0] == "else" && words[1] == "if") { 389 | if (parent_node == nullptr || parent_node->in_else_block) { 390 | parse_error("unexpected 'else if' tag"); 391 | } else { 392 | if (!parent_node->is_inline && is_inline) { 393 | parse_error("the associated 'if' tag is not inline"); 394 | } 395 | parent_node->in_else_block = true; 396 | parent_node->false_nodes().emplace_back( 397 | new conditional_node(words[2], parent_node, true, is_inline)); 398 | parent_node = static_cast(parent_node->false_nodes().back().get()); 399 | current_nodes = static_cast(parent_node)->true_nodes(); 400 | } 401 | } else { 402 | parse_error("expected 'else if' as first words of a three-words tag"); 403 | } 404 | } else { 405 | parse_error("too many words in tag"); 406 | } 407 | } else { 408 | current_nodes.get().emplace_back(new text_node(part.content, parent_node)); 409 | } 410 | } 411 | ++line_count; 412 | character_count = 1; 413 | } 414 | if (parent_node != nullptr) { 415 | parse_error("expected 'end' tag"); 416 | } 417 | if (!previous_part.content.empty()) { 418 | if (previous_part.is_expression) { 419 | parse_error("unclosed tag"); 420 | } else { 421 | current_nodes.get().emplace_back(new text_node(previous_part.content, parent_node)); 422 | } 423 | } 424 | return nodes; 425 | }; 426 | 427 | /// render writes HTML from parsed nodes and variables. 428 | inline void render( 429 | std::ostream& output, 430 | const std::vector>& nodes, 431 | const std::unordered_map& name_to_variable, 432 | std::size_t indent = 0) { 433 | for (const auto& generic_node : nodes) { 434 | if (const auto node = dynamic_cast(generic_node.get())) { 435 | if (indent == 0) { 436 | output << node->content; 437 | } else { 438 | std::size_t spaces_to_skip = indent * 4; 439 | for (auto character : node->content) { 440 | if (character == '\n') { 441 | spaces_to_skip = indent * 4; 442 | } else if (std::isspace(character) && spaces_to_skip > 0) { 443 | --spaces_to_skip; 444 | continue; 445 | } else { 446 | spaces_to_skip = 0; 447 | } 448 | output.put(character); 449 | } 450 | } 451 | } else if (const auto node = dynamic_cast(generic_node.get())) { 452 | const auto name_and_variable = name_to_variable.find(node->name); 453 | if (name_and_variable == name_to_variable.end()) { 454 | throw std::logic_error("unassigned variable '" + node->name + "'"); 455 | } 456 | if (name_and_variable->second.is_boolean()) { 457 | throw std::logic_error("a boolean is assigned to the text variable '" + node->name + "'"); 458 | } 459 | output << name_and_variable->second.to_text(); 460 | } else if (const auto node = dynamic_cast(generic_node.get())) { 461 | const auto name_and_variable = name_to_variable.find(node->name); 462 | if (name_and_variable == name_to_variable.end()) { 463 | throw std::logic_error("unassigned variable '" + node->name + "'"); 464 | } 465 | if (!name_and_variable->second.is_boolean()) { 466 | throw std::logic_error("a text is assigned to the boolean variable '" + node->name + "'"); 467 | } 468 | if (name_and_variable->second.to_boolean()) { 469 | render( 470 | output, 471 | node->ctrue_nodes(), 472 | name_to_variable, 473 | indent + (node->created_from_else_if || node->is_inline ? 0 : 1)); 474 | } else { 475 | render( 476 | output, 477 | node->cfalse_nodes(), 478 | name_to_variable, 479 | indent + (node->created_from_else_if || node->is_inline ? 0 : 1)); 480 | } 481 | } 482 | } 483 | } 484 | inline void render( 485 | std::unique_ptr output, 486 | const std::vector>& nodes, 487 | const std::unordered_map& name_to_variable, 488 | std::size_t indent = 0) { 489 | render(*output, nodes, name_to_variable, indent); 490 | } 491 | } 492 | --------------------------------------------------------------------------------