diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 93917a2928..912e71d4ad 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -25,17 +25,14 @@ jobs: submodules: true - name: Load llvm keys - if: matrix.compiler.compiler == 'LLVM' run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add - - name: Update clang - if: matrix.compiler.compiler == 'LLVM' uses: myci-actions/add-deb-repo@10 with: repo: deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-16 main repo-name: llvm-toolchain-focal-16 update: false - install: clang-16 - name: Install system dependencies run: | @@ -47,6 +44,9 @@ jobs: qt6-multimedia-dev \ libqt6svg6-dev \ qt6-l10n-tools \ + clang-16 \ + libboost-dev \ + libclang-16-dev \ libopencv-dev \ libdlib-dev \ libexiv2-dev \ @@ -81,4 +81,4 @@ jobs: build-type: Release cc: ${{ matrix.compiler.CC }} cxx: ${{ matrix.compiler.CXX }} - configure-options: -DBUILD_SHARED_LIBS=${{ matrix.shared }} -DLUPDATE=/usr/lib/qt6/bin/lupdate -DLRELEASE:FILEPATH=/usr/lib/qt6/bin/lrelease -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + configure-options: -DBUILD_SHARED_LIBS=${{ matrix.shared }} -DLUPDATE=/usr/lib/qt6/bin/lupdate -DLRELEASE:FILEPATH=/usr/lib/qt6/bin/lrelease -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DClang_DIR=/usr/lib/llvm-16/lib/cmake/clang diff --git a/.github/workflows/macos-build.yml.bak b/.github/workflows/macos-build.yml.bak index 8a7881dfd8..4af2471f78 100644 --- a/.github/workflows/macos-build.yml.bak +++ b/.github/workflows/macos-build.yml.bak @@ -47,7 +47,7 @@ jobs: with: build-dir: ${{ runner.workspace }}/build build-type: Release - configure-options: -DBUILD_TESTING=OFF + configure-options: -DBUILD_TESTING=OFF -DClang_DIR=/usr/local/opt/llvm/lib/cmake/clang cc: /usr/local/opt/llvm/bin/clang cxx: /usr/local/opt/llvm/bin/clang++ env: diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 207436f570..53941f2888 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -24,6 +24,7 @@ jobs: - name: Prepare build dir run: | New-Item -ItemType directory -Path "c:\" -Name "build" + compact.exe /C "c:\build\" New-Item -ItemType Junction -Path "out" -Target "c:\build" Move-Item -Path vcpkg -Destination c:\build New-Item -ItemType Junction -Path "vcpkg" -Target "c:\build\vcpkg" diff --git a/.gitmodules b/.gitmodules index 1ea2dd3c5d..66301d09af 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "src/gui/desktop/utils/animated_webp"] path = src/gui/desktop/utils/animated_webp url = https://github.com/Kicer86/AnimatedWebP.git +[submodule "tools/reflex++"] + path = tools/reflect++ + url = https://github.com/Kicer86/reflexpp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index c131277161..c537999c26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,8 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 20) +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) + #check if git modules are setup if(NOT EXISTS ${PROJECT_SOURCE_DIR}/cmake_modules/FindEasyExif.cmake) message(FATAL_ERROR "Git submodules were not updated. See docs/build.txt for instructions.") @@ -126,6 +128,7 @@ endif(UNIX OR CYGWIN) #subdirs add_subdirectory(src) +add_subdirectory(tools) add_subdirectory(tr) #documentation diff --git a/src/core/cast.hpp b/src/core/cast.hpp new file mode 100644 index 0000000000..7a9dd98d3d --- /dev/null +++ b/src/core/cast.hpp @@ -0,0 +1,30 @@ + +#ifndef DOWN_CAST_HPP +#define DOWN_CAST_HPP + +#include +#include + + +template +T down_cast(R* base) +{ + assert(dynamic_cast(base) != nullptr); + + return static_cast(base); +} + + +template +T safe_cast(const F& from) +{ + static_assert(sizeof(F) <= sizeof(T)); // TODO: handle size conversion when needed + static_assert(std::is_nothrow_convertible_v); + + if constexpr (std::is_signed_v && std::is_unsigned_v) // cast from signed to unsigned + assert(from >= 0); + + return static_cast(from); +} + +#endif diff --git a/src/core/containers_utils.hpp b/src/core/containers_utils.hpp index d05cfa7165..b29db440b9 100644 --- a/src/core/containers_utils.hpp +++ b/src/core/containers_utils.hpp @@ -191,4 +191,11 @@ ForwardIt remove_unique(ForwardIt first, ForwardIt last) return remove_unique(first, last, std::equal_to{}); } + +template +CT range_to(R&& range) +{ + return CT(range.begin(), range.end()); +} + #endif diff --git a/src/core/core_test.cmake b/src/core/core_test.cmake index b33c7302cd..57c9b450f8 100644 --- a/src/core/core_test.cmake +++ b/src/core/core_test.cmake @@ -5,6 +5,15 @@ find_package(GTest REQUIRED CONFIG) find_package(Qt6 REQUIRED COMPONENTS Core Gui) find_package(Qt6Test REQUIRED) +include(${PROJECT_SOURCE_DIR}/tools/reflect++/Reflect++.cmake) + +ReflectFiles( + ReflectionFiles + TARGET + core + SOURCES + unit_tests/json_serializer_tests.hpp +) addTestTarget(core SOURCES @@ -23,6 +32,7 @@ addTestTarget(core unit_tests/data_from_path_extractor_tests.cpp unit_tests/exiftool_video_details_reader_tests.cpp unit_tests/function_wrappers_tests.cpp + unit_tests/json_serializer_tests.cpp unit_tests/lazy_ptr_tests.cpp unit_tests/model_compositor_tests.cpp #unit_tests/oriented_image_tests.cpp @@ -31,6 +41,8 @@ addTestTarget(core unit_tests/status_tests.cpp unit_tests/tag_value_tests.cpp + ${ReflectionFiles} + LIBRARIES GTest::gtest GTest::gmock diff --git a/src/core/down_cast.hpp b/src/core/down_cast.hpp deleted file mode 100644 index 16fd0d68da..0000000000 --- a/src/core/down_cast.hpp +++ /dev/null @@ -1,15 +0,0 @@ - -#ifndef DOWN_CAST_HPP -#define DOWN_CAST_HPP - -#include - -template -T down_cast(R* base) -{ - assert(dynamic_cast(base) != nullptr); - - return static_cast(base); -} - -#endif diff --git a/src/core/function_wrappers.hpp b/src/core/function_wrappers.hpp index 0bd7567425..3c1ae04c76 100644 --- a/src/core/function_wrappers.hpp +++ b/src/core/function_wrappers.hpp @@ -8,8 +8,10 @@ #include #include +#include #include + // Internal struct with data shared between safe_callback and safe_callback_ctrl struct safe_callback_data { @@ -129,13 +131,34 @@ class safe_callback_ctrl final template void invokeMethod(Obj* object, const F& method, Args&&... args) requires std::is_base_of::value { - QMetaObject::invokeMethod(object, [object, method, args...]() + QMetaObject::invokeMethod(object, [object, method, ...args = std::forward(args)]() mutable { - (object->*method)(args...); + (object->*method)(std::forward(args)...); }); } +// Works as extended invokeMethod but waits for results +template +requires std::is_base_of_v +auto invoke_and_wait(QPointer object, const F& function, Args&&... args) +{ + QPromise promise; + QFuture future = promise.future(); + + call_from_object_thread(object, [&promise, &function, &args...]() + { + promise.start(); + promise.addResult(function(args...)); + promise.finish(); + }); + + future.waitForFinished(); + + return future.result(); +} + + // call_from_object_thread uses Qt mechanisms to invoke function in another thread // (thread of 'object' object) template diff --git a/src/core/id.hpp b/src/core/id.hpp index 67170b6eb5..bbfe895164 100644 --- a/src/core/id.hpp +++ b/src/core/id.hpp @@ -121,4 +121,4 @@ class Id bool m_valid = false; }; -#endif // ID_HPP +#endif diff --git a/src/core/implementation/task_executor.cpp b/src/core/implementation/task_executor.cpp index 51336d6f88..7306a44f0c 100644 --- a/src/core/implementation/task_executor.cpp +++ b/src/core/implementation/task_executor.cpp @@ -17,32 +17,102 @@ * */ -#include "task_executor.hpp" #include #include #include -#include +#include #include +#include "task_executor.hpp" #include "thread_utils.hpp" -TaskExecutor::TaskExecutor(ILogger& logger, int threadsToUse): +TaskExecutor::ProcessInfo::ProcessInfo(TaskExecutor& executor, ProcessState s) + : m_state(s) + , m_executor(executor) +{} + + +TaskExecutor::ProcessInfo::~ProcessInfo() +{ + if (m_co_h.done() == false) + m_co_h.destroy(); +} + + +void TaskExecutor::ProcessInfo::setCoroutine(const ProcessCoroutine& h) +{ + m_co_h = h; +} + +void TaskExecutor::ProcessInfo::terminate() +{ + m_work = false; + m_executor.wakeUpScheduler(); +} + + +void TaskExecutor::ProcessInfo::resume() +{ + setState(ITaskExecutor::ProcessState::Running); + m_executor.wakeUpScheduler(); +} + + +ITaskExecutor::ProcessState TaskExecutor::ProcessInfo::state() +{ + return m_state; +} + + +bool TaskExecutor::ProcessInfo::keepWorking() +{ + return m_work; +} + + +void TaskExecutor::ProcessInfo::setState(ProcessState s) +{ + std::lock_guard _(m_stateMtx); + assert(m_state != ProcessState::Finished || s == ProcessState::Finished); // no sense of changing state of finished process + m_state = s; +} + + +void TaskExecutor::ProcessInfo::run() +{ + // lock state and raturn lock to caller, so state is locked until re + std::lock_guard lk(m_stateMtx); + + m_co_h(); + + m_state = m_co_h.promise().nextState; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +TaskExecutor::TaskExecutor(ILogger& logger, unsigned int threadsToUse): m_tasks(), m_taskEater(), m_logger(logger), m_threads(threadsToUse), - m_lightTasks(0), m_working(true) { m_logger.info(QString("Using %1 threads.").arg(m_threads)); - m_taskEater = std::thread( [&] + m_taskEater = std::thread([&] { this->eat(); }); + + m_processRunner = std::thread([&] + { + this->runProcesses(); + }); } @@ -59,30 +129,16 @@ void TaskExecutor::add(std::unique_ptr&& task) } -void TaskExecutor::addLight(std::unique_ptr&& task) +std::shared_ptr TaskExecutor::add(Process&& task) { - assert(m_working); + auto process = std::make_shared(*this, ProcessState::Running); + ITaskExecutor::IProcessSupervisor* supervisor = process.get(); + process->setCoroutine(task(supervisor)); - // start a task and increase count of them - std::lock_guard guard(m_lightTasksMutex); - ++m_lightTasks; + m_processes.push_back(process); + wakeUpScheduler(); - auto light_task = std::thread( [this, lt = std::move(task)] - { - set_thread_name("TE::LightTask"); - - // do job - lt->perform(); - - // notify about finished task - std::unique_lock lock(m_lightTasksMutex); - assert(m_lightTasks > 0); - --m_lightTasks; - - std::notify_all_at_thread_exit(m_lightTaskFinished, std::move(lock)); - }); - - light_task.detach(); + return process; } @@ -98,18 +154,17 @@ void TaskExecutor::stop() { m_working = false; - // wait for heavy tasks + // stop processes + wakeUpScheduler(); + assert(m_processRunner.joinable()); + + // stop heavy tasks m_tasks.stop(); assert(m_taskEater.joinable()); - m_taskEater.join(); - - // wait for light tasks - std::unique_lock lock(m_lightTasksMutex); - m_lightTaskFinished.wait(lock, [this] - { - return m_lightTasks == 0; - }); + // wait for threads + m_processRunner.join(); + m_taskEater.join(); } } @@ -205,3 +260,75 @@ void TaskExecutor::execute(const std::shared_ptr& task) const { task->perform(); } + + +void TaskExecutor::runProcesses() +{ + while(m_working) + { + bool has_running = false; + + std::set toRemove; + + for(auto& process: m_processes) + { + const auto state = process->state(); + + switch(state) + { + case ProcessState::Suspended: + break; + + case ProcessState::Running: + { + const bool toBeStopped = !process->keepWorking(); + process->run(); + + const auto newState = process->state(); + + // if toBeStopped == true then process should have finished + assert(newState == ProcessState::Finished || toBeStopped == false); + + switch(newState) + { + case ProcessState::Running: + has_running = true; + break; + + case ProcessState::Suspended: + break; + + case ProcessState::Finished: + toRemove.insert(process.get()); + break; + } + + break; + } + + case ProcessState::Finished: + assert(!"Should not happend"); + break; + } + } + + if (toRemove.empty() == false) + m_processes.erase(std::remove_if( + m_processes.begin(), + m_processes.end(), + [&toRemove](const auto& process){ return toRemove.contains(process.get()); }), + m_processes.end()); + + if (has_running == false) + { + std::unique_lock lock(m_processesIdleMutex); + m_processesIdleCV.wait(lock); + } + }; +} + + +void TaskExecutor::wakeUpScheduler() +{ + m_processesIdleCV.notify_one(); +} diff --git a/src/core/implementation/task_executor_utils.cpp b/src/core/implementation/task_executor_utils.cpp index 50066c1725..7f9244715b 100644 --- a/src/core/implementation/task_executor_utils.cpp +++ b/src/core/implementation/task_executor_utils.cpp @@ -86,9 +86,12 @@ void TasksQueue::add(std::unique_ptr&& task) } -void TasksQueue::addLight(std::unique_ptr&& task) +std::shared_ptr TasksQueue::add(Process &&) { - m_executor.addLight(std::move(task)); + /// TODO: implement + assert(!"Not implemenbted yet"); + + return nullptr; } diff --git a/src/core/itask_executor.hpp b/src/core/itask_executor.hpp index a9d8363079..b6683f6a1e 100644 --- a/src/core/itask_executor.hpp +++ b/src/core/itask_executor.hpp @@ -20,6 +20,8 @@ #ifndef ITASKEXECUTOR_H #define ITASKEXECUTOR_H +#include +#include #include #include @@ -36,12 +38,61 @@ struct CORE_EXPORT ITaskExecutor virtual void perform() = 0; ///< @brief perform job }; + enum class ProcessState + { + Suspended, + Running, + Finished, + }; + + struct ProcessCoroutine + { + struct promise_type; + using handle_type = std::coroutine_handle; + + struct promise_type + { + ProcessState nextState = ProcessState::Running; + + ProcessCoroutine get_return_object() + { + return ProcessCoroutine(handle_type::from_promise(*this)); + } + std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void return_void() { nextState = ProcessState::Finished; } + void unhandled_exception() {} + std::suspend_always yield_value(ProcessState sr) { nextState = sr; return {}; } + }; + + explicit ProcessCoroutine(handle_type h_): h(h_) {} + + handle_type h = nullptr; + operator std::coroutine_handle() const { return h; } + operator std::coroutine_handle<>() const { return h; } + }; + + struct IProcessSupervisor + { + virtual bool keepWorking() = 0; + virtual void resume() = 0; + }; + + struct IProcessControl + { + virtual void terminate() = 0; + virtual void resume() = 0; + virtual ProcessState state() = 0; + }; + + using Process = std::function; + virtual ~ITaskExecutor() = default; - virtual void add(std::unique_ptr &&) = 0; // add short but heavy task (calculations) - virtual void addLight(std::unique_ptr &&) = 0; // add long but light task (awaiting results from other threads etc) + virtual void add(std::unique_ptr &&) = 0; // add short but heavy task (calculations) + virtual std::shared_ptr add(Process &&) = 0; // add task to be run in a ring with other Processes - virtual int heavyWorkers() const = 0; // return number of heavy task workers + virtual int heavyWorkers() const = 0; // return number of heavy task workers }; -#endif // TASKEXECUTOR_H +#endif diff --git a/src/core/json_serializer.hpp b/src/core/json_serializer.hpp new file mode 100644 index 0000000000..33b5336930 --- /dev/null +++ b/src/core/json_serializer.hpp @@ -0,0 +1,197 @@ + +#ifndef JSON_SERIALIZER_HPP_INCLUDED +#define JSON_SERIALIZER_HPP_INCLUDED + +#include +#include +#include +#include + +#include "generic_concepts.hpp" + + +namespace JSon +{ + template + struct CustomType; + + namespace impl + { + template + concept hasAssignmentOperator = requires(T a, const R b) + { + { a = b }; + }; + + template + concept isQtStreamable = requires(QDataStream s, T a) + { + { s << a }; + { s >> a }; + }; + + template + concept hasCustomSerialization = requires(T a) + { + { JSon::CustomType::serialize(a) } -> std::same_as::type>; + { JSon::CustomType::deserialize({}) } -> std::same_as; + }; + + template + JT convertTo(const T& valueRef) + { + if constexpr (std::is_same_v) + { + assert(valueRef.isArray()); + + QJsonArray array; + if constexpr (std::is_same_v) + array = valueRef.array(); + else + array = valueRef.toArray(); + + return array; + } + else if constexpr (std::is_same_v) + { + assert(valueRef.isObject()); + + QJsonObject object; + if constexpr (std::is_same_v) + object = valueRef.object(); + else + object = valueRef.toObject(); + + return object; + } + else + assert(!"Unexpected type"); + } + + + template + auto getSerialized(const T &); + + template + auto serialize(const T& obj) + { + if constexpr (hasCustomSerialization) + return JSon::CustomType::serialize(obj); + else if constexpr (Container) + { + QJsonArray jsonArr; + + for(const auto& i: obj) + jsonArr.append(getSerialized(i)); + + return jsonArr; + } + else + { + QJsonObject jsonObj; + + reflectpp::get_object_members(obj, [&jsonObj](auto member_info, const auto& member) + { + const QString name = QString::fromStdString(std::string(member_info.name)); + jsonObj[name] = getSerialized(member); + }); + + return jsonObj; + } + } + + template + auto getSerialized(const T& obj) + { + if constexpr (hasAssignmentOperator) + return obj; + else if constexpr (isQtStreamable) + { + QByteArray d; + QDataStream s(&d, QIODeviceBase::WriteOnly); + s << obj; + return d.toBase64().data(); + } + else + return serialize(obj); + } + + template + T getDeserialized(const JT &); + + template + T deserialize(const JT& json) + { + T r; + + if constexpr (hasCustomSerialization) + return JSon::CustomType::deserialize(convertTo::type>(json)); + else if constexpr (Container) + { + const QJsonArray array = convertTo(json); + + using VT = typename T::value_type; + + for(const auto& e: array) + r.push_back(getDeserialized(e)); + } + else + { + const QJsonObject object = convertTo(json); + + reflectpp::set_object_members(r, [&object](const auto member_info) + { + using VT = decltype(member_info)::type; + const QString name = QString::fromStdString(std::string(member_info.name)); + const auto value = object[name]; + + return getDeserialized(value); + }); + } + + return r; + } + + template + T getDeserialized(const JT& value) + { + if (value.isUndefined()) + return {}; + else if constexpr (hasAssignmentOperator) + return value.toVariant().template value(); + else if constexpr (isQtStreamable) + { + QByteArray d = QByteArray::fromBase64(value.toString().toUtf8()); + QDataStream s(&d, QIODeviceBase::ReadOnly); + T obj; + s >> obj; + return obj; + } + else + return deserialize(value); + } + } + + template + QJsonDocument serialize(const T& obj) + { + const auto jsonObj = impl::serialize(obj); + const QJsonDocument doc(jsonObj); + + return doc; + } + + template + T deserialize(const QJsonDocument& doc) + { + return impl::deserialize(doc); + } + + template + T deserialize(const QJsonValue& value) + { + return impl::deserialize(value); + } +} + +#endif diff --git a/src/core/lazy_ptr.hpp b/src/core/lazy_ptr.hpp index 72dd152d6e..7b59a3b4f4 100644 --- a/src/core/lazy_ptr.hpp +++ b/src/core/lazy_ptr.hpp @@ -3,21 +3,38 @@ #define LAZY_PTR_HPP_INCLUDED #include +#include +#include +#include -template + +template class lazy_ptr { public: lazy_ptr() - : m_constructor() { } + template explicit lazy_ptr(const C& constructor) - : m_constructor(constructor) { - + if constexpr (std::is_same_v>) + m_constructor = [constructor]() -> std::unique_ptr + { + return constructor(); + }; + else if constexpr (std::is_pointer_v) + m_constructor = [constructor]() -> std::unique_ptr + { + return std::unique_ptr(constructor()); + }; + else + m_constructor = [constructor]() -> std::unique_ptr + { + return std::make_unique(constructor()); + }; } T* operator->() @@ -31,46 +48,61 @@ class lazy_ptr } private: + using C = std::function()>; std::unique_ptr m_object; C m_constructor; - // used when C::operator() returns raw pointer - struct PtrCreator - { - std::unique_ptr operator()(C& constructor) const - { - return std::unique_ptr(constructor()); - } - }; - - // used when C::operator() returns value - struct CopyCreator - { - std::unique_ptr operator()(C& constructor) const - { - return std::make_unique(constructor()); - } - }; - T* get() { if (m_object.get() == nullptr) - { - typedef typename std::conditional::value, PtrCreator, CopyCreator>::type Creator; - const Creator creator; - - m_object = creator(m_constructor); - } + m_object = m_constructor(); return m_object.get(); } }; +/** + * @brief Construct lazy_ptr which will be initialized with result of C() called on first use. + */ template -lazy_ptr make_lazy_ptr(const C& c) +requires std::invocable +lazy_ptr make_lazy_ptr(const C& c) +{ + return lazy_ptr(c); +} + +namespace lazy_ptr_impl +{ + template + T getQtPropertyValue(QObject* obj, const char* name) + { + const QVariant valueVariant = obj->property(name); + + return valueVariant.value(); + } +} + +/** + * @brief Construct lazy_ptr which will be initialized (on first use) with values of Qt's properties passed under Args... which are expected to be const char * + * + * Use case for this function is a class which's member depends on values of properties marked as REQUIRED. + * As such properties are not set at construction time, but soon after, some members may need to wait for + * them to be set, and therfore need to me constructed later. + */ +template +requires (std::is_same_v && ...) +auto make_lazy_ptr(QObject* obj, Args... args) { - return lazy_ptr(c); + assert(obj != nullptr); + static_assert(sizeof...(Types) == sizeof...(Args), "Number of arguments and their types needs to be equal"); + + auto c = [obj, args...]() -> T + { + return T(lazy_ptr_impl::getQtPropertyValue(obj, args)...); + }; + + return make_lazy_ptr(c); } -#endif // LAZY_PTR_HPP_INCLUDED +#endif diff --git a/src/core/qt_operators.hpp b/src/core/qt_operators.hpp new file mode 100644 index 0000000000..40121fef46 --- /dev/null +++ b/src/core/qt_operators.hpp @@ -0,0 +1,24 @@ + +#ifndef QT_OPERATORS_HPP_INCLUDED +#define QT_OPERATORS_HPP_INCLUDED + +#include +#include +#include + +inline auto operator<=>(const QRect& lhs, const QRect& rhs) +{ + return std::make_tuple(lhs.x(), lhs.y(), lhs.width(), lhs.height()) <=> std::make_tuple(rhs.x(), rhs.y(), rhs.width(), rhs.height()); +} + +inline auto operator<=>(const QString& lhs, const QString& rhs) +{ + if (lhs < rhs) + return std::strong_ordering::less; + else if (lhs > rhs) + return std::strong_ordering::greater; + else + return std::strong_ordering::equal; +} + +#endif diff --git a/src/core/task_executor.hpp b/src/core/task_executor.hpp index 99a20195d2..15bf82414e 100644 --- a/src/core/task_executor.hpp +++ b/src/core/task_executor.hpp @@ -20,6 +20,9 @@ #ifndef TASKEXECUTOR_HPP #define TASKEXECUTOR_HPP +#include +#include +#include #include #include "core_export.h" @@ -31,33 +34,62 @@ struct ILogger; struct CORE_EXPORT TaskExecutor: public ITaskExecutor { - explicit TaskExecutor(ILogger &, int threadsToUse); + explicit TaskExecutor(ILogger &, unsigned int threadsToUse); TaskExecutor(const TaskExecutor &) = delete; virtual ~TaskExecutor(); TaskExecutor& operator=(const TaskExecutor &) = delete; void add(std::unique_ptr &&) override; - void addLight(std::unique_ptr &&) override; + std::shared_ptr add(Process &&) override; int heavyWorkers() const override; void stop(); private: + class ProcessInfo: public IProcessControl, public IProcessSupervisor + { + public: + ProcessInfo(TaskExecutor &, ProcessState); + ~ProcessInfo(); + + void setCoroutine(const ProcessCoroutine& h); + void terminate() override; + void resume() override; + ProcessState state() override; + bool keepWorking() override; + + void setState(ProcessState s); + void run(); + + private: + std::mutex m_stateMtx; + ProcessState m_state = ProcessState::Suspended; + ProcessCoroutine::handle_type m_co_h; + TaskExecutor& m_executor; + bool m_work = true; + }; + + friend class ProcessInfo; + typedef ol::TS_Queue> QueueT; QueueT m_tasks; + std::vector> m_processes; std::thread m_taskEater; - std::mutex m_lightTasksMutex; - std::condition_variable m_lightTaskFinished; + std::thread m_processRunner; + std::mutex m_processesIdleMutex; + std::mutex m_processAlternationMutex; + std::condition_variable m_processesIdleCV; ILogger& m_logger; unsigned int m_threads; - int m_lightTasks; bool m_working; void eat(); void execute(const std::shared_ptr& task) const; + void runProcesses(); + void wakeUpScheduler(); }; -#endif // TASKEXECUTOR_HPP +#endif diff --git a/src/core/task_executor_utils.hpp b/src/core/task_executor_utils.hpp index f94767cad0..a52c848abb 100644 --- a/src/core/task_executor_utils.hpp +++ b/src/core/task_executor_utils.hpp @@ -289,7 +289,6 @@ class Queue }; - /** * @brief A subqueue for ITaskExecutor. */ @@ -301,7 +300,7 @@ class CORE_EXPORT TasksQueue: public ITaskExecutor, public Queue &&) override; - void addLight(std::unique_ptr &&) override; + std::shared_ptr add(Process &&) override; int heavyWorkers() const override; private: diff --git a/src/core/unit_tests/json_serializer_tests.cpp b/src/core/unit_tests/json_serializer_tests.cpp new file mode 100644 index 0000000000..ae113321f3 --- /dev/null +++ b/src/core/unit_tests/json_serializer_tests.cpp @@ -0,0 +1,192 @@ + +#include + +#include "json_serializer_tests_r++.hpp" +#include "json_serializer.hpp" + + +namespace +{ + struct XYZ + { + int a = 7; + int b = 8; + + auto operator<=>(const XYZ &) const = default; + }; + + struct IJK + { + std::vector a; + + auto operator<=>(const IJK &) const = default; + }; +} + +namespace JSon +{ + template<> + struct CustomType + { + using type = QJsonObject; + + static QJsonObject serialize(const XYZ& xyz) + { + QJsonObject json; + json["a"] = xyz.a; + json["b"] = xyz.b; + + return json; + } + + static XYZ deserialize(const QJsonObject& json) + { + XYZ xyz; + + xyz.a = json["a"].toInt(); + xyz.b = json["b"].toInt(); + + return xyz; + } + }; + + template<> + struct CustomType + { + using type = QJsonArray; + + static QJsonArray serialize(const IJK& ijk) + { + QJsonArray array; + std::ranges::copy(ijk.a, std::back_inserter(array)); + + return array; + } + + static IJK deserialize(const QJsonArray& json) + { + IJK ijk; + + std::transform(json.begin(), json.end(), std::back_inserter(ijk.a), [](const auto& value) {return value.toInt();}); + + return ijk; + } + }; +} + + +static_assert(JSon::impl::hasCustomSerialization); + + +TEST(JsonSerializerTest, SerializationDeserialization) +{ + DEF def; + def.abc_vec.emplace_back(11, 15.f, -8., "World", QRect(3, 6, 9, 11)); + def.abc_deque.emplace_back(-6, -765.f, 89., "World", QRect(-1, -50, 9, 11)); + + const QJsonDocument json = JSon::serialize(def); + const DEF def2 = JSon::deserialize(json); + + EXPECT_EQ(def, def2); +} + + +TEST(JsonSerializerTest, CustomSerializationDeserialization) +{ + + XYZ xyz; + xyz.a = 77; + xyz.b = -156; + + const QJsonDocument json = JSon::serialize(xyz); + const XYZ xyz2 = JSon::deserialize(json); + + EXPECT_EQ(xyz, xyz2); +} + + +TEST(JsonSerializerTest, CustomSerializationDeserializationOfArray) +{ + std::vector xyz; + for(int i = 0; i < 5; i++) + xyz.emplace_back(rand(), rand()); + + const QJsonDocument json = JSon::serialize(xyz); + const auto xyz2 = JSon::deserialize>(json); + + EXPECT_EQ(xyz, xyz2); +} + + +TEST(JsonSerializerTest, CustomArraySerializationDeserialization) +{ + IJK ijk; + ijk.a.push_back(1); + ijk.a.push_back(2); + ijk.a.push_back(555); + + const QJsonDocument json = JSon::serialize(ijk); + const auto ijk2 = JSon::deserialize(json); + + EXPECT_EQ(ijk, ijk2); +} + + +TEST(JsonSerializerTest, CustomTypeSerialization) +{ + const XYZ xyz; + const auto json = JSon::serialize(xyz); + + const QJsonDocument expectedJson = QJsonDocument::fromJson( + R"( + { + "a": 7, + "b": 8 + } + )" + ); + + EXPECT_EQ(json, expectedJson); +} + + +TEST(JsonSerializerTest, ArrayOfCustomTypeSerialization) +{ + const std::vector xyz({ {1, 2}, {-5, -9}, {-100, 800} }); + const auto json = JSon::serialize(xyz); + + const QJsonDocument expectedJson = QJsonDocument::fromJson( + R"( + [ + { + "a": 1, "b": 2 + }, + { + "a": -5, "b": -9 + }, + { + "a": -100, "b": 800 + } + ] + )" + ); + + EXPECT_EQ(json, expectedJson); +} + + +TEST(JsonSerializerTest, ArrayOfSimpleTypeSerialization) +{ + const std::vector xyz({-10, -5, 1, 9, -500, 9}); + const auto json = JSon::serialize(xyz); + + const QJsonDocument expectedJson = QJsonDocument::fromJson( + R"( + [ + -10, -5, 1, 9, -500, 9 + ] + )" + ); + + EXPECT_EQ(json, expectedJson); +} diff --git a/src/core/unit_tests/json_serializer_tests.hpp b/src/core/unit_tests/json_serializer_tests.hpp new file mode 100644 index 0000000000..7753fdc3c2 --- /dev/null +++ b/src/core/unit_tests/json_serializer_tests.hpp @@ -0,0 +1,37 @@ + +#pragma once + +#include +#include +#include +#include + + +namespace +{ + struct ABC + { + int a = 5; + float b = 45.f; + double c = 123.; + std::string d = "Hello"; + QRect e = QRect(1, 2, 3, 4); + std::vector f = {}; + + bool operator==(const ABC& other) const + { + return std::tie(a, b, c, d, e, f) == std::tie(other.a, other.b, other.c, other.d, other.e, other.f); + } + }; + + struct DEF + { + std::vector abc_vec; + std::deque abc_deque; + + bool operator==(const DEF& other) const + { + return std::tie(abc_vec, abc_deque) == std::tie(other.abc_vec, other.abc_deque); + } + }; +} diff --git a/src/core/unit_tests/lazy_ptr_tests.cpp b/src/core/unit_tests/lazy_ptr_tests.cpp index f0b4491422..f22ab170b5 100644 --- a/src/core/unit_tests/lazy_ptr_tests.cpp +++ b/src/core/unit_tests/lazy_ptr_tests.cpp @@ -24,7 +24,7 @@ TEST(LazyPtrTest, noConstructionWhenNotNeeded) ConstructorMock constructor; EXPECT_CALL(constructor, build).Times(0); - make_lazy_ptr(std::ref(constructor)); + make_lazy_ptr([&constructor]() {return constructor();} ); } @@ -33,7 +33,7 @@ TEST(LazyPtrTest, onlyOneConstructionWhenNeeded) ConstructorMock constructor; EXPECT_CALL(constructor, build).Times(1).WillOnce(Return(new int)); - lazy_ptr ptr = make_lazy_ptr(std::ref(constructor)); + lazy_ptr ptr = make_lazy_ptr([&constructor]() {return constructor();} ); *ptr = 0; *ptr = 5; @@ -45,8 +45,21 @@ TEST(LazyPtrTest, onlyOneConstructionWhenNeededForComplexType) ConstructorMock> constructor; EXPECT_CALL(constructor, build).Times(1).WillOnce(Return(new std::pair)); - lazy_ptr ptr = make_lazy_ptr>(std::ref(constructor)); + lazy_ptr ptr = make_lazy_ptr>([&constructor]() {return constructor();} ); ptr->first = 0; ptr->second = 5.0; } + + +TEST(LazyPtrTest, qtPropertiesConstructor) +{ + QObject obj; + + using ObjectTakingQStringInContructor = QString; + + auto lazy_ptr = make_lazy_ptr(&obj, "objectName"); + obj.setObjectName("hello"); + + EXPECT_EQ(*lazy_ptr, "hello"); +} diff --git a/src/database/CMakeLists.txt b/src/database/CMakeLists.txt index 64674ab931..3602a07c99 100644 --- a/src/database/CMakeLists.txt +++ b/src/database/CMakeLists.txt @@ -10,6 +10,8 @@ find_package(Threads REQUIRED) find_package(MagicEnum REQUIRED) find_package(OpenCV REQUIRED) +include(${PROJECT_SOURCE_DIR}/tools/reflect++/Reflect++.cmake) + set(DATABASE_SOURCES actions.hpp apeople_information_accessor.hpp @@ -91,6 +93,15 @@ target_include_directories(database set_target_properties(database PROPERTIES AUTOMOC TRUE) +ReflectFiles(ReflectionFiles + TARGET + database + SOURCES + person_data.hpp +) + +target_sources(database PRIVATE ${ReflectionFiles}) + generate_export_header(database) hideSymbols(database) diff --git a/src/database/apeople_information_accessor.hpp b/src/database/apeople_information_accessor.hpp index 39c5511115..6d9dac1793 100644 --- a/src/database/apeople_information_accessor.hpp +++ b/src/database/apeople_information_accessor.hpp @@ -12,6 +12,11 @@ namespace Database { public: PersonInfo::Id store(const PersonInfo& pi) override; + PersonInfo::Id store(const Photo::Id&, const PersonFullInfo& pi) override; + + std::vector listPeopleFull(const Photo::Id &) override; + + using IPeopleInformationAccessor::store; private: virtual void dropPersonInfo(const PersonInfo::Id &) = 0; diff --git a/src/database/aphoto_change_log_operator.hpp b/src/database/aphoto_change_log_operator.hpp index 20bbe0ad3a..2632a18a92 100644 --- a/src/database/aphoto_change_log_operator.hpp +++ b/src/database/aphoto_change_log_operator.hpp @@ -10,7 +10,7 @@ namespace Database class DATABASE_EXPORT APhotoChangeLogOperator: public IPhotoChangeLogOperator { public: - void storeDifference(const Photo::FullDelta &, const Photo::DataDelta &) override; + void storeDifference(const DiffDelta &, const Photo::DataDelta &) override; void groupCreated(const Group::Id &, const Group::Type &, const Photo::Id& representative) override; void groupDeleted(const Group::Id &, const Photo::Id& representative, const std::vector& members) override; diff --git a/src/database/backend_utils.hpp b/src/database/backend_utils.hpp new file mode 100644 index 0000000000..779f003312 --- /dev/null +++ b/src/database/backend_utils.hpp @@ -0,0 +1,27 @@ + +#ifndef BACKEND_UTILS_HPP_INCLUDED +#define BACKEND_UTILS_HPP_INCLUDED + +#include "ibackend.hpp" +#include "iphoto_operator.hpp" + +namespace Database +{ + template + std::vector> getPhotoDelta(IBackend& backend, const Filter& filter = {}) + { + const auto ids = backend.photoOperator().getPhotos(filter); + std::vector> photos; + + std::ranges::transform(ids, std::back_inserter(photos), [&backend](const auto& id) + { + return backend.getPhotoDelta(id); + }); + + return photos; + } + +} + + +#endif diff --git a/src/database/backends/memory_backend/CMakeLists.txt b/src/database/backends/memory_backend/CMakeLists.txt index d137da3995..d2e07391ad 100644 --- a/src/database/backends/memory_backend/CMakeLists.txt +++ b/src/database/backends/memory_backend/CMakeLists.txt @@ -1,25 +1,27 @@ find_package(Qt6 REQUIRED COMPONENTS Core) +find_package(Boost REQUIRED) add_library(database_memory_backend - memory_backend.cpp - memory_backend.hpp + memory_backend.cpp + memory_backend.hpp ) target_include_directories(database_memory_backend - PUBLIC - ${PROJECT_SOURCE_DIR}/src - ${CMAKE_CURRENT_BINARY_DIR} + PUBLIC + ${PROJECT_SOURCE_DIR}/src + ${CMAKE_CURRENT_BINARY_DIR} ) target_link_libraries(database_memory_backend - PUBLIC - Qt::Core + PUBLIC + Qt::Core + Boost::boost - PRIVATE - core - database + PRIVATE + core + database ) generate_export_header(database_memory_backend) diff --git a/src/database/backends/memory_backend/memory_backend.cpp b/src/database/backends/memory_backend/memory_backend.cpp index 87aad5542e..2927107b75 100644 --- a/src/database/backends/memory_backend/memory_backend.cpp +++ b/src/database/backends/memory_backend/memory_backend.cpp @@ -1,117 +1,155 @@ #include +#include +#include +#include #include #include #include "memory_backend.hpp" -#include "database/transaction_wrapper.hpp" +#include "database/general_flags.hpp" #include "database/notifications_accumulator.hpp" #include "database/project_info.hpp" #include "database/photo_utils.hpp" +#include "database/transaction_wrapper.hpp" + + +using namespace boost::multi_index; namespace Database { + + struct pi_id_tag {}; + struct pi_ph_id_tag {}; + + using PeopleContainer = boost::multi_index_container< + PersonInfo, + indexed_by< + ordered_unique, BOOST_MULTI_INDEX_MEMBER(PersonInfo, PersonInfo::Id, id)>, + ordered_non_unique, BOOST_MULTI_INDEX_MEMBER(PersonInfo, Photo::Id, ph_id)> + > + >; + struct MemoryBackend::DB { std::map m_flags; std::map m_groups; - std::set> m_photos; + std::set> m_photos; std::set> m_peopleNames; - std::set> m_peopleInfo; + PeopleContainer m_peopleInfo; std::vector m_logEntries; - std::map, QByteArray> m_blobs; + std::map, QByteArray> m_blobs; int m_nextPhotoId = 0; int m_nextPersonName = 0; int m_nextGroup = 0; int m_nextPersonInfo = 0; }; -} -namespace -{ - template - bool compare(const T& lhs, const T& rhs, Qt::SortOrder order) - { - return order == Qt::AscendingOrder? lhs < rhs: rhs < lhs; - } - - int tristate_compare(const Photo::Data& lhs, const Photo::Data& rhs, const Tag::Types& tagType, Qt::SortOrder order) + namespace { - const auto l_it = lhs.tags.find(tagType); - const auto r_it = rhs.tags.find(tagType); - const auto l_tag = l_it == lhs.tags.end()? TagValue(): l_it->second; - const auto r_tag = r_it == rhs.tags.end()? TagValue(): r_it->second; + template + bool compare(const T& lhs, const T& rhs, Qt::SortOrder order) + { + return order == Qt::AscendingOrder? lhs < rhs: rhs < lhs; + } - const bool is_less = compare(l_tag, r_tag, order); - const bool is_greater = compare(r_tag, l_tag, order); + int tristate_compare(const Photo::Data& lhs, const Photo::Data& rhs, const Tag::Types& tagType, Qt::SortOrder order) + { + const auto l_it = lhs.tags.find(tagType); + const auto r_it = rhs.tags.find(tagType); + const auto l_tag = l_it == lhs.tags.end()? TagValue(): l_it->second; + const auto r_tag = r_it == rhs.tags.end()? TagValue(): r_it->second; - return (is_less? -1: 0) + (is_greater? 1: 0); - } + const bool is_less = compare(l_tag, r_tag, order); + const bool is_greater = compare(r_tag, l_tag, order); - std::vector filterPhotos(const std::vector& photos, - const Database::MemoryBackend::DB& db, - const Database::Filter& dbFilter) - { - std::vector result = photos; + return (is_less? -1: 0) + (is_greater? 1: 0); + } - std::visit([&result, &db](auto&& filter) + std::vector filterPhotos(const std::vector& photos, + const Database::MemoryBackend::DB& db, + const Database::Filter& dbFilter) { - using T = std::decay_t; - if constexpr (std::is_same_v) + std::vector result = photos; + + std::visit([&result, &db](auto&& filter) { - result.erase(std::remove_if(result.begin(), result.end(), [](const Photo::FullDelta& photo) { - return !photo.get().valid(); - }), result.end()); + using T = std::decay_t; + if constexpr (std::is_same_v) + { + result.erase(std::remove_if(result.begin(), result.end(), [](const MemoryBackend::StoregeDelta& photo) { + return !photo.get().valid(); + }), result.end()); - std::sort(result.begin(), result.end(), [](const Photo::FullDelta& lhs, const Photo::FullDelta& rhs) { - return lhs.get() < rhs.get(); - }); + std::sort(result.begin(), result.end(), [](const MemoryBackend::StoregeDelta& lhs, const MemoryBackend::StoregeDelta& rhs) { + return lhs.get() < rhs.get(); + }); - result.erase(remove_unique(result.begin(), result.end(), [](const Photo::FullDelta& lhs, const Photo::FullDelta& rhs) { - return lhs.get() == rhs.get(); - }), result.end()); - } - else if constexpr (std::is_same_v) - { - result.erase(std::remove_if(result.begin(), result.end(), [](const Photo::FullDelta& photo) { - return !photo.get().valid(); - }), result.end()); - } - else if constexpr (std::is_same_v) - { - result.erase(std::remove_if(result.begin(), result.end(), [&filter, &db](const Photo::FullDelta& photo) { + result.erase(remove_unique(result.begin(), result.end(), [](const MemoryBackend::StoregeDelta& lhs, const MemoryBackend::StoregeDelta& rhs) { + return lhs.get() == rhs.get(); + }), result.end()); + } + else if constexpr (std::is_same_v) + { + result.erase(std::remove_if(result.begin(), result.end(), [](const MemoryBackend::StoregeDelta& photo) { + return !photo.get().valid(); + }), result.end()); + } + else if constexpr (std::is_same_v) + { + result.erase(std::remove_if(result.begin(), result.end(), [&filter, &db](const MemoryBackend::StoregeDelta& photo) { - int value = 0; - auto it = db.m_flags.find(photo.getId()); + int value = 0; + auto it = db.m_flags.find(photo.getId()); - if (it != db.m_flags.end()) // if no flags for given photo, continue with value == 0 - { - const auto& flagsMap = it->second; - auto itm = flagsMap.find(filter.name); + if (it != db.m_flags.end()) // if no flags for given photo, continue with value == 0 + { + const auto& flagsMap = it->second; + auto itm = flagsMap.find(filter.name); - value = itm == flagsMap.end()? 0: itm->second; // if no flag value for given flag, continue with value == 0 - } + value = itm == flagsMap.end()? 0: itm->second; // if no flag value for given flag, continue with value == 0 + } - return filter.mode == Database::FilterPhotosWithGeneralFlag::Mode::Exact? - value != filter.value: - (value & filter.value) != filter.value; + return filter.mode == Database::FilterPhotosWithGeneralFlag::Mode::Exact? + value != filter.value: + (value & filter.value) != filter.value; - }), result.end()); - } + }), result.end()); + } + else if constexpr (std::is_same_v) + { + result.erase(std::remove_if(result.begin(), result.end(), [&filter, &db](const MemoryBackend::StoregeDelta& photo) { + bool performed = false; - }, dbFilter); + const auto ph_id = photo.getId(); + const auto it = db.m_flags.find(ph_id); - return result; - } -} + // 'analyzed' flag set? + if (it != db.m_flags.end()) + { + const auto f_it = it->second.find(CommonGeneralFlags::FacesAnalysisState); + if (f_it != it->second.end() && static_cast(f_it->second) == CommonGeneralFlags::FacesAnalysisType::AnalysedAndNotFound) + performed = true; + } + + // any people? TODO: use contains() when boost 1.78 is available on github actions + const auto& c = get(db.m_peopleInfo); + const auto c_it = c.find(ph_id); + performed = c_it != c.end(); + + return (filter.status == Database::FilterFaceAnalysisStatus::Performed && !performed) || + (filter.status == Database::FilterFaceAnalysisStatus::NotPerformed && performed); + }), result.end()); + } + + }, dbFilter); + + return result; + } -namespace Database -{ - namespace - { class Transaction { public: @@ -179,9 +217,24 @@ namespace Database const Photo::Id id(m_db->m_nextPhotoId); delta.setId(id); - auto [it, i] = m_db->m_photos.insert(Photo::FullDelta(delta)); + auto [it, i] = m_db->m_photos.insert(StoregeDelta(delta)); assert(i == true); + if (delta.has(Photo::Field::People)) + { + const auto& people = delta.get(); + auto& accessor = peopleInformationAccessor(); + + for(const auto& person: people) + { + const auto p_id = accessor.store(person.name); + const auto f_id = accessor.store(person.fingerprint); + + const PersonInfo pf(p_id, delta.getId(), f_id, person.position); + accessor.store(pf); + } + } + ids.push_back(id); m_db->m_nextPhotoId++; @@ -200,13 +253,21 @@ namespace Database for (const auto& delta: deltas) { - auto it = m_db->m_photos.find(delta.getId()); - Photo::FullDelta currentDelta(*it); + auto currentDelta = IBackend::getPhotoDelta< + Photo::Field::Tags, + Photo::Field::Flags, + Photo::Field::Path, + Photo::Field::Geometry, + Photo::Field::GroupInfo, + Photo::Field::PHash, + Photo::Field::People + >(delta.getId()); photoChangeLogOperator().storeDifference(currentDelta, delta); currentDelta |= delta; + auto it = m_db->m_photos.find(delta.getId()); it = m_db->m_photos.erase(it); m_db->m_photos.insert(it, currentDelta); @@ -289,6 +350,13 @@ namespace Database if (fields.contains(Photo::Field::PHash)) delta.insert(data.phash); + if (fields.contains(Photo::Field::People)) + { + auto& peopleAccessor = peopleInformationAccessor(); + const auto peopleData = peopleAccessor.listPeopleFull(data.id); + delta.insert(peopleData); + } + return delta; } @@ -345,13 +413,13 @@ namespace Database } - void MemoryBackend::writeBlob(const Photo::Id& id, BlobType bt, const QByteArray& blob) + void MemoryBackend::writeBlob(const Photo::Id& id, const QString& bt, const QByteArray& blob) { m_db->m_blobs[{id, bt}] = blob; } - QByteArray MemoryBackend::readBlob(const Photo::Id& id, BlobType bt) + QByteArray MemoryBackend::readBlob(const Photo::Id& id, const QString& bt) { return m_db->m_blobs[{id, bt}]; } @@ -582,7 +650,7 @@ namespace Database std::vector photosToClear; - for (const Photo::FullDelta& delta: m_db->m_photos) + for (const StoregeDelta& delta: m_db->m_photos) { const auto& groupInfo = delta.get(); @@ -626,15 +694,15 @@ namespace Database std::vector MemoryBackend::membersOf(const Group::Id& id) const { - std::vector members; - std::copy_if(m_db->m_photos.begin(), m_db->m_photos.end(), std::back_inserter(members), [id](const Photo::FullDelta& data) + std::vector members; + std::copy_if(m_db->m_photos.begin(), m_db->m_photos.end(), std::back_inserter(members), [id](const StoregeDelta& data) { const auto& groupInfo = data.get(); return groupInfo.group_id == id && groupInfo.role == GroupInfo::Role::Member; }); std::vector ids; - std::transform(members.begin(), members.end(), std::back_inserter(ids), [](const Photo::FullDelta& d){ return d.getId(); }); + std::transform(members.begin(), members.end(), std::back_inserter(ids), [](const StoregeDelta& d){ return d.getId(); }); return ids; } @@ -642,14 +710,14 @@ namespace Database std::vector MemoryBackend::listGroups() const { - std::vector representatives; - std::copy_if(m_db->m_photos.begin(), m_db->m_photos.end(), std::back_inserter(representatives), [](const Photo::FullDelta& data) + std::vector representatives; + std::copy_if(m_db->m_photos.begin(), m_db->m_photos.end(), std::back_inserter(representatives), [](const StoregeDelta& data) { return data.get().role == GroupInfo::Role::Representative; }); std::vector infos; - std::transform(representatives.begin(), representatives.end(), std::back_inserter(infos), [](const Photo::FullDelta& d){ return d.get(); }); + std::transform(representatives.begin(), representatives.end(), std::back_inserter(infos), [](const StoregeDelta& d){ return d.get(); }); std::vector ids; std::transform(infos.begin(), infos.end(), std::back_inserter(ids), &extract); @@ -691,7 +759,7 @@ namespace Database std::vector MemoryBackend::getPhotos(const Filter& filter) { - std::vector data(m_db->m_photos.begin(), m_db->m_photos.end()); + std::vector data(m_db->m_photos.begin(), m_db->m_photos.end()); data = filterPhotos(data, *m_db, filter); std::vector ids; @@ -737,7 +805,7 @@ namespace Database } - Photo::Id MemoryBackend::getIdFor(const Photo::FullDelta& d) + Photo::Id MemoryBackend::getIdFor(const StoregeDelta& d) { return d.getId(); } diff --git a/src/database/backends/memory_backend/memory_backend.hpp b/src/database/backends/memory_backend/memory_backend.hpp index 469612e3f7..bbd76424e4 100644 --- a/src/database/backends/memory_backend/memory_backend.hpp +++ b/src/database/backends/memory_backend/memory_backend.hpp @@ -26,6 +26,15 @@ namespace Database IPhotoOperator { public: + using StoregeDelta = Photo::ExplicitDelta< + Photo::Field::Tags, + Photo::Field::Flags, + Photo::Field::Path, + Photo::Field::Geometry, + Photo::Field::GroupInfo, + Photo::Field::PHash + >; + MemoryBackend(); ~MemoryBackend(); @@ -40,8 +49,8 @@ namespace Database std::optional get(const Photo::Id& id, const QString& name) override; void setBits(const Photo::Id& id, const QString& name, int bits) override final; void clearBits(const Photo::Id& id, const QString& name, int bits) override final; - void writeBlob(const Photo::Id &, BlobType, const QByteArray &) override; - QByteArray readBlob(const Photo::Id &, BlobType) override; + void writeBlob(const Photo::Id &, const QString& bt, const QByteArray &) override; + QByteArray readBlob(const Photo::Id &, const QString& bt) override; std::vector markStagedAsReviewed() override; BackendStatus init(const ProjectInfo &) override; void closeConnections() override; @@ -86,7 +95,7 @@ namespace Database bool hasPHash(const Photo::Id &) override; // - static Photo::Id getIdFor(const Photo::FullDelta& d); + static Photo::Id getIdFor(const StoregeDelta& d); static Person::Id getIdFor(const PersonName& pn); static PersonInfo::Id getIdFor(const PersonInfo& pn); diff --git a/src/database/backends/sql_backends/people_information_accessor.cpp b/src/database/backends/sql_backends/people_information_accessor.cpp index 8fb4ea1205..1dcfc4a53d 100644 --- a/src/database/backends/sql_backends/people_information_accessor.cpp +++ b/src/database/backends/sql_backends/people_information_accessor.cpp @@ -241,7 +241,7 @@ namespace Database if (query.numRowsAffected() == 0) // any update? id = Person::Id(); // nope - error } - else // id invalid? add new person or nothing when already exists + else if (d.name().isEmpty() == false) // id invalid? add new person (if name is set) or nothing when already exists { const PersonName pn = person(d.name()); @@ -280,7 +280,7 @@ namespace Database { if (fingerprint.fingerprint().empty()) { - const QString delete_query = QString ("DELETE from %1 WHERE id = %2") + const QString delete_query = QString("DELETE from %1 WHERE id = %2") .arg(TAB_FACES_FINGERPRINTS) .arg(fid.value()); @@ -300,7 +300,7 @@ namespace Database m_executor.exec(query); } } - else + else if (fingerprint_list.isEmpty() == false) { InsertQueryData insertData(TAB_FACES_FINGERPRINTS); insertData.addColumn("fingerprint"); diff --git a/src/database/backends/sql_backends/sql_backend.cpp b/src/database/backends/sql_backends/sql_backend.cpp index 203ea70c38..65dd2ec49b 100644 --- a/src/database/backends/sql_backends/sql_backend.cpp +++ b/src/database/backends/sql_backends/sql_backend.cpp @@ -427,6 +427,13 @@ namespace Database if (phash.has_value()) photoData.insert(*phash); } + + if (fields.contains(Photo::Field::People)) + { + auto& peopleAccessor = peopleInformationAccessor(); + const auto people = peopleAccessor.listPeopleFull(id); + photoData.insert(people); + } } return photoData; @@ -512,24 +519,24 @@ namespace Database } - void ASqlBackend::writeBlob(const Photo::Id& id, BlobType bt, const QByteArray& blob) + void ASqlBackend::writeBlob(const Photo::Id& id, const QString& bt, const QByteArray& blob) { UpdateQueryData data(TAB_BLOBS); data.addCondition("photo_id", QString::number(id.value())); - data.addCondition("type", QString::number(static_cast(bt))); + data.addCondition("type", bt); data.setColumns("photo_id", "type", "data"); - data.setValues(QString::number(id.value()), QString::number(static_cast(bt)), blob); + data.setValues(QString::number(id.value()), bt, blob); updateOrInsert(data); } - QByteArray ASqlBackend::readBlob(const Photo::Id& id, BlobType bt) + QByteArray ASqlBackend::readBlob(const Photo::Id& id, const QString& bt) { - const QString blobQuery = QString("SELECT data FROM %1 WHERE photo_id=%2 AND type=%3") + const QString blobQuery = QString("SELECT data FROM %1 WHERE photo_id=%2 AND type='%3'") .arg(TAB_BLOBS) .arg(QString::number(id.value())) - .arg(QString::number(static_cast(bt))); + .arg(bt); QSqlDatabase db = QSqlDatabase::database(m_connectionName); QSqlQuery query(db); @@ -743,7 +750,7 @@ namespace Database // move thumbnails to blob table const QString copy_thumbnails = QString("INSERT INTO blobs(photo_id, type, data) SELECT photo_id, %1 AS type, data FROM thumbnails") - .arg(static_cast(IBackend::BlobType::Thumbnail)); + .arg("thumbnail"); status = m_executor.exec(copy_thumbnails, &query); if (status == false) @@ -970,6 +977,15 @@ namespace Database if (status && data.has(Photo::Field::PHash)) photoOperator().setPHash(data.getId(), data.get()); + if (status && data.has(Photo::Field::People)) + { + const auto& people = data.get(); + auto& accessor = peopleInformationAccessor(); + + for(const PersonFullInfo& person: people) + accessor.store(data.getId(), person); + } + photoChangeLogOperator().storeDifference(currentStateOfPhoto, data); return status; diff --git a/src/database/backends/sql_backends/sql_backend.hpp b/src/database/backends/sql_backends/sql_backend.hpp index 74a2b1e31f..75a4b34b2f 100644 --- a/src/database/backends/sql_backends/sql_backend.hpp +++ b/src/database/backends/sql_backends/sql_backend.hpp @@ -130,7 +130,7 @@ namespace Database std::unique_ptr m_groupOperator; std::unique_ptr m_photoOperator; std::unique_ptr m_photoChangeLogOperator; - lazy_ptr> m_peopleInfoAccessor; + lazy_ptr m_peopleInfoAccessor; NotificationsAccumulator m_notificationsAccumulator; mutable TransactionManager m_tr_db; QString m_connectionName; @@ -154,8 +154,8 @@ namespace Database void setBits(const Photo::Id& id, const QString& name, int bits) override final; void clearBits(const Photo::Id& id, const QString& name, int bits) override final; - void writeBlob(const Photo::Id &, BlobType, const QByteArray& blob) override; - QByteArray readBlob(const Photo::Id &, BlobType) override; + void writeBlob(const Photo::Id &, const QString& bt, const QByteArray& blob) override; + QByteArray readBlob(const Photo::Id &, const QString& bt) override; std::vector markStagedAsReviewed() override final; // diff --git a/src/database/backends/sql_backends/sql_filter_query_generator.cpp b/src/database/backends/sql_backends/sql_filter_query_generator.cpp index 9aac0cca29..41445c7076 100644 --- a/src/database/backends/sql_backends/sql_filter_query_generator.cpp +++ b/src/database/backends/sql_backends/sql_filter_query_generator.cpp @@ -21,6 +21,7 @@ #include +#include #include "tables.hpp" @@ -381,4 +382,39 @@ namespace Database return finalQuery; } + + QString SqlFilterQueryGenerator::visit(const FilterFaceAnalysisStatus& analysisStatus) const + { + QString query; + switch (analysisStatus.status) + { + // find all photos which were analysed and no faces were found + all photos with found faces. + // See comment for FilterFaceAnalysisStatus flag + case FilterFaceAnalysisStatus::Performed: + query = QString("SELECT DISTINCT %1.id FROM %1 " + "LEFT JOIN %2 ON (%2.photo_id = %1.id AND %2.name = \"%4\") " + "LEFT JOIN %3 ON %3.photo_id = %1.id " + "WHERE COALESCE(%2.value, 0) = %5 OR %3.id IS NOT NULL") + .arg(TAB_PHOTOS) + .arg(TAB_GENERAL_FLAGS) + .arg(TAB_PEOPLE) + .arg(CommonGeneralFlags::FacesAnalysisState) + .arg(static_cast(CommonGeneralFlags::FacesAnalysisType::AnalysedAndNotFound)); + break; + + case FilterFaceAnalysisStatus::NotPerformed: + query = QString("SELECT DISTINCT %1.id FROM %1 " + "LEFT JOIN %2 ON (%2.photo_id = %1.id AND %2.name = \"%4\") " + "LEFT JOIN %3 ON %3.photo_id = %1.id " + "WHERE COALESCE(%2.value, 0) = %5 AND %3.id IS NULL") + .arg(TAB_PHOTOS) + .arg(TAB_GENERAL_FLAGS) + .arg(TAB_PEOPLE) + .arg(CommonGeneralFlags::FacesAnalysisState) + .arg(static_cast(CommonGeneralFlags::FacesAnalysisType::NotAnalysedOrAnalysedAndFound)); + break; + } + + return query; + } } diff --git a/src/database/backends/sql_backends/sql_filter_query_generator.hpp b/src/database/backends/sql_backends/sql_filter_query_generator.hpp index 1608666945..e2d9be6a20 100644 --- a/src/database/backends/sql_backends/sql_filter_query_generator.hpp +++ b/src/database/backends/sql_backends/sql_filter_query_generator.hpp @@ -55,6 +55,7 @@ namespace Database QString visit(const FilterPhotosWithGeneralFlag& genericFlagsFilter) const; QString visit(const FilterPhotosWithPHash& withPhashFilter) const; QString visit(const FilterSimilarPhotos& similarPhotosFilter) const; + QString visit(const FilterFaceAnalysisStatus &) const; }; } diff --git a/src/database/database_tools/implementation/json_to_backend.cpp b/src/database/database_tools/implementation/json_to_backend.cpp index 548f205126..8c89e63266 100644 --- a/src/database/database_tools/implementation/json_to_backend.cpp +++ b/src/database/database_tools/implementation/json_to_backend.cpp @@ -1,4 +1,5 @@ +#include #include #include #include @@ -6,6 +7,10 @@ #include #include +#include "person_data_r++.hpp" + +#include +#include #include "../json_to_backend.hpp" #include "database/ibackend.hpp" #include "database/igroup_operator.hpp" @@ -102,6 +107,20 @@ namespace Database const Photo::PHashT phash(it.value().toString().toULongLong(&ok, 16)); delta.insert(phash); } + else if (it.key() == "people") + { + std::vector people; + const auto array = it.value().toArray(); + + std::ranges::transform(array, std::back_inserter(people), [](const QJsonValue& value) + { + const auto personData = JSon::deserialize(value); + const PersonFullInfo personFullInfo(personData); + return personFullInfo; + }); + + delta.insert(people); + } else if (it.key() == "id") id = it.value().toString(); else diff --git a/src/database/explicit_photo_delta.hpp b/src/database/explicit_photo_delta.hpp index 28fc2c784c..12be1014bb 100644 --- a/src/database/explicit_photo_delta.hpp +++ b/src/database/explicit_photo_delta.hpp @@ -42,7 +42,7 @@ namespace Photo } template - explicit ExplicitDelta(const ExplicitDelta& other) noexcept + ExplicitDelta(const ExplicitDelta& other) noexcept { static_assert( (... && ExplicitDelta::template has()), "Other object needs to be superset of this"); @@ -77,7 +77,7 @@ namespace Photo { for(const Photo::Field field : magic_enum::enum_values()) if (other.has(field) && has(field) == false) - throw std::invalid_argument(std::string("Photo::Field: ") + magic_enum::enum_name(field).data() + " from DataDelta is part of this ExplicitDelta."); + throw std::invalid_argument(std::string("Photo::Field: ") + magic_enum::enum_name(field).data() + " from DataDelta is not part of this ExplicitDelta."); fill(); m_data |= other; @@ -103,6 +103,14 @@ namespace Photo return m_data.get(); } + template + typename DeltaTypes::Storage& get() + { + static_assert(has(), "ExplicitDelta has no required Photo::Field"); + + return m_data.get(); + } + template void insert(const typename DeltaTypes::Storage& d) { @@ -160,14 +168,14 @@ namespace Photo // based on: https://stackoverflow.com/questions/60434033/how-do-i-expand-a-compile-time-stdarray-into-a-parameter-pack namespace details { - template ())> struct Generator; + template())> struct Generator; - template + template struct Generator> { using type = ExplicitDelta; }; - template + template using Generator_t = typename Generator::type; } diff --git a/src/database/filter.hpp b/src/database/filter.hpp index 08efc7ae48..cdd1ff69cb 100644 --- a/src/database/filter.hpp +++ b/src/database/filter.hpp @@ -35,38 +35,6 @@ namespace Database { - //filters - - struct EmptyFilter; - struct GroupFilter; - struct FilterPhotosWithTag; - struct FilterPhotosWithFlags; - struct FilterNotMatchingFilter; - struct FilterPhotosWithId; - struct FilterPhotosMatchingExpression; - struct FilterPhotosWithPath; - struct FilterPhotosWithRole; - struct FilterPhotosWithPerson; - struct FilterPhotosWithGeneralFlag; - struct FilterPhotosWithPHash; - struct FilterSimilarPhotos; - - - typedef std::variant Filter; - enum class ComparisonOp { Equal, @@ -82,18 +50,14 @@ namespace Database Or, }; - struct DATABASE_EXPORT EmptyFilter - { + //filters - }; + struct GroupFilter; + struct FilterNotMatchingFilter; - struct DATABASE_EXPORT GroupFilter + struct DATABASE_EXPORT EmptyFilter { - GroupFilter(const std::vector &); - GroupFilter(const std::initializer_list &); - std::vector filters; - LogicalOp mode = LogicalOp::And; }; struct DATABASE_EXPORT FilterPhotosWithTag @@ -179,6 +143,44 @@ namespace Database struct DATABASE_EXPORT FilterSimilarPhotos { }; + struct DATABASE_EXPORT FilterFaceAnalysisStatus + { + enum Status + { + NotPerformed, + Performed, + } status; + + FilterFaceAnalysisStatus(Status s): status(s) {}; + }; + + + typedef std::variant Filter; + + + struct DATABASE_EXPORT GroupFilter + { + GroupFilter(const std::vector &); + GroupFilter(const std::initializer_list &); + + std::vector filters; + LogicalOp mode = LogicalOp::And; + }; + struct DATABASE_EXPORT FilterNotMatchingFilter { template @@ -190,12 +192,13 @@ namespace Database ol::data_ptr filter; }; + // helpers /** * @brief return filter which will filter out broken photos (missing, broken, deleted etc) */ - Filter getValidPhotosFilter(); + Filter DATABASE_EXPORT getValidPhotosFilter(); } #endif // FILTER_HPP diff --git a/src/database/general_flags.hpp b/src/database/general_flags.hpp index 8f39952b9d..570ec88d8b 100644 --- a/src/database/general_flags.hpp +++ b/src/database/general_flags.hpp @@ -4,6 +4,9 @@ #include +// Various flags for photos. +// Use 0 as most common/basic state, +// so all photos without flag are considered 'normal/basic/unchecked'. namespace Database::CommonGeneralFlags { const QString State("state"); // photo state. @@ -23,6 +26,20 @@ namespace Database::CommonGeneralFlags Normal = 0, // 0 (or nonexistent entry) - photo is in fine state Incompatible = 1, // 1 - could not generate phash. Not an image file. }; + + const QString FacesAnalysisState("faces_analysis_state"); + enum class FacesAnalysisType + { + // We could have use 3 states here - 0 for not analyzed, 1 for analyzed and 2 for no faces + // However that would cause many entries in db as each photo would eventualy went to state 1 or 2. + // So a bit tricky solution is used here: + // 0 (or nonexistent entry) means photo was not scaned for faces, + // or faces were found (and database will return faces for this photo in such case) + // 1 - no faces found. + // that should allow to distinguish between all 3 states with less db usage. + NotAnalysedOrAnalysedAndFound = 0, + AnalysedAndNotFound = 1, + }; } -#endif // GENERAL_FLAGS_HPP_INCLUDED +#endif diff --git a/src/database/group.hpp b/src/database/group.hpp index 53498197ac..8be832d204 100644 --- a/src/database/group.hpp +++ b/src/database/group.hpp @@ -2,8 +2,6 @@ #ifndef GROUP_HPP #define GROUP_HPP -#include - #include #include "database_export.h" diff --git a/src/database/ibackend.hpp b/src/database/ibackend.hpp index cb5fde23e5..eed6442349 100644 --- a/src/database/ibackend.hpp +++ b/src/database/ibackend.hpp @@ -90,11 +90,6 @@ namespace Database */ struct DATABASE_EXPORT IBackend: public QObject { - enum class BlobType - { - Thumbnail = 0, - }; - virtual ~IBackend() = default; /** \brief Add photos to database @@ -122,8 +117,8 @@ namespace Database const Filter &) = 0; /// get particular photo - [[deprecated("Use getPhotoDelta template ")]] virtual Photo::Data getPhoto(const Photo::Id &) = 0; - [[deprecated("Use getPhotoDelta template ")]] virtual Photo::DataDelta getPhotoDelta(const Photo::Id &, const std::set & = {}) = 0; + [[deprecated("Use getPhotoDelta template ")]] virtual Photo::Data getPhoto(const Photo::Id &) = 0; + [[deprecated("Use getPhotoDelta template ")]] virtual Photo::DataDelta getPhotoDelta(const Photo::Id &, const std::set & = {}) = 0; template Photo::ExplicitDelta getPhotoDelta(const Photo::Id& id) @@ -192,7 +187,7 @@ namespace Database * @arg bt blob type * @arg blob raw data */ - virtual void writeBlob(const Photo::Id& id, BlobType bt, const QByteArray& blob) = 0; + virtual void writeBlob(const Photo::Id& id, const QString& bt, const QByteArray& blob) = 0; /** * @brief Read blob of type @p bt associated with photo @p id @@ -200,7 +195,7 @@ namespace Database * @arg bt blob type * @return raw data */ - virtual QByteArray readBlob(const Photo::Id& id, BlobType bt) = 0; + virtual QByteArray readBlob(const Photo::Id& id, const QString& bt) = 0; /** * \brief mark all staged photos as reviewed. diff --git a/src/database/implementation/apeople_information_accessor.cpp b/src/database/implementation/apeople_information_accessor.cpp index 891919ab5b..2b9d90777c 100644 --- a/src/database/implementation/apeople_information_accessor.cpp +++ b/src/database/implementation/apeople_information_accessor.cpp @@ -23,15 +23,14 @@ namespace Database for(const auto& person: existing_people) { - if (fd.rect.isValid() && - person.rect == fd.rect) // same, valid rect + if (fd.rect.isValid() && person.rect == fd.rect) // same, valid rect { to_store.id = person.id; break; } else if (person.p_id.valid() && person.p_id == fd.p_id && - person.rect.isValid() == false) // same, valid person but no rect in db + person.rect.isValid() == false) // same, valid person but no rect in db { to_store.id = person.id; break; @@ -47,4 +46,46 @@ namespace Database return result; } + + PersonInfo::Id APeopleInformationAccessor::store(const Photo::Id& ph_id, const PersonFullInfo& pi) + { + const auto p_id = store(pi.name); + const auto f_id = store(pi.fingerprint); + + const PersonInfo pf(pi.pi_id, p_id, ph_id, f_id, pi.position); + return store(pf); + } + + std::vector Database::APeopleInformationAccessor::listPeopleFull(const Photo::Id& id) + { + const auto simpleList = listPeople(id); + + std::vector pi_ids; + std::ranges::transform(simpleList, std::back_inserter(pi_ids), [](const PersonInfo& pi) + { + return pi.id; + }); + + const auto fingerprints = fingerprintsFor(pi_ids); + + std::vector fullInfo; + std::ranges::transform(simpleList, std::back_inserter(fullInfo), [this, &fingerprints](const PersonInfo& pi) + { + PersonFullInfo pfi; + pfi.pi_id = pi.id; + pfi.position = pi.rect; + + auto fi_it = fingerprints.find(pi.id); + if (fi_it != fingerprints.end()) + pfi.fingerprint = fingerprints.find(pi.id)->second; + + if (pi.p_id.valid()) + pfi.name = person(pi.p_id); + + return pfi; + }); + + return fullInfo; + } + } diff --git a/src/database/implementation/aphoto_change_log_operator.cpp b/src/database/implementation/aphoto_change_log_operator.cpp index c8549edc6c..e22ff20355 100644 --- a/src/database/implementation/aphoto_change_log_operator.cpp +++ b/src/database/implementation/aphoto_change_log_operator.cpp @@ -77,7 +77,7 @@ namespace namespace Database { - void APhotoChangeLogOperator::storeDifference(const Photo::FullDelta& currentContent, const Photo::DataDelta& newContent) + void APhotoChangeLogOperator::storeDifference(const DiffDelta& currentContent, const Photo::DataDelta& newContent) { assert(currentContent.getId() == newContent.getId()); const Photo::Id& id = currentContent.getId(); diff --git a/src/database/implementation/async_database.cpp b/src/database/implementation/async_database.cpp index 8364aeccfb..93bbedd99d 100644 --- a/src/database/implementation/async_database.cpp +++ b/src/database/implementation/async_database.cpp @@ -23,7 +23,6 @@ #include #include -#include #include #include #include diff --git a/src/database/implementation/async_database.hpp b/src/database/implementation/async_database.hpp index ce82ebd2bb..6a3ffb44c9 100644 --- a/src/database/implementation/async_database.hpp +++ b/src/database/implementation/async_database.hpp @@ -72,7 +72,7 @@ namespace Database AsyncDatabase& m_db; }; - friend struct Client; + friend class Client; std::set m_clients; std::mutex m_clientsMutex; @@ -93,4 +93,4 @@ namespace Database } -#endif // DATABASETHREAD_HPP +#endif diff --git a/src/database/implementation/person_data.cpp b/src/database/implementation/person_data.cpp index 25516119e4..256214dc1a 100644 --- a/src/database/implementation/person_data.cpp +++ b/src/database/implementation/person_data.cpp @@ -41,14 +41,6 @@ PersonName::PersonName(const QString& name): } -PersonName::PersonName (const PersonName& other): - m_id(other.m_id), - m_name(other.m_name) -{ - -} - - const Person::Id& PersonName::id() const { return m_id; diff --git a/src/database/ipeople_information_accessor.hpp b/src/database/ipeople_information_accessor.hpp index 8d905cfa62..df5e9ee03b 100644 --- a/src/database/ipeople_information_accessor.hpp +++ b/src/database/ipeople_information_accessor.hpp @@ -18,6 +18,9 @@ namespace Database /// list people on photo virtual std::vector listPeople(const Photo::Id &) = 0; + /// list full people data for photo + virtual std::vector listPeopleFull(const Photo::Id &) = 0; + /** * \brief get person details * \arg id person id @@ -46,7 +49,7 @@ namespace Database * \arg pi Details about person. It needs to refer to a valid photo id. \n * Also at least one of \a rect, \a person or \a id need to be valid * - * If \a pi has valid id and rect is invalid and person is is not valid, \n + * If \a pi has valid id and rect is invalid and person id is not valid, \n * then information about person is removed. \n * if \a pi has invalid id then database will be searched for exisiting \n * rect or person matching information in \a pi. If found, id will be \n @@ -54,7 +57,18 @@ namespace Database */ virtual PersonInfo::Id store(const PersonInfo& pi) = 0; - virtual PersonFingerprint::Id store(const PersonFingerprint &) = 0; + virtual PersonInfo::Id store(const Photo::Id&, const PersonFullInfo& pfi) = 0; + + /** + * @brief Store person's fingerprint in db + * @arg fp Person's fingerprint + * @return fingerprint id assigned by db + * + * If @a fp has valid id but empty fingerprint, it will be removed from db. \n + * If @a fp has invalid id but valid fingerprint data, new entry will be added to db. \n + * If @a fp has both entries valid, fingerpritn data for given id will be updated. + */ + virtual PersonFingerprint::Id store(const PersonFingerprint& fp) = 0; }; } diff --git a/src/database/iphoto_change_log_operator.hpp b/src/database/iphoto_change_log_operator.hpp index c4580c4ce1..d56ceb854e 100644 --- a/src/database/iphoto_change_log_operator.hpp +++ b/src/database/iphoto_change_log_operator.hpp @@ -13,9 +13,11 @@ namespace Database { struct IPhotoChangeLogOperator { + using DiffDelta = Photo::ExplicitDelta; + virtual ~IPhotoChangeLogOperator() = default; - virtual void storeDifference(const Photo::FullDelta &, const Photo::DataDelta &) = 0; + virtual void storeDifference(const DiffDelta &, const Photo::DataDelta &) = 0; virtual void groupCreated(const Group::Id &, const Group::Type &, const Photo::Id& representative) = 0; virtual void groupDeleted(const Group::Id &, const Photo::Id& representative, const std::vector& members) = 0; diff --git a/src/database/person_data.hpp b/src/database/person_data.hpp index 51f40c9322..b27a17c55b 100644 --- a/src/database/person_data.hpp +++ b/src/database/person_data.hpp @@ -24,15 +24,16 @@ #include #include +#include -#include "photo_data.hpp" +#include "photo_types.hpp" #include "database_export.h" namespace Person { using Id = Id; - typedef std::vector Fingerprint; + using Fingerprint = std::vector; } @@ -40,16 +41,13 @@ class DATABASE_EXPORT PersonName final { public: PersonName(); - PersonName(const Person::Id &, const QString &); - PersonName(const QString &); - PersonName(const PersonName &); + explicit PersonName(const Person::Id &, const QString &); + explicit PersonName(const QString &); + PersonName(const PersonName &) = default; ~PersonName() = default; PersonName& operator=(const PersonName &) = default; - bool operator<(const PersonName& other) const - { - return std::tie(m_id, m_name) < std::tie(other.m_id, other.m_name); - } + auto operator<=>(const PersonName &) const = default; const Person::Id& id() const; const QString& name() const; @@ -66,8 +64,10 @@ class DATABASE_EXPORT PersonFingerprint using Id = ::Id; PersonFingerprint() {} - PersonFingerprint(const Person::Fingerprint& fingerprint): m_fingerprint(fingerprint) {} - PersonFingerprint(const Id& id, const Person::Fingerprint& fingerprint): m_fingerprint(fingerprint), m_id(id) {} + explicit PersonFingerprint(const Person::Fingerprint& fingerprint): m_fingerprint(fingerprint) {} + explicit PersonFingerprint(const Id& id, const Person::Fingerprint& fingerprint): m_fingerprint(fingerprint), m_id(id) {} + + auto operator<=>(const PersonFingerprint &) const = default; const Id& id() const { return m_id; } const Person::Fingerprint& fingerprint() const { return m_fingerprint; } @@ -77,7 +77,9 @@ class DATABASE_EXPORT PersonFingerprint Id m_id; }; - +/** + * @brief Container for DB IDs of people related entries. + */ class DATABASE_EXPORT PersonInfo { public: @@ -110,17 +112,41 @@ class DATABASE_EXPORT PersonInfo PersonInfo(const PersonInfo &) = default; PersonInfo& operator=(const PersonInfo &) = default; + auto operator<=>(const PersonInfo &) const = default; +}; - bool operator==(const PersonInfo& other) const - { - return id == other.id && - p_id == other.p_id && - ph_id == other.ph_id && - f_id == other.f_id && - rect == other.rect; - } +/** + * @brief Container for person data (without DB IDs) + */ +struct PersonData +{ + QRect rect; + Person::Fingerprint fingerprint; + QString name; }; -Q_DECLARE_METATYPE( PersonName ) +/** + * @brief Container for all people related data (DB IDs + values) + */ +class PersonFullInfo +{ +public: + PersonFullInfo() = default; + PersonFullInfo(const PersonData& data) + : position(data.rect) + , fingerprint(data.fingerprint) + , name(data.name) + {} + + QRect position; + PersonFingerprint fingerprint; + PersonName name; + PersonInfo::Id pi_id; + + auto operator<=>(const PersonFullInfo &) const = default; +}; + + +Q_DECLARE_METATYPE(PersonName) -#endif // PERSONDATA_HPP +#endif diff --git a/src/database/photo_data.hpp b/src/database/photo_data.hpp index 3f8c99340e..1a71411505 100644 --- a/src/database/photo_data.hpp +++ b/src/database/photo_data.hpp @@ -47,7 +47,7 @@ namespace Photo QString path; QSize geometry; GroupInfo groupInfo; - Photo::PHashT phash; + Photo::PHashT phash; Data() = default; Data(const Data &) = default; @@ -66,7 +66,7 @@ namespace Photo /** - * @brief Structure containing chosen of photo details + * @brief Structure containing chosen details of photo */ class DATABASE_EXPORT DataDelta { @@ -128,7 +128,8 @@ namespace Photo DeltaTypes::Storage, DeltaTypes::Storage, DeltaTypes::Storage, - DeltaTypes::Storage + DeltaTypes::Storage, + DeltaTypes::Storage > Storage; Photo::Id m_id; diff --git a/src/database/photo_data_fields.hpp b/src/database/photo_data_fields.hpp index 1bf08c2d33..00dfae4e4d 100644 --- a/src/database/photo_data_fields.hpp +++ b/src/database/photo_data_fields.hpp @@ -3,6 +3,8 @@ #define PHOTO_DATA_FIELDS_HPP_INCLUDED #include "photo_types.hpp" +#include "person_data.hpp" + namespace Photo { @@ -14,15 +16,17 @@ namespace Photo Geometry, GroupInfo, PHash, + People, }; template struct DeltaTypes {}; - template<> struct DeltaTypes { using Storage = Tag::TagsList; }; - template<> struct DeltaTypes { using Storage = Photo::FlagValues; }; - template<> struct DeltaTypes { using Storage = QString; }; - template<> struct DeltaTypes { using Storage = QSize; }; - template<> struct DeltaTypes { using Storage = GroupInfo; }; - template<> struct DeltaTypes { using Storage = Photo::PHashT; }; + template<> struct DeltaTypes { using Storage = Tag::TagsList; }; + template<> struct DeltaTypes { using Storage = Photo::FlagValues; }; + template<> struct DeltaTypes { using Storage = QString; }; + template<> struct DeltaTypes { using Storage = QSize; }; + template<> struct DeltaTypes { using Storage = GroupInfo; }; + template<> struct DeltaTypes { using Storage = Photo::PHashT; }; + template<> struct DeltaTypes { using Storage = std::vector; }; } #endif diff --git a/src/database/unit_tests_for_backends/filters_tests.cpp b/src/database/unit_tests_for_backends/filters_tests.cpp index 2e624c45e5..b33907f240 100644 --- a/src/database/unit_tests_for_backends/filters_tests.cpp +++ b/src/database/unit_tests_for_backends/filters_tests.cpp @@ -1,5 +1,7 @@ #include "common.hpp" +#include "database_tools/json_to_backend.hpp" +#include "unit_tests_utils/photos_with_people.json.hpp" using testing::UnorderedElementsAreArray; using testing::ElementsAre; @@ -51,3 +53,15 @@ TYPED_TEST(FiltersTest, generalFlagsFilterTests) EXPECT_THAT(value55Photos, ElementsAre(ids.back())); } } + + +TYPED_TEST(FiltersTest, faceAnalysisStatus) +{ + Database::JsonToBackend converter(*this->m_backend); + converter.append(PeopleDB::db); + + auto peopleFilter = Database::FilterFaceAnalysisStatus(Database::FilterFaceAnalysisStatus::Performed); + auto analyzedPhotos = this->m_backend->photoOperator().getPhotos({peopleFilter}); + + EXPECT_EQ(analyzedPhotos.size(), 3); +} diff --git a/src/database/unit_tests_for_backends/people_tests.cpp b/src/database/unit_tests_for_backends/people_tests.cpp index 2003a63e96..e53d997d46 100644 --- a/src/database/unit_tests_for_backends/people_tests.cpp +++ b/src/database/unit_tests_for_backends/people_tests.cpp @@ -1,9 +1,15 @@ +#include "database_tools/json_to_backend.hpp" +#include "unit_tests_utils/photos_with_people.json.hpp" +#include "backend_utils.hpp" + #include "common.hpp" // TODO: reenable +using testing::UnorderedElementsAre; + template struct PeopleTest: DatabaseTest { @@ -552,3 +558,25 @@ TYPED_TEST(PeopleTest, removePersonWhenItsRemovedFromTags) }); } */ + +TYPED_TEST(PeopleTest, readPeopleViaDataDelta) +{ + Database::JsonToBackend converter(*this->m_backend); + converter.append(PeopleDB::db); + + const auto photos = Database::getPhotoDelta(*this->m_backend); + + std::vector peopleInfo; + + for(const auto& photo: photos) + for(const auto& person: photo.template get()) + peopleInfo.push_back(person); + + std::set peopleNames; + std::ranges::transform(peopleInfo, std::inserter(peopleNames, peopleNames.end()), [](const auto& personInfo) + { + return personInfo.name.name(); + }); + + EXPECT_THAT(peopleNames, UnorderedElementsAre("person 1", "person 2", "person 3", "person 4", "person 5")); +} diff --git a/src/database/unit_tests_for_backends/photo_operator_tests.cpp b/src/database/unit_tests_for_backends/photo_operator_tests.cpp index 4744e42913..f2fa92c283 100644 --- a/src/database/unit_tests_for_backends/photo_operator_tests.cpp +++ b/src/database/unit_tests_for_backends/photo_operator_tests.cpp @@ -123,7 +123,7 @@ TYPED_TEST(PhotoOperatorTest, sortingByPHashReversed) std::vector photos; for (const auto& id: ids) - photos.push_back(this->m_backend->getPhotoDelta(id, {Photo::Field::PHash})); + photos.push_back(this->m_backend->template getPhotoDelta(id)); std::vector phashes; std::transform(photos.begin(), photos.end(), std::back_inserter(phashes), [](const Photo::DataDelta& data) { return data.get().value(); }); @@ -180,7 +180,7 @@ TYPED_TEST(PhotoOperatorTest, removal) // Some may ask Photo::DataDelta for it while photo is being deleted. // It is not convenient to protect them all against null result. // Instead db should mark such photos and delete them later (possibly on db close). - const Photo::DataDelta readData = this->m_backend->getPhotoDelta(id, {Photo::Field::Path}); // TODO: for some reason Photo::DataDelta cannot be replaced with auto. gcc 12.1.1 bug? + const Photo::DataDelta readData = this->m_backend->template getPhotoDelta(id); // TODO: for some reason Photo::DataDelta cannot be replaced with auto. gcc 12.1.1 bug? const auto& readDataPath = readData.get(); EXPECT_EQ(readDataPath, path); } diff --git a/src/database/unit_tests_for_backends/thumbnails_tests.cpp b/src/database/unit_tests_for_backends/thumbnails_tests.cpp index e5990fd24e..124abd0dc0 100644 --- a/src/database/unit_tests_for_backends/thumbnails_tests.cpp +++ b/src/database/unit_tests_for_backends/thumbnails_tests.cpp @@ -1,6 +1,10 @@ #include "common.hpp" +namespace +{ + const QString ThumbnailBlob = "thumbnail"; +} template struct ThumbnailsTest: DatabaseTest @@ -20,8 +24,8 @@ TYPED_TEST(ThumbnailsTest, storesThumbnail) this->m_backend->addPhotos(photos); const auto id = photos.front().getId(); - this->m_backend->writeBlob(id, Database::IBackend::BlobType::Thumbnail, QByteArray("thumbnail")); - const QByteArray thumbnail = this->m_backend->readBlob(id, Database::IBackend::BlobType::Thumbnail); + this->m_backend->writeBlob(id, ThumbnailBlob, QByteArray("thumbnail")); + const QByteArray thumbnail = this->m_backend->readBlob(id, ThumbnailBlob); EXPECT_EQ(thumbnail, "thumbnail"); } @@ -36,9 +40,9 @@ TYPED_TEST(ThumbnailsTest, thumbnailOverride) this->m_backend->addPhotos(photos); const auto id = photos.front().getId(); - this->m_backend->writeBlob(id, Database::IBackend::BlobType::Thumbnail, QByteArray("thumbnail")); - this->m_backend->writeBlob(id, Database::IBackend::BlobType::Thumbnail, QByteArray("thumbnail2")); - const QByteArray thumbnail = this->m_backend->readBlob(id, Database::IBackend::BlobType::Thumbnail); + this->m_backend->writeBlob(id, ThumbnailBlob, QByteArray("thumbnail")); + this->m_backend->writeBlob(id, ThumbnailBlob, QByteArray("thumbnail2")); + const QByteArray thumbnail = this->m_backend->readBlob(id, ThumbnailBlob); EXPECT_EQ(thumbnail, "thumbnail2"); } @@ -52,7 +56,7 @@ TYPED_TEST(ThumbnailsTest, emptyResultWhenMissing) this->m_backend->addPhotos(photos); const auto id = photos.front().getId(); - const QByteArray thumbnail = this->m_backend->readBlob(id, Database::IBackend::BlobType::Thumbnail); + const QByteArray thumbnail = this->m_backend->readBlob(id, ThumbnailBlob); EXPECT_TRUE(thumbnail.isEmpty()); } diff --git a/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.cpp b/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.cpp index 12f79a97fb..9c000af28b 100644 --- a/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.cpp +++ b/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.cpp @@ -135,8 +135,8 @@ namespace dlib_api struct FaceLocator::Data { - lazy_ptr cnn_face_detector; - lazy_ptr hog_face_detector; + lazy_ptr cnn_face_detector; + lazy_ptr hog_face_detector; std::unique_ptr logger; const bool cuda_available; @@ -322,14 +322,16 @@ namespace dlib_api { Data(ILogger* log) : face_encoder( modelPath().toStdString() ) + , predictor_5_point(ObjectDeserializer()) + , predictor_68_point(ObjectDeserializer()) , logger(log) { } face_recognition_model_v1 face_encoder; - lazy_ptr> predictor_5_point; - lazy_ptr> predictor_68_point; + lazy_ptr predictor_5_point; + lazy_ptr predictor_68_point; ILogger* logger; }; @@ -349,7 +351,7 @@ namespace dlib_api FaceEncodings FaceEncoder::face_encodings(const QImage& qimage, int num_jitters, EncodingsModel model) { - // here we assume, that given image is a face extraceted from image with help of face_locations() + // here we assume, that given image is a face extracted from image with help of face_locations() const QSize size = qimage.size(); m_data->logger->debug( diff --git a/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.hpp b/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.hpp index 774b360153..58a97b4044 100644 --- a/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.hpp +++ b/src/face_recognition/dlib_wrapper/dlib_face_recognition_api.hpp @@ -75,7 +75,7 @@ namespace dlib_api * @brief check if we have proper system setup to perform face recognition * @return true if face recognition will work. False if it would crash app. * - * if dlib was compiled with CUDA support yey no cuda is available, then + * if dlib was compiled with CUDA support yet no cuda is available, then * we cannot work - dlib will crash/throw on CUDA usage */ DLIB_WRAPPER_EXPORT bool check_system_prerequisites(); diff --git a/src/face_recognition/face_recognition.cpp b/src/face_recognition/face_recognition.cpp index 1cad16133d..c6c56f3b80 100644 --- a/src/face_recognition/face_recognition.cpp +++ b/src/face_recognition/face_recognition.cpp @@ -63,22 +63,9 @@ namespace } -struct FaceRecognition::Data -{ - explicit Data(ICoreFactoryAccessor* coreAccessor) - : m_logger(coreAccessor->getLoggerFactory().get("FaceRecognition")) - , m_exif(coreAccessor->getExifReaderFactory().get()) - { - - } - std::unique_ptr m_logger; - IExifReader& m_exif; -}; - - -FaceRecognition::FaceRecognition(ICoreFactoryAccessor* coreAccessor): - m_data(std::make_unique(coreAccessor)) +FaceRecognition::FaceRecognition(const ILogger& logger) + : m_logger(logger.subLogger("FaceRecognition")) { } @@ -96,15 +83,12 @@ bool FaceRecognition::checkSystem() } -QVector FaceRecognition::fetchFaces(const QString& path) const +std::vector FaceRecognition::fetchFaces(const OrientedImage& orientedPhoto) const { - OrientedImage orientedPhoto(m_data->m_exif, path); - const int pixels = orientedPhoto->width() * orientedPhoto->height(); const double mpixels = pixels / 1e6; - m_data->m_logger->debug(QString("Looking for faces in photo %1. Size: %2Mpx") - .arg(path) + m_logger->debug(QString("Looking for faces in photo of size: %2Mpx") .arg(mpixels, 0, 'f', 1) ); @@ -114,12 +98,12 @@ QVector FaceRecognition::fetchFaces(const QString& path) const const auto faces = fetchFaces(orientedPhoto, 1); const auto elapsed = timer.elapsed(); - m_data->m_logger->info(QString("Found %1 faces in time: %2ms") + m_logger->info(QString("Found %1 faces in time: %2ms") .arg(faces.size()) .arg(elapsed) ); - return faces; + return std::vector(faces.begin(), faces.end()); } @@ -127,7 +111,7 @@ Person::Fingerprint FaceRecognition::getFingerprint(const OrientedImage& image, { const QImage face = face_rect.isEmpty()? image.get(): image.get().copy(face_rect); - dlib_api::FaceEncoder faceEndoder(m_data->m_logger.get()); + dlib_api::FaceEncoder faceEndoder(m_logger.get()); const dlib_api::FaceEncodings face_encodings = faceEndoder.face_encodings(face); return face_encodings; @@ -151,7 +135,7 @@ QVector FaceRecognition::fetchFaces(const OrientedImage& orientedPhoto, d const QSize scaledSize = orientedPhoto.get().size() * scale; const QImage photo = orientedPhoto.get().scaled(scaledSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - result = dlib_api::FaceLocator(m_data->m_logger.get()).face_locations(photo, 0); + result = dlib_api::FaceLocator(m_logger.get()).face_locations(photo, 0); std::transform(result.begin(), result.end(), result.begin(), [scale](const QRect& face){ return QRectF(face.topLeft().x() / scale, face.topLeft().y() / scale, diff --git a/src/face_recognition/face_recognition.hpp b/src/face_recognition/face_recognition.hpp index 6201f00616..f430faebd6 100644 --- a/src/face_recognition/face_recognition.hpp +++ b/src/face_recognition/face_recognition.hpp @@ -43,7 +43,7 @@ namespace Database class FACE_RECOGNITION_EXPORT FaceRecognition final { public: - FaceRecognition(ICoreFactoryAccessor *); + FaceRecognition(const ILogger& logger); FaceRecognition(const FaceRecognition &) = delete; ~FaceRecognition(); @@ -54,17 +54,16 @@ class FACE_RECOGNITION_EXPORT FaceRecognition final static bool checkSystem(); // Locate faces on given photo. - QVector fetchFaces(const QString &) const; + std::vector fetchFaces(const OrientedImage &) const; Person::Fingerprint getFingerprint(const OrientedImage& image, const QRect& face = QRect()); int recognize(const Person::Fingerprint& unknown, const std::vector& known); private: - struct Data; - std::unique_ptr m_data; + std::unique_ptr m_logger; QVector fetchFaces(const OrientedImage &, double scale) const; }; -#endif // FACERECOGNITION_HPP +#endif diff --git a/src/gui/desktop/QmlItems/ImageButton.qml b/src/gui/desktop/QmlItems/ImageButton.qml index ec781715ad..231cca45b4 100644 --- a/src/gui/desktop/QmlItems/ImageButton.qml +++ b/src/gui/desktop/QmlItems/ImageButton.qml @@ -4,15 +4,24 @@ import QtQuick 2.15 Item { id: root + enum Style { + BackLight, + Scale + } + required property string source + property int style: Style.BackLight + signal clicked() + Rectangle { anchors.fill: parent id: background color: "black" + visible: root.style === root.BackLight opacity: 0.0 + mouseArea.containsMouse * 0.2 + mouseArea.pressed * 0.2 } @@ -20,6 +29,9 @@ Item { anchors.fill: parent source: root.source + scale: root.style === root.Scale? (mouseArea.containsMouse? 1.0: 0.7): 1.0 + + Behavior on scale { PropertyAnimation{ duration: 100 } } } MouseArea { diff --git a/src/gui/desktop/QmlItems/LineEdit.qml b/src/gui/desktop/QmlItems/LineEdit.qml new file mode 100644 index 0000000000..d7014735ee --- /dev/null +++ b/src/gui/desktop/QmlItems/LineEdit.qml @@ -0,0 +1,26 @@ + +import QtQuick 2.15 + +Rectangle { + + property alias text: input.text + + implicitHeight: input.implicitHeight + 8 + implicitWidth: input.implicitWidth + + border.color: "gray" + border.width: 1 + radius: 2 + + signal editingFinished() + + TextInput { + id: input + anchors.fill: parent + anchors.margins: 4 + + selectByMouse: true + + onEditingFinished: parent.editingFinished() + } +} diff --git a/src/gui/desktop/QmlItems/picture_item.cpp b/src/gui/desktop/QmlItems/picture_item.cpp index 59f2bc30ad..3724500434 100644 --- a/src/gui/desktop/QmlItems/picture_item.cpp +++ b/src/gui/desktop/QmlItems/picture_item.cpp @@ -34,7 +34,7 @@ const QImage& PictureItem::source() const void PictureItem::paint(QPainter* painter) { - const QRectF rect(QPointF(0, 0), QSizeF(implicitWidth(), implicitHeight())); + const QRectF rect(QPointF(0, 0), QSizeF(width(), height())); painter->drawImage(rect, m_source); } diff --git a/src/gui/desktop/models/CMakeLists.txt b/src/gui/desktop/models/CMakeLists.txt index d0d761b55f..74fbb99dd3 100644 --- a/src/gui/desktop/models/CMakeLists.txt +++ b/src/gui/desktop/models/CMakeLists.txt @@ -17,6 +17,7 @@ set(SRC photo_properties_model.cpp photo_properties_model.hpp photos_data_guesser.cpp + qml_flat_model.cpp roles_expansion.hpp series_model.cpp series_model.hpp diff --git a/src/gui/desktop/models/aphoto_data_model.hpp b/src/gui/desktop/models/aphoto_data_model.hpp index 816e9ceaac..d8d9a966f1 100644 --- a/src/gui/desktop/models/aphoto_data_model.hpp +++ b/src/gui/desktop/models/aphoto_data_model.hpp @@ -1,24 +1,6 @@ -/* - * Photo Broom - photos management tool. - * Copyright (C) 2015 Michał Walenciak - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#ifndef ASCALABLEIMAGESMODEL_HPP -#define ASCALABLEIMAGESMODEL_HPP + +#ifndef APHOTO_DATA_MODEL_HPP_INCLUDED +#define APHOTO_DATA_MODEL_HPP_INCLUDED #include #include @@ -50,5 +32,4 @@ class APhotoDataModel: public QAbstractItemModel void registerRole(int, const QByteArray &); }; - -#endif // ASCALABLEIMAGESMODEL_HPP +#endif diff --git a/src/gui/desktop/models/faces_model.cpp b/src/gui/desktop/models/faces_model.cpp index 0265ba678b..988f75e6ae 100644 --- a/src/gui/desktop/models/faces_model.cpp +++ b/src/gui/desktop/models/faces_model.cpp @@ -1,12 +1,13 @@ #include -#include #include #include #include +#include #include #include +#include #include #include @@ -22,10 +23,7 @@ ENUM_ROLES_SETUP(FacesModel::Roles); FacesModel::FacesModel(QObject *parent): - QAbstractListModel(parent), - m_id(), - m_peopleManipulator(), - m_faces() + QAbstractListModel(parent) { QMetaObject::invokeMethod(this, &FacesModel::initialSetup, Qt::QueuedConnection); } @@ -33,7 +31,8 @@ FacesModel::FacesModel(QObject *parent): FacesModel::~FacesModel() { - apply(); + if (m_database) + apply(); } @@ -61,25 +60,26 @@ int FacesModel::state() const QList FacesModel::facesMask() const { - if (m_photoSize.isValid()) + if (m_faces.empty()) + return {}; + else { - QRegion reg(0, 0, m_photoSize.width(), m_photoSize.height()); + const QSize photoSize = m_faces.front()->image()->size(); + QRegion reg(0, 0, photoSize.width(), photoSize.height()); - for (const QRect& rect: m_faces) - reg-=QRegion(rect); + for (const auto& face: m_faces) + reg -= QRegion(face->rect()); const QList qmlListOfRects(reg.begin(), reg.end()); return qmlListOfRects; } - else - return {}; } int FacesModel::rowCount(const QModelIndex& parent) const { - return parent.isValid() == false? static_cast(m_facesCount): 0; + return parent.isValid() == false? static_cast(m_faces.size()): 0; } @@ -87,12 +87,14 @@ QVariant FacesModel::data(const QModelIndex& index, int role) const { const std::size_t row = static_cast(index.row()); - if (index.column() == 0 && row < m_peopleManipulator->facesCount()) + if (index.column() == 0 && row < m_faces.size()) { if (role == Qt::DisplayRole) - return m_peopleManipulator->name(row); + return m_faces[row]->name(); else if (role == Roles::FaceRectRole) - return m_peopleManipulator->position(row); + return m_faces[row]->rect(); + else if (role == Roles::UncertainRole) + return m_isUncertain[row]; } return {}; @@ -107,32 +109,52 @@ QHash FacesModel::roleNames() const bool FacesModel::setData(const QModelIndex& index, const QVariant& data, int role) { - if (role == Qt::EditRole && index.column() == 0 && index.row() < m_facesCount) - m_peopleManipulator->setName(index.row(), data.toString()); + const std::size_t r = static_cast(index.row()); + if (role == Qt::EditRole && index.column() == 0 && r < m_faces.size()) + { + m_faces[r]->setName(data.toString()); + + if (m_isUncertain[r]) + { + m_isUncertain[r] = false; + + emit dataChanged(index, index, {UncertainRole}); + } + } return true; } -void FacesModel::updateFaceInformation() +void FacesModel::updateFaceInformation(std::shared_ptr>> faces) { - const auto faces_count = m_peopleManipulator->facesCount(); + const int faces_count = static_cast(faces->size()); updateDetectionState(faces_count == 0? 2: 1); m_faces.clear(); - for(std::size_t i = 0; i < faces_count; i++) - m_faces.push_back(m_peopleManipulator->position(i)); if (faces_count > 0) { - beginInsertRows(QModelIndex(), 0, static_cast(faces_count - 1)); - m_facesCount = static_cast(m_peopleManipulator->facesCount()); + beginInsertRows({}, 0, faces_count - 1); + m_faces = std::move(*faces); + m_isUncertain.resize(m_faces.size()); + + // perform recognition + for (unsigned int i = 0; i < m_faces.size(); i++) + { + const auto& person = m_faces[i]->person(); + + if (person.id().valid() == false) + { + m_isUncertain[i] = true; + m_faces[i]->recognize(); + } + } + endInsertRows(); } - m_photoSize = m_peopleManipulator->photoSize(); - emit facesMaskChanged(facesMask()); } @@ -142,11 +164,16 @@ void FacesModel::initialSetup() assert(m_id.valid()); assert(m_database); assert(m_core); - assert(m_peopleManipulator.get() == nullptr); - m_peopleManipulator = std::make_unique(m_id, *m_database, *m_core); - connect(m_peopleManipulator.get(), &PeopleManipulator::facesAnalyzed, - this, &FacesModel::updateFaceInformation); + runOn(m_core->getTaskExecutor(), [this]() + { + const auto logger = m_core->getLoggerFactory().get("FacesModel"); + const auto faces = std::make_shared>>(FaceEditor( + *m_database, + *m_core, + *logger).getFacesFor(m_id)); + invokeMethod(this, &FacesModel::updateFaceInformation, faces); + }); updateDetectionState(0); } @@ -160,6 +187,9 @@ void FacesModel::updateDetectionState(int state) void FacesModel::apply() { - if (m_database) - m_peopleManipulator->store(); + assert(m_faces.size() == m_isUncertain.size()); + + for(std::size_t i = 0; i < m_faces.size(); i++) + if (not m_isUncertain[i]) + m_faces[i]->store(); } diff --git a/src/gui/desktop/models/faces_model.hpp b/src/gui/desktop/models/faces_model.hpp index f84202b9f8..e51c4341ca 100644 --- a/src/gui/desktop/models/faces_model.hpp +++ b/src/gui/desktop/models/faces_model.hpp @@ -10,8 +10,7 @@ #include #include -#include "utils/people_manipulator.hpp" - +#include "utils/people_editor.hpp" class FacesModel: public QAbstractListModel { @@ -27,7 +26,7 @@ class FacesModel: public QAbstractListModel enum Roles { FaceRectRole = Qt::UserRole + 1, - _lastRole, + UncertainRole, }; Q_ENUMS(Roles) @@ -51,14 +50,12 @@ class FacesModel: public QAbstractListModel Photo::Id m_id; Database::IDatabase* m_database = nullptr; ICoreFactoryAccessor* m_core = nullptr; - std::unique_ptr m_peopleManipulator; - QVector m_faces; - QString m_photoPath; + std::vector> m_faces; + std::vector m_isUncertain; QSize m_photoSize; int m_state = 0; - int m_facesCount = 0; - void updateFaceInformation(); + void updateFaceInformation(std::shared_ptr>>); void initialSetup(); void updateDetectionState(int); diff --git a/src/gui/desktop/models/flat_model.cpp b/src/gui/desktop/models/flat_model.cpp index cd6106bac4..0d6906f572 100644 --- a/src/gui/desktop/models/flat_model.cpp +++ b/src/gui/desktop/models/flat_model.cpp @@ -17,8 +17,6 @@ #include "flat_model.hpp" -#include - #include #include #include diff --git a/src/gui/desktop/models/qml_flat_model.cpp b/src/gui/desktop/models/qml_flat_model.cpp new file mode 100644 index 0000000000..c9a4f8d691 --- /dev/null +++ b/src/gui/desktop/models/qml_flat_model.cpp @@ -0,0 +1,48 @@ + +#include "qml_flat_model.hpp" + +using namespace Database; + +QMLFlatModel::QMLFlatModel() +{ + +} + + +void QMLFlatModel::setTextFilters(const QStringList& filters) +{ + m_filters = filters; + std::vector dbFilter; + + for (const auto& filter: filters) + { + if (filter == facesNotAnalysedFilter()) + dbFilter.push_back(FilterFaceAnalysisStatus(FilterFaceAnalysisStatus::NotPerformed)); + else if (filter == validMediaFilter()) + dbFilter.push_back(getValidPhotosFilter()); + else + { + // log warning + } + } + + FlatModel::setFilter(dbFilter); +} + + +const QStringList& QMLFlatModel::textFilters() const +{ + return m_filters; +} + + +QString QMLFlatModel::facesNotAnalysedFilter() const +{ + return QStringLiteral("FNA"); // faces not analysed +} + + +QString QMLFlatModel::validMediaFilter() const +{ + return QStringLiteral("VMF"); // valid media files +} diff --git a/src/gui/desktop/models/qml_flat_model.hpp b/src/gui/desktop/models/qml_flat_model.hpp new file mode 100644 index 0000000000..54ac00ca77 --- /dev/null +++ b/src/gui/desktop/models/qml_flat_model.hpp @@ -0,0 +1,32 @@ + +#ifndef BATCH_FACE_RECOGNITION_MODEL_HPP_INCLUDED +#define BATCH_FACE_RECOGNITION_MODEL_HPP_INCLUDED + +#include "flat_model.hpp" + + +class QMLFlatModel: public FlatModel +{ + Q_OBJECT + + Q_PROPERTY(Database::IDatabase* database WRITE setDatabase READ database REQUIRED) + Q_PROPERTY(QStringList text_filters WRITE setTextFilters READ textFilters) + + // filters + Q_PROPERTY(QString facesNotAnalysed READ facesNotAnalysedFilter CONSTANT) + Q_PROPERTY(QString validMedia READ validMediaFilter CONSTANT) + +public: + QMLFlatModel(); + + void setTextFilters(const QStringList& filters); + const QStringList& textFilters() const; + + QString facesNotAnalysedFilter() const; + QString validMediaFilter() const; + +private: + QStringList m_filters; +}; + +#endif diff --git a/src/gui/desktop/quick_items/Views/BatchFaceDetection.qml b/src/gui/desktop/quick_items/Views/BatchFaceDetection.qml new file mode 100644 index 0000000000..efd3c89db8 --- /dev/null +++ b/src/gui/desktop/quick_items/Views/BatchFaceDetection.qml @@ -0,0 +1,140 @@ + +import QtQuick +import QtQuick.Controls + +import photo_broom.models +import photo_broom.singletons +import photo_broom.utils +import QmlItems +import "ViewsComponents" as Internals + + +Item { + + QMLFlatModel { + id: data_model + text_filters: [data_model.facesNotAnalysed, data_model.validMedia] + database: PhotoBroomProject.database + } + + BatchFaceDetector { + id: detector + core: PhotoBroomProject.coreFactory + db: PhotoBroomProject.database + photos_model: data_model + } + + SplitView { + anchors.fill: parent + orientation: Qt.Vertical + + GroupBox { + title: qsTr("Discovered faces") + clip: true + + SplitView.minimumHeight: 150 + + GridView { + anchors.fill: parent + model: detector + + cellWidth: 170 + cellHeight: 200 + + delegate: Item { + id: delegateItem + + required property var decoration + required property var display + required property var index + required property var model + + width: 170 + height: 200 + + Column { + id: faceItem + anchors.fill: parent + + Item { + anchors.horizontalCenter: parent.horizontalCenter + height: 150 + width: 150 + + Picture { + anchors.fill: parent + source: decoration + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + + Item { + anchors.fill: parent + + opacity: mouseArea.containsMouse? 1.0: 0.0 + Behavior on opacity { PropertyAnimation{} } + + ImageButton { + anchors.top: parent.top + anchors.right: parent.right + + width: 20 + height: 20 + source: "qrc:/gui/ok.svg" + + style: ImageButton.Scale + + onClicked: { + faceItem.updateModel(); + detector.accept(delegateItem.index); + } + } + + ImageButton { + anchors.top: parent.top + anchors.left: parent.left + + width: 20 + height: 20 + source: "qrc:/gui/trash.svg" + + style: ImageButton.Scale + + onClicked: detector.drop(delegateItem.index) + } + } + } + } + + LineEdit { + id: personName + + anchors.horizontalCenter: parent.horizontalCenter + text: display + width: parent.width - 40 + } + + function updateModel() { + model.edit = personName.text + } + } + } + } + } + + GroupBox { + title: qsTr("Photos to be analyzed") + clip: true + + SplitView.minimumHeight: 150 + + Internals.PhotosGridView { + anchors.fill: parent + model: data_model + } + } + } +} diff --git a/src/gui/desktop/quick_items/Views/FacesDialog.qml b/src/gui/desktop/quick_items/Views/FacesDialog.qml index 58729d60b2..df6258ee98 100644 --- a/src/gui/desktop/quick_items/Views/FacesDialog.qml +++ b/src/gui/desktop/quick_items/Views/FacesDialog.qml @@ -147,8 +147,8 @@ Item { anchors.top: parent.top anchors.right: parent.right + anchors.bottom: toolsArea.top implicitWidth: contentWidth - height: contentHeight model: facesModel @@ -157,14 +157,19 @@ Item { delegate: TextField { id: name - required property var display // DisplayRole required property var index + required property var display // DisplayRole + required property bool uncertain // UncertainRole readOnly: true hoverEnabled: true text: display placeholderText: qsTr("unknown") + palette { + base: uncertain? "yellow" : systemPalette.base + } + onPressed: { name.readOnly = false; editedItem(index); diff --git a/src/gui/desktop/quick_items/Views/MainWindow.qml b/src/gui/desktop/quick_items/Views/MainWindow.qml index 424cc0b9de..782f4c3138 100644 --- a/src/gui/desktop/quick_items/Views/MainWindow.qml +++ b/src/gui/desktop/quick_items/Views/MainWindow.qml @@ -90,6 +90,7 @@ ApplicationWindow { Action { text: qsTr("S&eries detector..."); onTriggered: { toolsStackView.currentIndex = 1; mainView.currentIndex = 1; } } Action { text: qsTr("Ph&oto data completion..."); onTriggered: { toolsStackView.currentIndex = 2; mainView.currentIndex = 1; } } Action { text: qsTr("Look for &duplicates"); onTriggered: { toolsStackView.currentIndex = 3; mainView.currentIndex = 1; } } + //Action { text: qsTr("&Face detection..."); onTriggered: { toolsStackView.currentIndex = 4; mainView.currentIndex = 1; } } } Menu { id: settingsMenu @@ -202,6 +203,16 @@ ApplicationWindow { sourceComponent: PhotoBroomProject.projectOpen? duplicates_view : undefined } + Loader { + active: PhotoBroomProject.projectOpen && toolsStackView.currentIndex == StackLayout.index + + Component { + id: batch_face_detection + BatchFaceDetection { } + } + + sourceComponent: PhotoBroomProject.projectOpen? batch_face_detection : undefined + } } } } diff --git a/src/gui/desktop/quick_items/Views/PhotosView.qml b/src/gui/desktop/quick_items/Views/PhotosView.qml index 287577a429..3cd83751f5 100644 --- a/src/gui/desktop/quick_items/Views/PhotosView.qml +++ b/src/gui/desktop/quick_items/Views/PhotosView.qml @@ -96,7 +96,6 @@ FocusScope { activeFocusOnTab: true keyNavigationEnabled: true - flickDeceleration: 10000 model: photosModelControllerId.photos thumbnailSize: thumbnailSliderId.size diff --git a/src/gui/desktop/quick_items/Views/ViewsComponents/PhotosGridView.qml b/src/gui/desktop/quick_items/Views/ViewsComponents/PhotosGridView.qml index c2204dbcdc..f536e5c5dc 100644 --- a/src/gui/desktop/quick_items/Views/ViewsComponents/PhotosGridView.qml +++ b/src/gui/desktop/quick_items/Views/ViewsComponents/PhotosGridView.qml @@ -16,8 +16,10 @@ Components.MultiselectGridView { signal itemDoubleClicked(int index) + flickDeceleration: 10000 cellWidth: thumbnailSize + thumbnailMargin * 2 cellHeight: thumbnailSize + thumbnailMargin * 2 + currentIndex: -1 delegate: PhotoDelegate { id: delegateId @@ -52,8 +54,6 @@ Components.MultiselectGridView { Component.onCompleted: selected = grid.isIndexSelected(index); } - currentIndex: -1 - ScrollBar.vertical: ScrollBar { } // TODO: without it parent (PhotosView) doesn't get focus and Esc key does not work. diff --git a/src/gui/desktop/quick_items/photos_model_controller_component.cpp b/src/gui/desktop/quick_items/photos_model_controller_component.cpp index 71963f731d..77cef560c8 100644 --- a/src/gui/desktop/quick_items/photos_model_controller_component.cpp +++ b/src/gui/desktop/quick_items/photos_model_controller_component.cpp @@ -342,17 +342,17 @@ QStringList PhotosModelControllerComponent::rawCategories() const void PhotosModelControllerComponent::getTimeRangeForFilters(Database::IBackend& backend) { - auto dates = backend.listTagValues(Tag::Types::Date, {}); + auto date = backend.listTagValues(Tag::Types::Date, {}); const Database::FilterPhotosWithTag with_date_filter(Tag::Types::Date); const Database::FilterNotMatchingFilter without_date_filter(with_date_filter); const auto photos_without_date_tag = backend.photoOperator().getPhotos(without_date_filter); if (photos_without_date_tag.empty() == false) - dates.push_back(QDate()); + date.push_back(QDate()); - std::sort(dates.begin(), dates.end()); - invokeMethod(this, &PhotosModelControllerComponent::setAvailableDates, dates); + std::sort(date.begin(), date.end()); + invokeMethod(this, &PhotosModelControllerComponent::setAvailableDates, date); } diff --git a/src/gui/desktop/ui/mainwindow.cpp b/src/gui/desktop/ui/mainwindow.cpp index 7676383e24..5c54cb95f1 100644 --- a/src/gui/desktop/ui/mainwindow.cpp +++ b/src/gui/desktop/ui/mainwindow.cpp @@ -5,10 +5,7 @@ #include #include -#include -#include #include -#include #include #include diff --git a/src/gui/desktop/ui/photos_grouping_dialog.cpp b/src/gui/desktop/ui/photos_grouping_dialog.cpp index e74f186849..33bce56175 100644 --- a/src/gui/desktop/ui/photos_grouping_dialog.cpp +++ b/src/gui/desktop/ui/photos_grouping_dialog.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include diff --git a/src/gui/desktop/utils/CMakeLists.txt b/src/gui/desktop/utils/CMakeLists.txt index d0ac8c15a4..9c6c01c350 100644 --- a/src/gui/desktop/utils/CMakeLists.txt +++ b/src/gui/desktop/utils/CMakeLists.txt @@ -4,6 +4,8 @@ add_subdirectory(animated_webp) find_package(Qt6 REQUIRED COMPONENTS Svg Quick QuickWidgets) find_package(OpenCV REQUIRED) +include(${PROJECT_SOURCE_DIR}/tools/reflect++/Reflect++.cmake) + set(UTILS_SOURCES grouppers/animation_generator.cpp grouppers/animation_generator.hpp @@ -13,6 +15,8 @@ set(UTILS_SOURCES grouppers/generator_utils.hpp grouppers/hdr_generator.cpp grouppers/hdr_generator.hpp + batch_face_detector.cpp + batch_face_detector.hpp collection_scanner.cpp collection_scanner.hpp config_tools.cpp @@ -29,10 +33,10 @@ set(UTILS_SOURCES model_index_utils.hpp painter_helpers.cpp painter_helpers.hpp + people_editor.cpp + people_editor.hpp people_list_model.cpp people_list_model.hpp - people_manipulator.cpp - people_manipulator.hpp photos_collector.cpp photos_collector.hpp qml_setup.cpp @@ -53,7 +57,7 @@ set(UTILS_SOURCES variant_display.hpp ) -add_library(gui_utils OBJECT ${UTILS_SOURCES}) +add_library(gui_utils OBJECT ${UTILS_SOURCES} ${ReflectionFiles}) set_target_properties(gui_utils PROPERTIES AUTOMOC TRUE) target_link_libraries(gui_utils PRIVATE @@ -75,5 +79,7 @@ target_include_directories(gui_utils PRIVATE ${CMAKE_SOURCE_DIR}/src/gui/ ${CMAKE_SOURCE_DIR}/src/gui/desktop + ${CMAKE_CURRENT_BINARY_DIR} ) +target_sources(gui_utils PRIVATE ${ReflectionFiles}) diff --git a/src/gui/desktop/utils/batch_face_detector.cpp b/src/gui/desktop/utils/batch_face_detector.cpp new file mode 100644 index 0000000000..55bcc76a14 --- /dev/null +++ b/src/gui/desktop/utils/batch_face_detector.cpp @@ -0,0 +1,246 @@ + +#include +#include +#include +#include +#include +#include + +#include "batch_face_detector.hpp" + +#include + + +BatchFaceDetector::BatchFaceDetector() + : m_faceEditor(make_lazy_ptr([this]{ return std::make_unique(*this->db(), *this->core(), *this->m_logger); })) +{ + +} + + +BatchFaceDetector::~BatchFaceDetector() +{ + // db client should be destroyed by now + assert(m_dbClient.get() == nullptr); +} + + +void BatchFaceDetector::setPhotosModel(APhotoDataModel* model) +{ + if (m_photosModel != nullptr) + { + disconnect(m_photosModel, &QAbstractItemModel::rowsInserted, this, &BatchFaceDetector::newPhotos); + } + + m_photosModel = model; + + if (m_photosModel != nullptr) + { + assert(m_core != nullptr); + + connect(m_photosModel, &QAbstractItemModel::rowsInserted, this, &BatchFaceDetector::newPhotos); + + QMetaObject::invokeMethod(this, [this] + { + const auto rows = m_photosModel->rowCount(); + m_faces.clear(); + m_ids.clear(); + + if (rows > 0) + newPhotos({}, 0, rows - 1); + }, + Qt::QueuedConnection); + } +} + + +void BatchFaceDetector::setCore(ICoreFactoryAccessor* core) +{ + assert(m_core == nullptr); + m_core = core; + m_logger = m_core->getLoggerFactory().get("BatchFaceDetector"); +} + + +void BatchFaceDetector::setDB(Database::IDatabase* db) +{ + m_dbClient = db->attach(tr("Batch face detector")); + if (m_dbClient) + { + m_dbClient->onClose([this]() + { + m_photosProcessingProcess->terminate(); + }); + + // begin photo analysis + auto process = std::bind(&BatchFaceDetector::processPhotos, this, std::placeholders::_1); + m_photosProcessingProcess = m_core->getTaskExecutor().add(process); + } +} + + +APhotoDataModel* BatchFaceDetector::photosModel() const +{ + return m_photosModel; +} + + +ICoreFactoryAccessor* BatchFaceDetector::core() const +{ + return m_core; +} + + +Database::IDatabase* BatchFaceDetector::db() const +{ + return m_dbClient? &m_dbClient->db(): nullptr; +} + + +int BatchFaceDetector::rowCount(const QModelIndex& parent) const +{ + return parent.isValid()? 0: static_cast(m_faces.size()); +} + + +QVariant BatchFaceDetector::data(const QModelIndex& idx, int role) const +{ + assert(idx.row() < static_cast(m_faces.size())); + assert(idx.column() == 0); + + const size_t row = static_cast(idx.row()); + + if (role == Qt::DisplayRole) + return m_faces[row].first->name(); + else if (role == Qt::DecorationRole) + return m_faces[row].second; + else + return {}; +} + + +bool BatchFaceDetector::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + assert(idx.row() < static_cast(m_faces.size())); + assert(idx.column() == 0); + const QString name = value.toString(); + const size_t row = static_cast(idx.row()); + + auto& face = m_faces[row].first; + + if (role == Qt::EditRole && name != face->name()) + { + face->setName(name); + + return true; + } + else + return false; +} + + +void BatchFaceDetector::accept(int idx) +{ + m_faces[safe_cast(idx)].first->store(); + removeFace(idx); +} + + +void BatchFaceDetector::drop(int idx) +{ + removeFace(idx); +} + + +ITaskExecutor::ProcessCoroutine BatchFaceDetector::processPhotos(ITaskExecutor::IProcessSupervisor* supervisor) +{ + while(supervisor->keepWorking()) + { + const auto id_opt = getNextId(); + + if (id_opt) + { + const auto id = *id_opt; + + // no data in db, generate + runOn(m_core->getTaskExecutor(), [id, this, supervisor]() mutable + { + loadFacesFromPhoto(id); + supervisor->resume(); // restore this task after faces were loaded from photo + }); + } + + co_yield ITaskExecutor::ProcessState::Suspended; + } + + // face scanning is done, db won't be needed anymore, release it + m_dbClient.reset(); +} + + +void BatchFaceDetector::appendFaces(std::vector&& faces) +{ + if (faces.empty() == false) + { + const int newFaces = static_cast(faces.size()); + const int existingFaces = static_cast(m_faces.size()); + beginInsertRows({}, existingFaces, existingFaces + newFaces - 1); + m_faces.insert(m_faces.end(), std::make_move_iterator(faces.begin()), std::make_move_iterator(faces.end())); + endInsertRows(); + } +} + + +void BatchFaceDetector::removeFace(int idx) +{ + beginRemoveRows({}, idx, idx); + m_faces.erase(m_faces.begin() + idx); + endRemoveRows(); +} + + +void BatchFaceDetector::newPhotos(const QModelIndex &, int first, int last) +{ + std::lock_guard _(m_idsMtx); + + for(int row = first; row <= last; row++) + { + const Photo::Id id(m_photosModel->data(m_photosModel->index(row, 0), APhotoDataModel::PhotoIdRole)); + m_ids.push_back(id); + } + + // make sure processing process is not suspended + m_photosProcessingProcess->resume(); +} + + +std::optional BatchFaceDetector::getNextId() +{ + std::lock_guard lk(m_idsMtx); + + std::optional result; + + if (m_ids.empty() == false) + { + result = m_ids.front(); + m_ids.pop_front(); + } + + return result; +} + + +void BatchFaceDetector::loadFacesFromPhoto(const Photo::Id& id) +{ + auto faces = m_faceEditor->getFacesFor(id); + std::vector facesDetails; + + // prepare details for model + for (auto& face: faces) + { + const auto faceImg = face->image()->copy(face->rect()); + facesDetails.emplace_back(std::move(face), faceImg); + } + + invokeMethod(this, &BatchFaceDetector::appendFaces, std::move(facesDetails)); +} diff --git a/src/gui/desktop/utils/batch_face_detector.hpp b/src/gui/desktop/utils/batch_face_detector.hpp new file mode 100644 index 0000000000..8b08c30a30 --- /dev/null +++ b/src/gui/desktop/utils/batch_face_detector.hpp @@ -0,0 +1,65 @@ + +#ifndef BATCH_FACE_DETECTOR_HPP_INCLUDED +#define BATCH_FACE_DETECTOR_HPP_INCLUDED + +#include +#include + +#include +#include +#include +#include +#include + +#include "models/aphoto_data_model.hpp" +#include "people_editor.hpp" + + +class BatchFaceDetector: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(ICoreFactoryAccessor* core WRITE setCore READ core REQUIRED) + Q_PROPERTY(Database::IDatabase* db WRITE setDB READ db REQUIRED) + Q_PROPERTY(APhotoDataModel* photos_model WRITE setPhotosModel READ photosModel) + +public: + BatchFaceDetector(); + ~BatchFaceDetector(); + + void setPhotosModel(APhotoDataModel *); + void setCore(ICoreFactoryAccessor *); + void setDB(Database::IDatabase *); + + APhotoDataModel* photosModel() const; + ICoreFactoryAccessor* core() const; + Database::IDatabase* db() const; + + int rowCount(const QModelIndex &) const override; + QVariant data(const QModelIndex &, int) const override; + bool setData(const QModelIndex &, const QVariant &, int) override; + + Q_INVOKABLE void accept(int); + Q_INVOKABLE void drop(int); + +private: + using Face = std::pair, QImage>; + + std::deque m_ids; + std::mutex m_idsMtx; + std::vector m_faces; + std::unique_ptr m_logger; + std::unique_ptr m_dbClient; + lazy_ptr m_faceEditor; + APhotoDataModel* m_photosModel = nullptr; + ICoreFactoryAccessor* m_core = nullptr; + std::shared_ptr m_photosProcessingProcess; + + ITaskExecutor::ProcessCoroutine processPhotos(ITaskExecutor::IProcessSupervisor *); + void appendFaces(std::vector &&); + void removeFace(int); + void newPhotos(const QModelIndex &, int, int); + std::optional getNextId(); + void loadFacesFromPhoto(const Photo::Id &); +}; + +#endif diff --git a/src/gui/desktop/utils/collection_scanner.cpp b/src/gui/desktop/utils/collection_scanner.cpp index f7a616e671..476fb6ebca 100644 --- a/src/gui/desktop/utils/collection_scanner.cpp +++ b/src/gui/desktop/utils/collection_scanner.cpp @@ -86,13 +86,13 @@ void CollectionScanner::scan() photoDeltas.reserve(photos.size()); for(const Photo::Id& id: photos) - photoDeltas.push_back(backend.getPhotoDelta(id, {Photo::Field::Path})); + photoDeltas.push_back(backend.getPhotoDelta(id)); std::vector missingPhotoDeltas; missingPhotoDeltas.reserve(missingPhotos.size()); for(const Photo::Id& id: missingPhotos) - missingPhotoDeltas.push_back(backend.getPhotoDelta(id, {Photo::Field::Path})); + missingPhotoDeltas.push_back(backend.getPhotoDelta(id)); db_callback(photoDeltas, missingPhotoDeltas); }); diff --git a/src/gui/desktop/utils/groups_manager.cpp b/src/gui/desktop/utils/groups_manager.cpp index 6cfcd97b14..602205ad80 100644 --- a/src/gui/desktop/utils/groups_manager.cpp +++ b/src/gui/desktop/utils/groups_manager.cpp @@ -89,14 +89,15 @@ void GroupsManager::group(Database::IDatabase& database, QPromise&& promis const Group::Type& type = group.type; // copy details of first member to representative - const Photo::Data firstPhoto = backend.getPhoto(photos[0]); + const auto firstPhoto = backend.getPhotoDelta(photos[0]); - auto it = firstPhoto.flags.find(Photo::FlagsE::StagingArea); - const Photo::FlagValues flags = { {Photo::FlagsE::StagingArea, it == firstPhoto.flags.end()? 0: it->second} }; + const auto firstPhotoFlags = firstPhoto.get(); + auto it = firstPhotoFlags.find(Photo::FlagsE::StagingArea); + const Photo::FlagValues flags = { {Photo::FlagsE::StagingArea, it == firstPhotoFlags.end()? 0: it->second} }; Photo::DataDelta data; data.insert(representativePath); - data.insert(firstPhoto.tags); + data.insert(firstPhoto.get()); data.insert(flags); // store representative photo diff --git a/src/gui/desktop/utils/implementation/people_editor_impl.hpp b/src/gui/desktop/utils/implementation/people_editor_impl.hpp new file mode 100644 index 0000000000..468e45ccd7 --- /dev/null +++ b/src/gui/desktop/utils/implementation/people_editor_impl.hpp @@ -0,0 +1,76 @@ + +#ifndef PEOPLE_EDITOR_IMPL_HPP_INCLUDED +#define PEOPLE_EDITOR_IMPL_HPP_INCLUDED + +#include + +#include +#include + +#include "../people_editor.hpp" + +struct IRecognizePerson +{ + virtual ~IRecognizePerson() = default; + virtual PersonName recognize(const PersonFullInfo &) = 0; +}; + + +struct Face: public IFace +{ + Face(const Photo::Id& id, const PersonFullInfo& fi, std::shared_ptr recognizer, std::shared_ptr image, std::shared_ptr dbClient) + : m_faceInfo(fi) + , m_recognizer(recognizer) + , m_image(image) + , m_dbClient(dbClient) + , m_id(id) + {} + + const QRect& rect() const override + { + return m_faceInfo.position; + } + + const QString& name() const override + { + return m_faceInfo.name.name(); + } + + const PersonName& person() const override + { + return m_faceInfo.name; + } + + const OrientedImage& image() const override + { + return *m_image; + } + + void setName(const QString& name) override + { + m_faceInfo.name = PersonName(name); + } + + bool recognize() override + { + m_faceInfo.name = m_recognizer->recognize(m_faceInfo); + + return m_faceInfo.name.id().valid(); + } + + void store() override + { + m_dbClient->db().exec([id = m_id, pfi = m_faceInfo](Database::IBackend& backend) + { + backend.peopleInformationAccessor().store(id, pfi); + }); + } + + PersonFullInfo m_faceInfo; + std::shared_ptr m_recognizer; + std::shared_ptr m_image; + std::shared_ptr m_dbClient; + Photo::Id m_id; +}; + +#endif diff --git a/src/gui/desktop/utils/people_editor.cpp b/src/gui/desktop/utils/people_editor.cpp new file mode 100644 index 0000000000..e3f44a54d5 --- /dev/null +++ b/src/gui/desktop/utils/people_editor.cpp @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2020 Michał Walenciak + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "implementation/people_editor_impl.hpp" + +#include "people_editor.hpp" + + +using namespace Database::CommonGeneralFlags; + + +namespace +{ + Person::Fingerprint average_fingerprint(const std::vector& faces) + { + if (faces.empty()) + return {}; + + Person::Fingerprint avg_face; + + for(const PersonFingerprint& fingerprint: faces) + avg_face += fingerprint.fingerprint(); + + avg_face /= static_cast(faces.size()); + + return avg_face; + } + + bool wasPhotoAnalyzedAndHasNoFaces(Database::IDatabase& db, const Photo::Id& ph_id) + { + return evaluate(db, [ph_id](Database::IBackend& backend) + { + const auto analysisState = backend.get(ph_id, FacesAnalysisState); + + return analysisState? *analysisState == static_cast(FacesAnalysisType::AnalysedAndNotFound): false; + }); + } + + QString pathFor(Database::IDatabase& db, const Photo::Id& id) + { + return evaluate(db, [id](Database::IBackend& backend) + { + auto photo = backend.getPhotoDelta(id, {Photo::Field::Path}); + + return photo.get(); + }); + } + + std::vector detectFaces(const OrientedImage& image, const ILogger& logger) + { + return FaceRecognition(logger).fetchFaces(image); + } + + void calculateMissingFingerprints(std::vector& faces, const OrientedImage& image, const ILogger& logger) + { + FaceRecognition face_recognition(logger); + for (auto& faceInfo: faces) + if (faceInfo.fingerprint.id().valid() == false) + { + const auto fingerprint = face_recognition.getFingerprint(image, faceInfo.position); + + faceInfo.fingerprint = PersonFingerprint(fingerprint); + } + } + + FaceEditor::PeopleData findPeople(const OrientedImage& image, const Photo::Id& id, const ILogger& logger) + { + FaceEditor::PeopleData data(id); + + // analyze photo - look for faces + const auto detected_faces = detectFaces(image, logger); + auto& people = data.get(); + std::ranges::transform(detected_faces, std::back_inserter(people), [](const QRect& rect) + { + PersonFullInfo pfi; + pfi.position = rect; + + return pfi; + }); + + //calculate fingerprints + calculateMissingFingerprints(people, image, logger); + + return data; + } + + void sortFaces(std::vector& faces) + { + // sort faces so they appear from left to right + std::sort(faces.begin(), faces.end(), [](const PersonFullInfo& lhs, const PersonFullInfo& rhs) { + const auto& lhs_face = lhs.position; + const auto& rhs_face = rhs.position; + + if (lhs_face.left() < rhs_face.left()) // lhs if left to rhs? - in order + return true; + else if (lhs_face.left() > rhs_face.right()) // lhs is right to rhs? - not in order + return false; + else + return lhs_face.top() < rhs_face.top(); // when in line - lhs needs to be above + }); + } +} + +class Recognizer: public IRecognizePerson +{ + public: + Recognizer(Database::IDatabase& db, const ILogger& logger) + : m_logger(logger.subLogger("Recognizer")) + { + fetchPeopleAndFingerprints(db); + } + + PersonName recognize(const PersonFullInfo& pi) override + { + FaceRecognition face_recognition(*m_logger); + PersonName result; + + const std::vector& known_fingerprints = std::get<0>(m_fingerprints); + + if (pi.name.name().isEmpty()) + { + const int pos = face_recognition.recognize(pi.fingerprint.fingerprint(), known_fingerprints); + + if (pos >= 0) + { + const std::vector& known_people = std::get<1>(m_fingerprints); + const Person::Id found_person = known_people[safe_cast(pos)]; + result = m_people.at(found_person); + } + } + + return result; + } + + private: + using Fingerprints = std::tuple, std::vector>; + using People = std::map; + Fingerprints m_fingerprints; + People m_people; + std::unique_ptr m_logger; + + void fetchPeopleAndFingerprints(Database::IDatabase& db) + { + typedef std::tuple, std::vector> Result; + + evaluate(db, [this](Database::IBackend& backend) + { + std::vector people_fingerprints; + std::vector people; + + const auto all_people = backend.peopleInformationAccessor().listPeople(); + + for(const auto& person: all_people) + { + m_people.emplace(person.id(), person); + const auto fingerprints = backend.peopleInformationAccessor().fingerprintsFor(person.id()); + + if (fingerprints.empty() == false) + { + people_fingerprints.push_back(average_fingerprint(fingerprints)); + people.push_back(person.id()); + } + } + + m_fingerprints = std::tuple(people_fingerprints, people); + }); + } +}; + + +FaceEditor::FaceEditor(Database::IDatabase& db, ICoreFactoryAccessor& core, const ILogger& logger) + : m_logger(logger.subLogger("FaceEditor")) + , m_recognizer(std::make_shared(db, *m_logger)) + , m_db(db) + , m_core(core) +{ + +} + + +std::vector> FaceEditor::getFacesFor(const Photo::Id& id) +{ + const QString path = pathFor(m_db, id); + const QFileInfo pathInfo(path); + const QString full_path = pathInfo.absoluteFilePath(); + auto image = std::make_shared(m_core.getExifReaderFactory().get(), full_path); + + auto faces = findFaces(*image, id); + + std::shared_ptr dbClient = m_db.attach("FaceEditor"); + std::vector> result; + + std::ranges::transform(faces.get(), std::back_inserter(result), [id = faces.getId(), &image, &dbClient, recognizer = m_recognizer](const auto& personData) + { + return std::make_unique(id, personData, recognizer, image, dbClient); + }); + + return result; +} + + +FaceEditor::PeopleData FaceEditor::findFaces(const OrientedImage& image, const Photo::Id& id) +{ + FaceEditor::PeopleData result(id); + + const bool facesNotFound = wasPhotoAnalyzedAndHasNoFaces(m_db, id); + + // photo not analyzed yet (no records in db) or analyzed and we have data in db + if (facesNotFound == false) + { + result = evaluate(m_db, [id](Database::IBackend& backend) + { + return backend.getPhotoDelta(id); + }); + + // no data in db + if (result.get().empty()) + { + result = findPeople(image, id, *m_logger); + + if (result.get().empty()) + { + // mark photo as one without faces + m_db.exec([id](Database::IBackend& backend) + { + backend.set( + id, + FacesAnalysisState, + FacesAnalysisType::AnalysedAndNotFound); + }); + } + else + { + // store face location and fingerprint in db and update ids + evaluate(m_db, [&result](Database::IBackend& backend) + { + backend.update({result}); + + // refetch data to get updated ids + result = backend.getPhotoDelta(result.getId()); + }); + } + } + } + + sortFaces(result.get()); + + return result; +} diff --git a/src/gui/desktop/utils/people_editor.hpp b/src/gui/desktop/utils/people_editor.hpp new file mode 100644 index 0000000000..6aebe0dac8 --- /dev/null +++ b/src/gui/desktop/utils/people_editor.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Michał Walenciak + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PEOPLEMANIPULATOR_HPP +#define PEOPLEMANIPULATOR_HPP + +#include +#include +#include +#include +#include +#include + + +class Recognizer; + +class IFace +{ +public: + virtual ~IFace() = default; + + virtual const QRect& rect() const = 0; + virtual const QString& name() const = 0; + virtual const PersonName& person() const = 0; + virtual const OrientedImage& image() const = 0; + + virtual void setName(const QString &) = 0; + virtual bool recognize() = 0; + virtual void store() = 0; +}; + + +class FaceEditor +{ +public: + using PeopleData = Photo::ExplicitDelta; + + FaceEditor(Database::IDatabase &, ICoreFactoryAccessor &, const ILogger &); + + std::vector> getFacesFor(const Photo::Id &); + +private: + std::unique_ptr m_logger; + std::shared_ptr m_recognizer; + Database::IDatabase& m_db; + ICoreFactoryAccessor& m_core; + + PeopleData findFaces(const OrientedImage &, const Photo::Id &); +}; + +#endif diff --git a/src/gui/desktop/utils/people_manipulator.cpp b/src/gui/desktop/utils/people_manipulator.cpp deleted file mode 100644 index 7857ce30b9..0000000000 --- a/src/gui/desktop/utils/people_manipulator.cpp +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Copyright (C) 2020 Michał Walenciak - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "people_manipulator.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include - - -namespace -{ - Person::Fingerprint average_fingerprint(const std::vector& faces) - { - if (faces.empty()) - return {}; - - Person::Fingerprint avg_face; - - for(const PersonFingerprint& fingerprint: faces) - avg_face += fingerprint.fingerprint(); - - avg_face /= static_cast(faces.size()); - - return avg_face; - } -} - - -PeopleManipulator::PeopleManipulator(const Photo::Id& pid, Database::IDatabase& db, ICoreFactoryAccessor& core) - : m_pid(pid) - , m_core(core) - , m_db(db) -{ - findFaces(); -} - - -PeopleManipulator::~PeopleManipulator() -{ - m_callback_ctrl.invalidate(); -} - - -std::size_t PeopleManipulator::facesCount() const -{ - return m_faces.size(); -} - - -const QString& PeopleManipulator::name(std::size_t n) const -{ - return m_faces[n].person.name(); -} - - -const QRect& PeopleManipulator::position(std::size_t n) const -{ - return m_faces[n].face.rect; -} - - -QSize PeopleManipulator::photoSize() const -{ - return m_image->size(); -} - - -void PeopleManipulator::setName(std::size_t n, const QString& name) -{ - const QString trimmed_name = name.trimmed(); - - if (n < m_faces.size() && m_faces[n].person.name() != trimmed_name) - { - PersonName new_name(trimmed_name); - m_faces[n].person = new_name; - } -} - - -void PeopleManipulator::store() -{ - store_people_names(); - store_fingerprints(); - - // update names assigned to face locations - for (auto& face: m_faces) - face.face.p_id = face.person.id(); - - // update fingerprints assigned to face locations - for (auto& face: m_faces) - if (face.face.f_id.valid() == false) - face.face.f_id = face.fingerprint.id(); - - store_people_information(); -} - - -void PeopleManipulator::runOnThread(void (PeopleManipulator::*method)()) -{ - auto task = std::bind(method, this); - auto safe_task = m_callback_ctrl.make_safe_callback<>(task); - auto& executor = m_core.getTaskExecutor(); - - runOn(executor, safe_task, "PeopleManipulator"); -} - - -void PeopleManipulator::findFaces() -{ - runOnThread(&PeopleManipulator::findFaces_thrd); -} - - -void PeopleManipulator::findFaces_thrd() -{ - QVector result; - - const QString path = pathFor(m_pid); - const QFileInfo pathInfo(path); - const QString full_path = pathInfo.absoluteFilePath(); - m_image = OrientedImage(m_core.getExifReaderFactory().get(), full_path); - - const std::vector list_of_faces = fetchFacesFromDb(); - - if (list_of_faces.empty()) - { - FaceRecognition face_recognition(&m_core); - const auto faces = face_recognition.fetchFaces(full_path); - - for(const QRect& face: faces) - result.append(face); - } - else - { - result.reserve(static_cast(list_of_faces.size())); - - std::copy(list_of_faces.cbegin(), list_of_faces.cend(), std::back_inserter(result)); - } - - std::sort(result.begin(), result.end(), [](const QRect& lhs, const QRect& rhs) { - if (lhs.right() < rhs.left()) // lhs if left to rhs? - in order - return true; - else if (lhs.left() > rhs.right()) // lhs is right to rhs? - not in order - return false; - else - return lhs.top() < rhs.top(); // when in line - lhs needs to be above - }); - - invokeMethod(this, &PeopleManipulator::findFaces_result, result); -} - - -void PeopleManipulator::findFaces_result(const QVector& faces) -{ - m_faces.reserve(faces.size()); - - std::copy(faces.cbegin(), faces.cend(), std::back_inserter(m_faces)); - - for (auto& face: m_faces) - face.face.ph_id = m_pid; - - recognizeFaces(); -} - - -void PeopleManipulator::recognizeFaces() -{ - runOnThread(&PeopleManipulator::recognizeFaces_thrd); -} - - -void PeopleManipulator::recognizeFaces_thrd_fetch_from_db() -{ - const std::vector peopleData = fetchPeopleFromDb(); - - for (FaceInfo& faceInfo: m_faces) - { - // check if we have data for given face rect in db - auto person_it = std::find_if(peopleData.cbegin(), peopleData.cend(), [faceInfo](const PersonInfo& pi) - { - return pi.rect == faceInfo.face.rect; - }); - - if (person_it != peopleData.cend()) // rect matches - { - if (person_it->p_id.valid()) - faceInfo.person = personData(person_it->p_id); // fill name - - faceInfo.face = *person_it; - } - } - - // collect faces and try to access theirs fingerprints - std::vector faces_ids; - - for (FaceInfo& faceInfo: m_faces) - if (faceInfo.face.id.valid()) - faces_ids.push_back(faceInfo.face.id); - - const auto fingerprints = fetchFingerprints(faces_ids); - - for (FaceInfo& faceInfo: m_faces) - if (faceInfo.face.id.valid()) - { - auto it = fingerprints.find(faceInfo.face.id); - - if (it != fingerprints.end()) - faceInfo.fingerprint = it->second; - } -} - - -void PeopleManipulator::recognizeFaces_thrd_calculate_missing_fingerprints() -{ - for (FaceInfo& faceInfo: m_faces) - if (faceInfo.fingerprint.id().valid() == false) - { - FaceRecognition face_recognition(&m_core); - - const auto fingerprint = face_recognition.getFingerprint(m_image, faceInfo.face.rect); - - faceInfo.fingerprint = fingerprint; - } -} - - -void PeopleManipulator::recognizeFaces_thrd_recognize_people() -{ - FaceRecognition face_recognition(&m_core); - const auto people_fingerprints = fetchPeopleAndFingerprints(); - const std::vector& known_fingerprints = std::get<0>(people_fingerprints); - - for (FaceInfo& faceInfo: m_faces) - if (faceInfo.person.name().isEmpty()) - { - const int pos = face_recognition.recognize(faceInfo.fingerprint.fingerprint(), known_fingerprints); - - if (pos >=0) - { - const std::vector& known_people = std::get<1>(people_fingerprints); - const Person::Id found_person = known_people[pos]; - faceInfo.person = personData(found_person); - } - } -} - - -void PeopleManipulator::recognizeFaces_thrd() -{ - recognizeFaces_thrd_fetch_from_db(); - - const bool missing_fingerprints = - std::any_of(m_faces.cbegin(), - m_faces.cend(), - [](const auto& face) - { - return face.fingerprint.id().valid() == false; - }); - - if (missing_fingerprints) - { - recognizeFaces_thrd_calculate_missing_fingerprints(); - recognizeFaces_thrd_recognize_people(); - } - - invokeMethod(this, &PeopleManipulator::recognizeFaces_result); -} - - -void PeopleManipulator::recognizeFaces_result() -{ - emit facesAnalyzed(); -} - - -void PeopleManipulator::store_people_names() -{ - auto nameChanged = [](const PeopleManipulator::FaceInfo& faceInfo) { - // no person id and name is not empty? - return faceInfo.person.id().valid() == false && faceInfo.person.name().isEmpty() == false; - }; - - const bool anyNameChanged = std::ranges::any_of(m_faces, nameChanged); - - if (anyNameChanged) - { - const std::vector people = fetchPeople(); - - // make sure each name is known (exists in db) - for (auto& face: m_faces) - if (nameChanged(face)) - { - const QString& name = face.person.name(); - - auto it = std::find_if(people.cbegin(), people.cend(), [name](const PersonName& d) - { - return d.name() == name; - }); - - if (it == people.cend()) // new name, store it in db - face.person = storeNewPerson(name); - else - face.person = *it; - } - } -} - - -void PeopleManipulator::store_fingerprints() -{ - for (auto& face: m_faces) - if (face.fingerprint.id().valid() == false) - { - const PersonFingerprint::Id fid = - evaluate(m_db, [fingerprint = face.fingerprint](Database::IBackend& backend) - { - return backend.peopleInformationAccessor().store(fingerprint); - }); - - const PersonFingerprint fingerprint(fid, face.fingerprint.fingerprint()); - face.fingerprint = fingerprint; - } -} - - -void PeopleManipulator::store_people_information() -{ - for (const auto& face: m_faces) - { - const PersonInfo& faceInfo = face.face; - const PersonFingerprint& fingerprint = face.fingerprint; - - m_db.exec([faceInfo, fingerprint](Database::IBackend& backend) - { - backend.peopleInformationAccessor().store(faceInfo); - }); - } -} - - -std::vector PeopleManipulator::fetchFacesFromDb() const -{ - return evaluate(Database::IBackend &)> - (m_db, [id = m_pid](Database::IBackend& backend) - { - std::vector faces; - - const auto people = backend.peopleInformationAccessor().listPeople(id); - for(const auto& person: people) - if (person.rect.isValid()) - faces.push_back(person.rect); - - return faces; - }); -} - - -std::vector PeopleManipulator::fetchPeopleFromDb() const -{ - return evaluate(Database::IBackend &)> - (m_db, [id = m_pid](Database::IBackend& backend) - { - auto people = backend.peopleInformationAccessor().listPeople(id); - - return people; - }); -} - - -std::tuple, std::vector> PeopleManipulator::fetchPeopleAndFingerprints() const -{ - typedef std::tuple, std::vector> Result; - - return evaluate(m_db, [](Database::IBackend& backend) - { - std::vector people_fingerprints; - std::vector people; - - const auto all_people = backend.peopleInformationAccessor().listPeople(); - for(const auto& person: all_people) - { - const auto fingerprints = backend.peopleInformationAccessor().fingerprintsFor(person.id()); - - if (fingerprints.empty() == false) - { - people_fingerprints.push_back(average_fingerprint(fingerprints)); - people.push_back(person.id()); - } - } - - return std::tuple(people_fingerprints, people); - }); -} - - -std::map PeopleManipulator::fetchFingerprints(const std::vector& ids) const -{ - typedef std::map Result; - - return evaluate(m_db, [ids](Database::IBackend& backend) - { - const Result result = backend.peopleInformationAccessor().fingerprintsFor(ids); - - return result; - }); -} - - -std::vector PeopleManipulator::fetchPeople() const -{ - return evaluate(Database::IBackend &)>(m_db, [](Database::IBackend& backend) - { - auto people = backend.peopleInformationAccessor().listPeople(); - - return people; - }); -} - - -PersonName PeopleManipulator::personData(const Person::Id& id) const -{ - const PersonName person = evaluate - (m_db, [id](Database::IBackend& backend) - { - const auto people = backend.peopleInformationAccessor().person(id); - - return people; - }); - - return person; -} - - -PersonName PeopleManipulator::storeNewPerson(const QString& name) const -{ - const PersonName person = evaluate - (m_db, [name](Database::IBackend& backend) - { - const PersonName d(Person::Id(), name); - const auto id = backend.peopleInformationAccessor().store(d); - return PersonName(id, name); - }); - - return person; -} - - -QString PeopleManipulator::pathFor(const Photo::Id& id) const -{ - return evaluate(m_db, [id](Database::IBackend& backend) - { - auto photo = backend.getPhotoDelta(id, {Photo::Field::Path}); - - return photo.get(); - }); -} diff --git a/src/gui/desktop/utils/people_manipulator.hpp b/src/gui/desktop/utils/people_manipulator.hpp deleted file mode 100644 index 3e59970495..0000000000 --- a/src/gui/desktop/utils/people_manipulator.hpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2020 Michał Walenciak - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef PEOPLEMANIPULATOR_HPP -#define PEOPLEMANIPULATOR_HPP - -#include -#include -#include -#include - - -struct ICoreFactoryAccessor; - - -class PeopleManipulator: public QObject -{ - Q_OBJECT - - public: - PeopleManipulator(const Photo::Id &, Database::IDatabase &, ICoreFactoryAccessor &); - ~PeopleManipulator(); - - std::size_t facesCount() const; - const QString& name(std::size_t) const; - const QRect& position(std::size_t) const; - QSize photoSize() const; - - void setName(std::size_t, const QString &); - void store(); - - signals: - void facesAnalyzed() const; - - private: - struct FaceInfo - { - PersonInfo face; - PersonName person; - PersonFingerprint fingerprint; - - FaceInfo(const QRect& r) - { - face.rect = r; - } - }; - - safe_callback_ctrl m_callback_ctrl; - std::vector m_faces; - OrientedImage m_image; - Photo::Id m_pid; - ICoreFactoryAccessor& m_core; - Database::IDatabase& m_db; - - void runOnThread(void (PeopleManipulator::*)()); - - void findFaces(); - void findFaces_thrd(); - void findFaces_result(const QVector &); - - void recognizeFaces(); - void recognizeFaces_thrd_fetch_from_db(); - void recognizeFaces_thrd_calculate_missing_fingerprints(); - void recognizeFaces_thrd_recognize_people(); - void recognizeFaces_thrd(); - void recognizeFaces_result(); - - void store_people_names(); - void store_fingerprints(); - void store_people_information(); - - std::vector fetchFacesFromDb() const; - std::vector fetchPeopleFromDb() const; - std::tuple, std::vector> fetchPeopleAndFingerprints() const; - std::map fetchFingerprints(const std::vector& ids) const; - std::vector fetchPeople() const; - PersonName personData(const Person::Id& id) const; - PersonName storeNewPerson(const QString& name) const; - QString pathFor(const Photo::Id& id) const; -}; - -#endif // PEOPLEMANIPULATOR_HPP diff --git a/src/gui/desktop/utils/qml_setup.cpp b/src/gui/desktop/utils/qml_setup.cpp index 810886d612..913617fa15 100644 --- a/src/gui/desktop/utils/qml_setup.cpp +++ b/src/gui/desktop/utils/qml_setup.cpp @@ -8,9 +8,11 @@ #include "models/duplicates_model.hpp" #include "models/faces_model.hpp" #include "models/photos_data_guesser.hpp" +#include "models/qml_flat_model.hpp" #include "models/series_model.hpp" #include "utils/variant_display.hpp" #include "inotifications.hpp" +#include "batch_face_detector.hpp" void register_qml_types() @@ -21,9 +23,11 @@ void register_qml_types() qmlRegisterType("photo_broom.models", 1, 0, "PhotoPropertiesModel"); qmlRegisterType("photo_broom.models", 1, 0, "DuplicatesModel"); qmlRegisterType("photo_broom.models", 1, 0, "FacesModel"); + qmlRegisterType("photo_broom.models", 1, 0, "QMLFlatModel"); qmlRegisterType("photo_broom.models", 1, 0, "SeriesModel"); qmlRegisterType("photo_broom.models", 1, 0, "TagsModel"); qmlRegisterType("photo_broom.utils", 1, 0, "Variant"); + qmlRegisterType("photo_broom.utils", 1, 0, "BatchFaceDetector"); qRegisterMetaType("QAbstractItemModel*"); qmlRegisterUncreatableMetaObject(Photo::staticMetaObject, "photo_broom.enums", 1, 0, "PhotoEnums", "Error: only enums"); qmlRegisterUncreatableMetaObject(Tag::staticMetaObject, "photo_broom.enums", 1, 0, "TagEnums", "Error: only enums"); diff --git a/src/gui/desktop/utils/thumbnail_manager.cpp b/src/gui/desktop/utils/thumbnail_manager.cpp index 84ea2b4e18..0b28c223b1 100644 --- a/src/gui/desktop/utils/thumbnail_manager.cpp +++ b/src/gui/desktop/utils/thumbnail_manager.cpp @@ -25,6 +25,10 @@ #include #include "ithumbnails_cache.hpp" +namespace +{ + constexpr auto BlobType = "thumbnail"; +} using namespace std::placeholders; @@ -65,7 +69,7 @@ void ThumbnailManager::fetch(const Photo::Id& id, const QSize& desired_size, con if (m_db) // load thumbnail from db dbThumb = evaluate(*m_db, [id](Database::IBackend& backend) { - return backend.readBlob(id, Database::IBackend::BlobType::Thumbnail); + return backend.readBlob(id, BlobType); }); QImage baseThumbnail; @@ -76,7 +80,7 @@ void ThumbnailManager::fetch(const Photo::Id& id, const QSize& desired_size, con // load path to photo const Photo::DataDelta photoData = evaluate(*m_db, [id](Database::IBackend& backend) { - return backend.getPhotoDelta(id, {Photo::Field::Path}); + return backend.getPhotoDelta(id); }); // generate base thumbnail @@ -92,7 +96,7 @@ void ThumbnailManager::fetch(const Photo::Id& id, const QSize& desired_size, con execute(*m_db, [id, dbThumb](Database::IBackend& backend) { - backend.writeBlob(id, Database::IBackend::BlobType::Thumbnail, dbThumb); + backend.writeBlob(id, BlobType, dbThumb); }); } } diff --git a/src/gui/images/images.qrc b/src/gui/images/images.qrc index 6596a1b398..a9beeacb42 100644 --- a/src/gui/images/images.qrc +++ b/src/gui/images/images.qrc @@ -9,8 +9,10 @@ half_star.svg missing.svg new.svg + ok.svg paper.svg star.svg + trash.svg video.svg diff --git a/src/gui/images/ok.svg b/src/gui/images/ok.svg new file mode 100644 index 0000000000..6c427ff57e --- /dev/null +++ b/src/gui/images/ok.svg @@ -0,0 +1,64 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/gui/images/trash.svg b/src/gui/images/trash.svg new file mode 100644 index 0000000000..0266b74032 --- /dev/null +++ b/src/gui/images/trash.svg @@ -0,0 +1,69 @@ + +image/svg+xml + + + + + diff --git a/src/gui/unit_tests/utils/thumbnails_manager_tests.cpp b/src/gui/unit_tests/utils/thumbnails_manager_tests.cpp index c0dc7cecc0..bea53bbb31 100644 --- a/src/gui/unit_tests/utils/thumbnails_manager_tests.cpp +++ b/src/gui/unit_tests/utils/thumbnails_manager_tests.cpp @@ -21,6 +21,11 @@ using testing::NiceMock; using testing::Return; using testing::ReturnRef; +namespace +{ + const QString ThumbnailBlob = "thumbnail"; +} + struct NullCache: IThumbnailsCache { std::optional find(const Photo::Id &, const IThumbnailsCache::ThumbnailParameters &) override @@ -141,7 +146,7 @@ TEST_F(ThumbnailManagerTest, generatedThumbnailsIsBeingCached) .WillRepeatedly(Return(img)); // database behavior (thumbnail storage) - EXPECT_CALL(backend, writeBlob(id, Database::IBackend::BlobType::Thumbnail, _)).Times(1); + EXPECT_CALL(backend, writeBlob(id, ThumbnailBlob, _)).Times(1); ThumbnailManager tm(executor, generator, cache, EmptyLogger{}); tm.setDatabaseCache(&db); @@ -161,7 +166,7 @@ TEST_F(ThumbnailManagerTest, doNotGenerateThumbnailFoundInCache) NiceMock cache; EXPECT_CALL(cache, find(id, IThumbnailsCache::ThumbnailParameters(QSize(height, height)))).Times(1).WillOnce(Return(img)); - EXPECT_CALL(backend, writeBlob(id, Database::IBackend::BlobType::Thumbnail, _)).Times(0); + EXPECT_CALL(backend, writeBlob(id, ThumbnailBlob, _)).Times(0); MockThumbnailsGenerator generator; diff --git a/src/photos_crawler/CMakeLists.txt b/src/photos_crawler/CMakeLists.txt index 1093175249..1e97f0d653 100644 --- a/src/photos_crawler/CMakeLists.txt +++ b/src/photos_crawler/CMakeLists.txt @@ -4,8 +4,6 @@ find_package(Threads) include(GenerateExportHeader) -include_directories(${CMAKE_BINARY_DIR}/exports) - set(ANALYZER_SOURCES default_analyzers/file_analyzer.cpp default_filesystem_scanners/filesystemscanner.cpp diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 4ef9c31a21..3542797da2 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -18,7 +18,6 @@ set_target_properties(plugins PROPERTIES POSITION_INDEPENDENT_CODE ON) target_include_directories(plugins PRIVATE - ${CMAKE_BINARY_DIR}/exports ${CMAKE_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE diff --git a/src/plugins/implementation/plugin_loader.cpp b/src/plugins/implementation/plugin_loader.cpp index 1f080852a8..ae32f99ab6 100644 --- a/src/plugins/implementation/plugin_loader.cpp +++ b/src/plugins/implementation/plugin_loader.cpp @@ -26,7 +26,7 @@ #include #include -#include +#include #include #include diff --git a/src/unit_tests_utils/CMakeLists.txt b/src/unit_tests_utils/CMakeLists.txt index bb7f89cec9..2fb5a8bf82 100644 --- a/src/unit_tests_utils/CMakeLists.txt +++ b/src/unit_tests_utils/CMakeLists.txt @@ -3,6 +3,7 @@ include(${CMAKE_SOURCE_DIR}/cmake/functions.cmake) stringify_file(${CMAKE_CURRENT_BINARY_DIR}/db_for_series_detection.json.hpp ${CMAKE_CURRENT_SOURCE_DIR}/sample_dbs/db_for_series_detection.json "const char* db" "SeriesDB") stringify_file(${CMAKE_CURRENT_BINARY_DIR}/phash_db.json.hpp ${CMAKE_CURRENT_SOURCE_DIR}/sample_dbs/phash_db.json "const char* db" "PHashDB") +stringify_file(${CMAKE_CURRENT_BINARY_DIR}/photos_with_people.json.hpp ${CMAKE_CURRENT_SOURCE_DIR}/sample_dbs/photos_with_people.json "const char* db" "PeopleDB") stringify_file(${CMAKE_CURRENT_BINARY_DIR}/rich_db.json.hpp ${CMAKE_CURRENT_SOURCE_DIR}/sample_dbs/rich_db.json "const char* db1" "RichDB") stringify_file(${CMAKE_CURRENT_BINARY_DIR}/sample_db.json.hpp ${CMAKE_CURRENT_SOURCE_DIR}/sample_dbs/sample_db.json "const char* db1" "SampleDB") stringify_file(${CMAKE_CURRENT_BINARY_DIR}/sample_db2.json.hpp ${CMAKE_CURRENT_SOURCE_DIR}/sample_dbs/sample_db2.json "const char* db2" "SampleDB") @@ -14,6 +15,7 @@ target_sources(sample_dbs PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/db_for_series_detection.json.hpp ${CMAKE_CURRENT_BINARY_DIR}/phash_db.json.hpp + ${CMAKE_CURRENT_BINARY_DIR}/photos_with_people.json.hpp ${CMAKE_CURRENT_BINARY_DIR}/rich_db.json.hpp ${CMAKE_CURRENT_BINARY_DIR}/sample_db.json.hpp ${CMAKE_CURRENT_BINARY_DIR}/sample_db2.json.hpp diff --git a/src/unit_tests_utils/fake_task_executor.hpp b/src/unit_tests_utils/fake_task_executor.hpp index 92d9a8fa1c..a037b9b446 100644 --- a/src/unit_tests_utils/fake_task_executor.hpp +++ b/src/unit_tests_utils/fake_task_executor.hpp @@ -1,4 +1,7 @@ +#ifndef FAKE_TASK_EXECUTOR_HPP_INCLUDED +#define FAKE_TASK_EXECUTOR_HPP_INCLUDED + #include class FakeTaskExecutor: public ITaskExecutor @@ -9,9 +12,12 @@ class FakeTaskExecutor: public ITaskExecutor task->perform(); } - void addLight(std::unique_ptr&& task) override + std::shared_ptr add(Process &&) override { - task->perform(); + /// TODO: implement + assert(!"Not implemented yet"); + + return nullptr; } int heavyWorkers() const override @@ -19,3 +25,5 @@ class FakeTaskExecutor: public ITaskExecutor return 1; } }; + +#endif diff --git a/src/unit_tests_utils/mock_backend.hpp b/src/unit_tests_utils/mock_backend.hpp index 9bf2698e86..4ab9ff3f0c 100644 --- a/src/unit_tests_utils/mock_backend.hpp +++ b/src/unit_tests_utils/mock_backend.hpp @@ -27,8 +27,8 @@ struct MockBackend: public Database::IBackend MOCK_METHOD(std::optional, get, (const Photo::Id &, const QString &), (override)); MOCK_METHOD(void, setBits, (const Photo::Id& id, const QString& name, int bits), (override)); MOCK_METHOD(void, clearBits, (const Photo::Id& id, const QString& name, int bits), (override)); - MOCK_METHOD(void, writeBlob, (const Photo::Id &, BlobType, const QByteArray &), (override)); - MOCK_METHOD(QByteArray, readBlob, (const Photo::Id &, BlobType), (override)); + MOCK_METHOD(void, writeBlob, (const Photo::Id &, const QString& bt, const QByteArray &), (override)); + MOCK_METHOD(QByteArray, readBlob, (const Photo::Id &, const QString& bt), (override)); MOCK_METHOD(std::vector, markStagedAsReviewed, (), (override)); MOCK_METHOD(Database::BackendStatus, init, (const Database::ProjectInfo &), (override)); MOCK_METHOD(void, closeConnections, (), (override)); diff --git a/src/unit_tests_utils/sample_dbs/photos_with_people.json b/src/unit_tests_utils/sample_dbs/photos_with_people.json new file mode 100644 index 0000000000..235c973da6 --- /dev/null +++ b/src/unit_tests_utils/sample_dbs/photos_with_people.json @@ -0,0 +1,28 @@ + +{ + "photos": [ + { + "path": "/some/path1.jpeg", + "tags": { "date": "2001.01.01", "time": "10:00", "event": "Some event", "place": "Internet" }, + "people": [ { "name": "person 1" }, { "name": "person 2" } ] + }, + { + "path": "/some/path2.jpeg", + "tags": { "date": "2001.01.01", "time": "10:00", "event": "", "place": "Internet" }, + "people": [ { "name": "person 2" }, { "name": "person 3" } ] + }, + { + "path": "/some/path3.jpeg", + "tags": { "date": "2001.01.01", "time": "11:00", "event": "Another event", "place": "" } + }, + { + "path": "/some/path4.jpeg", + "tags": { "date": "2001.01.01", "time": "11:00", "event": "Another event", "place": "" }, + "people": [ { "name": "person 4" }, { "name": "person 5" } ] + }, + { + "path": "/some/path5.jpeg", + "tags": { "date": "2001.01.01", "time": "11:00", "event": "Another event", "place": "" } + } + ] +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000000..340ebb1329 --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,2 @@ + +add_subdirectory(reflect++) diff --git a/tools/reflect++ b/tools/reflect++ new file mode 160000 index 0000000000..5dc631cb12 --- /dev/null +++ b/tools/reflect++ @@ -0,0 +1 @@ +Subproject commit 5dc631cb129dd6a5dbdcab8836ba7e0772c2b02c diff --git a/tools/update_licence.py b/tools/update_license.py similarity index 100% rename from tools/update_licence.py rename to tools/update_license.py diff --git a/tr/photo_broom_en.ts b/tr/photo_broom_en.ts index a8f7c49b1a..2e509bec22 100644 --- a/tr/photo_broom_en.ts +++ b/tr/photo_broom_en.ts @@ -57,6 +57,27 @@ + + BatchFaceDetection + + + Discovered faces + + + + + Photos to be analyzed + + + + + BatchFaceDetector + + + Batch face detector + + + CollectionScanner @@ -189,22 +210,22 @@ FacesDialog - + unknown unknown - + Mark found faces Mark found faces - + Detecting and analyzing faces Detecting and analyzing faces - + Could not detect any face. Could not detect any face. @@ -350,17 +371,17 @@ Check paths in configuration window. - + &Settings - + Tasks - + Back to photos @@ -385,7 +406,7 @@ Check paths in configuration window. &Refresh collection... - + &Configuration @@ -395,54 +416,54 @@ Check paths in configuration window. - + New version - + New version of PhotoBroom is available <a href="%1">here</a>. - + Internet connection problem - + Could not check if there is new version of PhotoBroom. Please check your internet connection. - + Open collection - + Photo Broom files (*.bpj) - + About Photo Broom - + About Qt - - + + Unsupported photo collection version - + Photo collection you are trying to open uses database in version which is not supported. It means your application is too old to open it. @@ -450,7 +471,7 @@ Please upgrade application to open this collection. - + Photo collection you are trying to open uses database in version which is not supported. It means your database is too old to open it. @@ -458,12 +479,12 @@ It means your database is too old to open it. - + Could not open collection - + Photo collection could not be opened. It usually means that collection files are broken or you don't have rights to access them. @@ -473,23 +494,23 @@ Please check collection files: - + Collection locked - + Photo collection could not be opened. It is already opened by another Photo Broom instance. - + Unexpected error - + An unexpected error occured while opening photo collection. Please report a bug. Error code: %1 @@ -695,38 +716,38 @@ Error code: %1 - - + + Cancel operation? - + Do you really want to stop current work and quit? - + Do you really want to stop current work? - + Error during collage generation. Possibly too many images, or height to small or too big. - + photo path - + sequence number - + exposure (EV) @@ -742,17 +763,17 @@ Error code: %1 PhotosView - + <b>Properties</b> - + <b>Media information</b> - + <b>Debug window</b> diff --git a/tr/photo_broom_pl.ts b/tr/photo_broom_pl.ts index b94ae18cab..19934aa81f 100644 --- a/tr/photo_broom_pl.ts +++ b/tr/photo_broom_pl.ts @@ -58,6 +58,27 @@ Wydarzenie + + BatchFaceDetection + + + Discovered faces + + + + + Photos to be analyzed + + + + + BatchFaceDetector + + + Batch face detector + + + CollectionScanner @@ -196,12 +217,12 @@ FacesDialog - + unknown nieznane - + Mark found faces Podświetl znalezione twarze @@ -354,7 +375,7 @@ Sprawdź poprawność ścieżek w konfiguracji. Wyszukaj &duplikaty - + &Settings U&stawienia @@ -369,7 +390,7 @@ Sprawdź poprawność ścieżek w konfiguracji. Powrót - + P&hotos &Zdjęcia @@ -394,17 +415,17 @@ Sprawdź poprawność ścieżek w konfiguracji. &Odśwież kolekcję... - + &Configuration &Ustawienia programu - + T&asks &Operacje - + New version Nowa wersja @@ -722,7 +743,7 @@ Kod błędu: %1 Podgląd: - + Cancel operation? Anulować operację? @@ -774,7 +795,7 @@ Kod błędu: %1 PhotosView - + <b>Properties</b> <b>Właściwości</b> diff --git a/vcpkg.json b/vcpkg.json index f1ce2e7331..03764ffcc4 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,15 +7,16 @@ "description": "Photo Broom is a tool for managing your photos.", "dependencies": [ { - "name": "dlib", - "default-features": false, - "features": [ "fftw3" ] + "name": "dlib", + "default-features": false, + "features": [ "fftw3" ] }, { - "name": "opencv4", - "default-features": false, - "features": [ "contrib" ] + "name": "opencv4", + "default-features": false, + "features": [ "contrib" ] }, + "boost-multi-index", "exiv2", "jsoncpp", "libguarded",