Упрощение применения небольших водяных знаков

Я недавно начал работать с C ++ и работаю над приложением, которое может помечать водяными знаками сразу целые каталоги изображений.

В настоящее время вы можете вызвать программу с аргументом целевого каталога, в котором хранятся изображения, и аргументом, указывающим путь, к которому использовать изображение водяного знака.

Пока это работает, но я думаю, что мой код немного запутан. Я действительно не знаю, как сделать его более читаемым и чистым, поэтому прошу вашей помощи.

Я был бы очень признателен, если бы люди могли посмотреть мой код и просмотреть его. Любые отзывы приветствуются.

Ссылка на мой репозиторий GitHub: репозиторий github

Main.CPP:

#include <iostream>
#include "FileSystem.h"
#include "Watermarker.h"

int main(int arc, const char** argv)
{
    // Get the given argument for the target directory
    std::string targetDirectory = argv[1];

    // Get the given argument for the watermark file
    std::string watermarkPath = argv[2];

    FileSystem fileSystem;

    // Check if the target directory is valid
    if (!fileSystem.checkDirectory(targetDirectory)) { return 1; }

    // Create an output directory where the new watermarked images will be stored
    std::string path = targetDirectory + "\watermarked_images";
    fileSystem.createDirectory(path);

    Watermarker marker;

    // Mark all the images in the target directory with the watermarkPath image
    marker.mark(targetDirectory, watermarkPath);
}

FileSystem.h

#pragma once

#include <string>
#include <vector>
#include <boost/filesystem.hpp>

class FileSystem
{
private:
    std::vector<std::string> validExtensions = {
        ".png",
        ".jpg",
        ".tiff",
        ".jpeg"
    };

public:
    void createDirectory(std::string& directory);
    bool checkDirectory(std::string& directory);
    bool checkFile(boost::filesystem::path& filePath);
};

FileSystem.CPP

#include "FileSystem.h"
#include <iostream>
#include <boost/algorithm/string.hpp>

void FileSystem::createDirectory(std::string& directory)
{
    boost::filesystem::create_directory(directory);
}

bool FileSystem::checkDirectory(std::string& directory)
{
    if (boost::filesystem::exists(directory))
    {
        if (!boost::filesystem::is_directory(directory))
        {
            std::cout << "The given path is not a directory.n";
            return false;
        }
    }
    else
    {
        std::cout << directory << " does not existn";
        return false;
    }

    return true;
}

bool FileSystem::checkFile(boost::filesystem::path& filePath)
{
    // Check if the file is a regular file
    if (!boost::filesystem::is_regular_file(filePath)) { return false; }

    // Get the file extension in lower case
    std::string fileExtension = filePath.extension().string();
    boost::to_lower(fileExtension);

    // Check if the file extension matches one of the valid ones
    if (std::find(validExtensions.begin(), validExtensions.end(), fileExtension) != validExtensions.end())
    {
        return true;
    }
    else
    {
        return false;
    }
}

Watermarker.h

#pragma once

#include <iostream>

class Watermarker
{
public:
    void mark(std::string& targetDirectory, std::string& watermarkPath);
};

Watermarker.cpp

#include "Watermarker.h"
#include "FileSystem.h"
#include <boost/filesystem.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

void Watermarker::mark(std::string& targetDirectory, std::string& watermarkPath)
{
    boost::filesystem::path path;

    // Loop through all the files in the target directory
    for (auto& entry : boost::filesystem::directory_iterator(targetDirectory))
    {
        // Set the boost filesystem path to the path of the file
        path = entry.path();

        FileSystem fileSystem;
        
        // If the file is not valid, skip it
        if (!fileSystem.checkFile(path)) { continue; }

        // Get the original and watermark image
        cv::Mat image = cv::imread(path.string());
        cv::Mat watermarkImage = cv::imread(watermarkPath, cv::IMREAD_UNCHANGED);

        // Make the watermark image the same size as the original image
        cv::resize(watermarkImage, watermarkImage, image.size(), 0, 0, cv::InterpolationFlags::INTER_LINEAR);

        // Give the images 4 channels
        cv::cvtColor(image, image, cv::COLOR_RGB2RGBA);
        cv::cvtColor(watermarkImage, watermarkImage, cv::COLOR_RGB2RGBA);

        cv::Mat newImage;

        // Add the watermark to the original image
        cv::addWeighted(watermarkImage, 0.3, image, 1, 0.0, newImage);

        // Save the new image to the new watermarked_images folder
        cv::imwrite(std::string(targetDirectory + "\watermarked_images\" + path.filename().string()).c_str(), newImage);
    }
}

2 ответа
2

#pragma once не является стандартным C ++ – используйте обычный #ifndef/#define включать охранников до тех пор, пока это не будет стандартизировано (к тому времени модули будут лучшим подходом).

Это выглядит странно:

#include <iostream>

class Watermarker
{
public:
    void mark(std::string& targetDirectory, std::string& watermarkPath);
};

Зачем включать <iostream>? Мы оттуда ничего не используем. Что еще более важно, нам не хватает включения <string>.

Почему mark() нестатическая функция-член? Он не требует ни состояния объекта, ни даже состояния класса. Это может быть бесплатная функция.

Почему мы передаем изменяемые ссылки на пути? Я не думаю, что есть веская причина, по которой вызывающий абонент хотел бы, чтобы какая-либо из этих строк была изменена, поэтому просто возьмите копию (или используйте константную ссылку, если реализация не будет изменять значения).

class FileSystem

Это запутанное имя, учитывая, что мы используем boost::filesystem в том же коде.

int main(int arc, const char** argv)
{
    // Get the given argument for the target directory
    std::string targetDirectory = argv[1];

    // Get the given argument for the watermark file
    std::string watermarkPath = argv[2];

Условно мы называем первый аргумент argcне arc. Более важно то, что мы не можем получить доступ argv[1] и argv[2] если мы не знаем argc составляет не менее 3. А если не точно 3, мы должны сообщить пользователю (через std::cerr), что использование неверно, и выход с ненулевым статусом (например, EXIT_FAILURE, от <cstdlib>).

    if (!fileSystem.checkFile(path)) { continue; }

    // Get the original and watermark image
    cv::Mat image = cv::imread(path.string());

Между проверкой и чтением есть промежуток, когда в многопроцессорной среде может произойти что угодно. И учитывая, что мы никогда не проверяли, есть ли у нас разрешение на чтение файла, cv::imread() может выйти из строя, даже если за это время ничего не изменится. Читать прекрасное руководство:

Если изображение не может быть прочитано (из-за отсутствия файла, неправильных разрешений, неподдерживаемого или недопустимого формата), функция возвращает пустую матрицу (Mat::data==NULL).

Наверное лучше просто удалить FileSystem::checkFile() и полагаться на OpenCV, чтобы сообщить, доступен ли каждый файл для чтения.

    cv::imwrite(std::string(targetDirectory + "\watermarked_images\" + path.filename().string()).c_str(), newImage);

Вот еще одно место, где мы забыли проверить, удалась ли операция. (Честно говоря, пример в руководство тоже делает эту ошибку). Плохая практика – продолжать молча, когда мы не достигли того, о чем просил пользователь.

Также здесь у нас есть фрагмент строки "\watermarked_images\" это должно быть согласовано с кодом вызова. Во-первых, это выглядит так предполагается, что это разделитель путей, который излишне непереносим (особенно с учетом того, что мы используем boost::filesystem). Во-вторых, необходимо передать путь назначения (и было бы лучше, если бы его можно было указать как параметр программы).

    #pragma once:

    Если вы создаете библиотеку, которая должна работать с любым компилятором на любой платформе, вам нужны соответствующие средства защиты. Для чего-то вроде этого #pragma once это путь. (Это проще и менее подвержено ошибкам и поддерживается всеми основными современными компиляторами).

    [Other than this I mainly agree with Toby Speight’s answer ;).]


    #include порядок:

    #include <iostream>
    #include "FileSystem.h"
    #include "Watermarker.h"
    

    Обычный порядок включения – это размещение локальных включений вверху, сторонняя библиотека включает следующие, а стандартная библиотека включает последние:

    #include "FileSystem.h"
    #include "Watermarker.h"
    
    #include <boost/filesystem.hpp> // added for illustration
    
    #include <iostream>
    

    Наши локальные заголовки должны #include все, что они используют сами. При таком порядке мы получим ошибку компилятора, если забыли #include что-то в местной шапке.

    например, если FileSystem.h зависит от <iostream>, но забывает включить его, мы хотим всегда получаем ошибку компилятора, когда пытаемся использовать FileSystem.h в .cpp файл (чтобы мы могли исправить проблему). Если мы включим <iostream> первый в .cpp файла, мы не получим здесь ошибку компилятора, но может появиться в произвольный момент в будущем, когда мы не будем включать <iostream> тоже.


    class Filesystem:

    std::vector<std::string> validExtensions должно быть static, поскольку это не относится к экземпляру класса. Фактически, у этого класса нет состояния экземпляра (переменных-членов), поэтому он не обязательно должен быть class. Возможно, мы могли бы сделать это namespace вместо.

    Я бы, вероятно, просто поместил весь код этого приложения в одно пространство имен, вместо того, чтобы иметь отдельное пространство имен для нескольких утилит.

    Возможно, мы могли бы встроить содержимое этих функций в функции, которые их вызывают, без потери удобочитаемости.


    class Watermarker:

    Опять же, у этого класса нет членов, поэтому он не обязательно должен быть class.

    Хотя верно, что нет смысла проверять, существует ли файл или доступен для чтения, прежде чем мы действительно попытаемся открыть файл, мы все же должны проверить, что наш итератор указывает на файл, а не на каталог, и что он имеет одно из расширений, которые мы заботиться о.

    [Another slight disagreement with Toby Speight’s answer.]

    Так checkFile() является необходимым. Но я считаю, что его следует разделить на две части, например: isFile(path) и hasExtension(path, validExtensions);.

    Я думаю, мы могли бы разделить mark функция на две части. Одна функция (processDir()), чтобы иметь дело с итерацией каталога (здесь мы могли бы просто встроить проверку файла и расширения). Вторая функция addWatermark() для нанесения водяных знаков с помощью OpenCV.


    возможное улучшение производительности:

    Мы читаем один и тот же файл изображения водяного знака снова и снова.

    Мы можем прочитать изображение и преобразовать его в RGBA только один раз. Затем мы можем каждый раз копировать и изменять его размер, что, вероятно, быстрее, чем чтение с диска.


    структура проекта:

    Хотя разделение частей проекта на разные файлы, как правило, очень хорошо, на самом деле это может сделать вещи менее удобочитаемыми для такого очень небольшого приложения, как это!

    validExtensions, например, зависит от приложения и довольно важна, но ее трудно найти в отдельном файле. Между тем Watermarker.cpp также зависит от FileSystem.h (и эти допустимые расширения), что делает его менее читаемым (потому что нам приходится обходить кодовую базу, чтобы понять его) и менее пригодным для повторного использования.

    Возможно, в этом случае лучше было бы поместить все в main.cpp, примерно так:

    #include <boost/algorithm/string.hpp>
    #include <boost/filesystem.hpp>
    
    #include <opencv2/core.hpp>
    #include <opencv2/highgui.hpp>
    #include <opencv2/imgproc.hpp>
    
    #include <algorithm>
    #include <cstdlib>
    #include <initializer_list>
    #include <iostream>
    #include <string>
    
    namespace fs = boost::filesystem;
    
    namespace wtr
    {
    
        void addWatermark(std::string const& targetFile, std::string const& outputFile, std::string const& watermarkFile)
        {
            cv::Mat original = cv::imread(targetFile); // todo: handle failure!
            cv::cvtColor(original, original, cv::COLOR_RGB2RGBA);
    
            cv::Mat watermark = cv::imread(watermarkFile, cv::IMREAD_UNCHANGED); // todo: handle failure!
            cv::resize(watermark, watermark, image.size(), 0, 0, cv::InterpolationFlags::INTER_LINEAR);
            cv::cvtColor(watermark, watermark, cv::COLOR_RGB2RGBA);
    
            cv::Mat composite;
            cv::addWeighted(watermark, 0.3, original, 1, 0.0, composite); // todo: make 0.3 a function argument!
    
            cv::imwrite(outputFile, composite); // todo: handle failure!
        }
    
        template<class It, class T>
        bool contains(It begin, It end, T const& value)
        {
            return std::find(begin, end, value) != end;
        }
    
        void processDir(std::string const& targetDir, std::string const& outputDir, std::string const& watermarkFile)
        {
            for (auto& entry : boost::filesystem::directory_iterator(targetDir))
            {
                fs::path path = entry.path();
    
                if (!fs::is_regular_file(path))
                    continue;
    
                auto validExtensions = { ".png", ".jpg", ".jpeg", ".tiff" };
                if (!contains(validExtensions.begin(), validExtensions.end(), boost::to_lower_copy(path.extension().string())))
                    continue;
                
                addWatermark(path.string(), outputDir + path.filename().string(), watermarkFile);
            }
        }
    
    } // wtr
    
    int main(int argc, const char** argv)
    {
        // todo: check argc is correct!!!
        std::string targetDir = argv[1];
        std::string watermarkFile = argv[2];
    
        if (!fs::is_directory(targetDir)) // note: only one check needed - if the file doesn't exist, it won't be a directory!
        {
            std::cout << "The given path is not a directory.n";
            return EXIT_FAILURE;
        }
    
        std::string outputDir = targetDir + "\watermarked_images";
        fs::create_directory(outputDir);
        // todo: handle failure!!!
    
        wtr::processDir(targetDir, outputDir, watermarkFile); // note: pass in output directory path instead of recalculating it inside the function.
    }
    

    [note: not tested]

    Если наша кодовая база растет, или мы хотим повторно использовать функции или переместить их в библиотеку, легко разделить части. Например, сейчас addWatermark зависит только от стандартной библиотеки и OpenCV (поэтому его можно сгруппировать с другими служебными функциями OpenCV), и contains аналогичным образом можно переместить в отдельный файл.

    Но … до тех пор, Т.е..


    • 1

      Я не согласен с использованием имени файла в качестве индикатора того, является ли что-то изображением – OpenCV намного надежнее обнаруживает это! Но я полагаю, что действительно спрашивающий должен решить, что ему нужно – все файлы, соответствующие этому шаблону, или все файлы, содержащие растровые изображения? (Я пошел с описанием, а вы с кодом).

      – Тоби Спейт


    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *