diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e80dcc..704de04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,9 @@ configure_file(version.h.in version.h) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets LinguistTools) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets LinguistTools) +find_package(Glog REQUIRED) + + set(TS_FILES labelmeplus_zh_CN.ts) set(PROJECT_SOURCES main.cpp app.cpp app.h ${TS_FILES}) @@ -72,7 +75,7 @@ else() endif() target_include_directories(labelmeplus PRIVATE "${PROJECT_BINARY_DIR}") -target_link_libraries(labelmeplus PRIVATE Qt${QT_VERSION_MAJOR}::Widgets yaml-cpp::yaml-cpp argparse) +target_link_libraries(labelmeplus PRIVATE Qt${QT_VERSION_MAJOR}::Widgets yaml-cpp::yaml-cpp argparse glog::glog) # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. If # you are developing for iOS or macOS you should consider setting an explicit, diff --git a/config.h b/config.h index 6ee2190..5320321 100644 --- a/config.h +++ b/config.h @@ -1,21 +1,86 @@ #ifndef CONFIG_H #define CONFIG_H +#include #include #include +#include +#include -typedef struct configuration { - QString here = QDir::current().absolutePath(); +static const QString here = QDir::current().absolutePath(); - void get_default_config() { - QString config_file = - QDir::cleanPath(here + QDir::separator() + "config" + - QDir::separator() + "default_config.yaml"); +static void update_dict( + YAML::Node& target_dict, const YAML::Node& new_dict, + std::function validate_item = nullptr) { + for (YAML::const_iterator it = new_dict.begin(); it != new_dict.end(); ++it) { + if (validate_item) { + validate_item(it->first.as(), it->second); + } + if (!target_dict[it->first]) { + LOG(WARNING) << "Skipping unexpected key in config: " + << it->first.as(); + continue; + } + if (target_dict[it->first].IsMap() && it->second.IsMap()) { + YAML::Node item{}; + update_dict(item, it->second, validate_item); + target_dict[it->first.as()] = item; + } else { + target_dict[it->first] = it->second; + } + } +} - YAML::Node config = YAML::Load(config_file.toStdString()); - }; +static YAML::Node get_default_config() { + QString config_file = + QDir::cleanPath(here + QDir::separator() + "config" + QDir::separator() + + "default_config.yaml"); -} configuration; + YAML::Node config = YAML::LoadFile(config_file.toStdString()); + + // save default config to ~/.labelmerc + QString user_config_file = QDir::homePath() + "/.labelmerc"; + if (!QFile::exists(user_config_file)) { + QFile::copy(config_file, user_config_file); + if (!QFile::exists(user_config_file)) { + LOG(WARNING) << "Failed to save config: " + << user_config_file.toStdString(); + } + } + + return config; +}; + +static void validateConfigItem(const std::string& key, + const YAML::Node& value) { + if (key == "validate_label" && value && value.as() != "exact") { + throw std::runtime_error( + "Unexpected value for config key 'validate_label': " + + value.as()); + } + if (key == "shape_color" && value && value.as() != "auto" && + value.as() != "manual") { + throw std::runtime_error("Unexpected value for config key 'shape_color': " + + value.as()); + } + if (key == "labels" && value) { + std::vector labels = value.as>(); + std::set unique_labels(labels.begin(), labels.end()); + if (labels.size() != unique_labels.size()) { + throw std::runtime_error( + "Duplicates are detected for config key 'labels'"); + } + } +} + +static YAML::Node get_config(const YAML::Node& config_file_or_yaml, + const YAML::Node& config_from_args) { + // default config + auto config = get_default_config(); + update_dict(config, config_file_or_yaml, validateConfigItem); + update_dict(config, config_from_args, validateConfigItem); + return config; +}; #endif // CONFIG_H diff --git a/main.cpp b/main.cpp index 89c4a41..703570b 100644 --- a/main.cpp +++ b/main.cpp @@ -1,14 +1,32 @@ +#include +#include + #include +#include #include +#include +#include #include #include +#include #include "app.h" +#include "config.h" #include "version.h" +static std::vector split(const std::string &s, char delimiter) { + std::vector tokens; + std::string token; + std::stringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + return tokens; +} + int main(int argc, char *argv[]) { argparse::ArgumentParser parser("labelme++", "0.0", - argparse::default_arguments::none); + argparse::default_arguments::help); parser.add_argument("--version", "-v") .default_value(false) .implicit_value(true) @@ -18,9 +36,67 @@ int main(int argc, char *argv[]) { .implicit_value(true) .help("reset qt config"); parser.add_argument("--logger-level") - .default_value(std::string{"debug"}) - .choices("debug", "info", "warning", "fatal", "error") + .default_value(std::string{"info"}) + // .choices("info", "warning", "fatal", "error") + // .nargs(1) .help("logger level"); + parser.add_argument("filename") + .help("image or label filename") + .nargs(argparse::nargs_pattern::optional); + parser.add_argument("--output", "-O", "-o") + .help( + "output file or directory (if it ends with .json it is recognized as " + "file, else as directory)"); + + QString home = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + QDir dir(home); + QString default_config_file = dir.filePath(".labelmerc"); + QString config_help_string = + QString("config file or yaml-format string (default: %1)") + .arg(default_config_file); + parser.add_argument("--config") + .default_value(default_config_file.toStdString()) + .help(config_help_string.toStdString()); + + // config for the gui + parser.add_argument("--nodata") + .default_value(true) + .implicit_value(false) + .help("stop storing image data to JSON file"); + + parser.add_argument("--autosave") + .default_value(false) + .implicit_value(true) + .help("auto save"); + + parser.add_argument("--nosortlabels") + .default_value(false) + .implicit_value(true) + .help("stop sorting labels"); + + parser.add_argument("--flags").help( + "comma separated list of flags OR file containing flags"); + + parser.add_argument("--labelflags") + .help( + R"(yaml string of label specific flags OR file containing json string of label specific flags (ex. {person-\d+: [male, tall], dog-\d+: [black, brown, white], .*: [occluded]}))"); + + parser.add_argument("--labels") + .help("comma separated list of labels OR file containing labels"); + + parser.add_argument("--validatelabel") + .default_value(std::string("exact")) + .choices("exact") + .help("label validation types"); + + parser.add_argument("--keep-prev") + .default_value(true) + .implicit_value(false) + .help("keep annotation of previous frame"); + + parser.add_argument("--epsilon") + .scan<'g', float>() + .help("epsilon to find nearest vertex on canvas"); try { parser.parse_args(argc, argv); @@ -39,6 +115,67 @@ int main(int argc, char *argv[]) { return 0; } + std::map log_level_map{{"info", google::GLOG_INFO}, + {"warning", google::WARNING}, + {"fatal", google::FATAL}, + {"error", google::ERROR}}; + std::string logger_level = parser.get("--logger-level"); + FLAGS_minloglevel = log_level_map[logger_level]; + + std::vector flags{}; + if (parser.is_used("--flags")) { + std::string flags_string = parser.get("--flags"); + QFileInfo file_info(QString::fromStdString(flags_string)); + if (file_info.exists() && file_info.isFile()) { + std::ifstream in(flags_string); + std::string line{}; + while (in >> line) { + flags.emplace_back(line); + } + } else { + flags = split(flags_string, ','); + } + } + + std::vector labels{}; + if (parser.is_used("--labels")) { + std::string labels_string = parser.get("--labels"); + QFileInfo file_info(QString::fromStdString(labels_string)); + if (file_info.exists() && file_info.isFile()) { + std::ifstream in(labels_string); + std::string line{}; + while (in >> line) { + labels.emplace_back(line); + } + } else { + labels = split(labels_string, ','); + } + } + + YAML::Node label_flags{}; + if (parser.is_used("--labelflags")) { + std::string label_flags_string = parser.get("--labelflags"); + QFileInfo file_info(QString::fromStdString(label_flags_string)); + if (file_info.exists() && file_info.isFile()) { + label_flags = YAML::LoadFile(label_flags_string); + } else { + label_flags = YAML::Load(label_flags_string); + } + } + + YAML::Node config_yaml{}; + if (parser.is_used("--config")) { + std::string config_yaml_string = parser.get("--config"); + QFileInfo file_info(QString::fromStdString(config_yaml_string)); + if (file_info.exists() && file_info.isFile()) { + config_yaml = YAML::LoadFile(config_yaml_string); + } else { + config_yaml = YAML::Load(config_yaml_string); + } + } + + YAML::Node config = get_config(config_yaml, config_yaml); + QApplication a(argc, argv); QTranslator translator;