├── .gitmodules ├── src ├── lib │ ├── mat-to-texture.hpp │ ├── bitset-serialise.hpp │ ├── edit-image-edges.hpp │ ├── window.hpp │ ├── detect-edge.hpp │ ├── bitset-serialise.cpp │ ├── image-list.hpp │ ├── frame-collection.hpp │ ├── mat-to-texture.cpp │ ├── edged-image.hpp │ ├── detect-edge.cpp │ ├── window.cpp │ ├── image-list.cpp │ ├── frame-collection.cpp │ ├── edged-image.cpp │ └── edit-image-edges.cpp ├── precompiled.h ├── config.h ├── process_images.cpp ├── build.cpp └── match.cpp ├── README.md ├── .gitignore ├── cmake └── imgui.cmake └── CMakeLists.txt /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extern/imgui"] 2 | path = extern/imgui 3 | url = https://github.com/ocornut/imgui 4 | -------------------------------------------------------------------------------- /src/lib/mat-to-texture.hpp: -------------------------------------------------------------------------------- 1 | #include "../precompiled.h" 2 | 3 | void matToTexture(const cv::Mat &mat, GLuint* outTexture); 4 | -------------------------------------------------------------------------------- /src/lib/bitset-serialise.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #import "../precompiled.h" 4 | 5 | #import 6 | 7 | boost::dynamic_bitset stringToBitset(const char* str, int size); 8 | std::string bitsetToString(const boost::dynamic_bitset &bitset); 9 | -------------------------------------------------------------------------------- /src/lib/edit-image-edges.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../precompiled.h" 4 | #include "../config.h" 5 | 6 | #include "detect-edge.hpp" 7 | #include "edged-image.hpp" 8 | #include "mat-to-texture.hpp" 9 | #include "window.hpp" 10 | 11 | std::optional editImageEdges(EdgedImage &image); 12 | -------------------------------------------------------------------------------- /src/precompiled.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | -------------------------------------------------------------------------------- /src/lib/window.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../precompiled.h" 4 | 5 | using onFrameFn = std::function; 6 | 7 | void initWindow(int width, int height, const char* title); 8 | void openWindow(const onFrameFn &onFrame); 9 | 10 | const ImGuiWindowFlags staticWindowFlags = 11 | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | 12 | ImGuiWindowFlags_NoSavedSettings; 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aerial shapes 2 | 3 | An experiment in animations + shapes in aerial photos. 4 | 5 | ## To build 6 | 7 | ``` 8 | brew install cmake opencv boost 9 | cmake . 10 | make 11 | ``` 12 | 13 | (There's a good chance some of the dependencies will be installed already) 14 | 15 | ## To process assets 16 | 17 | Images must be processed in advance (it takes a while if you have a lot of 18 | images). Fill a directory with images and process them like this: 19 | 20 | ``` 21 | ./out/process_images assets/test 22 | ``` 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Built files 2 | out 3 | 4 | ### CMake 5 | CMakeLists.txt.user 6 | CMakeCache.txt 7 | CMakeFiles 8 | CMakeScripts 9 | Testing 10 | Makefile 11 | cmake_install.cmake 12 | install_manifest.txt 13 | compile_commands.json 14 | CTestTestfile.cmake 15 | _deps 16 | 17 | ### C++ 18 | # Prerequisites 19 | *.d 20 | 21 | # Compiled Object files 22 | *.slo 23 | *.lo 24 | *.o 25 | *.obj 26 | 27 | # Precompiled Headers 28 | *.gch 29 | *.pch 30 | 31 | # Compiled Dynamic libraries 32 | *.so 33 | *.dylib 34 | *.dll 35 | 36 | # Fortran module files 37 | *.mod 38 | *.smod 39 | 40 | # Compiled Static libraries 41 | *.lai 42 | *.la 43 | *.a 44 | *.lib 45 | 46 | # Executables 47 | *.exe 48 | *.out 49 | *.app 50 | 51 | # Assets 52 | assets 53 | 54 | # Imgui cache 55 | imgui.ini 56 | -------------------------------------------------------------------------------- /src/lib/detect-edge.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../precompiled.h" 4 | #include "../config.h" 5 | 6 | cv::Mat detectEdgesCanny(const cv::Mat &sourceImage, 7 | int blurSize = EDGE_DETECTION_BLUR_SIZE, 8 | int sigmaX = EDGE_DETECTION_BLUR_SIGMA_X, 9 | int sigmaY = EDGE_DETECTION_BLUR_SIGMA_Y, 10 | int threshold1 = EDGE_DETECTION_CANNY_THRESHOLD_1, 11 | int threshold2 = EDGE_DETECTION_CANNY_THRESHOLD_1, 12 | int joinByX = EDGE_DETECTION_CANNY_JOIN_BY_X, 13 | int joinByY = EDGE_DETECTION_CANNY_JOIN_BY_Y); 14 | 15 | cv::Mat detectEdgesThreshold(const cv::Mat &sourceImage, 16 | int blurSize = EDGE_DETECTION_BLUR_SIZE, 17 | int sigmaX = EDGE_DETECTION_BLUR_SIGMA_X, 18 | int sigmaY = EDGE_DETECTION_BLUR_SIGMA_Y, 19 | int binaryThreshold = EDGE_DETECTION_BINARY_THRESHOLD); 20 | 21 | boost::dynamic_bitset edgesToBitset(cv::Mat &edgeMatrix); 22 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define EDGE_DETECTION_WIDTH 1000 4 | #define STORED_EDGES_WIDTH 500 5 | 6 | #define EDGE_DETECTION_BLUR_SIZE 21 7 | #define EDGE_DETECTION_BLUR_SIGMA_X 5 8 | #define EDGE_DETECTION_BLUR_SIGMA_Y 5 9 | #define EDGE_DETECTION_CANNY_THRESHOLD_1 69 10 | #define EDGE_DETECTION_CANNY_THRESHOLD_2 69 11 | #define EDGE_DETECTION_CANNY_JOIN_BY_X 11 12 | #define EDGE_DETECTION_CANNY_JOIN_BY_Y 11 13 | #define EDGE_DETECTION_BINARY_THRESHOLD 100 14 | 15 | #define MATCH_OFFSET_SCALE_STEP 0.025 16 | #define MATCH_OFFSET_X_STEP 1 17 | #define MATCH_OFFSET_Y_STEP 10 18 | #define MATCH_MIN_OFFSET_SCALE 0.2 19 | #define MATCH_MAX_OFFSET 9 20 | #define MATCH_WHITE_BIAS 0.75 21 | 22 | #define CANVAS_WIDTH 300 23 | #define CANVAS_HEIGHT 200 24 | 25 | #define OUTPUT_WIDTH 1500 26 | #define OUTPUT_HEIGHT 1000 27 | 28 | enum ImageEdgeModes { 29 | ImageEdgeMode_Canny, 30 | ImageEdgeMode_Threshold, 31 | ImageEdgeMode_Manual 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/bitset-serialise.cpp: -------------------------------------------------------------------------------- 1 | #import "bitset-serialise.hpp" 2 | 3 | static int charToInt(char c) { 4 | if (c >= '0' && c <= '9') { 5 | return c - '0'; 6 | } 7 | if (c >= 'A' && c <= 'F') { 8 | return c - 'A' + 10; 9 | } 10 | return -1; 11 | } 12 | 13 | boost::dynamic_bitset stringToBitset(const char* str, int size) { 14 | boost::dynamic_bitset bitset(size); 15 | 16 | for (int i = size - 4; i > -4; i -= 4) { 17 | char part = str[(i + 3) / 4]; 18 | int partInt = charToInt(part); 19 | 20 | for (int j = 0; j < 4; j++) { 21 | if (i + j >= 0) { 22 | bitset[i + j] = (partInt & (1 << (3 - j))) > 0; 23 | } 24 | } 25 | } 26 | 27 | return std::move(bitset); 28 | } 29 | 30 | std::string bitsetToString(const boost::dynamic_bitset &bitset) { 31 | const int size = bitset.size(); 32 | 33 | char resStr[size / 4 + 2]; 34 | std::memset(resStr, '\0', sizeof(resStr)); 35 | char resStrPart[2]; 36 | 37 | for (int i = size - 4; i > -4; i -= 4) { 38 | int resInt = 0; 39 | 40 | for (int j = i; j < i + 4; ++j) { 41 | if (j >= 0) { 42 | resInt = (resInt << 1) | bitset[j]; 43 | } 44 | } 45 | 46 | sprintf(resStrPart, "%X", resInt); 47 | 48 | resStr[(i + 3) / 4] = resStrPart[0]; 49 | resInt = 0; 50 | } 51 | 52 | return resStr; 53 | } 54 | -------------------------------------------------------------------------------- /cmake/imgui.cmake: -------------------------------------------------------------------------------- 1 | # somewhat based on https://github.com/Pesc0/imgui-cmake/blob/master/libs/CMakeLists.txt 2 | find_package(OpenGL REQUIRED) 3 | 4 | # Use gl3w as GL loader. It is the default in imgui's examples. 5 | # todo: use OpenGL::GL? 6 | set(GL3W_DIR "extern/imgui/examples/libs/gl3w") 7 | add_library(GL3W STATIC) 8 | 9 | target_sources(GL3W PRIVATE ${GL3W_DIR}/GL/gl3w.c) 10 | target_include_directories(GL3W PUBLIC ${GL3W_DIR}) 11 | target_link_libraries(GL3W PUBLIC ${OPENGL_LIBRARIES}) 12 | 13 | set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) 14 | set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) 15 | set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) 16 | 17 | add_library(IMGUI STATIC) 18 | 19 | FetchContent_Declare( 20 | glfw 21 | GIT_REPOSITORY https://github.com/glfw/glfw 22 | ) 23 | FetchContent_MakeAvailable(glfw) 24 | 25 | set(IMGUI_DIR "extern/imgui") 26 | 27 | file(GLOB imgui_source_files "${IMGUI_DIR}/*.cpp") 28 | 29 | target_sources( IMGUI 30 | PRIVATE ${imgui_source_files} 31 | 32 | PRIVATE 33 | ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp 34 | ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp 35 | ) 36 | 37 | target_include_directories( IMGUI 38 | PUBLIC ${IMGUI_DIR} 39 | PUBLIC ${IMGUI_DIR}/backends 40 | ) 41 | 42 | 43 | target_compile_definitions(IMGUI PUBLIC -DIMGUI_IMPL_OPENGL_LOADER_GL3W) 44 | target_link_libraries(IMGUI PUBLIC glfw GL3W ${CMAKE_DL_LIBS}) 45 | -------------------------------------------------------------------------------- /src/lib/image-list.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../precompiled.h" 11 | 12 | #include "detect-edge.hpp" 13 | #include "edged-image.hpp" 14 | #include "image-list.hpp" 15 | 16 | class ImageList { 17 | public: 18 | typedef std::vector> image_store; 19 | typedef std::function, std::shared_ptr)> sort_predicate; 20 | 21 | private: 22 | std::string dirPath; 23 | int _matchContextOffsetX; 24 | int _matchContextOffsetY; 25 | 26 | bool getStored(); 27 | void addFile(const std::filesystem::directory_entry &file); 28 | 29 | public: 30 | image_store store; 31 | ImageList(std::string dirPath); 32 | 33 | void generate(); 34 | void save(bool async = true); 35 | int sync(); 36 | 37 | void provideMatchContext(int templateOffsetX, int templateOffsetY); 38 | void resetMatchContext(); 39 | 40 | int matchTo(const cv::Mat &templateImage, ImageMatch *match, 41 | EdgedImage **bestMatchImage, 42 | float offsetScaleStep = MATCH_OFFSET_SCALE_STEP, 43 | int offsetXStep = MATCH_OFFSET_X_STEP, 44 | int offsetYStep = MATCH_OFFSET_Y_STEP, 45 | float minOffsetScale = MATCH_MIN_OFFSET_SCALE, 46 | int maxOffset = MATCH_MAX_OFFSET, 47 | float whiteBias = MATCH_WHITE_BIAS); 48 | 49 | void sortBy(const sort_predicate &sortFn); 50 | void sortBy(const char* sorter); 51 | 52 | image_store::iterator begin(); 53 | image_store::iterator end(); 54 | image_store::reference at(size_t pos); 55 | 56 | void erase(size_t pos); 57 | int count() const; 58 | }; 59 | -------------------------------------------------------------------------------- /src/lib/frame-collection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../precompiled.h" 7 | 8 | #include "image-list.hpp" 9 | 10 | struct MatchData { 11 | std::string path; 12 | float percentage, scale; 13 | int originX, originY; 14 | 15 | MatchData() {} 16 | MatchData(std::string &path, float percentage, float scale, int originX, 17 | int originY) 18 | : path(path), percentage(percentage), scale(scale), originX(originX), 19 | originY(originY) {} 20 | }; 21 | 22 | // todo refactor this 23 | struct FrameData { 24 | std::vector frames; 25 | }; 26 | 27 | class FrameCollection : public std::vector { 28 | std::vector _isCachedMatches; 29 | std::vector::iterator> _cachedMatches; 30 | std::vector _isCachedImages; 31 | std::vector _cachedImages; 32 | 33 | std::vector::iterator _uncached; 34 | 35 | void purgeCache(); 36 | 37 | public: 38 | FrameCollection() {}; 39 | FrameCollection(const std::string &name); 40 | void addFrame(ImageList imageList); 41 | void popFrame(); 42 | void save(const std::string &name); 43 | 44 | std::vector::iterator matchAt(int pos); 45 | std::vector* matchesAt(int pos); 46 | cv::Mat imageAt(int pos); 47 | cv::Mat imageFor(std::vector::iterator match); 48 | void forceMatch(int pos, std::vector::iterator match); 49 | void removeMatch(int pos, std::vector::iterator match); 50 | void editMatchScale(int pos, float newScale); 51 | void editMatchOriginX(int pos, int originX); 52 | void editMatchOriginY(int pos, int originY); 53 | 54 | void preloadAll(); 55 | void writeImages(const std::string &name); 56 | 57 | friend std::ostream& operator<<(std::ostream& os, const FrameCollection& frames); 58 | }; 59 | 60 | std::ostream& operator<<(std::ostream& os, const MatchData& matchData); 61 | std::ostream& operator<<(std::ostream& os, const FrameData& frameData); 62 | std::ostream& operator<<(std::ostream& os, const FrameCollection& frames); 63 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | include(FetchContent) 4 | 5 | if(NOT CMAKE_BUILD_TYPE) 6 | set(CMAKE_BUILD_TYPE debug) 7 | endif() 8 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_HOME_DIRECTORY}/out") 9 | 10 | set(CMAKE_CXX_COMPILER "/usr/bin/clang++") 11 | set(CMAKE_CXX_STANDARD 17) 12 | set(CMAKE_CXX_STANDARD_REQUIRED True) 13 | # set(CMAKE_CXX_FLAGS "-Wall -Wextra") 14 | set(CMAKE_CXX_FLAGS_DEBUG "-fsanitize=address,undefined") 15 | set(CMAKE_CXX_FLAGS_RELEASE "-O3") 16 | 17 | project(AerialShapes) 18 | 19 | set(LibraryFiles 20 | src/lib/bitset-serialise.cpp 21 | src/lib/detect-edge.cpp 22 | src/lib/edged-image.cpp 23 | src/lib/edit-image-edges.cpp 24 | src/lib/frame-collection.cpp 25 | src/lib/image-list.cpp 26 | src/lib/mat-to-texture.cpp 27 | src/lib/window.cpp) 28 | add_executable(build src/build.cpp ${LibraryFiles}) 29 | add_executable(process_images src/process_images.cpp ${LibraryFiles}) 30 | add_executable(match src/match.cpp ${LibraryFiles}) 31 | 32 | target_precompile_headers(process_images PRIVATE src/precompiled.h) 33 | target_precompile_headers(match REUSE_FROM process_images) 34 | target_precompile_headers(build REUSE_FROM process_images) 35 | 36 | find_package(OpenCV REQUIRED) 37 | include_directories(${OpenCV_INCLUDE_DIRS}) 38 | target_link_libraries(process_images ${OpenCV_LIBS}) 39 | target_link_libraries(match ${OpenCV_LIBS}) 40 | target_link_libraries(build ${OpenCV_LIBS}) 41 | 42 | find_package(Boost 1.76.0 REQUIRED COMPONENTS container) 43 | target_link_libraries(process_images Boost::container) 44 | target_link_libraries(match Boost::container) 45 | target_link_libraries(build Boost::container) 46 | 47 | include(cmake/imgui.cmake) 48 | target_link_libraries(process_images glfw IMGUI GL3W) 49 | target_link_libraries(match glfw IMGUI GL3W) 50 | target_link_libraries(build glfw IMGUI GL3W) 51 | 52 | FetchContent_Declare( 53 | linenoise 54 | GIT_REPOSITORY https://github.com/yhirose/cpp-linenoise 55 | ) 56 | 57 | FetchContent_MakeAvailable(linenoise) 58 | target_link_libraries(process_images linenoise) 59 | 60 | add_custom_target( 61 | clang-format 62 | COMMAND clang-format -i src/process_images.cpp src/match.cpp ${LibraryFiles} 63 | COMMENT "Formatting with clang-format" 64 | ) 65 | -------------------------------------------------------------------------------- /src/lib/mat-to-texture.cpp: -------------------------------------------------------------------------------- 1 | #include "mat-to-texture.hpp" 2 | 3 | // From https://gist.github.com/insaneyilin/038a022f2ece61c923315306ddcea081 4 | // With fix from https://stackoverflow.com/a/53566791 5 | void matToTexture(const cv::Mat &mat, GLuint *outTexture) { 6 | // Generate a number for our textureID's unique handle 7 | GLuint textureID; 8 | glGenTextures(1, &textureID); 9 | 10 | // Bind to our texture handle 11 | glBindTexture(GL_TEXTURE_2D, textureID); 12 | 13 | // Set texture interpolation methods for minification and magnification 14 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 15 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 16 | 17 | // Set texture clamping method 18 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 19 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 20 | 21 | // Use fast 4-byte alignment (default anyway) if possible 22 | glPixelStorei(GL_UNPACK_ALIGNMENT, (mat.cols % 4 == 0) ? 4 : 1); 23 | 24 | // Set length of one complete row in data (doesn't need to equal mat.cols) 25 | glPixelStorei(GL_UNPACK_ROW_LENGTH, mat.step / mat.elemSize()); 26 | 27 | // Set incoming texture format to: 28 | // GL_BGR for CV_CAP_OPENNI_BGR_IMAGE, 29 | // GL_LUMINANCE for CV_CAP_OPENNI_DISPARITY_MAP, 30 | // Work out other mappings as required ( there's a list in comments in main() 31 | // ) 32 | GLenum inputColourFormat = GL_BGR; 33 | if (mat.channels() == 1) { 34 | inputColourFormat = GL_RED; 35 | } 36 | 37 | if (!mat.isContinuous()) { 38 | throw std::runtime_error("Matrix not continuous"); 39 | } 40 | 41 | // Create the texture 42 | glTexImage2D(GL_TEXTURE_2D, // Type of texture 43 | 0, // Pyramid level (for mip-mapping) - 0 is the top level 44 | GL_RGB, // Internal colour format to convert to 45 | mat.cols, // Image width i.e. 640 for Kinect in standard mode 46 | mat.rows, // Image height i.e. 480 for Kinect in standard mode 47 | 0, // Border width in pixels (can either be 1 or 0) 48 | inputColourFormat, // Input image format (i.e. GL_RGB, GL_RGBA, 49 | // GL_BGR etc.) 50 | GL_UNSIGNED_BYTE, // Image data type 51 | mat.data); // The actual image data itself 52 | 53 | *outTexture = textureID; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/edged-image.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../precompiled.h" 6 | #include "../config.h" 7 | 8 | #include "bitset-serialise.hpp" 9 | 10 | struct ImageMatch { 11 | float percentage = 0, scale = 1; 12 | int originX = 0, originY = 0; 13 | }; 14 | 15 | class EdgedImage { 16 | using bitset = boost::dynamic_bitset; 17 | 18 | void matchToStep(const cv::Mat &templateImage, const uchar edgesAry[], 19 | ImageMatch *match, float scale, int originX, int originY, 20 | int rowStep = 1, int colStep = 1, 21 | float whiteBias = MATCH_WHITE_BIAS) const; 22 | 23 | cv::Mat originalImage; 24 | int _matchContextOffsetX; 25 | int _matchContextOffsetY; 26 | 27 | // @todo make this a bit more classey 28 | public: 29 | std::string path; 30 | int width, height; 31 | bitset edges; 32 | 33 | int detectionMode, detectionBlurSize, detectionBlurSigmaX, 34 | detectionBlurSigmaY, detectionCannyThreshold1, detectionCannyThreshold2, 35 | detectionCannyJoinByX, detectionCannyJoinByY,detectionBinaryThreshold; 36 | 37 | ImageMatch lastMatch; 38 | 39 | EdgedImage() {} 40 | EdgedImage(std::string path, int width, int height, bitset &edges, 41 | int detectionMode = ImageEdgeMode_Canny, 42 | int detectionBlurSize = EDGE_DETECTION_BLUR_SIZE, 43 | int detectionBlurSigmaX = EDGE_DETECTION_BLUR_SIGMA_X, 44 | int detectionBlurSigmaY = EDGE_DETECTION_BLUR_SIGMA_Y, 45 | int detectionCannyThreshold1 = EDGE_DETECTION_CANNY_THRESHOLD_1, 46 | int detectionCannyThreshold2 = EDGE_DETECTION_CANNY_THRESHOLD_2, 47 | int detectionCannyJoinByX = EDGE_DETECTION_CANNY_JOIN_BY_X, 48 | int detectionCannyJoinByY = EDGE_DETECTION_CANNY_JOIN_BY_Y, 49 | int detectionBinaryThreshold = EDGE_DETECTION_BINARY_THRESHOLD) 50 | : path(path), width(width), height(height), edges(edges), 51 | detectionMode(detectionMode), detectionBlurSize(detectionBlurSize), 52 | detectionBlurSigmaX(detectionBlurSigmaX), 53 | detectionBlurSigmaY(detectionBlurSigmaY), 54 | detectionCannyThreshold1(detectionCannyThreshold1), 55 | detectionCannyThreshold2(detectionCannyThreshold2), 56 | detectionCannyJoinByX(detectionCannyJoinByX), 57 | detectionCannyJoinByY(detectionCannyJoinByY), 58 | detectionBinaryThreshold(detectionBinaryThreshold) {} 59 | 60 | void provideMatchContext(int templateOffsetX, int templateOffsetY); 61 | void resetMatchContext(); 62 | 63 | int matchTo(const cv::Mat &templateImage, ImageMatch *match, 64 | float offsetScaleStep = MATCH_OFFSET_SCALE_STEP, 65 | int offsetXStep = MATCH_OFFSET_X_STEP, 66 | int offsetYStep = MATCH_OFFSET_Y_STEP, 67 | float minOffsetScale = MATCH_MIN_OFFSET_SCALE, 68 | int maxOffset = MATCH_MAX_OFFSET, 69 | float whiteBias = MATCH_WHITE_BIAS); 70 | cv::Mat edgesAsMatrix() const; 71 | cv::Mat getOriginal(bool cache = true); 72 | 73 | friend std::ostream& operator<<(std::ostream& os, const EdgedImage& image); 74 | }; 75 | 76 | std::ostream& operator<<(std::ostream& os, const EdgedImage& image); 77 | -------------------------------------------------------------------------------- /src/lib/detect-edge.cpp: -------------------------------------------------------------------------------- 1 | #include "detect-edge.hpp" 2 | 3 | static cv::Mat blurImage(const cv::Mat &sourceImage, int blurSize, int sigmaX, 4 | int sigmaY) { 5 | cv::Mat image, imageBlurred; 6 | 7 | int width = EDGE_DETECTION_WIDTH; 8 | int height = (double)sourceImage.rows / sourceImage.cols * width; 9 | cv::resize(sourceImage, image, {width, height}); 10 | cv::GaussianBlur(image, imageBlurred, {blurSize, blurSize}, sigmaX, sigmaY); 11 | 12 | return imageBlurred; 13 | } 14 | 15 | cv::Mat detectEdgesCanny(const cv::Mat &sourceImage, int blurSize, int sigmaX, 16 | int sigmaY, int threshold1, int threshold2, 17 | int joinByX, int joinByY) { 18 | cv::Mat imageBlurred = blurImage(sourceImage, blurSize, sigmaX, sigmaY); 19 | cv::Mat imageCanny; 20 | 21 | cv::Canny(imageBlurred, imageCanny, threshold1, threshold2); 22 | 23 | if (joinByX != 0 || joinByY != 0) { 24 | if (joinByX == 0) { 25 | joinByX = 1; 26 | } else if (joinByY == 0) { 27 | joinByY = 1; 28 | } 29 | 30 | cv::Mat imageDilated; 31 | cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, 32 | cv::Size(joinByX, joinByY)); 33 | 34 | cv::dilate(imageCanny, imageDilated, kernel); 35 | cv::erode(imageDilated, imageCanny, kernel); 36 | } 37 | 38 | if (EDGE_DETECTION_WIDTH == STORED_EDGES_WIDTH) { 39 | return imageCanny; 40 | } 41 | 42 | cv::Mat imageResized; 43 | int finalWidth = STORED_EDGES_WIDTH; 44 | int finalHeight = (double)sourceImage.rows / sourceImage.cols * finalWidth; 45 | 46 | cv::resize(imageCanny, imageResized, {finalWidth, finalHeight}); 47 | 48 | return imageResized; 49 | } 50 | 51 | cv::Mat detectEdgesThreshold(const cv::Mat &sourceImage, int blurSize, 52 | int sigmaX, int sigmaY, int binaryThreshold) { 53 | cv::Mat imageBlurred = blurImage(sourceImage, blurSize, sigmaX, sigmaY); 54 | cv::Mat imageGray, imageThreshold; 55 | 56 | cv::cvtColor(imageBlurred, imageGray, cv::COLOR_BGR2GRAY); 57 | cv::threshold(imageGray, imageThreshold, binaryThreshold, 255, 58 | cv::THRESH_BINARY); 59 | 60 | cv::Mat imageResized; 61 | if (EDGE_DETECTION_WIDTH == STORED_EDGES_WIDTH) { 62 | imageResized = imageThreshold; 63 | } 64 | 65 | int finalWidth = STORED_EDGES_WIDTH; 66 | int finalHeight = (double)sourceImage.rows / sourceImage.cols * finalWidth; 67 | 68 | cv::resize(imageThreshold, imageResized, {finalWidth, finalHeight}); 69 | 70 | std::vector> contours; 71 | std::vector hierarchy; 72 | 73 | cv::findContours(imageResized, contours, hierarchy, cv::RETR_TREE, 74 | cv::CHAIN_APPROX_NONE); 75 | 76 | imageResized.setTo(cv::Scalar::all(0)); 77 | cv::drawContours(imageResized, contours, -1, cv::Scalar(255)); 78 | 79 | return imageResized; 80 | } 81 | 82 | boost::dynamic_bitset edgesToBitset(cv::Mat &edgeMatrix) { 83 | int channels = edgeMatrix.channels(); 84 | CV_Assert(channels == 1); 85 | 86 | int nRows = edgeMatrix.rows; 87 | int nCols = edgeMatrix.cols; 88 | 89 | if (edgeMatrix.isContinuous()) { 90 | nCols *= nRows; 91 | nRows = 1; 92 | } 93 | 94 | boost::dynamic_bitset bitset(nRows * nCols); 95 | 96 | int i = 0; 97 | for (int y = 0; y < nRows; ++y) { 98 | uchar *p = edgeMatrix.ptr(y); 99 | 100 | for (int x = 0; x < nCols; ++x) { 101 | bitset[i] = p[x] != 0; 102 | ++i; 103 | } 104 | } 105 | 106 | return bitset; 107 | } 108 | -------------------------------------------------------------------------------- /src/lib/window.cpp: -------------------------------------------------------------------------------- 1 | #include "window.hpp" 2 | 3 | GLFWwindow *window; 4 | 5 | static void glfw_error_callback(int error, const char *description) { 6 | char errorText[50]; 7 | sprintf(errorText, "Glfw error %i: %s", error, description); 8 | throw std::runtime_error(errorText); 9 | } 10 | 11 | void initWindow(int width, int height, const char *title) { 12 | glfwSetErrorCallback(glfw_error_callback); 13 | 14 | if (!glfwInit()) { 15 | throw std::runtime_error("Error initiating glfw"); 16 | } 17 | 18 | // Decide GL+GLSL versions 19 | #if defined(IMGUI_IMPL_OPENGL_ES2) 20 | // GL ES 2.0 + GLSL 100 21 | const char *glsl_version = "#version 100"; 22 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); 23 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); 24 | glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); 25 | #elif defined(__APPLE__) 26 | // GL 3.2 + GLSL 150 27 | const char *glsl_version = "#version 150"; 28 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 29 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); 30 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only 31 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac 32 | #else 33 | // GL 3.0 + GLSL 130 34 | const char *glsl_version = "#version 130"; 35 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 36 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); 37 | // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ 38 | // only glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only 39 | #endif 40 | 41 | GLFWmonitor *monitor = glfwGetPrimaryMonitor(); 42 | const GLFWvidmode *mode = glfwGetVideoMode(monitor); 43 | 44 | // Create window with graphics context 45 | int attemptWidth = std::min(mode->width, width); 46 | int attemptHeight = std::min(mode->height, height); 47 | window = glfwCreateWindow(attemptWidth, attemptHeight, title, NULL, NULL); 48 | 49 | if (window == NULL) { 50 | throw std::runtime_error("Error initiating window"); 51 | } 52 | 53 | int actualWidth, actualHeight; 54 | glfwGetWindowSize(window, &actualWidth, &actualHeight); 55 | 56 | if (actualWidth < width || actualHeight < height) { 57 | float scale = 58 | fmin((float)actualWidth / width, (float)actualHeight / height); 59 | width *= scale; 60 | height *= scale; 61 | } 62 | 63 | glfwSetWindowSize(window, width, height); 64 | glfwSetWindowAspectRatio(window, width, height); 65 | 66 | glfwMakeContextCurrent(window); 67 | glfwSwapInterval(1); // Enable vsync 68 | 69 | // Initialize OpenGL loader 70 | bool err = gl3wInit() != 0; 71 | if (err) { 72 | throw std::runtime_error("Failed to initialize OpenGL loader!"); 73 | } 74 | 75 | IMGUI_CHECKVERSION(); 76 | ImGui::CreateContext(); 77 | ImGuiIO &io = ImGui::GetIO(); 78 | (void)io; 79 | ImGui::StyleColorsDark(); 80 | ImGui_ImplGlfw_InitForOpenGL(window, true); 81 | ImGui_ImplOpenGL3_Init(glsl_version); 82 | } 83 | 84 | void openWindow(const onFrameFn &onFrame) { 85 | while (!glfwWindowShouldClose(window)) { 86 | glfwPollEvents(); 87 | 88 | ImGui_ImplOpenGL3_NewFrame(); 89 | ImGui_ImplGlfw_NewFrame(); 90 | ImGui::NewFrame(); 91 | 92 | ImGuiIO &io = ImGui::GetIO(); 93 | 94 | bool shouldClose = onFrame(window, io); 95 | if (shouldClose) { 96 | glfwSetWindowShouldClose(window, GLFW_TRUE); 97 | break; 98 | } 99 | 100 | ImGui::Render(); 101 | int display_w, display_h; 102 | glfwGetFramebufferSize(window, &display_w, &display_h); 103 | glViewport(0, 0, display_w, display_h); 104 | glClearColor(0.45f, 0.55f, 0.6f, 1.f); 105 | glClear(GL_COLOR_BUFFER_BIT); 106 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 107 | 108 | glfwSwapBuffers(window); 109 | } 110 | 111 | ImGui_ImplOpenGL3_Shutdown(); 112 | ImGui_ImplGlfw_Shutdown(); 113 | ImGui::DestroyContext(); 114 | 115 | glfwDestroyWindow(window); 116 | glfwTerminate(); 117 | } 118 | -------------------------------------------------------------------------------- /src/process_images.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "precompiled.h" 4 | 5 | #include "config.h" 6 | 7 | #include "lib/detect-edge.hpp" 8 | #include "lib/edit-image-edges.hpp" 9 | #include "lib/image-list.hpp" 10 | 11 | std::optional imageFromArg(ImageList &imageList, const std::string &arg) { 12 | if (arg.empty()) { 13 | std::cerr << "Argument required\n"; 14 | return {}; 15 | } 16 | 17 | int id = -1; 18 | try { 19 | id = stoi(arg); 20 | } catch (std::invalid_argument) { 21 | for (size_t i = 0; i < imageList.count(); ++i) { 22 | if (imageList.at(i)->path == arg) { 23 | id = i; 24 | break; 25 | } 26 | } 27 | } 28 | 29 | if (id == -1) { 30 | std::cerr << "Image not found\n"; 31 | return {}; 32 | } 33 | 34 | if (id > imageList.count() - 1) { 35 | std::cerr << "That image doesn't exist: highest ID is " 36 | << (imageList.count() - 1) << '\n'; 37 | return {}; 38 | } 39 | 40 | return id; 41 | } 42 | 43 | int main(int argc, const char *argv[]) { 44 | auto readStart = std::chrono::high_resolution_clock::now(); 45 | 46 | if (!argv[1]) { 47 | std::cerr << "No directory specified, exiting\n"; 48 | exit(1); 49 | } 50 | 51 | std::cout << std::fixed << std::setprecision(2); 52 | 53 | ImageList imageList(argv[1]); 54 | 55 | if (imageList.count()) { 56 | auto readFinish = std::chrono::high_resolution_clock::now(); 57 | std::chrono::duration readElapsed = readFinish - readStart; 58 | 59 | std::cout << "Loaded " << imageList.count() << " images from store in " 60 | << readElapsed.count() << "s\n"; 61 | } else { 62 | std::cout << "No store found for this directory, type \"generate\" to " 63 | << "generate one\n"; 64 | } 65 | 66 | while (true) { 67 | std::cout << '\n'; // This bugs out if added to the linenoise call 68 | std::string line; 69 | auto quit = linenoise::Readline("> ", line); 70 | 71 | if (quit) { 72 | break; 73 | } 74 | 75 | std::string_view query(line); 76 | auto splitPos = query.find(' '); 77 | std::string_view command = query.substr(0, splitPos); 78 | std::string_view arg; 79 | 80 | if (splitPos != query.npos) { 81 | arg = query.substr(query.find(' ') + 1); 82 | } 83 | 84 | linenoise::AddHistory(line.c_str()); 85 | 86 | if (command == "exit" || command == "q") { 87 | std::cout << "bye\n"; 88 | break; 89 | } 90 | 91 | auto start = std::chrono::high_resolution_clock::now(); 92 | 93 | if (command == "generate" || command == "reset") { 94 | if (command == "generate" && imageList.count() > 0) { 95 | std::cerr 96 | << "Store has already been generated: use \"reset\" to reset\n"; 97 | continue; 98 | } else if (command == "reset") { 99 | std::cout << '\n'; 100 | std::string line; 101 | auto quit = 102 | linenoise::Readline("Are you sure you wish to reset? (y/N) ", line); 103 | 104 | if (quit) { 105 | break; 106 | } 107 | 108 | if (line != "y") { 109 | std::cout << "Aborting reset\n"; 110 | continue; 111 | } 112 | } 113 | 114 | imageList.generate(); 115 | imageList.save(); 116 | 117 | auto finish = std::chrono::high_resolution_clock::now(); 118 | std::chrono::duration elapsed = finish - start; 119 | std::cout << "Generated in " << elapsed.count() << "s\n"; 120 | } else if (command == "sync") { 121 | auto imageListBackup = imageList; 122 | 123 | int newImages = imageList.sync(); 124 | 125 | if (!newImages) { 126 | std::cout << "Synced: no new images found\n"; 127 | continue; 128 | } 129 | 130 | char prompt[35]; 131 | sprintf(prompt, "%i new images found, save? (Y/n) ", newImages); 132 | 133 | std::string line; 134 | auto quit = linenoise::Readline(prompt, line); 135 | 136 | if (quit) { 137 | break; 138 | } 139 | 140 | if (line != "n") { 141 | std::cout << "Saving " << newImages << " new images to store\n"; 142 | imageList.save(); 143 | } else { 144 | imageList = imageListBackup; 145 | } 146 | } else if (command == "ls") { 147 | std::cout << imageList.count() << " images in store:\n\n"; 148 | 149 | int i = 0; 150 | for (const std::shared_ptr &image : imageList) { 151 | std::cout << i << ": " << image->path << " (" << image->width << "x" 152 | << image->height << ")\n"; 153 | i++; 154 | } 155 | 156 | std::cout << '\n'; 157 | } else if (command == "edit") { 158 | auto maybeImage = imageFromArg(imageList, std::string(arg)); 159 | 160 | if (!maybeImage.has_value()) { 161 | continue; 162 | } 163 | 164 | std::shared_ptr &image = imageList.at(maybeImage.value()); 165 | 166 | std::cout << "Editing: " << image->path << "\n"; 167 | 168 | std::optional maybeNewImage = editImageEdges(*image); 169 | 170 | if (maybeNewImage.has_value()) { 171 | image = std::shared_ptr(maybeNewImage.value()); 172 | imageList.save(); 173 | std::cout << "Changes saved\n"; 174 | } else { 175 | std::cout << "Changes discarded\n"; 176 | } 177 | } else if (command == "rm") { 178 | auto maybeImage = imageFromArg(imageList, std::string(arg)); 179 | 180 | if (!maybeImage.has_value()) { 181 | continue; 182 | } 183 | 184 | int id = maybeImage.value(); 185 | std::string path = imageList.at(id)->path; 186 | 187 | std::cout << "Removing: " << path << "\n"; 188 | 189 | imageList.erase(id); 190 | imageList.save(); 191 | 192 | char prompt[60]; 193 | sprintf(prompt, "Image removed from store: delete file from disk? (Y/n) "); 194 | 195 | std::string line; 196 | auto quit = linenoise::Readline(prompt, line); 197 | 198 | if (quit) { 199 | break; 200 | } 201 | 202 | if (line != "n") { 203 | std::filesystem::remove(path); 204 | std::cout << "File removed\n"; 205 | } 206 | } else if (command == "sort") { 207 | imageList.sortBy("path"); 208 | std::cout << "Sorted by file path - this will not be saved to store\n"; 209 | } else if (command == "save") { 210 | imageList.save(false); 211 | std::cout << "Store saved\n"; 212 | } else { 213 | std::cout << "?\n"; 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/build.cpp: -------------------------------------------------------------------------------- 1 | #include "precompiled.h" 2 | 3 | #include "config.h" 4 | 5 | #include "lib/frame-collection.hpp" 6 | #include "lib/mat-to-texture.hpp" 7 | #include "lib/window.hpp" 8 | 9 | int main(int argc, const char *argv[]) { 10 | std::cout << "Preloading frames…\n"; 11 | 12 | auto lastFrame = std::chrono::high_resolution_clock::now(); 13 | 14 | std::string name(argv[1]); 15 | FrameCollection frames(name); 16 | frames.preloadAll(); 17 | 18 | auto loadFinish = std::chrono::high_resolution_clock::now(); 19 | std::chrono::duration loadDuration = loadFinish - lastFrame; 20 | 21 | std::cout << "Preloaded frames in " << loadDuration.count() << "s\n"; 22 | 23 | if (argv[2] && strcmp(argv[2], "--write") == 0) { 24 | auto writeBegin = std::chrono::high_resolution_clock::now(); 25 | 26 | frames.writeImages(name); 27 | 28 | auto writeFinish = std::chrono::high_resolution_clock::now(); 29 | std::chrono::duration writeDuration = writeFinish - writeBegin; 30 | std::cout << "Written " << frames.size() << " frames to disk in " 31 | << writeDuration.count() << "s\n"; 32 | 33 | return 0; 34 | } 35 | 36 | bool edited = false; 37 | bool saved = false; 38 | int frameId = 0; 39 | bool playing = false; 40 | int fps = 5; 41 | 42 | initWindow(OUTPUT_WIDTH, OUTPUT_HEIGHT, "Build preview"); 43 | GLuint image_tex; 44 | std::vector::iterator matchForFrame; 45 | std::vector *matchesForFrame; 46 | bool doPreview = false; 47 | std::vector::iterator previewMatch; 48 | 49 | auto generateTexture = [&]() { 50 | cv::Mat image = doPreview ? frames.imageFor(previewMatch) : frames.imageAt(frameId); 51 | doPreview = false; 52 | matToTexture(image, &image_tex); 53 | matchForFrame = frames.matchAt(frameId); 54 | matchesForFrame = frames.matchesAt(frameId); 55 | }; 56 | 57 | generateTexture(); 58 | 59 | openWindow([&](GLFWwindow *window, ImGuiIO &io) { 60 | bool changed = false; 61 | 62 | if (playing) { 63 | auto current = std::chrono::high_resolution_clock::now(); 64 | std::chrono::duration delta = current - lastFrame; 65 | if (delta.count() > 1.f / fps) { 66 | frameId = frameId == frames.size() - 1 ? 0 : frameId + 1; 67 | lastFrame = current; 68 | 69 | changed = true; 70 | } 71 | } 72 | 73 | ImGui::SetNextWindowFocus(); 74 | ImGui::Begin("Controls"); 75 | 76 | changed |= ImGui::SliderInt("Frame ID", &frameId, 0, frames.size() - 1); 77 | ImGui::SameLine(); 78 | if ((ImGui::SmallButton("-") || ImGui::IsKeyPressed(GLFW_KEY_LEFT, false)) 79 | && frameId > 0) { 80 | --frameId; 81 | changed = true; 82 | } 83 | ImGui::SameLine(); 84 | if ((ImGui::SmallButton("+") || ImGui::IsKeyPressed(GLFW_KEY_RIGHT, false)) 85 | && frameId < frames.size() - 1) { 86 | ++frameId; 87 | changed = true; 88 | } 89 | 90 | ImGui::NewLine(); 91 | 92 | if (ImGui::Button(playing ? "Pause" : "Play")) { 93 | playing = !playing; 94 | } 95 | if (playing) { 96 | ImGui::SameLine(0, 10); 97 | ImGui::SetNextItemWidth(100); 98 | ImGui::SliderInt("FPS", &fps, 1, 30); 99 | } 100 | 101 | ImGui::NewLine(); 102 | 103 | if (edited || saved) { 104 | if (edited && ImGui::Button("Save changes")) { 105 | frames.save(name); 106 | edited = false; 107 | saved = true; 108 | } else if (saved) { 109 | ImGui::Text("Saved!"); 110 | } 111 | 112 | ImGui::NewLine(); 113 | } 114 | 115 | if (ImGui::TreeNode("Reposition")) { 116 | ImGui::Text("Scale: %.2f", matchForFrame->scale); 117 | ImGui::SameLine(); 118 | if (ImGui::SmallButton("-##scale-minus")) { 119 | frames.editMatchScale(frameId, matchForFrame->scale + 0.01); 120 | changed = true; 121 | edited = true; 122 | } 123 | ImGui::SameLine(); 124 | if (ImGui::SmallButton("+##scale-plus")) { 125 | frames.editMatchScale(frameId, matchForFrame->scale - 0.01); 126 | changed = true; 127 | edited = true; 128 | } 129 | 130 | ImGui::Text("x: %i", matchForFrame->originX); 131 | ImGui::SameLine(); 132 | if (ImGui::SmallButton("-##x-minus")) { 133 | frames.editMatchOriginX(frameId, matchForFrame->originX - 1); 134 | changed = true; 135 | edited = true; 136 | } 137 | ImGui::SameLine(); 138 | if (ImGui::SmallButton("+##x-plus")) { 139 | frames.editMatchOriginX(frameId, matchForFrame->originX + 1); 140 | changed = true; 141 | edited = true; 142 | } 143 | 144 | ImGui::Text("y: %i", matchForFrame->originY); 145 | ImGui::SameLine(); 146 | if (ImGui::SmallButton("-##y-minus")) { 147 | frames.editMatchOriginY(frameId, matchForFrame->originY - 1); 148 | changed = true; 149 | edited = true; 150 | } 151 | ImGui::SameLine(); 152 | if (ImGui::SmallButton("+##y-plus")) { 153 | frames.editMatchOriginY(frameId, matchForFrame->originY + 1); 154 | changed = true; 155 | edited = true; 156 | } 157 | 158 | ImGui::NewLine(); 159 | ImGui::TreePop(); 160 | } 161 | 162 | if (ImGui::TreeNode("All matches")) { 163 | if (ImGui::BeginChild("matches")) { 164 | for (std::vector::iterator match = matchesForFrame->begin(); 165 | match != matchesForFrame->end(); ++match) { 166 | ImGui::PushID(match->path.c_str()); 167 | if (ImGui::SmallButton("Preview")) { 168 | previewMatch = match; 169 | doPreview = true; 170 | changed = true; 171 | } 172 | ImGui::SameLine(); 173 | if (ImGui::SmallButton("y")) { 174 | frames.forceMatch(frameId, match); 175 | changed = true; 176 | // Stop usages of match below segfaulting 177 | match = matchesForFrame->begin(); 178 | edited = true; 179 | } 180 | ImGui::SameLine(); 181 | if (ImGui::SmallButton("n")) { 182 | frames.removeMatch(frameId, match); 183 | edited = true; 184 | } 185 | ImGui::SameLine(); 186 | ImGui::Text("%.1f%%: %s", match->percentage * 100, match->path.c_str()); 187 | ImGui::SameLine(); 188 | if (ImGui::SmallButton("copy")) { 189 | ImGui::SetClipboardText(match->path.c_str()); 190 | } 191 | ImGui::PopID(); 192 | } 193 | } 194 | ImGui::EndChild(); 195 | ImGui::TreePop(); 196 | } 197 | 198 | ImGui::End(); 199 | 200 | if (changed) { 201 | saved = false; 202 | generateTexture(); 203 | } 204 | 205 | int actualWidth, actualHeight; 206 | glfwGetWindowSize(window, &actualWidth, &actualHeight); 207 | ImGui::SetNextWindowPos(ImVec2(.0f, .0f)); 208 | ImGui::SetNextWindowSize(ImVec2(actualWidth, actualHeight)); 209 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f)); 210 | ImGui::Begin("Image", NULL, staticWindowFlags); 211 | ImGui::Image((void *)(intptr_t)image_tex, ImGui::GetContentRegionMax()); 212 | ImGui::End(); 213 | ImGui::PopStyleVar(1); 214 | 215 | return false; 216 | }); 217 | } 218 | -------------------------------------------------------------------------------- /src/lib/image-list.cpp: -------------------------------------------------------------------------------- 1 | #include "image-list.hpp" 2 | #include "bitset-serialise.hpp" 3 | 4 | ImageList::ImageList(std::string dirPath) : dirPath(dirPath) { 5 | namespace fs = std::filesystem; 6 | 7 | if (!fs::exists(dirPath)) { 8 | throw std::runtime_error("Directory doesn't exist: " + dirPath); 9 | } 10 | 11 | getStored(); 12 | } 13 | 14 | bool ImageList::getStored() { 15 | namespace fs = std::filesystem; 16 | 17 | fs::path storePath{dirPath}; 18 | storePath.append(".store"); 19 | std::ifstream storeFile(storePath); 20 | if (!storeFile) { 21 | return false; 22 | } 23 | 24 | std::string line; 25 | 26 | while (std::getline(storeFile, line)) { 27 | std::string path; 28 | int width, height, bitsetSize; 29 | boost::dynamic_bitset edges; 30 | 31 | int detectionMode, detectionBlurSize, detectionBlurSigmaX, 32 | detectionBlurSigmaY, detectionCannyThreshold1, detectionCannyThreshold2, 33 | detectionCannyJoinByX, detectionCannyJoinByY, detectionBinaryThreshold; 34 | 35 | std::stringstream lineStream(line); 36 | int i = 0; 37 | while (lineStream.good()) { 38 | std::string substr; 39 | std::getline(lineStream, substr, ','); 40 | 41 | if (i == 0) { 42 | path = substr; 43 | } else if (i == 1) { 44 | width = std::stoi(substr); 45 | } else if (i == 2) { 46 | height = std::stoi(substr); 47 | } else if (i == 3) { 48 | bitsetSize = std::stoi(substr); 49 | } else if (i == 4) { 50 | edges = stringToBitset(substr.c_str(), bitsetSize); 51 | } else if (i == 5) { 52 | detectionMode = std::stoi(substr); 53 | } else if (i == 6) { 54 | detectionBlurSize = std::stoi(substr); 55 | } else if (i == 7) { 56 | detectionBlurSigmaX = std::stoi(substr); 57 | } else if (i == 8) { 58 | detectionBlurSigmaY = std::stoi(substr); 59 | } else if (i == 9) { 60 | detectionCannyThreshold1 = std::stoi(substr); 61 | } else if (i == 10) { 62 | detectionCannyThreshold2 = std::stoi(substr); 63 | } else if (i == 11) { 64 | detectionBinaryThreshold = std::stoi(substr); 65 | } else if (i == 12) { 66 | detectionCannyJoinByX = std::stoi(substr); 67 | } else if (i == 13) { 68 | detectionCannyJoinByY = std::stoi(substr); 69 | } 70 | 71 | ++i; 72 | } 73 | 74 | store.push_back(std::make_shared( 75 | path, width, height, edges, detectionMode, detectionBlurSize, 76 | detectionBlurSigmaX, detectionBlurSigmaY, detectionCannyThreshold1, 77 | detectionCannyThreshold2, detectionCannyJoinByX, detectionCannyJoinByY, 78 | detectionBinaryThreshold)); 79 | } 80 | 81 | storeFile.close(); 82 | 83 | return true; 84 | } 85 | 86 | void ImageList::generate() { 87 | namespace fs = std::filesystem; 88 | 89 | store.clear(); 90 | 91 | for (const auto &file : fs::directory_iterator(dirPath)) { 92 | if (std::string(file.path().filename())[0] == '.') { 93 | std::cout << "Skipping: " << file.path() << '\n'; 94 | continue; 95 | } 96 | 97 | addFile(file); 98 | } 99 | } 100 | 101 | int ImageList::sync() { 102 | namespace fs = std::filesystem; 103 | 104 | int added = 0; 105 | 106 | for (const auto &file : fs::directory_iterator(dirPath)) { 107 | if (std::string(file.path().filename())[0] == '.') { 108 | continue; 109 | } 110 | 111 | bool exists = false; 112 | for (const std::shared_ptr &image : store) { 113 | if (image->path == std::string(file.path())) { 114 | exists = true; 115 | break; 116 | } 117 | } 118 | if (exists) { 119 | continue; 120 | } 121 | 122 | addFile(file); 123 | added++; 124 | } 125 | 126 | return added; 127 | } 128 | 129 | void ImageList::addFile(const std::filesystem::directory_entry &file) { 130 | std::cout << "Reading: " << file.path(); 131 | 132 | cv::Mat sourceImage = cv::imread(file.path()); 133 | 134 | if (sourceImage.empty()) { 135 | std::cout << " (skipping, cannot read)\n"; 136 | return; 137 | } else { 138 | std::cout << '\n'; 139 | } 140 | 141 | auto edgesMat = detectEdgesCanny(sourceImage); 142 | auto edges = edgesToBitset(edgesMat); 143 | store.push_back(std::make_shared(file.path(), sourceImage.cols, 144 | sourceImage.rows, edges)); 145 | } 146 | 147 | // If saving before quitting, make sure you call async=false or you might 148 | // destroy the store file 149 | void ImageList::save(bool async) { 150 | auto threadFn = [&]() { 151 | std::filesystem::path storePath{dirPath}; 152 | storePath.append(".store"); 153 | std::ofstream storeFile(storePath); 154 | if (!storeFile) { 155 | throw std::runtime_error("Failed to open store file."); 156 | } 157 | 158 | for (const std::shared_ptr &image : store) { 159 | storeFile << *image << '\n'; 160 | } 161 | }; 162 | 163 | if (async) { 164 | std::thread saveThread(threadFn); 165 | saveThread.detach(); 166 | } else { 167 | threadFn(); 168 | } 169 | } 170 | 171 | void ImageList::provideMatchContext(int templateOffsetX, int templateOffsetY) { 172 | _matchContextOffsetX = templateOffsetX; 173 | _matchContextOffsetY = templateOffsetY; 174 | } 175 | void ImageList::resetMatchContext() { 176 | _matchContextOffsetX = 0; 177 | _matchContextOffsetY = 0; 178 | } 179 | 180 | int ImageList::matchTo(const cv::Mat &templateImage, ImageMatch *bestMatch, 181 | EdgedImage **bestMatchImage, float offsetScaleStep, 182 | int offsetXStep, int offsetYStep, float minOffsetScale, 183 | int maxOffset, float whiteBias) { 184 | std::atomic_int runs(0); 185 | int maxThreads = std::thread::hardware_concurrency() - 1; 186 | if (maxThreads == -1) { 187 | throw std::runtime_error("hardware_concurrency() returning 0, unsupported"); 188 | } 189 | int threads = std::min(maxThreads, count()); 190 | 191 | std::vector threadVector; 192 | threadVector.reserve(threads); 193 | 194 | std::atomic_int imageIndex(0); 195 | std::mutex bestMatchMutex; 196 | 197 | auto threadFn = [&]() { 198 | while (true) { 199 | int indexToGet = imageIndex++; 200 | if (indexToGet > store.size() - 1) { 201 | break; 202 | } 203 | std::shared_ptr sourceImage = store[indexToGet]; 204 | 205 | sourceImage->provideMatchContext(_matchContextOffsetX, 206 | _matchContextOffsetY); 207 | 208 | ImageMatch match; 209 | runs += sourceImage->matchTo(templateImage, &match, offsetScaleStep, 210 | offsetXStep, offsetYStep, minOffsetScale, 211 | maxOffset, whiteBias); 212 | 213 | std::lock_guard bestMatchLock(bestMatchMutex); 214 | 215 | if (match.percentage > bestMatch->percentage) { 216 | *bestMatch = match; 217 | *bestMatchImage = sourceImage.get(); 218 | } 219 | } 220 | }; 221 | 222 | for (int i = 0; i < threads; ++i) { 223 | threadVector.emplace_back(threadFn); 224 | } 225 | for (std::thread &t : threadVector) { 226 | if (t.joinable()) { 227 | t.join(); 228 | } 229 | } 230 | 231 | return runs; 232 | }; 233 | 234 | void ImageList::sortBy(const ImageList::sort_predicate &sortFn) { 235 | std::sort(store.begin(), store.end(), sortFn); 236 | } 237 | void ImageList::sortBy(const char *sorter) { 238 | if (strcmp(sorter, "match-percentage") == 0) { 239 | sortBy([](std::shared_ptr a, 240 | std::shared_ptr b) -> bool { 241 | return a->lastMatch.percentage > b->lastMatch.percentage; 242 | }); 243 | } else if (strcmp(sorter, "path") == 0) { 244 | sortBy([](std::shared_ptr a, 245 | std::shared_ptr b) -> bool { 246 | return a->path < b->path; 247 | }); 248 | } else { 249 | throw std::runtime_error("Invalid sorter specified"); 250 | } 251 | } 252 | 253 | // @todo is there a nicer way to do this? 254 | ImageList::image_store::iterator ImageList::begin() { return store.begin(); } 255 | 256 | ImageList::image_store::iterator ImageList::end() { return store.end(); } 257 | 258 | ImageList::image_store::reference ImageList::at(size_t pos) { 259 | return store.at(pos); 260 | } 261 | 262 | void ImageList::erase(size_t pos) { 263 | store.erase(begin() + pos); 264 | } 265 | 266 | int ImageList::count() const { return store.size(); } 267 | -------------------------------------------------------------------------------- /src/lib/frame-collection.cpp: -------------------------------------------------------------------------------- 1 | #include "frame-collection.hpp" 2 | 3 | FrameCollection::FrameCollection(const std::string &name) { 4 | namespace fs = std::filesystem; 5 | 6 | std::filesystem::path storePath("assets/collections"); 7 | storePath.append(name); 8 | storePath.append(".frame-data"); 9 | std::ifstream storeFile(storePath); 10 | 11 | if (!storeFile) { 12 | throw std::runtime_error("Failed to open store file."); 13 | } 14 | 15 | std::string line; 16 | while (std::getline(storeFile, line)) { 17 | FrameData frameData; 18 | 19 | std::stringstream lineStream(line); 20 | while (lineStream.good()) { 21 | std::string matchDataStrRaw; 22 | std::getline(lineStream, matchDataStrRaw, ']'); 23 | 24 | if (matchDataStrRaw.find('[') == std::string::npos) { 25 | break; 26 | } 27 | 28 | std::string matchDataStr = 29 | matchDataStrRaw.substr(matchDataStrRaw.find('[') + 1); 30 | MatchData matchData; 31 | 32 | std::stringstream matchStream(matchDataStr); 33 | int i = 0; 34 | while (matchStream.good()) { 35 | std::string substr; 36 | std::getline(matchStream, substr, ','); 37 | 38 | if (i == 0) { 39 | matchData.path = substr; 40 | } else if (i == 1) { 41 | matchData.percentage = std::stof(substr); 42 | } else if (i == 2) { 43 | matchData.scale = std::stof(substr); 44 | } else if (i == 3) { 45 | matchData.originX = std::stoi(substr); 46 | } else if (i == 4) { 47 | matchData.originY = std::stoi(substr); 48 | } 49 | 50 | i++; 51 | } 52 | 53 | frameData.frames.push_back(std::move(matchData)); 54 | } 55 | 56 | push_back(std::move(frameData)); 57 | } 58 | 59 | _cachedMatches.resize(size()); 60 | _isCachedMatches.resize(size()); 61 | std::fill(_isCachedMatches.begin(), _isCachedMatches.end(), false); 62 | 63 | _cachedImages.resize(size()); 64 | _isCachedImages.resize(size()); 65 | std::fill(_isCachedImages.begin(), _isCachedImages.end(), false); 66 | } 67 | 68 | void FrameCollection::addFrame(ImageList imageList) { 69 | FrameData frameData; 70 | 71 | imageList.sortBy("match-percentage"); 72 | 73 | for (std::shared_ptr &image : imageList) { 74 | frameData.frames.emplace_back( 75 | image->path, image->lastMatch.percentage, image->lastMatch.scale, 76 | image->lastMatch.originX, image->lastMatch.originY); 77 | } 78 | 79 | push_back(std::move(frameData)); 80 | } 81 | 82 | void FrameCollection::popFrame() { pop_back(); } 83 | 84 | void FrameCollection::save(const std::string &name) { 85 | namespace fs = std::filesystem; 86 | 87 | if (empty() || name.empty()) { 88 | // todo do something less damaging 89 | throw std::runtime_error("Invalid save"); 90 | } 91 | 92 | fs::path storePath("assets/collections"); 93 | storePath.append(name); 94 | 95 | fs::create_directory(storePath); 96 | 97 | storePath.append(".frame-data"); 98 | 99 | std::ofstream storeFile(storePath); 100 | if (!storeFile) { 101 | throw std::runtime_error("Failed to open store file."); 102 | } 103 | 104 | storeFile << *this; 105 | } 106 | 107 | // Currently this function uses greedy matching to grab the first match 108 | // containing an image that hasn't already been used. In the future it would 109 | // be great if it implemented the hungarian algorithm for better matching. 110 | std::vector::iterator FrameCollection::matchAt(int pos) { 111 | if (_isCachedMatches.at(pos)) { 112 | return _cachedMatches.at(pos); 113 | } 114 | 115 | std::vector::iterator proposedMatch = at(pos).frames.begin(); 116 | for (; proposedMatch < at(pos).frames.end(); ++proposedMatch) { 117 | bool alreadyUsed = false; 118 | for (int i = 0; i < pos; ++i) { 119 | if (matchAt(i)->path == proposedMatch->path) { 120 | alreadyUsed = true; 121 | break; 122 | } 123 | } 124 | 125 | if (!alreadyUsed) { 126 | break; 127 | } 128 | } 129 | 130 | _cachedMatches.at(pos) = proposedMatch; 131 | _isCachedMatches.at(pos) = true; 132 | 133 | return proposedMatch; 134 | } 135 | 136 | std::vector* FrameCollection::matchesAt(int pos) { 137 | return &at(pos).frames; 138 | } 139 | 140 | cv::Mat FrameCollection::imageAt(int pos) { 141 | if (_isCachedImages.at(pos)) { 142 | return _cachedImages.at(pos); 143 | } 144 | 145 | std::vector::iterator match = matchAt(pos); 146 | cv::Mat image = imageFor(match); 147 | 148 | _cachedImages.at(pos) = image; 149 | _isCachedImages.at(pos) = true; 150 | 151 | return image; 152 | } 153 | 154 | // todo move caching to this? 155 | cv::Mat FrameCollection::imageFor(std::vector::iterator match) { 156 | cv::Mat image = cv::imread(match->path); 157 | 158 | if (image.empty()) { 159 | throw std::runtime_error("Couldn't read source image"); 160 | } 161 | 162 | float realScale = (float)image.cols / STORED_EDGES_WIDTH; 163 | 164 | cv::Rect roi; 165 | roi.x = round(match->originX * realScale); 166 | roi.y = round(match->originY * realScale); 167 | roi.width = round(CANVAS_WIDTH * realScale * match->scale); 168 | roi.height = round(CANVAS_HEIGHT * realScale * match->scale); 169 | 170 | cv::Mat cropped = image(roi); 171 | cv::Mat scaledImage; 172 | cv::resize(cropped, scaledImage, cv::Size(OUTPUT_WIDTH, OUTPUT_HEIGHT)); 173 | 174 | return scaledImage; 175 | } 176 | 177 | void FrameCollection::purgeCache() { 178 | std::fill(_isCachedMatches.begin(), _isCachedMatches.end(), false); 179 | std::fill(_isCachedImages.begin(), _isCachedImages.end(), false); 180 | } 181 | 182 | void FrameCollection::forceMatch(int pos, std::vector::iterator match) { 183 | MatchData matchCopy = *match; 184 | at(pos).frames.clear(); 185 | at(pos).frames.push_back(matchCopy); 186 | 187 | purgeCache(); 188 | 189 | match = at(pos).frames.begin(); 190 | 191 | // Delete this image from all other frames 192 | FrameData* current = &at(pos); 193 | for (FrameData &otherFramesData : *this) { 194 | if (&otherFramesData == current) { 195 | continue; 196 | } 197 | 198 | std::vector &otherFrames = otherFramesData.frames; 199 | 200 | auto otherPos = std::find_if(otherFrames.begin(), otherFrames.end(), 201 | [&match](const MatchData &otherMatch) { 202 | return match->path == otherMatch.path; 203 | }); 204 | 205 | otherFrames.erase(otherPos); 206 | } 207 | } 208 | 209 | void FrameCollection::removeMatch(int pos, std::vector::iterator match) { 210 | at(pos).frames.erase(match); 211 | purgeCache(); 212 | } 213 | 214 | void FrameCollection::editMatchScale(int pos, float newScale) { 215 | auto match = matchAt(pos); 216 | MatchData matchBackup = *match; 217 | 218 | float oldScale = match->scale; 219 | match->scale = newScale; 220 | 221 | // This is aproximate and will need manual adjustment due to precision loss 222 | match->originX = std::round((float)match->originX / newScale * oldScale); 223 | match->originY = std::round((float)match->originY / newScale * oldScale); 224 | 225 | _isCachedImages.at(pos) = false; 226 | 227 | try { 228 | imageAt(pos); 229 | } catch(cv::Exception) { 230 | *match = matchBackup; 231 | } 232 | } 233 | 234 | void FrameCollection::editMatchOriginX(int pos, int originX) { 235 | auto match = matchAt(pos); 236 | MatchData matchBackup = *match; 237 | 238 | match->originX = originX; 239 | _isCachedImages.at(pos) = false; 240 | 241 | try { 242 | imageAt(pos); 243 | } catch(cv::Exception) { 244 | *match = matchBackup; 245 | } 246 | } 247 | 248 | void FrameCollection::editMatchOriginY(int pos, int originY) { 249 | auto match = matchAt(pos); 250 | MatchData matchBackup = *match; 251 | 252 | match->originY = originY; 253 | _isCachedImages.at(pos) = false; 254 | 255 | try { 256 | imageAt(pos); 257 | } catch(cv::Exception) { 258 | *match = matchBackup; 259 | } 260 | } 261 | 262 | void FrameCollection::preloadAll() { 263 | for (int i = 0; i < size(); ++i) { 264 | imageAt(i); 265 | } 266 | } 267 | 268 | void FrameCollection::writeImages(const std::string &name) { 269 | for (size_t i = 0; i < size(); ++i) { 270 | cv::Mat image = imageAt(i); 271 | 272 | char frameName[8]; 273 | sprintf(frameName, "%03d.jpg", (int)i); 274 | std::filesystem::path imagePath("assets/collections"); 275 | imagePath.append(name); 276 | imagePath.append(frameName); 277 | 278 | cv::imwrite(imagePath.string(), image); 279 | } 280 | } 281 | 282 | std::ostream &operator<<(std::ostream &os, const MatchData &matchData) { 283 | os << matchData.path << ',' << matchData.percentage << ',' << matchData.scale 284 | << ',' << matchData.originX << ',' << matchData.originY; 285 | return os; 286 | } 287 | 288 | std::ostream &operator<<(std::ostream &os, const FrameData &frameData) { 289 | for (int i = 0; i < frameData.frames.size(); ++i) { 290 | if (i != 0) { 291 | os << ','; 292 | } 293 | os << '[' << frameData.frames[i] << ']'; 294 | } 295 | return os; 296 | } 297 | 298 | std::ostream &operator<<(std::ostream &os, const FrameCollection &frames) { 299 | for (const FrameData &frameData : frames) { 300 | os << frameData << '\n'; 301 | } 302 | return os; 303 | } 304 | -------------------------------------------------------------------------------- /src/lib/edged-image.cpp: -------------------------------------------------------------------------------- 1 | #include "edged-image.hpp" 2 | 3 | void EdgedImage::provideMatchContext(int templateOffsetX, int templateOffsetY) { 4 | _matchContextOffsetX = templateOffsetX; 5 | _matchContextOffsetY = templateOffsetY; 6 | } 7 | void EdgedImage::resetMatchContext() { 8 | _matchContextOffsetX = 0; 9 | _matchContextOffsetY = 0; 10 | } 11 | 12 | int EdgedImage::matchTo(const cv::Mat &templateImageIn, ImageMatch *match, 13 | float offsetScaleStep, int offsetXStep, int offsetYStep, 14 | float minOffsetScale, int maxOffset, float whiteBias) { 15 | int channels = templateImageIn.channels(); 16 | CV_Assert(channels == 1); 17 | 18 | cv::Mat templateImage; 19 | if (_matchContextOffsetX || _matchContextOffsetY) { 20 | cv::Mat normalisedTemplateImage = 21 | cv::Mat::zeros(templateImageIn.size(), templateImageIn.type()); 22 | int x1 = _matchContextOffsetX < 0 ? 0 : _matchContextOffsetX; 23 | int rectWidth = templateImageIn.cols - std::abs(_matchContextOffsetX); 24 | int y1 = _matchContextOffsetY < 0 ? 0 : _matchContextOffsetY; 25 | int rectHeight = templateImageIn.rows - std::abs(_matchContextOffsetY); 26 | templateImageIn(cv::Rect(x1, y1, rectWidth, rectHeight)) 27 | .copyTo(normalisedTemplateImage(cv::Rect(x1 - _matchContextOffsetX, 28 | y1 - _matchContextOffsetY, 29 | rectWidth, rectHeight))); 30 | 31 | // todo fix these copies, especially the second one 32 | templateImage = normalisedTemplateImage; 33 | } else { 34 | templateImage = templateImageIn; 35 | } 36 | 37 | // Converting to an array means we only have to do the bitwise operations 38 | // required to access a bitset once per run 39 | uchar edgesAry[edges.size()]; 40 | for (int i = 0; i < edges.size(); ++i) { 41 | edgesAry[i] = edges[i] ? 255 : 0; 42 | } 43 | 44 | int sourceImageActualHeight = (float)STORED_EDGES_WIDTH / width * height; 45 | float scaleX = (float)STORED_EDGES_WIDTH / templateImage.cols; 46 | float scaleY = (float)sourceImageActualHeight / templateImage.rows; 47 | 48 | float scaleBase = fmin(scaleX, scaleY); 49 | 50 | ImageMatch bestMatch; 51 | 52 | minOffsetScale = fmax(minOffsetScale, (float)OUTPUT_WIDTH / width); 53 | 54 | int runs = 0; 55 | int fullRuns = 0; 56 | 57 | for (float offsetScale = 1; offsetScale >= minOffsetScale; 58 | offsetScale -= offsetScaleStep) { 59 | float scale = scaleBase * offsetScale; 60 | 61 | int originX = 0; 62 | int originY = 0; 63 | 64 | if (scaleX != 0) { 65 | originX = (STORED_EDGES_WIDTH - templateImage.cols * scale) / 2; 66 | } 67 | if (scaleX != 0) { 68 | originY = (sourceImageActualHeight - templateImage.rows * scale) / 2; 69 | } 70 | 71 | // todo vary step and max depending on scale? 72 | int maxOffsetX = std::min(maxOffset, originX); 73 | int maxOffsetY = std::min(maxOffset, originY); 74 | 75 | for (int offsetXRoot = 0; offsetXRoot <= maxOffsetX; 76 | offsetXRoot += offsetXStep) { 77 | for (int offsetXMultiplier = -1; offsetXMultiplier <= 1; 78 | offsetXMultiplier += 2) { 79 | // Search inside out, e.g. 0 -1 1 -2 2 -3 3 80 | int offsetX = offsetXRoot * offsetXMultiplier; 81 | 82 | for (int offsetYRoot = 0; offsetYRoot <= maxOffsetY; 83 | offsetYRoot += offsetYStep) { 84 | for (int offsetYMultiplier = -1; offsetYMultiplier <= 1; 85 | offsetYMultiplier += 2) { 86 | int offsetY = offsetYRoot * offsetYMultiplier; 87 | 88 | // Calculate if template offset is viable 89 | { 90 | float realScale = (float)width / STORED_EDGES_WIDTH; 91 | int finalX = originX + offsetX - _matchContextOffsetX * scale; 92 | int finalY = originY + offsetY - _matchContextOffsetY * scale; 93 | cv::Rect roi; 94 | roi.x = round(finalX * realScale); 95 | roi.y = round(finalY * realScale); 96 | roi.width = round(CANVAS_WIDTH * realScale * scale); 97 | roi.height = round(CANVAS_HEIGHT * realScale * scale); 98 | 99 | if (roi.x < 0 || roi.x + roi.width > width) { 100 | // We're in the y loop so we can break here - will be invalid 101 | // for every item in the loop 102 | break; 103 | } 104 | if (roi.y < 0 || roi.y + roi.height > height) { 105 | continue; 106 | } 107 | } 108 | 109 | ImageMatch match; 110 | if (runs != 0) { 111 | matchToStep(templateImage, edgesAry, &match, scale, 112 | originX + offsetX, originY + offsetY, 10, 1, 113 | whiteBias); 114 | 115 | // If partial match on rows isn't good enough, run again on cols 116 | if (match.percentage < 0.5 || 117 | match.percentage < bestMatch.percentage - 0.1) { 118 | matchToStep(templateImage, edgesAry, &match, scale, 119 | originX + offsetX, originY + offsetY, 1, 10, 120 | whiteBias); 121 | } 122 | } 123 | 124 | if (runs == 0 || (match.percentage > 0.5 && 125 | match.percentage > bestMatch.percentage - 0.1)) { 126 | matchToStep(templateImage, edgesAry, &match, scale, 127 | originX + offsetX, originY + offsetY, 1, 1, whiteBias); 128 | fullRuns++; 129 | 130 | if (match.percentage > bestMatch.percentage) { 131 | bestMatch = match; 132 | bestMatch.originX -= _matchContextOffsetX * scale; 133 | bestMatch.originY -= _matchContextOffsetY * scale; 134 | } 135 | } 136 | 137 | runs++; 138 | 139 | if (offsetY == 0) { 140 | break; 141 | } 142 | } 143 | } 144 | 145 | // No need to multiply 0 by both -1 and 1 146 | if (offsetX == 0) { 147 | break; 148 | } 149 | } 150 | } 151 | } 152 | 153 | *match = bestMatch; 154 | lastMatch = bestMatch; // Have to copy here or can cause segfaults 155 | return runs; 156 | } 157 | 158 | void EdgedImage::matchToStep(const cv::Mat &templateImage, 159 | const uchar edgesAry[], ImageMatch *match, 160 | float scale, int originX, int originY, int rowStep, 161 | int colStep, float whiteBias) const { 162 | int testedBlack = 0; 163 | int matchingBlack = 0; 164 | int testedWhite = 0; 165 | int matchingWhite = 0; 166 | 167 | for (int y = 0; y < templateImage.rows; y += rowStep) { 168 | const uchar *p = templateImage.ptr(y); 169 | 170 | for (int x = 0; x < templateImage.cols; x += colStep) { 171 | bool templatePixVal = p[x] != 0; 172 | 173 | int transformedX = originX + floor((float)x * scale); 174 | int transformedY = originY + floor((float)y * scale); 175 | int i = transformedY * STORED_EDGES_WIDTH + transformedX; 176 | bool sourcePixVal = edgesAry[i]; 177 | 178 | if (templatePixVal == 1) { 179 | if (templatePixVal == sourcePixVal) { 180 | ++matchingWhite; 181 | } 182 | ++testedWhite; 183 | } else { 184 | if (templatePixVal == sourcePixVal) { 185 | ++matchingBlack; 186 | } 187 | ++testedBlack; 188 | } 189 | } 190 | } 191 | 192 | float percentageBlack = (float)matchingBlack / testedBlack; 193 | float percentageWhite = (float)matchingWhite / testedWhite; 194 | float percentage = 195 | percentageWhite * whiteBias + percentageBlack * (1 - whiteBias); 196 | *match = ImageMatch{percentage, scale, originX, originY}; 197 | } 198 | 199 | cv::Mat EdgedImage::edgesAsMatrix() const { 200 | uchar edgesAry[edges.size()]; 201 | for (int i = 0; i < edges.size(); ++i) { 202 | edgesAry[i] = edges[i] ? 255 : 0; 203 | } 204 | 205 | int cols = STORED_EDGES_WIDTH; 206 | int rows = edges.size() / cols; 207 | 208 | cv::Mat mat(rows, cols, CV_8UC1); 209 | memcpy(mat.data, edgesAry, edges.size() * sizeof(uchar)); 210 | 211 | return mat; 212 | } 213 | 214 | // Cache in memory - takes surprisingly long to read from disk every time 215 | cv::Mat EdgedImage::getOriginal(bool cache) { 216 | if (!cache) { 217 | return cv::imread(path); 218 | } 219 | 220 | if (!originalImage.empty()) { 221 | return originalImage; 222 | } 223 | 224 | originalImage = cv::imread(path); 225 | return originalImage; 226 | } 227 | 228 | std::ostream &operator<<(std::ostream &os, const EdgedImage &image) { 229 | os << image.path << ',' << image.width << ',' << image.height << ',' 230 | << image.edges.size() << ',' << bitsetToString(image.edges) << ',' 231 | << image.detectionMode << ',' << image.detectionBlurSize << ',' 232 | << image.detectionBlurSigmaX << ',' << image.detectionBlurSigmaY << ',' 233 | << image.detectionCannyThreshold1 << ',' << image.detectionCannyThreshold2 234 | << ',' << image.detectionBinaryThreshold << ',' 235 | << image.detectionCannyJoinByX << ',' << image.detectionCannyJoinByY; 236 | return os; 237 | } 238 | -------------------------------------------------------------------------------- /src/lib/edit-image-edges.cpp: -------------------------------------------------------------------------------- 1 | #include "../config.h" 2 | 3 | #include "edit-image-edges.hpp" 4 | 5 | enum ManualEditModes { 6 | ManualEditMode_Erase, 7 | ManualEditMode_Line, 8 | ManualEditMode_Square, 9 | ManualEditMode_Circle 10 | }; 11 | 12 | std::optional editImageEdges(EdgedImage &image) { 13 | int detectionMode = image.detectionMode; 14 | int blurSize = image.detectionBlurSize; 15 | int sigmaX = image.detectionBlurSigmaX; 16 | int sigmaY = image.detectionBlurSigmaY; 17 | int threshold1 = image.detectionCannyThreshold1; 18 | int threshold2 = image.detectionCannyThreshold2; 19 | int joinByX = image.detectionCannyJoinByX; 20 | int joinByY = image.detectionCannyJoinByY; 21 | int binaryThreshold = image.detectionBinaryThreshold; 22 | int manualEditMode = ManualEditMode_Line; 23 | 24 | const cv::Mat sourceImage = cv::imread(image.path); 25 | cv::Mat templateImage = image.edgesAsMatrix(); 26 | 27 | if (sourceImage.empty()) { 28 | throw std::runtime_error("Could not read source image"); 29 | } 30 | 31 | std::string title = std::string("Editing ") + image.path; 32 | initWindow(sourceImage.cols, sourceImage.rows, title.c_str()); 33 | GLuint image_tex; 34 | 35 | ImVec2 mouseDownPos(-1, -1); 36 | ImVec2 mouseCurrentPos(-1, -1); 37 | 38 | auto drawTo = [&](cv::Mat &edges, bool preview = false) { 39 | int storedEdgesHeight = (float)STORED_EDGES_WIDTH / edges.cols * edges.rows; 40 | 41 | cv::Point pointA(mouseDownPos.x * STORED_EDGES_WIDTH, 42 | mouseDownPos.y * storedEdgesHeight); 43 | cv::Point pointB(mouseCurrentPos.x * STORED_EDGES_WIDTH, 44 | mouseCurrentPos.y * storedEdgesHeight); 45 | 46 | cv::Scalar color = 47 | edges.channels() == 3 ? cv::Scalar(0, 0, 255) : cv::Scalar(255); 48 | 49 | if (manualEditMode == ManualEditMode_Erase) { 50 | cv::rectangle(edges, pointA, pointB, cv::Scalar(0), cv::FILLED); 51 | if (preview) { 52 | cv::rectangle(edges, pointA, pointB, color); 53 | } 54 | } else if (manualEditMode == ManualEditMode_Square) { 55 | cv::rectangle(edges, pointA, pointB, color); 56 | } else if (manualEditMode == ManualEditMode_Line) { 57 | cv::line(edges, pointA, pointB, color); 58 | } else if (manualEditMode == ManualEditMode_Circle) { 59 | cv::Point center = (pointA + pointB) / 2; 60 | int radius = 61 | sqrt(pow(pointA.x - pointB.x, 2) + pow(pointA.y - pointB.y, 2)) / 2; 62 | cv::circle(edges, center, radius, color); 63 | } 64 | }; 65 | 66 | auto generatePreviewTexture = [&](bool initial) { 67 | // previewTemplateImage is for previewing manual edits 68 | cv::Mat previewTemplateImage; 69 | bool drawing = 70 | detectionMode == ImageEdgeMode_Manual && mouseDownPos.x != -1; 71 | 72 | if (!initial) { 73 | if (detectionMode == ImageEdgeMode_Canny) { 74 | templateImage = 75 | detectEdgesCanny(sourceImage, blurSize, sigmaX, sigmaY, threshold1, 76 | threshold2, joinByX, joinByY); 77 | } else if (detectionMode == ImageEdgeMode_Threshold) { 78 | templateImage = detectEdgesThreshold(sourceImage, blurSize, sigmaX, 79 | sigmaY, binaryThreshold); 80 | } else if (drawing) { 81 | previewTemplateImage = templateImage; 82 | } 83 | } 84 | 85 | cv::Mat &templateImageTmp = 86 | previewTemplateImage.empty() ? templateImage : previewTemplateImage; 87 | 88 | cv::Mat edges, mask, scaledEdges, scaledPlusEdges; 89 | cv::cvtColor(templateImageTmp, edges, cv::COLOR_GRAY2BGR); 90 | 91 | cv::threshold(templateImageTmp, mask, 0, 255, cv::THRESH_BINARY); 92 | edges.setTo(cv::Scalar(255, 0, 0), mask); 93 | 94 | if (drawing) { 95 | drawTo(edges, true); 96 | } 97 | 98 | cv::resize(edges, scaledEdges, sourceImage.size(), 0, 0, cv::INTER_NEAREST); 99 | cv::bitwise_or(scaledEdges, sourceImage, scaledPlusEdges); 100 | 101 | matToTexture(scaledPlusEdges, &image_tex); 102 | }; 103 | 104 | generatePreviewTexture(true); 105 | 106 | bool save = false; 107 | 108 | openWindow([&](GLFWwindow *window, ImGuiIO &io) { 109 | int actualWidth, actualHeight; 110 | glfwGetWindowSize(window, &actualWidth, &actualHeight); 111 | 112 | bool changed = false; 113 | 114 | if (!io.WantCaptureMouse && detectionMode == ImageEdgeMode_Manual) { 115 | if (io.MouseDown[0] && mouseDownPos.x == -1) { 116 | mouseDownPos = {io.MousePos.x / actualWidth, 117 | io.MousePos.y / actualHeight}; 118 | mouseCurrentPos = mouseDownPos; 119 | changed = true; 120 | } else if (mouseDownPos.x != -1) { 121 | if (mouseCurrentPos.x != io.MousePos.x || 122 | mouseCurrentPos.y != io.MousePos.y) { 123 | mouseCurrentPos = {io.MousePos.x / actualWidth, 124 | io.MousePos.y / actualHeight}; 125 | changed = true; 126 | } 127 | 128 | if (!io.MouseDown[0]) { 129 | drawTo(templateImage); 130 | mouseDownPos = {-1, -1}; 131 | } 132 | } 133 | } 134 | 135 | ImGui::SetNextWindowFocus(); 136 | /* ImGui::ShowDemoWindow(); */ 137 | ImGui::Begin("Controls"); 138 | 139 | ImGui::Text("Detection Mode:"); 140 | ImGui::SameLine(); 141 | changed |= ImGui::RadioButton("Canny", &detectionMode, ImageEdgeMode_Canny); 142 | ImGui::SameLine(); 143 | changed |= ImGui::RadioButton("Threshold", &detectionMode, 144 | ImageEdgeMode_Threshold); 145 | ImGui::SameLine(); 146 | changed |= 147 | ImGui::RadioButton("Manual", &detectionMode, ImageEdgeMode_Manual); 148 | 149 | ImGui::NewLine(); 150 | 151 | if (detectionMode != ImageEdgeMode_Manual) { 152 | changed |= ImGui::SliderInt("Blur size", &blurSize, 0, 50); 153 | changed |= ImGui::SliderInt("Blur sigma X", &sigmaX, 0, 20); 154 | changed |= ImGui::SliderInt("Blur sigma Y", &sigmaY, 0, 20); 155 | 156 | ImGui::NewLine(); 157 | } 158 | 159 | if (detectionMode == ImageEdgeMode_Canny) { 160 | changed |= ImGui::SliderInt("Canny threshold 1", &threshold1, 0, 255); 161 | changed |= ImGui::SliderInt("Canny threshold 2", &threshold2, 0, 255); 162 | changed |= ImGui::SliderInt("Join by x", &joinByX, 0, 39); 163 | changed |= ImGui::SliderInt("Join by y", &joinByY, 0, 39); 164 | } else if (detectionMode == ImageEdgeMode_Threshold) { 165 | changed |= ImGui::SliderInt("Binary threshold", &binaryThreshold, 0, 255); 166 | } else { 167 | // These don't change the edges, no need to set `changed` 168 | ImGui::Text("Tool:"); 169 | ImGui::SameLine(); 170 | ImGui::RadioButton("Eraser", &manualEditMode, ManualEditMode_Erase); 171 | ImGui::SameLine(); 172 | ImGui::RadioButton("Line", &manualEditMode, ManualEditMode_Line); 173 | ImGui::SameLine(); 174 | ImGui::RadioButton("Square", &manualEditMode, ManualEditMode_Square); 175 | ImGui::SameLine(); 176 | ImGui::RadioButton("Circle", &manualEditMode, ManualEditMode_Circle); 177 | 178 | if (ImGui::Button("Clear")) { 179 | templateImage.setTo(cv::Scalar(0)); 180 | changed = true; 181 | } 182 | } 183 | 184 | ImGui::NewLine(); 185 | 186 | if (ImGui::Button("Save")) { 187 | save = true; 188 | return true; 189 | } 190 | ImGui::SameLine(); 191 | if (ImGui::Button("Discard")) { 192 | return true; 193 | } 194 | ImGui::SameLine(); 195 | if (ImGui::Button("Reset")) { 196 | detectionMode = ImageEdgeMode_Canny; 197 | blurSize = EDGE_DETECTION_BLUR_SIZE; 198 | sigmaX = EDGE_DETECTION_BLUR_SIGMA_X; 199 | sigmaY = EDGE_DETECTION_BLUR_SIGMA_Y; 200 | threshold1 = EDGE_DETECTION_CANNY_THRESHOLD_1; 201 | threshold2 = EDGE_DETECTION_CANNY_THRESHOLD_2; 202 | joinByX = EDGE_DETECTION_CANNY_JOIN_BY_X; 203 | joinByY = EDGE_DETECTION_CANNY_JOIN_BY_Y; 204 | binaryThreshold = EDGE_DETECTION_BINARY_THRESHOLD; 205 | changed = true; 206 | } 207 | ImGui::End(); 208 | 209 | if (changed) { 210 | if (blurSize % 2 == 0) { 211 | blurSize++; 212 | } 213 | 214 | if (joinByX != 0 && joinByX % 2 == 0) { 215 | joinByX++; 216 | } 217 | 218 | if (joinByY != 0 && joinByY % 2 == 0) { 219 | joinByY++; 220 | } 221 | 222 | generatePreviewTexture(false); 223 | } 224 | 225 | // Image window is full screen and non-interactive - basically, a background 226 | ImGui::SetNextWindowPos(ImVec2(.0f, .0f)); 227 | ImGui::SetNextWindowSize(ImVec2(actualWidth, actualHeight)); 228 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f)); 229 | ImGui::Begin("Image", NULL, staticWindowFlags); 230 | ImGui::Image((void *)(intptr_t)image_tex, ImGui::GetContentRegionMax()); 231 | ImGui::End(); 232 | ImGui::PopStyleVar(1); 233 | 234 | return false; 235 | }); 236 | 237 | glDeleteTextures(1, &image_tex); 238 | 239 | if (!save) { 240 | return std::nullopt; 241 | } 242 | 243 | auto edges = edgesToBitset(templateImage); 244 | return new EdgedImage(image.path, image.width, image.height, edges, 245 | detectionMode, blurSize, sigmaX, sigmaY, threshold1, 246 | threshold2, joinByX, joinByY, binaryThreshold); 247 | } 248 | -------------------------------------------------------------------------------- /src/match.cpp: -------------------------------------------------------------------------------- 1 | #include "precompiled.h" 2 | 3 | #include "config.h" 4 | 5 | #include "lib/detect-edge.hpp" 6 | #include "lib/edged-image.hpp" 7 | #include "lib/frame-collection.hpp" 8 | #include "lib/image-list.hpp" 9 | #include "lib/mat-to-texture.hpp" 10 | #include "lib/window.hpp" 11 | 12 | enum TemplateShapes { TemplateShape_Rect, TemplateShape_Circle }; 13 | 14 | int main(int argc, const char *argv[]) { 15 | auto readStart = std::chrono::high_resolution_clock::now(); 16 | 17 | ImageList sourceImages = ImageList(argv[1]); 18 | ImageList orderedImages = sourceImages; 19 | 20 | FrameCollection frames; 21 | char frameCollectionName[50] = ""; 22 | bool frameCollectionSaved = false; 23 | 24 | int shape = TemplateShape_Rect; 25 | int width = 48; 26 | int height = 550; 27 | // Algorithm is flawed: it'll probably zoom all the way in if lineWidth > 1 28 | int lineWidth = 1; 29 | int templateOffsetX = 0; 30 | int templateOffsetY = 0; 31 | 32 | float offsetScaleStep = MATCH_OFFSET_SCALE_STEP; 33 | int offsetXStep = MATCH_OFFSET_X_STEP; 34 | int offsetYStep = MATCH_OFFSET_Y_STEP; 35 | 36 | float minOffsetScale = MATCH_MIN_OFFSET_SCALE; 37 | int maxOffset = MATCH_MAX_OFFSET; 38 | float whiteBias = MATCH_WHITE_BIAS; 39 | 40 | bool showEdges = false; 41 | bool showTemplate = true; 42 | 43 | bool updateWhenChanged = true; 44 | 45 | auto readFinish = std::chrono::high_resolution_clock::now(); 46 | std::chrono::duration readElapsed = readFinish - readStart; 47 | 48 | std::cout << "Loaded " << sourceImages.count() << " images from store in " 49 | << readElapsed.count() << "s\n"; 50 | 51 | initWindow(OUTPUT_WIDTH, OUTPUT_HEIGHT, "Match debugger"); 52 | GLuint image_tex; 53 | 54 | ImageMatch bestMatch; 55 | // Warning: sourceImages is managing this memory 56 | EdgedImage *bestMatchImage; 57 | EdgedImage *previewImage; 58 | int runs; 59 | std::chrono::duration matchElapsed; 60 | std::chrono::duration previewElapsed; 61 | 62 | auto generatePreviewTexture = [&]() { 63 | cv::Mat canvas = cv::Mat::zeros(CANVAS_HEIGHT, CANVAS_WIDTH, CV_8UC3); 64 | 65 | cv::Point center(CANVAS_WIDTH / 2 + templateOffsetX, 66 | CANVAS_HEIGHT / 2 + templateOffsetY); 67 | if (shape == TemplateShape_Rect) { 68 | cv::Point diff(width / 2, height / 2); 69 | cv::rectangle(canvas, center - diff, center + diff, cv::Scalar(0, 0, 255), 70 | lineWidth); 71 | } else if (shape == TemplateShape_Circle) { 72 | cv::circle(canvas, center, width / 2, cv::Scalar(0, 0, 255), lineWidth); 73 | } 74 | 75 | cv::Mat greyCanvas; 76 | cv::cvtColor(canvas, greyCanvas, cv::COLOR_BGR2GRAY); 77 | 78 | ImageMatch oldBestMatch = bestMatch; 79 | EdgedImage *oldBestMatchImage = bestMatchImage; 80 | 81 | bestMatch = ImageMatch(); 82 | bestMatchImage = nullptr; 83 | 84 | if (previewImage != nullptr) { 85 | bestMatch = previewImage->lastMatch; 86 | bestMatchImage = previewImage; 87 | previewImage = nullptr; 88 | matchElapsed = std::chrono::seconds(0); 89 | } else { 90 | auto matchStart = std::chrono::high_resolution_clock::now(); 91 | 92 | sourceImages.provideMatchContext(templateOffsetX, templateOffsetY); 93 | runs = sourceImages.matchTo(greyCanvas, &bestMatch, &bestMatchImage, 94 | offsetScaleStep, offsetXStep, offsetYStep, 95 | minOffsetScale, maxOffset, whiteBias); 96 | 97 | auto matchFinish = std::chrono::high_resolution_clock::now(); 98 | matchElapsed = matchFinish - matchStart; 99 | 100 | orderedImages.sortBy("match-percentage"); 101 | 102 | // This displays the wrong image but stops a segfault 103 | if (!bestMatchImage) { 104 | bestMatch = oldBestMatch; 105 | bestMatchImage = oldBestMatchImage; 106 | } 107 | } 108 | 109 | auto previewStart = std::chrono::high_resolution_clock::now(); 110 | 111 | cv::Mat originalImage = bestMatchImage->getOriginal(); 112 | 113 | if (originalImage.empty()) { 114 | throw std::runtime_error("Couldn't read source image"); 115 | } 116 | 117 | cv::Mat sourcePlusEdges; 118 | if (showEdges) { 119 | cv::Mat greyEdges = bestMatchImage->edgesAsMatrix(); 120 | cv::Mat edges, scaledEdges, scaledPlusEdges; 121 | cv::cvtColor(greyEdges, edges, cv::COLOR_GRAY2BGR); 122 | 123 | cv::Mat mask; 124 | cv::threshold(greyEdges, mask, 0, 255, cv::THRESH_BINARY); 125 | edges.setTo(cv::Scalar(255, 0, 0), mask); 126 | 127 | cv::resize(edges, scaledEdges, originalImage.size(), cv::INTER_NEAREST); 128 | cv::bitwise_or(scaledEdges, originalImage, scaledPlusEdges); 129 | 130 | float edgesAlpha = 0.9; 131 | cv::addWeighted(scaledPlusEdges, edgesAlpha, originalImage, 132 | 1.f - edgesAlpha, 0, sourcePlusEdges); 133 | } else { 134 | sourcePlusEdges = originalImage; 135 | } 136 | 137 | float realScale = (float)bestMatchImage->width / STORED_EDGES_WIDTH; 138 | 139 | cv::Rect roi; 140 | roi.x = round(bestMatch.originX * realScale); 141 | roi.y = round(bestMatch.originY * realScale); 142 | roi.width = round(CANVAS_WIDTH * realScale * bestMatch.scale); 143 | roi.height = round(CANVAS_HEIGHT * realScale * bestMatch.scale); 144 | 145 | cv::Mat scaledImage; 146 | try { 147 | cv::Mat cropped = sourcePlusEdges(roi); 148 | cv::resize(cropped, scaledImage, cv::Size(OUTPUT_WIDTH, OUTPUT_HEIGHT)); 149 | } catch (cv::Exception) { 150 | // This sucks but is better than crashing the programme 151 | scaledImage = cv::Mat::zeros(cv::Size(OUTPUT_WIDTH, OUTPUT_HEIGHT), 152 | sourcePlusEdges.type()); 153 | } 154 | 155 | cv::Mat out; 156 | if (showTemplate) { 157 | cv::Mat scaledCanvas, scaledPlusCanvas; 158 | cv::resize(canvas, scaledCanvas, cv::Size(OUTPUT_WIDTH, OUTPUT_HEIGHT), 159 | cv::INTER_NEAREST); 160 | cv::bitwise_or(scaledCanvas, scaledImage, scaledPlusCanvas); 161 | 162 | float canvasAlpha = 0.6; 163 | cv::addWeighted(scaledPlusCanvas, canvasAlpha, scaledImage, 164 | 1.f - canvasAlpha, 0, out); 165 | } else { 166 | out = scaledImage; 167 | } 168 | 169 | matToTexture(out, &image_tex); 170 | 171 | auto previewFinish = std::chrono::high_resolution_clock::now(); 172 | previewElapsed = previewFinish - previewStart; 173 | }; 174 | 175 | generatePreviewTexture(); 176 | 177 | openWindow([&](GLFWwindow *window, ImGuiIO &io) { 178 | bool changed = false; 179 | 180 | ImGui::SetNextWindowFocus(); 181 | ImGui::Begin("Controls"); 182 | 183 | changed |= ImGui::RadioButton("Rectangle", &shape, TemplateShape_Rect); 184 | ImGui::SameLine(); 185 | changed |= ImGui::RadioButton("Circle", &shape, TemplateShape_Circle); 186 | changed |= ImGui::SliderInt("Width", &width, 0, CANVAS_WIDTH + 50); 187 | ImGui::SameLine(); 188 | if (ImGui::SmallButton("-##lesswidth")) { 189 | width--; 190 | changed = true; 191 | } 192 | ImGui::SameLine(); 193 | if (ImGui::SmallButton("+##morewidth")) { 194 | width++; 195 | changed = true; 196 | } 197 | if (shape == TemplateShape_Rect) { 198 | changed |= ImGui::SliderInt("Height", &height, 0, CANVAS_HEIGHT + 50); 199 | } 200 | /* changed |= ImGui::SliderInt("Line width", &lineWidth, 0, 50); */ 201 | changed |= ImGui::SliderInt("Offset X", &templateOffsetX, -100, 100); 202 | ImGui::SameLine(); 203 | if (ImGui::SmallButton("-##lessoffx")) { 204 | templateOffsetX--; 205 | changed = true; 206 | } 207 | ImGui::SameLine(); 208 | if (ImGui::SmallButton("+##moreoffx")) { 209 | templateOffsetX++; 210 | changed = true; 211 | } 212 | changed |= ImGui::SliderInt("Offset Y", &templateOffsetY, -100, 100); 213 | ImGui::SameLine(); 214 | if (ImGui::SmallButton("-##lessoffy")) { 215 | templateOffsetY--; 216 | changed = true; 217 | } 218 | ImGui::SameLine(); 219 | if (ImGui::SmallButton("+##moreoffy")) { 220 | templateOffsetY++; 221 | changed = true; 222 | } 223 | 224 | ImGui::NewLine(); 225 | 226 | changed |= 227 | ImGui::SliderFloat("Offset scale step", &offsetScaleStep, 0.025, 0.5); 228 | changed |= ImGui::SliderInt("Offset x step", &offsetXStep, 1, 20); 229 | changed |= ImGui::SliderInt("Offset y step", &offsetYStep, 1, 20); 230 | changed |= ImGui::SliderFloat("Min offset scale", &minOffsetScale, 0, 0.9); 231 | changed |= ImGui::SliderInt("Max offset", &maxOffset, 1, 50); 232 | changed |= ImGui::SliderFloat("White bias", &whiteBias, 0, 1); 233 | 234 | ImGui::NewLine(); 235 | 236 | changed |= ImGui::Checkbox("Show edges?", &showEdges); 237 | ImGui::SameLine(); 238 | changed |= ImGui::Checkbox("Show template?", &showTemplate); 239 | 240 | ImGui::NewLine(); 241 | 242 | // This only works when below the controls 243 | ImGui::Checkbox("Update when changed?", &updateWhenChanged); 244 | if (!updateWhenChanged) { 245 | changed = false; 246 | ImGui::SameLine(); 247 | if (ImGui::SmallButton("Update")) { 248 | changed = true; 249 | } 250 | } 251 | 252 | ImGui::NewLine(); 253 | 254 | if (ImGui::TreeNode("Match info")) { 255 | float child_height = ImGui::GetTextLineHeight(); 256 | 257 | if (ImGui::BeginChild("path", ImVec2(0, child_height))) { 258 | ImGui::Text("Best match: %s", bestMatchImage->path.c_str()); 259 | } 260 | ImGui::EndChild(); 261 | ImGui::Text("%% match: %.1f%%", bestMatch.percentage * 100); 262 | ImGui::Text("Scale: %.2f", bestMatch.scale); 263 | ImGui::Text("Offset: (%i,%i)", bestMatch.originX, bestMatch.originY); 264 | ImGui::Text("Runs: %i", runs); 265 | ImGui::Text("Match timer: %.2fs (%.2fs avg)", matchElapsed.count(), 266 | matchElapsed.count() / orderedImages.count()); 267 | ImGui::Text("Preview timer: %.2fs", previewElapsed.count()); 268 | 269 | ImGui::TreePop(); 270 | } 271 | 272 | if (ImGui::TreeNode("All matches")) { 273 | if (ImGui::BeginChild("matches")) { 274 | for (auto &image : orderedImages) { 275 | ImGui::PushID(image->path.c_str()); 276 | if (ImGui::SmallButton("Preview")) { 277 | previewImage = image.get(); 278 | changed = true; 279 | } 280 | ImGui::SameLine(); 281 | ImGui::Text("%.1f%%: %s", image->lastMatch.percentage * 100, 282 | image->path.c_str()); 283 | ImGui::SameLine(); 284 | if (ImGui::SmallButton("copy")) { 285 | ImGui::SetClipboardText(image->path.c_str()); 286 | } 287 | ImGui::PopID(); 288 | } 289 | } 290 | ImGui::EndChild(); 291 | ImGui::TreePop(); 292 | } 293 | 294 | if (ImGui::TreeNode("Build")) { 295 | ImGui::Text("Currently %i frames", (int)frames.size()); 296 | 297 | if (ImGui::Button("Add frame")) { 298 | frames.addFrame(sourceImages); 299 | frameCollectionSaved = false; 300 | } 301 | ImGui::SameLine(); 302 | if (ImGui::Button("Remove last frame")) { 303 | frames.popFrame(); 304 | frameCollectionSaved = false; 305 | } 306 | 307 | ImGui::NewLine(); 308 | 309 | if (ImGui::InputText("Name", frameCollectionName, 50)) { 310 | frameCollectionSaved = false; 311 | } 312 | 313 | if (strlen(frameCollectionName)) { 314 | if (frames.size()) { 315 | if (ImGui::Button("Save")) { 316 | std::string name(frameCollectionName); 317 | frames.save(name); 318 | 319 | frameCollectionSaved = true; 320 | } 321 | } else { 322 | if (ImGui::Button("Load")) { 323 | frames = FrameCollection(frameCollectionName); 324 | } 325 | } 326 | } 327 | 328 | if (frameCollectionSaved) { 329 | ImGui::SameLine(); 330 | ImGui::Text("Saved"); 331 | } 332 | 333 | ImGui::TreePop(); 334 | } 335 | 336 | ImGui::End(); 337 | 338 | if (changed) { 339 | generatePreviewTexture(); 340 | } 341 | 342 | // Image window is full screen and non-interactive - basically, a background 343 | int actualWidth, actualHeight; 344 | glfwGetWindowSize(window, &actualWidth, &actualHeight); 345 | ImGui::SetNextWindowPos(ImVec2(.0f, .0f)); 346 | ImGui::SetNextWindowSize(ImVec2(actualWidth, actualHeight)); 347 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f)); 348 | ImGui::Begin("Image", NULL, staticWindowFlags); 349 | ImGui::Image((void *)(intptr_t)image_tex, ImGui::GetContentRegionMax()); 350 | ImGui::End(); 351 | ImGui::PopStyleVar(1); 352 | 353 | return false; 354 | }); 355 | 356 | glDeleteTextures(1, &image_tex); 357 | } 358 | --------------------------------------------------------------------------------