diff --git a/DocImages/after-corner-adjustment.png b/DocImages/after-corner-adjustment.png new file mode 100644 index 0000000..aeca960 Binary files /dev/null and b/DocImages/after-corner-adjustment.png differ diff --git a/DocImages/before-corner-adjustment.png b/DocImages/before-corner-adjustment.png new file mode 100644 index 0000000..174b1c7 Binary files /dev/null and b/DocImages/before-corner-adjustment.png differ diff --git a/DocImages/contours.png b/DocImages/contours.png new file mode 100644 index 0000000..e615691 Binary files /dev/null and b/DocImages/contours.png differ diff --git a/DocImages/guided-match-window.png b/DocImages/guided-match-window.png new file mode 100644 index 0000000..63d3d03 Binary files /dev/null and b/DocImages/guided-match-window.png differ diff --git a/DocImages/partial-matched-back.jpg b/DocImages/partial-matched-back.jpg new file mode 100644 index 0000000..33f4521 Binary files /dev/null and b/DocImages/partial-matched-back.jpg differ diff --git a/DocImages/partial-matched-front.jpg b/DocImages/partial-matched-front.jpg new file mode 100644 index 0000000..f9bd665 Binary files /dev/null and b/DocImages/partial-matched-front.jpg differ diff --git a/DocImages/pieces.png b/DocImages/pieces.png new file mode 100644 index 0000000..884e68c Binary files /dev/null and b/DocImages/pieces.png differ diff --git a/PuzzleSolver.xcodeproj/project.pbxproj b/PuzzleSolver.xcodeproj/project.pbxproj deleted file mode 100644 index 648f0ae..0000000 --- a/PuzzleSolver.xcodeproj/project.pbxproj +++ /dev/null @@ -1,346 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - B51935A9170E016100FF1235 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B51935A8170E016100FF1235 /* main.cpp */; }; - B51935AB170E016100FF1235 /* PuzzleSolver.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = B51935AA170E016100FF1235 /* PuzzleSolver.1 */; }; - B5915DD0170E39F600AD2C90 /* libopencv_core.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B5915D97170E39F500AD2C90 /* libopencv_core.dylib */; }; - B5AD446F171A1AA500B6497F /* libopencv_video.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AD446D171A1AA500B6497F /* libopencv_video.dylib */; }; - B5AD4474171A209A00B6497F /* libopencv_highgui.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AD4473171A209A00B6497F /* libopencv_highgui.dylib */; }; - B5AD4476171A214900B6497F /* libopencv_imgproc.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AD4475171A214900B6497F /* libopencv_imgproc.dylib */; settings = {ATTRIBUTES = (Required, ); }; }; - B5F5BF5F170F315700D4776D /* edge.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B5F5BF5D170F315700D4776D /* edge.cpp */; }; - B5F5BF62170F316000D4776D /* piece.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B5F5BF60170F316000D4776D /* piece.cpp */; }; - B5F5BF65170F316A00D4776D /* puzzle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B5F5BF63170F316A00D4776D /* puzzle.cpp */; }; - B5F5BF681714D2FE00D4776D /* utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B5F5BF661714D2FE00D4776D /* utils.cpp */; }; - B5F5BF7B17185B2700D4776D /* PuzzleDisjointSet.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B5F5BF7917185B2700D4776D /* PuzzleDisjointSet.cpp */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - B51935A3170E016100FF1235 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = /usr/share/man/man1/; - dstSubfolderSpec = 0; - files = ( - B51935AB170E016100FF1235 /* PuzzleSolver.1 in CopyFiles */, - ); - runOnlyForDeploymentPostprocessing = 1; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - B51935A5170E016100FF1235 /* PuzzleSolver */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PuzzleSolver; sourceTree = BUILT_PRODUCTS_DIR; }; - B51935A8170E016100FF1235 /* main.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = ""; }; - B51935AA170E016100FF1235 /* PuzzleSolver.1 */ = {isa = PBXFileReference; lastKnownFileType = text.man; path = PuzzleSolver.1; sourceTree = ""; }; - B5915D97170E39F500AD2C90 /* libopencv_core.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libopencv_core.dylib; path = /usr/local/lib/libopencv_core.dylib; sourceTree = ""; }; - B5AD446D171A1AA500B6497F /* libopencv_video.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libopencv_video.dylib; path = /usr/local/lib/libopencv_video.dylib; sourceTree = ""; }; - B5AD4473171A209A00B6497F /* libopencv_highgui.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libopencv_highgui.dylib; path = /usr/local/lib/libopencv_highgui.dylib; sourceTree = ""; }; - B5AD4475171A214900B6497F /* libopencv_imgproc.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libopencv_imgproc.dylib; path = /usr/local/lib/libopencv_imgproc.dylib; sourceTree = ""; }; - B5F5BF5D170F315700D4776D /* edge.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = edge.cpp; sourceTree = ""; }; - B5F5BF5E170F315700D4776D /* edge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = edge.h; sourceTree = ""; }; - B5F5BF60170F316000D4776D /* piece.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = piece.cpp; sourceTree = ""; }; - B5F5BF61170F316000D4776D /* piece.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = piece.h; sourceTree = ""; }; - B5F5BF63170F316A00D4776D /* puzzle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = puzzle.cpp; sourceTree = ""; }; - B5F5BF64170F316A00D4776D /* puzzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = puzzle.h; sourceTree = ""; }; - B5F5BF661714D2FE00D4776D /* utils.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = utils.cpp; sourceTree = ""; }; - B5F5BF671714D2FE00D4776D /* utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = utils.h; sourceTree = ""; }; - B5F5BF7917185B2700D4776D /* PuzzleDisjointSet.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PuzzleDisjointSet.cpp; sourceTree = ""; }; - B5F5BF7A17185B2700D4776D /* PuzzleDisjointSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PuzzleDisjointSet.h; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B51935A2170E016100FF1235 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B5AD4476171A214900B6497F /* libopencv_imgproc.dylib in Frameworks */, - B5AD4474171A209A00B6497F /* libopencv_highgui.dylib in Frameworks */, - B5AD446F171A1AA500B6497F /* libopencv_video.dylib in Frameworks */, - B5915DD0170E39F600AD2C90 /* libopencv_core.dylib in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - B519359C170E016000FF1235 = { - isa = PBXGroup; - children = ( - B5915E01170E3A0C00AD2C90 /* opencv */, - B51935A7170E016100FF1235 /* PuzzleSolver */, - B51935A6170E016100FF1235 /* Products */, - ); - sourceTree = ""; - }; - B51935A6170E016100FF1235 /* Products */ = { - isa = PBXGroup; - children = ( - B51935A5170E016100FF1235 /* PuzzleSolver */, - ); - name = Products; - sourceTree = ""; - }; - B51935A7170E016100FF1235 /* PuzzleSolver */ = { - isa = PBXGroup; - children = ( - B51935A8170E016100FF1235 /* main.cpp */, - B51935AA170E016100FF1235 /* PuzzleSolver.1 */, - B5F5BF5D170F315700D4776D /* edge.cpp */, - B5F5BF5E170F315700D4776D /* edge.h */, - B5F5BF60170F316000D4776D /* piece.cpp */, - B5F5BF61170F316000D4776D /* piece.h */, - B5F5BF63170F316A00D4776D /* puzzle.cpp */, - B5F5BF64170F316A00D4776D /* puzzle.h */, - B5F5BF661714D2FE00D4776D /* utils.cpp */, - B5F5BF671714D2FE00D4776D /* utils.h */, - B5F5BF7917185B2700D4776D /* PuzzleDisjointSet.cpp */, - B5F5BF7A17185B2700D4776D /* PuzzleDisjointSet.h */, - ); - path = PuzzleSolver; - sourceTree = ""; - }; - B5915E01170E3A0C00AD2C90 /* opencv */ = { - isa = PBXGroup; - children = ( - B5AD4475171A214900B6497F /* libopencv_imgproc.dylib */, - B5AD4473171A209A00B6497F /* libopencv_highgui.dylib */, - B5AD446D171A1AA500B6497F /* libopencv_video.dylib */, - B5915D97170E39F500AD2C90 /* libopencv_core.dylib */, - ); - name = opencv; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - B51935A4170E016100FF1235 /* PuzzleSolver */ = { - isa = PBXNativeTarget; - buildConfigurationList = B51935AE170E016100FF1235 /* Build configuration list for PBXNativeTarget "PuzzleSolver" */; - buildPhases = ( - B51935A1170E016100FF1235 /* Sources */, - B51935A2170E016100FF1235 /* Frameworks */, - B51935A3170E016100FF1235 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = PuzzleSolver; - productName = PuzzleSolver; - productReference = B51935A5170E016100FF1235 /* PuzzleSolver */; - productType = "com.apple.product-type.tool"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - B519359D170E016000FF1235 /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 0460; - ORGANIZATIONNAME = "Joe Zeimen"; - }; - buildConfigurationList = B51935A0170E016000FF1235 /* Build configuration list for PBXProject "PuzzleSolver" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = B519359C170E016000FF1235; - productRefGroup = B51935A6170E016100FF1235 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - B51935A4170E016100FF1235 /* PuzzleSolver */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXSourcesBuildPhase section */ - B51935A1170E016100FF1235 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B51935A9170E016100FF1235 /* main.cpp in Sources */, - B5F5BF5F170F315700D4776D /* edge.cpp in Sources */, - B5F5BF62170F316000D4776D /* piece.cpp in Sources */, - B5F5BF65170F316A00D4776D /* puzzle.cpp in Sources */, - B5F5BF681714D2FE00D4776D /* utils.cpp in Sources */, - B5F5BF7B17185B2700D4776D /* PuzzleDisjointSet.cpp in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - B51935AC170E016100FF1235 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_ENABLE_OBJC_EXCEPTIONS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_SYMBOLS_PRIVATE_EXTERN = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.8; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - }; - name = Debug; - }; - B51935AD170E016100FF1235 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_ENABLE_OBJC_EXCEPTIONS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.8; - SDKROOT = macosx; - }; - name = Release; - }; - B51935AF170E016100FF1235 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ENABLE_OPENMP_SUPPORT = YES; - GCC_ENABLE_FLOATING_POINT_LIBRARY_CALLS = NO; - GCC_ENABLE_SSE3_EXTENSIONS = YES; - GCC_ENABLE_SSE41_EXTENSIONS = YES; - GCC_ENABLE_SSE42_EXTENSIONS = YES; - GCC_ENABLE_SUPPLEMENTAL_SSE3_INSTRUCTIONS = YES; - GCC_GENERATE_DEBUGGING_SYMBOLS = YES; - GCC_NO_COMMON_BLOCKS = NO; - GCC_STRICT_ALIASING = YES; - GCC_VERSION = com.intel.compilers.icc.13_0_0; - HEADER_SEARCH_PATHS = ( - /opt/local/include, - /opt/local/include/opencv, - ); - ICC_CILK_SERIALIZE = NO; - ICC_DIAG_VEC_REPORT = vec1; - ICC_EMIT_DIAGNOSTICS_TO_FILE = YES; - ICC_FP_SPECULATION = default; - ICC_GUIDE_TO_FILE = YES; - ICC_IPP = None; - ICC_LANG_ANSI = c89; - ICC_LANG_OPENMP = parallel; - ICC_LOGO = NO; - ICC_MKL = None; - ICC_OPTLEVEL = none; - ICC_OPT_PARALLEL = NO; - ICC_OPT_REQUIRE_ARCH_IA32 = AVX; - ICC_OPT_USE_ARCH_IA32 = AVX; - ICC_RT_LIBRARY = static; - ICC_USE_OPTIMIZED_HEADERS = NO; - LINK_WITH_STANDARD_LIBRARIES = YES; - LLVM_LTO = NO; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = ""; - VALID_ARCHS = "i386 x86_64"; - }; - name = Debug; - }; - B51935B0170E016100FF1235 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ENABLE_OPENMP_SUPPORT = YES; - GCC_ENABLE_FLOATING_POINT_LIBRARY_CALLS = NO; - GCC_ENABLE_SSE3_EXTENSIONS = YES; - GCC_ENABLE_SSE41_EXTENSIONS = YES; - GCC_ENABLE_SSE42_EXTENSIONS = YES; - GCC_ENABLE_SUPPLEMENTAL_SSE3_INSTRUCTIONS = YES; - GCC_GENERATE_DEBUGGING_SYMBOLS = YES; - GCC_NO_COMMON_BLOCKS = NO; - GCC_OPTIMIZATION_LEVEL = 3; - GCC_STRICT_ALIASING = YES; - GCC_VERSION = com.intel.compilers.icc.13_0_0; - HEADER_SEARCH_PATHS = ( - /opt/local/include, - /opt/local/include/opencv, - ); - ICC_CILK_SERIALIZE = NO; - ICC_DIAG_VEC_REPORT = vec1; - ICC_EMIT_DIAGNOSTICS_TO_FILE = YES; - ICC_FP_SPECULATION = default; - ICC_GUIDE_TO_FILE = YES; - ICC_IPP = None; - ICC_LANG_ANSI = c89; - ICC_LANG_OPENMP = parallel; - ICC_LOGO = NO; - ICC_MKL = None; - ICC_OPTLEVEL = speed; - ICC_OPT_PARALLEL = NO; - ICC_OPT_REQUIRE_ARCH_IA32 = AVX; - ICC_OPT_USE_ARCH_IA32 = AVX; - ICC_RT_LIBRARY = static; - ICC_USE_OPTIMIZED_HEADERS = NO; - LINK_WITH_STANDARD_LIBRARIES = YES; - LLVM_LTO = NO; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = ""; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - B51935A0170E016000FF1235 /* Build configuration list for PBXProject "PuzzleSolver" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B51935AC170E016100FF1235 /* Debug */, - B51935AD170E016100FF1235 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B51935AE170E016100FF1235 /* Build configuration list for PBXNativeTarget "PuzzleSolver" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B51935AF170E016100FF1235 /* Debug */, - B51935B0170E016100FF1235 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = B519359D170E016000FF1235 /* Project object */; -} diff --git a/PuzzleSolver.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/PuzzleSolver.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 710665a..0000000 --- a/PuzzleSolver.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/PuzzleSolver.xcodeproj/project.xcworkspace/xcuserdata/jzeimen.xcuserdatad/UserInterfaceState.xcuserstate b/PuzzleSolver.xcodeproj/project.xcworkspace/xcuserdata/jzeimen.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 8b84bf7..0000000 Binary files a/PuzzleSolver.xcodeproj/project.xcworkspace/xcuserdata/jzeimen.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist b/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist deleted file mode 100644 index 05301bc..0000000 --- a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/PuzzleSolver-release.xcscheme b/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/PuzzleSolver-release.xcscheme deleted file mode 100644 index d912201..0000000 --- a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/PuzzleSolver-release.xcscheme +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/PuzzleSolver.xcscheme b/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/PuzzleSolver.xcscheme deleted file mode 100644 index 0705dac..0000000 --- a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/PuzzleSolver.xcscheme +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/xcschememanagement.plist b/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 67bb199..0000000 --- a/PuzzleSolver.xcodeproj/xcuserdata/jzeimen.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - SchemeUserState - - PuzzleSolver-release.xcscheme - - orderHint - 1 - - PuzzleSolver.xcscheme - - orderHint - 0 - - - SuppressBuildableAutocreation - - B51935A4170E016100FF1235 - - primary - - - - - diff --git a/PuzzleSolver/PuzzleDisjointSet.cpp b/PuzzleSolver/PuzzleDisjointSet.cpp deleted file mode 100644 index 8ce6946..0000000 --- a/PuzzleSolver/PuzzleDisjointSet.cpp +++ /dev/null @@ -1,189 +0,0 @@ -// -// PuzzleDisjointSet.cpp -// PuzzleSolver -// -// Created by Joe Zeimen on 4/12/13. -// Copyright (c) 2013 Joe Zeimen. All rights reserved. -// - -#include "PuzzleDisjointSet.h" -#include - -PuzzleDisjointSet::PuzzleDisjointSet(int number){ - set_count=0; - for(int i=0; i(1,1,new_id); - f.rotations = cv::Mat_(1,1,0); - sets.push_back(f); - set_count++; -} - - -bool PuzzleDisjointSet::join_sets(int a, int b, int how_a, int how_b){ - int rep_a = find(a); - int rep_b = find(b); - if(rep_a==rep_b) return false; //Already in same set... - - - -// std::cout << std::endl << sets[rep_a].rotations << std::endl << sets[rep_b].rotations << std::endl; - - //We need A to have its adjoining edge to be to the right, position 2 - // meaning if its rotation was 0 it would need to be rotated by 2 - cv::Point loc_of_a = find_location(sets[rep_a].locations, a); - int rot_a = sets[rep_a].rotations(loc_of_a); - int to_rot_a = (6 - how_a - rot_a)%4; - rotate_ccw(rep_a, to_rot_a); - - //We need B to have its adjoinign edge to the left, position 0 - //if its position was 0, - cv::Point loc_of_b = find_location(sets[rep_b].locations, b); - int rot_b = sets[rep_b].rotations(loc_of_b); - int to_rot_b = (8-rot_b-how_b)%4; - rotate_ccw(rep_b, to_rot_b); - - - //figure out the size of the new Mats - loc_of_a = find_location(sets[rep_a].locations, a); - cv::Mat::MSize size_of_a = sets[rep_a].locations.size; - loc_of_b = find_location(sets[rep_b].locations, b); - cv::Mat::MSize size_of_b = sets[rep_b].locations.size; - - int width = std::max(size_of_a[1], loc_of_a.x - loc_of_b.x +1 +size_of_b[1]) - std::min(0, loc_of_a.x-loc_of_b.x +1); - int height = std::max(size_of_a[0], loc_of_a.y - loc_of_b.y +size_of_b[0]) - std::min(0, loc_of_a.y-loc_of_b.y); - - - //place old A and B into the new Mats of same size - - cv::Mat_ new_a_locs(height, width, -1); - cv::Mat_ new_b_locs(height, width, -1); - cv::Mat_ new_a_rots(height, width, 0); - cv::Mat_ new_b_rots(height, width, 0); - - int ax_offset = std::abs(std::min(0, loc_of_a.x-loc_of_b.x+1)); - int ay_offset = std::abs(std::min(0, loc_of_a.y-loc_of_b.y)); - - int bx_offset = -(loc_of_b.x -( loc_of_a.x+ax_offset+1)); - int by_offset = -(loc_of_b.y -(loc_of_a.y+ay_offset)); - - sets[rep_a].locations.copyTo(new_a_locs(cv::Rect(ax_offset,ay_offset,size_of_a[1],size_of_a[0]))); - sets[rep_a].rotations.copyTo(new_a_rots(cv::Rect(ax_offset,ay_offset,size_of_a[1],size_of_a[0]))); - sets[rep_b].locations.copyTo(new_b_locs(cv::Rect(bx_offset,by_offset,size_of_b[1],size_of_b[0]))); - sets[rep_b].rotations.copyTo(new_b_rots(cv::Rect(bx_offset,by_offset,size_of_b[1],size_of_b[0]))); - - - - - //check for overlap while combining... - for(int i = 0; i m, int number ){ - for(int i = 0; i -#include - -class PuzzleDisjointSet{ -public: - struct forest{ - cv::Mat_ locations; - cv::Mat_ rotations; - int representative; - int id; - }; -private: - //A count of how many sets are left. - int set_count; - std::vector sets; - void rotate_ccw(int id, int times); - void make_set(int x); - cv::Point find_location(cv::Mat_, int number ); -public: - PuzzleDisjointSet( int number); - bool join_sets(int a, int b, int how_a, int how_b); - int find(int a); - bool in_same_set(int a, int b); - bool in_one_set(); - forest get(int id); - - - -}; - - - - - -#endif /* defined(__PuzzleSolver__PuzzleDisjointSet__) */ diff --git a/PuzzleSolver/main.cpp b/PuzzleSolver/main.cpp deleted file mode 100644 index 30891c4..0000000 --- a/PuzzleSolver/main.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// -// main.cpp -// PuzzleSolver -// -// Created by Joe Zeimen on 4/4/13. -// Copyright (c) 2013 Joe Zeimen. All rights reserved. -// - -#include -#include -#include "puzzle.h" -#include -#include "util.h" -#include "PuzzleDisjointSet.h" -#include - -//Dont forget final "/" in directory name. -static const std::string input = "/Users/jzeimen/Documents/school/College/Spring2013/ComputerVision/FinalProject/PuzzleSolver/PuzzleSolver/Scans/"; -static const std::string output = "/tmp/final/finaloutput.png"; - - -int main(int argc, const char * argv[]) -{ - - - std::cout << "Starting..." << std::endl; - timeval time; - gettimeofday(&time, NULL); - long millis = (time.tv_sec * 1000) + (time.tv_usec / 1000); - long inbetween_millis = millis; - //Toy Story Color & breaks with median filter, needs filter() 48 pc -// puzzle puzzle(input+"Toy Story/", 200, 22, false); - - - //Toy Story back works w/ median filter 48pc -// puzzle puzzle(input+"Toy Story back/", 200, 50); - - //Angry Birds color works with median, or filter 24 pc -// puzzle puzzle(input+"Angry Birds/color/",300,30); - - //Angry Birds back works with median 24 pc -// puzzle puzzle(input+"Angry Birds/Scanner Open/",300,30); - - //Horses back not numbered 104 pc -// puzzle puzzle(input+"horses/", 380, 50); - - //Horses back numbered 104 pc - puzzle puzzle(input+"horses numbered/", 380, 50); - - gettimeofday(&time, NULL); - std::cout << std::endl << "time to initialize:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-inbetween_millis)/1000.0 << std::endl; - inbetween_millis = ((time.tv_sec * 1000) + (time.tv_usec / 1000)); - - puzzle.solve(); - gettimeofday(&time, NULL); - std::cout << std::endl << "time to solve:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-inbetween_millis)/1000.0 << std::endl; - inbetween_millis = ((time.tv_sec * 1000) + (time.tv_usec / 1000)); - puzzle.save_image(output); - gettimeofday(&time, NULL); - std::cout << std::endl << "Time to draw:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-inbetween_millis)/1000.0 << std::endl; - - - gettimeofday(&time, NULL); - std::cout << std::endl << "total time:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-millis)/1000.0 << std::endl; - - system("/usr/bin/open /tmp/final/finaloutput.png"); - - return 0; -} - - diff --git a/PuzzleSolver/piece.cpp b/PuzzleSolver/piece.cpp deleted file mode 100644 index b74b6a1..0000000 --- a/PuzzleSolver/piece.cpp +++ /dev/null @@ -1,244 +0,0 @@ -// -// piece.cpp -// PuzzleSolver -// -// Created by Joe Zeimen on 4/5/13. -// Copyright (c) 2013 Joe Zeimen. All rights reserved. -// - -#include "piece.h" -#include -#include -#include "edge.h" -#include "utils.h" - -int number = 0; - -//This function takes in the beginning and ending of one vector, and returns -//an iterator representing the point where the first item in the second vector is. -std::vector::iterator find_first_in(std::vector::iterator begin, std::vector::iterator end, const std::vector &v){ - for(; begin != end; begin++){ - for(std::vector::const_iterator i = v.begin(); i!=v.end(); i++){ - if(begin->x == i->x && begin->y == i->y) return begin; - } - } - return end; -} - -//This returns iterators from the first vector where the value is equal places in the second vector. -std::vector::iterator> find_all_in(std::vector::iterator begin, std::vector::iterator end, const std::vector &v){ - - std::vector::iterator> places; - for(; begin != end; begin++){ - for(std::vector::const_iterator i = v.begin(); i!=v.end(); i++){ - if(begin->x == i->x && begin->y == i->y) places.push_back(begin); - } - } - return places; -} - - -//Euclidian distance between 2 points. -double distance(cv::Point a, cv::Point b){ - return cv::norm(a-b); -} - - -piece::piece(cv::Mat color, cv::Mat black_and_white, int estimated_piece_size){ -// edges = std::vector(); - full_color = color; - bw = black_and_white; - piece_size = estimated_piece_size; - process(); -} - - -void piece::process(){ - find_corners(); - extract_edges(); - classify(); -} - - - - - -//Gets the piece ready to use. -//This code has been adapted from http://docs.opencv.org/doc/tutorials/features2d/trackingmotion/corner_subpixeles/corner_subpixeles.html -void piece::find_corners(){ - - - //How close can 2 corners be? - double minDistance = piece_size; - //How big of an area to look for the corner in. - int blockSize = 25; - bool useHarrisDetector = true; - double k = 0.04; - - double min =0; - double max =1; - int max_iterations = 100; - bool found_all_corners = false; - - //Binary search, altering quality until exactly 4 corners are found. - //Usually done in 1 or 2 iterations - while(0 4){ - //Found too many corners increase quality - min = qualityLevel; - } else if (corners.size() < 4){ - max = qualityLevel; - } else { - //found all corners - found_all_corners = true; - break; - } - - } - - - - - //Find the sub-pixel locations of the corners. - cv::Size winSize = cv::Size( blockSize, blockSize ); - cv::Size zeroZone = cv::Size( -1, -1 ); - cv::TermCriteria criteria = cv::TermCriteria( CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 40, 0.001 ); - - /// Calculate the refined corner locations - cv::cornerSubPix( bw, corners, winSize, zeroZone, criteria ); - - - //More debug stuff, this will mark the corners with a white circle and save the image - // int r = 4; -// for( int i = 0; i < corners.size(); i++ ) -// { circle( full_color, corners[i],(int) corners.size(), cv::Scalar(255,255,255), -1, 8, 0 ); } -// std::stringstream out_file_name; -// out_file_name << "/tmp/final/test"< > contours; - std::vector hierarchy; - cv::findContours(bw.clone(), contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_NONE); - assert(corners.size() == 4); - if( 1 != contours.size() ){ - std::cerr << "Found incorrect number of contours." << std::endl; - exit(3); - } - std::vector contour = contours[0]; - - contour = remove_duplicates(contour); - - //out of all of the found corners, find the closest points in the contour, - //these will become the endpoints of the edges - for(int i = 0; i::iterator> sections; - sections = find_all_in(contour.begin(), contour.end(), corners); - - //Make corners go in the correct order - for(int i = 0; i<4; i++){ - corners[i]=*sections[i]; - } - - - assert(corners[1]!=corners[0] && corners[0]!=corners[2] && corners[0]!=corners[3] && corners[1]!=corners[2] && - corners[1]!=corners[3] && corners[2]!=corners[3]); - - edges[0] = edge(std::vector(sections[0],sections[1])); - edges[1] = edge(std::vector(sections[1],sections[2])); - edges[2] = edge(std::vector(sections[2],sections[3])); - edges[3] = edge(std::vector(sections[3],contour.end())); - - - -} - - - - - - - -//Classify the type of piece -void piece::classify(){ - int count = 0; - for(int i = 0; i<4; i++){ - if(edges[i].get_type() == OUTER_EDGE) count ++; - } - if(count ==0){ - type = MIDDLE; - } else if (count == 1){ - type = FRAME; - } else if (count == 2){ - type = CORNER; - } else { - std::cerr << "Proble, found too many outer edges for this piece" << std:: endl; - exit(4); - } -} - -pieceType piece::get_type(){ - return type; -} - -//Remember the paradigm is that we go in ccw order -//this rotates it ccw "90 degrees" for each "time" -void piece::rotate(int times){ - int times_to_rotate = times%4; - std::rotate(edges, edges+times_to_rotate, edges+4); - std::rotate(corners.begin(), corners.begin()+times_to_rotate, corners.end()); -} - -cv::Point2f piece::get_corner(int id){ - return corners[id]; -} - - - diff --git a/PuzzleSolver/puzzle.cpp b/PuzzleSolver/puzzle.cpp deleted file mode 100644 index 232fc4e..0000000 --- a/PuzzleSolver/puzzle.cpp +++ /dev/null @@ -1,308 +0,0 @@ -// -// puzzle.cpp -// PuzzleSolver -// -// Created by Joe Zeimen on 4/5/13. -// Copyright (c) 2013 Joe Zeimen. All rights reserved. -// - -#include "puzzle.h" -#include -#include "PuzzleDisjointSet.h" -#include -#include "omp.h" - - -/* - _________ _____ - \ \ / / - | / \ / _ - ___/ \____/ |__/ \ - / PUZZLE SOLVER } - \__/\ JOE ___ ZEIMEN ___/ - \ / / / - | | | | - /_____/ \_______\ -*/ - - - -puzzle::puzzle(std::string folderpath, int estimated_piece_size, int thresh, bool filter ){ - threshold = thresh; - piece_size = estimated_piece_size; - std::cout << "extracting pieces" << std::endl; - pieces = extract_pieces(folderpath, filter); - solved = false; -// print_edges(); -} - - -void puzzle::print_edges(){ - for(int i =0; i > contours; - contours.push_back(pieces[i].edges[j].get_translated_contour(200, 0)); - //This isn't used but the opencv function wants it anyways. - std::vector hierarchy; - - cv::drawContours(m, contours, -1, cv::Scalar(255)); - - putText(m, pieces[i].edges[j].edge_type_to_s(), cvPoint(300,300), - cv::FONT_HERSHEY_COMPLEX_SMALL, 0.8, cvScalar(255), 1, CV_AA); - - std::stringstream file_name; - file_name << "/tmp/final/contour" << i << "_" << j<<".png"; - cv::imwrite(file_name.str(), m); - - } - } -} - -std::vector puzzle::extract_pieces(std::string path, bool needs_filter){ - std::vector pieces; - imlist color_images = getImages(path); - - //Threshold the image, anything of intensity greater than 45 becomes white (255) - //anything below becomes 0 -// imlist blured_images = blur(color_images, 7, 5); - - imlist bw; - if(needs_filter){ - imlist blured_images = median_blur(color_images, 5); - bw = color_to_bw(blured_images,threshold); - } else{ - bw= color_to_bw(color_images, threshold); - filter(bw,2); - } - -// cv::imwrite("/tmp/final/thresh.png", bw[0]); - - - //For each input image - for(int i = 0; i > contours; - - //This isn't used but the opencv function wants it anyways. - std::vector hierarchy; - - //Need to clone b/c it will get modified - cv::findContours(bw[i].clone(), contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_NONE); - - - //For each contour in that image - //TODO: (In anticipation of the other TODO's Re-create the b/w image - // based off of the contour to eliminate noise in the layer mask - for(int j = 0; j > contours_to_draw; - contours_to_draw.push_back(translate_contour(contours[j], bordersize-r.x, bordersize-r.y)); - cv::drawContours(new_bw, contours_to_draw, -1, cv::Scalar(255), CV_FILLED); - // std::cout << out_file_name.str() << std::endl; - // cv::imwrite(out_file_name.str(), m); -// cv::imwrite("/tmp/final/new_bw.png", new_bw); - - r.width += bordersize*2; - r.height += bordersize*2; - r.x -= bordersize; - r.y -= bordersize; -// cv::imwrite("/tmp/final/bw.png", bw[i](r)); - cv::Mat mini_color = color_images[i](r); - cv::Mat mini_bw = new_bw;//bw[i](r); - //Create a copy so it can't conflict. - mini_color = mini_color.clone(); - mini_bw = mini_bw.clone(); - - piece p(mini_color, mini_bw, piece_size); - pieces.push_back(p); - - } - } - - return pieces; -} - - - - -void puzzle::fill_costs(){ - int no_edges = (int) pieces.size()*4; - - //TODO: use openmp to speed up this loop w/o blocking the commented lines below -// omp_set_num_threads(4); -#pragma omp parallel for schedule(dynamic) - for(int i =0; i::iterator i= matches.begin(); - PuzzleDisjointSet p((int)pieces.size()); - - -//You can save the individual pieces with their id numbers in the file name -//If the following loop is uncommented. -// for(int i=0; iedge1/4; - int e1 = i->edge1%4; - int p2 = i->edge2/4; - int e2 = i->edge2%4; - -//Uncomment the following lines to spit out pictures of the matched edges... -// cv::Mat m = cv::Mat::zeros(500,500,CV_8UC1); -// std::stringstream out_file_name; -// out_file_name << "/tmp/final/match" << output_id++ << "_" << p1<< "_" << e1 << "_" < > contours; -// contours.push_back(pieces[p1].edges[e1].get_translated_contour(200, 0)); -// contours.push_back(pieces[p2].edges[e2].get_translated_contour_reverse(200, 0)); -// cv::drawContours(m, contours, -1, cv::Scalar(255)); -// std::cout << out_file_name.str() << std::endl; -// cv::imwrite(out_file_name.str(), m); -// std::cout << "Attempting to merge: " << p1 << " with: " << p2 << " using edges:" << e1 << ", " << e2 << " c:" << i->score << " count: " << output_id++ < src; - std::vector dst; - - if(i==0 && j==0){ - points[i][j] = cv::Point2f(border,border); - } - if(i==0){ - points[i][j+1] = cv::Point2f(points[i][j].x+border+x_dist,border); - } - if(j==0){ - points[i+1][j] = cv::Point2f(border,points[i][j].y+border+y_dist); - } - - dst.push_back(points[i][j]); - dst.push_back(points[i+1][j]); - dst.push_back(points[i][j+1]); - src.push_back(pieces[piece_number].get_corner(0)); - src.push_back(pieces[piece_number].get_corner(1)); - src.push_back(pieces[piece_number].get_corner(3)); - - //true means use affine transform - cv::Mat a_trans_mat = cv::estimateRigidTransform(src, dst,true); - cv::Mat_ A = a_trans_mat; - - //Lower right corner of each piece - cv::Point2f l_r_c = pieces[piece_number].get_corner(2); - - //Doing my own matrix multiplication - points[i+1][j+1] = cv::Point2f((float)(A(0,0)*l_r_c.x+A(0,1)*l_r_c.y+A(0,2)),(float)(A(1,0)*l_r_c.x+A(1,1)*l_r_c.y+A(1,2))); - - - - cv::Mat layer; - cv::Mat layer_mask; - - int layer_size = out_image_size; - - cv::warpAffine(pieces[piece_number].full_color, layer, a_trans_mat, cv::Size2i(layer_size,layer_size),cv::INTER_LINEAR,cv::BORDER_TRANSPARENT); - cv::warpAffine(pieces[piece_number].bw, layer_mask, a_trans_mat, cv::Size2i(layer_size,layer_size),cv::INTER_NEAREST,cv::BORDER_TRANSPARENT); - - layer.copyTo(final_out_image(cv::Rect(0,0,layer_size,layer_size)), layer_mask); - - } - std::cout << std::endl; - if(failed){ - std::cout << "Failed, only partial image generated" << std::endl; - break; - } - } - - - cv::imwrite(filepath,final_out_image); - - - - for(int i = 0; i < solution.size[0]+1; ++i) - delete points[i]; - delete points; - -} diff --git a/PuzzleSolver/puzzle.h b/PuzzleSolver/puzzle.h deleted file mode 100644 index fec933f..0000000 --- a/PuzzleSolver/puzzle.h +++ /dev/null @@ -1,46 +0,0 @@ -// -// puzzle.h -// PuzzleSolver -// -// Created by Joe Zeimen on 4/5/13. -// Copyright (c) 2013 Joe Zeimen. All rights reserved. -// - -#ifndef __PuzzleSolver__puzzle__ -#define __PuzzleSolver__puzzle__ - -#include -#include "edge.h" -#include "piece.h" -#include -#include "utils.h" - -class puzzle{ -private: - struct match_score{ - uint16_t edge1, edge2; - double score; - static bool compare(match_score a, match_score b){ - return a.score matches; - std::vector extract_pieces(std::string path, bool needs_filter); - std::vector pieces; - cv::Mat_ solution; - void print_edges(); - cv::Mat_ solution_rotations; - void fill_costs(); - int piece_size; - std::string edgeType_to_s(edgeType e); -public: - puzzle(std::string path,int estimated_piece_size, int threshold, bool filter = true); - void solve(); - void save_image(std::string filepath); -}; - - - -#endif /* defined(__PuzzleSolver__puzzle__) */ diff --git a/PuzzleSolver/utils.cpp b/PuzzleSolver/utils.cpp deleted file mode 100644 index e01a789..0000000 --- a/PuzzleSolver/utils.cpp +++ /dev/null @@ -1,122 +0,0 @@ -// -// Utils.cpp -// PuzzleSolver -// -// Created by Joe Zeimen on 4/9/13. -// Copyright (c) 2013 Joe Zeimen. All rights reserved. -// - -#include "utils.h" - - - - -//This function takes a directory, and returns a vector of every image opencv could extract from it. -imlist getImages(std::string path){ - imlist v; - - DIR *dp; - struct dirent *ep; - dp = opendir (path.c_str()); - - if (dp != NULL) - { - while ((ep = readdir(dp))){ - cv::Mat image = cv::imread(path+ep->d_name); -// std::cout << path+ep->d_name << std::endl; - if(image.data!=NULL) v.push_back(image); - } - closedir(dp); - } - else{ - std::cout << "Couldn't open the directory" << std::endl; - - exit(1); - } - return v; -} - - -//Easy way to take a list of images and create a bw image at a specified threshold. -imlist color_to_bw(imlist color, int threshold){ - imlist black_and_white; - for(imlist::iterator i = color.begin(); i != color.end(); i++){ - cv::Mat bw; - cv::cvtColor(*i, bw, CV_BGR2GRAY); - cv::threshold(bw, bw, threshold, 255, cv::THRESH_BINARY); - black_and_white.push_back(bw); - } - return black_and_white; -} - -//Performs a open then a close operation in order to remove small anomolies. -void filter(imlist to_filter, int size){ - cv::Mat k = cv::getStructuringElement(cv::MORPH_ELLIPSE,cv::Size(size,size)); - for(imlist::iterator i = to_filter.begin(); i != to_filter.end(); i++){ - cv::Mat bw; - //Opening and closing removes anything smaller than size - cv::morphologyEx(*i, bw, CV_MOP_OPEN, k); - cv::morphologyEx(bw, *i, CV_MOP_CLOSE, k); - } -} - -//Performs a open then a close operation in order to remove small anomolies. -imlist blur(imlist to_filter, int size, double sigma){ - imlist ret; - for(imlist::iterator i = to_filter.begin(); i != to_filter.end(); i++){ - cv::Mat m; - cv::GaussianBlur(*i, m, cv::Size(size,size), sigma); - ret.push_back(m); - } - return ret; -} - - -//Performs a open then a close operation in order to remove small anomolies. -imlist median_blur(imlist to_filter, int k){ - imlist ret; - for(imlist::iterator i = to_filter.begin(); i != to_filter.end(); i++){ - cv::Mat m; - cv::medianBlur(*i, m, k); - ret.push_back(m); - } - return ret; -} - -imlist bilateral_blur(imlist to_blur){ - imlist ret; - for(imlist::iterator i = to_blur.begin(); i != to_blur.end(); i++){ - cv::Mat m; - cv::bilateralFilter(*i, m, 5, 152, 5); - - cv::imwrite("/tmp/final/bilat.png", m); - cv::imwrite("/tmp/final/before_bilat.png", *i); - ret.push_back(m); - } - return ret; -} -std::vector remove_duplicates(std::vector vec){ - bool dupes_found = true; - while(dupes_found){ - dupes_found=false; - int dup_at=-1; - for(int i =0; i - -#include -#include -#include -#include -#include -#include -#include -#include "opencv/cv.h" -#include "opencv/highgui.h" -#include "piece.h" -typedef std::vector imlist; - - -void filter(imlist to_filter, int size); -imlist color_to_bw(imlist color, int threshold); -void filter(imlist to_filter, int size); -imlist getImages(std::string path); -imlist blur(imlist to_blur, int size, double sigma); -imlist median_blur(imlist to_blur, int size); -imlist bilateral_blur(imlist to_blur); - -template std::vector translate_contour(std::vector in , int offset_x, int offset_y); -std::vector remove_duplicates(std::vector vec); -//Return a contour that is translated -template -std::vector translate_contour(std::vector in , int offset_x, int offset_y){ - std::vector ret_contour; - cv::Point2f offset(offset_x,offset_y); - for(int i = 0; i [options]``` + +The input directory must only contain the scanned images of your puzzle pieces. The output directory will be created if it doesn't exist. + +Try starting with options `--estimated-piece-size 100` and `--verify-contours`. When the contour window pops up, press +the 't' key on your keyboard to toggle between viewing the numbered contours and the original color image. Press the 'n' +key to advance to the next image. If contours do not show for some pieces, then try again with a lower estimated piece +size value. If too many contours are shown, then increase the estimated piece size. If the assigned contour numbers are +different compared to the numbers shown in the input images, then read up on the `--order` option, below, or re-scan +the pieces after adjusting their layout on the scanner bed. + +| Contour verification window | +| :--- | +| ![contour verification view](DocImages/contours.png)
With focus on this window, press the 't' key to toggle between viewing the contours
and the original input image. Press the 'n' key to advance to the contours of the next input image. | + + +**Corner quality warnings** + +If you get warnings about poor corners quality, try lowering the estimated piece size. If that doesn't help, then try using +a lower `--threshold` value. The `--corners-blocksize` value might also need to be reduced, especially for smaller pieces or if scanned with lower DPIs. +For persistent corner quality issues, use `--adjust-corners` to trigger a popup GUI window for each piece with poorly located corners. +This window allows the corner locations to be adjusted -- click and drag the red circles until they are each positioned +over a corner, then press the 'n' key to dismiss the window and advance to the next problematic piece, if any. If no edits +are necessary, press the 'n' key. Manual adjustments are persisted in data files in the output directory so that they don't +have to be adjusted again when re-running PuzzleSolver. + +The rendering scale of this window can be changed using the -/+ keys, or you can use the `--scale` option to set it for all popup windows. +Toggle between the color and black and white version of the piece with the 'c' key. + +| Corners before adjustment | Corners after adjustment | +| :--- | :--- | +| ![before adjustment](DocImages/before-corner-adjustment.png) | ![after adjustment](DocImages/after-corner-adjustment.png) | + +## Solution Modes + +When you are ready for PuzzleSolver to solve the puzzle, rerun with the same command line options as before, but add either the +`--solve` or `--guided` option. PuzzleSolver loads and processes the input images as it did before, but then advances into +solution mode by first computing a score for every possible edge-edge matchup. The computed scores are used to determine +how to fit peices together when finding the overall solution. + + +### Auto Solution Mode +In automatic mode (`--solve`), PuzzleSolver will attempt to solve the entire puzzle without any further input. This +works for the demos, and is likely to work for entire puzzles with low piece counts and relatively large phyiscal piece +sizes. Automatic solution mode, when successful, will show you an image of the completed puzzle. The solution image +file is also saved to the output directory as `solution.png`, and the console output is appended to `solution.log`. + +### Guided Solution Mode +In guided solution mode (`--guided`) you will be prompted via a popup GUI window to match pieces together. Find the two +pieces shown and check to see if they are a proper match -- if so, press the 'y' key on your keyboard, otherwise press +the 'n' key. For those pieces that fit to form a proper match, leave them connected. With each 'y' or 'n' keypress, +the GUI window will disappear and re-appear to prompt you to check another proposed match. Notice that the piece depicted +on the left side of this window always belongs to the matched group, and the one on the right is from the set of unmatched +pieces. The 'y' and 'n' answers are remembered in a data file in the output directory. PuzzleSolver picks up from where it +left off as long as it is invoked with the same command line parameters. + +In guided mode, PuzzleSolver attempts to find the best possible match of a new piece to the group of matched pieces you are currently +working on. It does this by finding the lowest edge-edge match score which includes an exterior edge in the group. The process +is not perfect, especially for puzzles with small piece sizes, and often the suggested matches aren't the correct fit. When a piece does fit, leave it attached and press the 'y' key. + +The rendering scale of this window can be adjusted using the -/+ keys, or you can use the `--scale` option to set it for all popup windows. + +Press the 'h' key, and a list of keystroke commands with descriptions will be displayed on the console. Focus must be on +the GUI window to trigger the keystroke commands. + +PuzzleSolver saves/loads data to/from files in the output directory such that it will pick up from where it left off +given the same set of input images and command line options. In other words, it is not necessary to solve the entire puzzle +in this mode in a single sitting. + +| Guided match prompt window | +| :--- | +| ![guided match prompt](DocImages/guided-match-window.png) | + +## Finishing partially completed puzzles + +PuzzleSolver can be used to help finish partially completed puzzles, including cases where there are more than one +uncompleted sections. To do this, number and scan only the loose pieces. Use guided solution mode. + +_Example work-in-progress on an unfinished section (i.e., matched group)..._ +![bacl side, matched group work-in-progress](DocImages/partial-matched-back.jpg) + +_Front side of the same matched set of pieces..._ +![front side, matched group work-in-progress](DocImages/partial-matched-front.jpg) + +#### Starting a new matched group + +Suppose you are using PuzzleSolver to help complete a partially completed puzzle that has two separate unfinished sections. +You've scanned the loose pieces and in guided mode have performed enough successful matches that one of the unfinished +sections is now solved, but PuzzleSolver keeps prompting you to match loose pieces to the edges of the already completed section. +PuzzleSolver needs to be told to work on the other section instead. To do this, press 'w' key while the guided match +window is in focus. The window will disappear and an input prompt on the console will ask for a piece number to work on. +Enter the piece number of a loose piece and the guided match window will reappear prompting you with a match for that piece. +You will have to return focus to the guided match window in order to proceed, but from this point forward you will be +prompted to match and build a new group of pieces. + +Alternatively, you can restart PuzzleSolver with the `--work-on` option and provide the piece number. + +It is possible to switch back to working on a previous group by entering the piece number of any piece in the group you +want to work on, either via the 'w' key or `--work-on` option. + +#### Setting boundary edges + +This concept is hard to explain, so here are some definitions which should help: + +"original finished section" - is comprised of pieces that were fit together prior to numbering and scanning any loose pieces. + +"matched group" - is comprised of the numbered, matched and fitted pieces in any uncompleted section that PuzzleSolver is helping to solve. + +At some point you'll realize that PuzzleSolver is suggesting matches for pieces in the current matched group which are +known to match pieces in the original finished section of the puzzle. These suggested matches are a waste of time. +To prevent PuzzleSolver from suggesting a match to the right edge of the left piece currently shown in the guided match +window, press the 'b' key. The decision to mark an edge as a "boundary edge" is remembered (saved) in a data file in +the output directory. + + +# How PuzzleSolver works +This section introduces the internals of PuzzleSolver and its command line options. +- The program begins by scanning the input directory for image files, sorts them by name into alphabetical order, and then loads them. +- For each image it finds, it identifies puzzle pieces within the image via a series of steps... + - The image is filtered to reduce noise. By default, OpenCV's medianBlur() function is used. Use the `--median-blur-ksize` command line option to override the default ksize value for the medianBlur() call. Alternatively, a built-in filter function can be used instead of medianBlur() via the `--filter` option. + - A two color, black-and-white version of the image is created by first converting the image to greyscale, and then tresholding the image. The threshold value can be controlled via the `--threshold` option. + - OpenCV's findContours() method is used to find the piece contours in the black and white image. Contours with a width or height less than the estimated piece size are assumed to be from image noise and are rejected. Use `--estimated-piece-size` to override the default value. This value is also important when detecting the piece corners later on. + - The piece contours are sorted and numbered in the order they appear in the input image. By understanding how PuzzleSolver numbers the pieces and by carefully arranging the numbered pieces on the glass when scanning, the piece numbers used in PuzzleSolver can be made to match those written on the peices. The default numbering order is "left to right, top to bottom" (`lrtb`) within the image, as in the order of written words on a page. The `--order` option can be used to change the default order value of `lrtb`. Eight different ordering options are available: lrtb, rltb, lrbt, rlbt, tblr, tbrl, btlr, and btrl. To see what PuzzleSolver is doing here, use the `--verify-contours` option and the numbered contours will be displayed in a popup GUI window. Press the 't' key when focus is on this window to toggle between the contours and the original color image. Press the 'n' key to advance to the contours of the next image. +- For each puzzle piece that is found... + - Small color and black and white images representing the piece are extracted from the input image and associated with the piece data. + - The locations of the piece corners are identified via an iterative process that calls OpenCV's goodFeaturesToTrack() function on the piece's black and white image. Corner cobminations are rejected if the distance between corners is shorter than the value given by `--estimated-piece-size`. The `blockSize` parameter passed to goodFeaturesToTrack() can be controlled via the `--corners-blocksize` option. + - A check on the quality of the identified corner locations is performed by comparing the corners found to those of a rectangle. The result is a 'corners quality' metric for which the value is higher for a piece with corner locations less like those of a rectangle, and lower for a piece where the corner locations are more like the corners of a rectangle. If the value for a piece exceeds the `--corners-quality` option value, then a warning is reported to the console. If a large number of warnings is issued, then the estimated piece size is probably set too high and should be lowered. The `--threshold` and `--corners-blocksize` option values are also important and should be adjusted when trying to reduce or eliminate these warnings. If warnings persist, then the corner locations for these pieces can be viewed and manually adjusted in a popup GUI window by re-running PuzzleSolver with the `--adjust-corners` option. + - The piece contour is divided into four edge contours at the corner locations. A copy of each edge contour is "normalized" -- i.e., translated and rotated so that one end is positioned at the origin and the other end is positioned above it on the y-axis. This allows for easy automatic classification and comparison of edges within the software. + - The shape of each edge is analysed and classified into one of three types: OUTER_EDGE, TAB, or HOLE. + - If you have not directed PuzzleSolver to proceed to the solution phase via `--solve`, `--guided`, or `--demo`, processing stop here and PuzzleSolver exits. + - PuzzleSolver computes scores for each possible edge-edge combination. Lower scores indicate a better match. Impossible matches such as a TAB edge matched to another TAB edge are given the highest possible score. Otherwise for every point in "this" contour the distances to the closest point in "that" contour are summed up and then added to the square of the difference in the distances between the two edge endpoints. + - The edge-edge combinations and thier scores are sorted into ascending order by the score values. + - In automatic mode, a solution is attempted by iterating down the sorted list, matching the two edges of each entry + and rejecting the match if it results in an impossible physical arrangement of pieces such as overlaps, etc. If the + solution were to be visualized over time, it would appear as if the pieces randomly coalesce until all pieces have + been matched into a single group of pieces. The solution is attempted in a single pass down the sorted edge-edge match list, and + not by trying to reduce the overall sum of all matched edge scores. + - Guided solution mode is similar except that the the human operator participates in accepting and rejecting possible + matches, and the number of matched sets is intentionally kept to a minimum to make it easier for the user to find the + pieces suggested for matching. In this mode, additional checks are performed prior to suggesting a match where the + new piece would fit against more than just the other edge from the edge-edge match score list entry. + - Adjacent edge matches are checked for impossible fits, such as TAB to TAB or HOLE to HOLE matches, and rejected as appropriate. + - The adjacent edge-edge score values can also result in a rejected match when they exceed certain thresholds. + - If an adjacent edge match score has a corner-corner distance difference component that exceeds the value configured + with `--cscore-limit`, then the match is rejected. + - If an adjacent edge match score has a point-point distance sum component that exceeds the value configured with + `--escore-limit`, then the match is rejected. - g++ -O3 `pkg-config --cflags opencv` -o PuzzleSolver *.cpp `pkg-config --libs opencv` -This will result in an executable called PuzzleSolver. Currently the input/output settings are just hard coded in the program. diff --git a/Src/ChangeLog b/Src/ChangeLog new file mode 100644 index 0000000..ecafceb --- /dev/null +++ b/Src/ChangeLog @@ -0,0 +1,13 @@ +Version: 2019.1 + * Updated to compile and build with OpenCV versions 2.x, 3.x, or 4.x. + * Added autotools support (./configure, make). If available, openmp is automatically enabled. + * Added command-line option support for many of the previously hard-coded parameters. + * Added support for numbering pieces based on their position within the input images. + * Added support for detecting low quality corner placement during find_corners(). + * Added a corner editing GUI for manual correction of low quality corners. Edited + corner locations are remembered via data files saved to the output directory. + * Updated the edge cost function to factor in the differences in corner-corner distances. + * Append the console output to a log file in the output directory. + * Added 'guided solution' mode which allows the operator to accept or veto piece placements suggested by the software. + * Added auto-cropping of the solution image for a better viewing experience. + * Display the solution image via OpenCV HighGUI instead of relying on a host-specific image viewer. diff --git a/Src/Makefile.am b/Src/Makefile.am new file mode 100644 index 0000000..cbbbefd --- /dev/null +++ b/Src/Makefile.am @@ -0,0 +1,12 @@ +bin_PROGRAMS = PuzzleSolver # gmtest +if USE_OPENCV4 +OPENCV_CXXFLAGS = $(opencv4_CFLAGS) +OPENCV_LDDFLAGS = $(opencv4_LIBS) +else +OPENCV_CXXFLAGS = $(opencv_CFLAGS) +OPENCV_LDDFLAGS = $(opencv_LIBS) +endif +AM_CXXFLAGS = -std=c++11 $(OPENCV_CXXFLAGS) $(OPENMP_CFLAGS) +LDADD = $(OPENCV_LDDFLAGS) $(OPENMP_CFLAGS) +PuzzleSolver_SOURCES = adjust_corners.cpp contours.cpp edge.cpp guided_match.cpp image_viewer.cpp logger.cpp main.cpp params.cpp piece.cpp puzzle.cpp PuzzleDisjointSet.cpp utils.cpp +#gmtest_SOURCES = adjust_corners.cpp contours.cpp edge.cpp guided_match.cpp gmtest.cpp logger.cpp params.cpp piece.cpp utils.cpp diff --git a/Src/PuzzleDisjointSet.cpp b/Src/PuzzleDisjointSet.cpp new file mode 100644 index 0000000..275c4e0 --- /dev/null +++ b/Src/PuzzleDisjointSet.cpp @@ -0,0 +1,295 @@ +// +// PuzzleDisjointSet.cpp +// PuzzleSolver +// +// Created by Joe Zeimen on 4/12/13. +// Copyright (c) 2013 Joe Zeimen. All rights reserved. +// + +#include "PuzzleDisjointSet.h" + +#include +#include +#include +#include + +#include "compat_opencv.h" +#include "logger.h" + +PuzzleDisjointSet::PuzzleDisjointSet(params& user_params, int number, match_checker checker, void* match_check_data) + : user_params(user_params), edge_checker(checker), match_check_data(match_check_data) { + set_count=0; + merge_failures = 0; + for(int i=0; i(1,1,new_id); + f.rotations = cv::Mat_(1,1,0); + sets.push_back(f); + set_count++; +} + +void PuzzleDisjointSet::init_join(PuzzleDisjointSet::join_context& c, int a, int b, int how_a, int how_b) { + c.a = a; + c.b = b; + c.how_a = how_a; + c.how_b = how_b; + c.rep_a = find(a); + c.rep_b = find(b); + c.joinable = c.rep_a != c.rep_b; +} + + +bool PuzzleDisjointSet::compute_join(PuzzleDisjointSet::join_context& c) { + if (!c.joinable) return false; //Already in same set... + + c.joinable = false; + +// std::cout << std::endl << sets[rep_a].rotations << std::endl << sets[rep_b].rotations << std::endl; + + //We need A to have its adjoining edge to be to the right, position 2 + // meaning if its rotation was 0 it would need to be rotated by 2 + cv::Point loc_of_a = find_location(sets[c.rep_a].locations, c.a); + int rot_a = sets[c.rep_a].rotations(loc_of_a); + int to_rot_a = (6 - c.how_a - rot_a)%4; + rotate_ccw(c.rep_a, to_rot_a); + + //We need B to have its adjoining edge to the left, position 0 + //if its position was 0, + cv::Point loc_of_b = find_location(sets[c.rep_b].locations, c.b); + int rot_b = sets[c.rep_b].rotations(loc_of_b); + int to_rot_b = (8-rot_b-c.how_b)%4; + rotate_ccw(c.rep_b, to_rot_b); + + + //figure out the size of the new Mats + loc_of_a = find_location(sets[c.rep_a].locations, c.a); + COMPAT_CV_MAT_SIZE size_of_a = sets[c.rep_a].locations.size; + loc_of_b = find_location(sets[c.rep_b].locations, c.b); + COMPAT_CV_MAT_SIZE size_of_b = sets[c.rep_b].locations.size; + + int width = std::max(size_of_a[1], loc_of_a.x - loc_of_b.x +1 +size_of_b[1]) - std::min(0, loc_of_a.x-loc_of_b.x +1); + int height = std::max(size_of_a[0], loc_of_a.y - loc_of_b.y +size_of_b[0]) - std::min(0, loc_of_a.y-loc_of_b.y); + + + //place old A and B into the new Mats of same size + + cv::Mat_ new_a_locs(height, width, -1); + cv::Mat_ new_b_locs(height, width, -1); + cv::Mat_ new_a_rots(height, width, 0); + cv::Mat_ new_b_rots(height, width, 0); + + int ax_offset = std::abs(std::min(0, loc_of_a.x-loc_of_b.x+1)); + int ay_offset = std::abs(std::min(0, loc_of_a.y-loc_of_b.y)); + + int bx_offset = -(loc_of_b.x -( loc_of_a.x+ax_offset+1)); + int by_offset = -(loc_of_b.y -(loc_of_a.y+ay_offset)); + + sets[c.rep_a].locations.copyTo(new_a_locs(cv::Rect(ax_offset,ay_offset,size_of_a[1],size_of_a[0]))); + sets[c.rep_a].rotations.copyTo(new_a_rots(cv::Rect(ax_offset,ay_offset,size_of_a[1],size_of_a[0]))); + sets[c.rep_b].locations.copyTo(new_b_locs(cv::Rect(bx_offset,by_offset,size_of_b[1],size_of_b[0]))); + sets[c.rep_b].rotations.copyTo(new_b_rots(cv::Rect(bx_offset,by_offset,size_of_b[1],size_of_b[0]))); + + +// std::cout << "w: " << width << ", h: " << height << std::endl; + + //check for overlap while combining... + for(int i = 0; i 0 && new_a_locs(i-1,j) != -1) { +// std::cout << "b("< 0 && new_a_locs(i,j-1) != -1) { +// std::cout << "a("<::iterator ci = std::find(csets.begin(), csets.end(), c.rep_a); + if (ci == csets.end()) { + csets.insert(csets.begin(), c.rep_a); + } + + ci = std::find(csets.begin(), csets.end(), c.rep_b); + if (ci != csets.end()) { + csets.erase(ci); + } + + //Representative is the same idea as a disjoint set datastructure + sets[c.rep_b].representative = c.rep_a; +} + +void PuzzleDisjointSet::match_failure() { + if (user_params.isVerbose()) { + logger::stream() << "Failed to merge because of low quality or impossible adjoining edge match" << std::endl; logger::flush(); + merge_failures++; + } +} + +void PuzzleDisjointSet::finish() { + if (merge_failures > 0) { + std::cout << std::endl; + logger::stream() << "Failed to merge because of overlap (" << ++merge_failures << " times)" << std::endl; logger::flush(); + } +} +int PuzzleDisjointSet::find(int a){ + int rep = a; + while(sets[rep].representative != -1){ + rep = sets[rep].representative; + } + return rep; +} + +std::vector PuzzleDisjointSet::get_collection_sets() { + return csets; +} + +bool PuzzleDisjointSet::is_collection_set(int rep) { + std::vector::iterator i = std::find(csets.begin(), csets.end(), rep); + return i != csets.end(); +} + +bool PuzzleDisjointSet::is_unmatched_set(int rep) { + forest s = sets[rep]; + return (s.representative == -1 && s.locations.rows == 1 && s.locations.cols == 1); +} + +int PuzzleDisjointSet::collection_set_count() { + int result = csets.size(); + return result; +} + +bool PuzzleDisjointSet::in_same_set(int a, int b){ + return (find(a) == find(b)); +} + + +void PuzzleDisjointSet::rotate_ccw(int id,int times){ + int direction = times%4; + switch (direction) { + case 0: //Don't rotate + return; + break; + case 1: //Rotate ccw 90 degrees + cv::flip(sets[id].locations,sets[id].locations,1); //flip around y axis + cv::transpose(sets[id].locations,sets[id].locations); + cv::flip(sets[id].rotations,sets[id].rotations,1); + cv::transpose(sets[id].rotations, sets[id].rotations); + sets[id].rotations+=1; + break; + case 2: //rotate 180 + cv::flip(sets[id].locations,sets[id].locations,-1); //flip around both axises + cv::flip(sets[id].rotations ,sets[id].rotations,-1); //flip around both axises + sets[id].rotations+=2; + break; + case 3: //rotate cw 90 degrees + cv::transpose(sets[id].locations,sets[id].locations); + cv::flip(sets[id].locations,sets[id].locations,1); //flip around y axis + cv::transpose(sets[id].rotations, sets[id].rotations); + cv::flip(sets[id].rotations,sets[id].rotations,1); + sets[id].rotations+=3; + break; + default://Should never get here!!! + return; + break; + } + //If there is no piece at the location, the rotation needs to be set back + //to zero + for(int i=0; i m, int number ){ + for(int i = 0; i +#include +#include "compat_opencv.h" +#include "params.h" +#include "piece.h" + +// Function pointer type for functions that can verify if the two pieces match on the given edges. +typedef bool (*match_checker) (void* data, int p1, int p2, int e1, int e2); + +class PuzzleDisjointSet{ +public: + struct forest{ + cv::Mat_ locations; + cv::Mat_ rotations; + int representative; + int id; + }; + struct join_context { + bool joinable; + int a; + int b; + int how_a; + int how_b; + int rep_a; + int rep_b; + cv::Mat_ new_a_locs; + cv::Mat_ new_a_rots; + }; +private: + //A count of how many sets are left. + int set_count; + uint merge_failures; + std::vector sets; + std::vector csets; // collector sets... matched sets that are currently unmatched with any other set + match_checker edge_checker; + void* match_check_data; + params& user_params; + void rotate_ccw(int id, int times); + void make_set(int x); + cv::Point find_location(cv::Mat_, int number ); +public: + PuzzleDisjointSet(params& user_params, int number, match_checker edge_checker, void* match_check_data); + void init_join(join_context& context, int a, int b, int how_a, int how_b); + bool compute_join(join_context& context); + void complete_join(join_context& context); + void match_failure(); + int find(int a); + std::vector get_collection_sets(); + // returns true if the set is a matched set that is unmatched with any other sets + bool is_collection_set(int rep); + // returns true of the set is unmatched + bool is_unmatched_set(int rep); + int collection_set_count(); + bool in_same_set(int a, int b); + bool in_one_set(); + forest get(int id); + void finish(); +}; + + + + + +#endif /* defined(__PuzzleSolver__PuzzleDisjointSet__) */ diff --git a/PuzzleSolver/PuzzleSolver.1 b/Src/PuzzleSolver.1 similarity index 100% rename from PuzzleSolver/PuzzleSolver.1 rename to Src/PuzzleSolver.1 diff --git a/Src/adjust_corners.cpp b/Src/adjust_corners.cpp new file mode 100644 index 0000000..0d9f4e5 --- /dev/null +++ b/Src/adjust_corners.cpp @@ -0,0 +1,244 @@ +#include "opencv2/opencv.hpp" + +#include "adjust_corners.h" + +#define DEFAULT_SCALE_FACTOR 3.0f +#define UNSCALED_CIRCLE_SIZE 4 + +void ce_mouse_callback(int event, int x, int y, int flags, void* userdata); + +// GUI for manually editing the corners of a puzzle piece. Implemented using OpenCV highgui. +class corner_editor { +public: + std::string& window_name; + float scale_factor; + std::vector& original_corners; + std::vector& edited_corners; + bool verbose; + + cv::Mat& image; + cv::Mat resized; + cv::Mat rendered; + std::vector scaled_corners; + int circle_size; + int corner_index; + bool edited; + + + corner_editor(std::string& window_name, cv::Mat& image, float scale_factor, std::vector& original_corners, std::vector& edited_corners, bool verbose) : + image(image), window_name(window_name), scale_factor(scale_factor), original_corners(original_corners), edited_corners(edited_corners), verbose(verbose) + { + corner_index = -1; + edited = false; + + init_scaled_corners(original_corners); + } + + + void init_scaled_corners(std::vector& corners) { + circle_size = image.size().width * scale_factor / 50; + + cv::resize(image, resized, cv::Size(), scale_factor, scale_factor, cv::INTER_CUBIC); + rendered = resized.clone(); + + scaled_corners.clear(); + for (uint i = 0; i < std::min((size_t)4, corners.size()); i++) { + scaled_corners.push_back( corners[i] * scale_factor); + } + + // If original_corners was short, add new ones + if (scaled_corners.size() < 4) { + float newx = resized.size().width / 2.0f; + float newy = resized.size().height / 2.0f; + + for (uint i = scaled_corners.size(); i < 4; i++) { + scaled_corners.push_back( cv::Point2f(newx, newy)); + newx = newx + circle_size * 4.0f; + } + } + } + + void adjust_scale( float adjustment) { + float new_scale = scale_factor + adjustment; + if (new_scale < 0.25) { + new_scale = 0.25; + } + if (new_scale == scale_factor) { + return; + } + + std::vector corners; + for (uint i = 0; i < scaled_corners.size(); i++) { + cv::Point2f c = scaled_corners[i]; + corners.push_back(cv::Point2f(c.x / scale_factor, c.y / scale_factor)); + } + scale_factor = new_scale; + init_scaled_corners(corners); + render_circles(); + std::cout << "Scale factor is now " << scale_factor << std::endl; + } + + bool edit() { + + render_circles(); + + cv::namedWindow(window_name); + cv::setMouseCallback(window_name, ce_mouse_callback, this); + cv::imshow(window_name, rendered); + + bool done = false; + do { + int c = cv::waitKey(0); + // std::cout << c << std::endl; + switch (c) { + case -1: // User probably clicked on the "x" in the window title bar + case 13: // Return key + case 141: // Enter key + case 'n': // "next" + done = true; + break; + case 'r': // "reset" + if (corner_index == -1) { + reset_circles(); + } + break; + case '-': + adjust_scale(-0.25); + break; + case '+': + case '=': + adjust_scale(0.25); + break; + default: + break; + } + } + while (!done); + try { + cv::destroyWindow(window_name); + } + catch (cv::Exception x) { + // Ignore + } + + if (edited) { + edited_corners.clear(); + for (uint i = 0; i < 4; i++) { + cv::Point2f c = scaled_corners[i]; + edited_corners.push_back(cv::Point2f(c.x / scale_factor, c.y / scale_factor)); + } + } + return edited; + } + + void reset_circles() { + edited = false; + init_scaled_corners(original_corners); + render_circles(); + } + + void mouse_down(int x, int y) { + for (uint i = 0; i < scaled_corners.size(); i++) { + int cx = scaled_corners[i].x; + int cy = scaled_corners[i].y; + if (cx - circle_size < x && x < cx + circle_size && cy - circle_size < y && y < cy + circle_size) { + corner_index = i; + if (verbose) { + std::cout << "Click in circle #" << i << std::endl; + } + } + } + } + + void mouse_move(int x, int y) { + if (corner_index == -1) { + return; + } + scaled_corners[corner_index].x = x; + scaled_corners[corner_index].y = y; + render_circles(); + } + + void mouse_up(int x, int y) { + if (corner_index == -1) { + return; + } + + scaled_corners[corner_index].x = x; + scaled_corners[corner_index].y = y; + render_circles(); + edited = true; + corner_index = -1; + } + +private: + void render_circles() { + resized.copyTo(rendered); + for(uint i = 0; i < scaled_corners.size(); i++ ) { + circle( rendered, scaled_corners[i], circle_size, cv::Scalar(0,0,255), 2, 8, 0 ); + } + cv::imshow(window_name, rendered); + } +}; + +void ce_mouse_callback(int event, int x, int y, int flags, void* userdata) { + corner_editor* data = (corner_editor*)userdata; + + if ( event == cv::EVENT_LBUTTONDOWN ) + { + if (data->verbose) { + std::cout << "Left mouse down - position (" << x << ", " << y << ")" << std::endl; + } + data->mouse_down(x, y); + } + else if ( event == cv::EVENT_LBUTTONUP ) + { + if (data->verbose) { + std::cout << "Left mouse up - position (" << x << ", " << y << ")" << std::endl; + } + data->mouse_up(x, y); + } + else if ( event == cv::EVENT_MOUSEMOVE ) + { + data->mouse_move(x, y); + // std::cout << "Mouse move - position (" << x << ", " << y << ")" << std::endl; + } +} + + +bool adjust_corners(std::string& window_name, cv::Mat& image, float scale_factor, std::vector& original_corners, std::vector& edited_corners, bool verbose) +{ + corner_editor editor(window_name, image, scale_factor, original_corners, edited_corners, verbose); + return editor.edit(); +} + +/* +int main(int argc, char * argv[]) +{ + + std::vector corners; + corners.push_back(cv::Point2f(180.021484,163.588364)); + corners.push_back(cv::Point2f(14.8587542,61.6872482)); + corners.push_back(cv::Point2f(22.5145721,173.08255)); +// corners.push_back(cv::Point2f(132.522491,54.6961975)); + + std::string filename = "color-003-002-0106.png"; + cv::Mat image = cv::imread(filename); + if (!image.data) { + return -1; + } + + std::vector edited_corners; + + + bool edited = edit_corners(filename, image, 3.0f, corners, edited_corners, false); + if (edited) { + // re-invoke the editor to check that edited_corners contains the correct data + // i.e., this window should show the corners in the edited (not original) locations. + std::vector edited_corners2; + edit_corners(filename, image, 3.0f, edited_corners, edited_corners2, false); + } + + return 0; +} +*/ \ No newline at end of file diff --git a/Src/adjust_corners.h b/Src/adjust_corners.h new file mode 100644 index 0000000..3a9371a --- /dev/null +++ b/Src/adjust_corners.h @@ -0,0 +1,21 @@ +#ifndef ADJUST_CORNERS_H +#define ADJUST_CORNERS_H + +/** @brief Display a GUI which allows the corners of a piece to be manually adjusted. + +Returns true if the corners are moved to new locations, false otherwise. +Keyboard presses on 'n', 'Return', or 'Enter' will end the editing operation, causing this method to return. +A keyboard press on 'r' will reset the editor to its original, unedited state. +The '+' and '-' keys can be used to increase/decrease the display scale factor. + +@param window_name The name to use for the highgui window. +@param image The image of the puzzle piece +@param scale_factor The image shown in the editor is scaled up by this factor to make editing easier +@param original_corners The unedited (input) corners +@param edited_corners If the corners are moved, this vector is populated with the new corner locations. +@param verbose If true, some information will be sent to the console during the edit process. +*/ +bool adjust_corners(std::string& window_name, cv::Mat& image, float scale_factor, std::vector& original_corners, std::vector& edited_corners, bool verbose); + +#endif /* ADJUST_CORNERS_H */ + diff --git a/Src/build.sh b/Src/build.sh new file mode 100755 index 0000000..eebba61 --- /dev/null +++ b/Src/build.sh @@ -0,0 +1,6 @@ +#! /bin/bash + +touch NEWS README AUTHORS +autoreconf -vif && \ + ./configure && \ + make diff --git a/Src/clean.sh b/Src/clean.sh new file mode 100755 index 0000000..572b8d7 --- /dev/null +++ b/Src/clean.sh @@ -0,0 +1,28 @@ +#! /bin/bash + +# Clean out all of the cruft created when running autotools +if [ -e Makefile ]; then + make clean +fi + +rm -rf \ + Makefile.in \ + install-sh \ + config.h.in \ + configure \ + depcomp \ + INSTALL \ + compile \ + missing \ + autom4te.cache \ + aclocal.m4 \ + AUTHORS \ + NEWS \ + README \ + COPYING \ + .deps \ + Makefile \ + config.status \ + config.h \ + config.log \ + stamp-h1 diff --git a/Src/compat_opencv.h b/Src/compat_opencv.h new file mode 100644 index 0000000..4a3ba4c --- /dev/null +++ b/Src/compat_opencv.h @@ -0,0 +1,74 @@ +/* + * Preprocessor defines that allow PuzzleSolver to be compatible with OpenCV 2.x, 3.x, and 4.x. + */ + +#ifndef COMPAT_OPENCV_H +#define COMPAT_OPENCV_H + +#include "opencv2/opencv.hpp" +#include "opencv2/core/version.hpp" + +#ifdef CV_VERSION_EPOCH +#define OPENCV_VERSION_MAJOR CV_VERSION_EPOCH +#define OPENCV_VERSION_MINOR CV_VERSION_MAJOR +#else +#define OPENCV_VERSION_MAJOR CV_VERSION_MAJOR +#define OPENCV_VERSION_MINOR CV_VERSION_MINOR +#endif + +#if OPENCV_VERSION_MAJOR == 2 +#include +#else +#include +#endif + + + +#if OPENCV_VERSION_MAJOR == 2 + +#define COMPAT_CV_MAT_SIZE cv::Mat::MSize +#define COMPAT_CV_BGR2GRAY CV_BGR2GRAY +#define COMPAT_CV_MORPH_TYPE_OPEN CV_MOP_OPEN +#define COMPAT_CV_MORPH_TYPE_CLOSE CV_MOP_CLOSE +#define COMPAT_CV_TERM_CRITERIA_EPS CV_TERMCRIT_EPS +#define COMPAT_CV_TERM_CRITERIA_MAX_ITER CV_TERMCRIT_ITER +#define COMPAT_CV_CONTOURS_MATCH_I2 CV_CONTOURS_MATCH_I2 +#define COMPAT_CV_FILLED CV_FILLED +#define COMPAT_CV_LINE_AA CV_AA +#define ROTATE_90_CLOCKWISE 0 //!& namevec) { + for(std::map::iterator it = items.begin(); it != items.end(); ++it) { + namevec.push_back(it->first); + } +} + +piece_order* piece_order::lookup(std::string name) { + if (items.size() == 0) { + piece_order::init(); + } + std::map::iterator it = items.find(name); + return (it == items.end()) ? NULL : it->second; +} + +std::map piece_order::items; + +contour::contour(cv::Rect _bounds, std::vector _points) : bounds(_bounds), points(_points) {} + +contour_partition::contour_partition(int index) { + this->index = index; + offset = INT_MAX; +} + +void contour_partition::update_offset(int off) { + offset = std::min(offset, off); +} + + + + +contour_mgr::contour_mgr(int _container_width, int _container_height, params& _user_params) : user_params(_user_params) { + container_width = _container_width; + container_height = _container_height; +} + +void contour_mgr::add_contour(cv::Rect _bounds, std::vector _points) { + contours.push_back(contour(_bounds,_points)); +} + +// Sort the contours so that the pieces end up being identified based on their position in the original image -- +// i.e., assuming the pieces are arranged in a grid in the image, then number them left to right going +// from the top to the bottom. The point of doing this is to provide some way to correlate hand-written piece +// numbers with the numerical (text) output of this program and is especially helpful if a solution is found +// but a final solution image can't be generated. The piece layout in the image does not need to be exact, +// but the differences in y of each row (or x of each column) must be less than the estimated piece size multiplied +// by the partition factor. If "landscape" is true, then pieces are ordered top to bottom going left to right. +void contour_mgr::sort_contours() { + // First, partition the contours based on x (or y) position. + std::vector labels; + + piece_order* porder = piece_order::lookup(user_params.getPieceOrder()); + + // partition the contours into rows (or columns if partition_rows==false) + cv::partition(contours, labels, [=](const contour& a, const contour& b) { + int diff = porder->partition_rows ? (a.bounds.y - b.bounds.y) : (a.bounds.x - b.bounds.x); + return std::abs(diff) < user_params.getEstimatedPieceSize() * user_params.getPartitionFactor(); + }); + + // Determine the number of partitions + int num_partitions = 0; + for (uint i = 0; i < labels.size(); i++) { + num_partitions = std::max(labels[i], num_partitions); + } + num_partitions += 1; + + // Create an array of partition objects + contour_partition* partition_array[num_partitions]; + // Create a sortable vector of partitions + std::vector partition_vector; + for (uint i = 0; i < num_partitions; i++) { + partition_array[i] = new contour_partition(i); + partition_vector.push_back(partition_array[i]); + } + + // Determine the min offset (x or y) for each partition + for (uint i = 0; i < contours.size(); i++) { + int partition = labels[i]; + int current_offset = partition_array[partition]->offset; + int extent = porder->partition_rows ? contours[i].bounds.y : contours[i].bounds.x; + partition_array[partition]->offset = std::min(extent, current_offset); + } + + // Sort the partitions into offset order + std::sort(partition_vector.begin(), partition_vector.end(), [=](contour_partition* a, contour_partition* b) { + return porder->partitions_asc ? (a->offset) < (b-> offset) : (b->offset) < (a-> offset); + }); + + // Assign the partition order attribute based on the sorted order + for (uint i = 0 ; i < partition_vector.size(); i++) { + partition_vector[i]->order = i; + } + + int container_dimension = porder->partition_rows ? container_width : container_height; + // Assign the sort_factor to each contour + for (uint i = 0; i < contours.size(); i++) { + int offset_in_partition = porder->partition_rows ? contours[i].bounds.x : contours[i].bounds.y; + if (!porder->items_asc) { + offset_in_partition = container_dimension - offset_in_partition; + } + contours[i].sort_factor = partition_array[labels[i]]->order * container_dimension + offset_in_partition; + } + + // Sort the contours + std::sort(contours.begin(), contours.end(), + [](const contour & a, const contour & b) -> bool + { + return a.sort_factor < b.sort_factor; + }); +} diff --git a/Src/contours.h b/Src/contours.h new file mode 100644 index 0000000..828eb9c --- /dev/null +++ b/Src/contours.h @@ -0,0 +1,82 @@ +/* + * + * Classes that participate in ordering the piece contours into rows (or columns) + * so that the resulting pieces can be identified by their position within the + * input image. + * + */ + +#ifndef CONTOURS_H +#define CONTOURS_H + +#include +#include +#include "compat_opencv.h" +#include "params.h" + +// Associate names such as "lrtb" (left->right, top->bottom) with the parameters required to +// order them via our row/colum partitioning scheme. +class piece_order { +public: + std::string name; // "lrtb", etc. + bool partition_rows; // true if initially partitioned into rows, false if columns + bool partitions_asc; // true if partitions are sorted into ascending order, false if descending + bool items_asc; // true if items in a partition are sorted into ascending order, false if descending + piece_order(std::string name, bool partition_rows, bool partition_order_asc, bool item_order_asc); + static piece_order* lookup(std::string name); + static void names(std::vector& namevec); +private: + static void init(); + static std::map items; +}; + +// Associate piece contour points and bounds so they can be sorted +class contour { +public: + cv::Rect bounds; + std::vector points; + int sort_factor; // The value to sort on + contour(cv::Rect _bounds, std::vector _points); +}; + +// A contour partition is created for each row (or column) found, so that +// we can sort the contours into the proper order. +class contour_partition { +public: + int index; // The unsorted partition index + int offset; // The offset in x (or y), used to order partition + int order; // Order of the partition among other partitions after sorting by offset + + contour_partition(int index); + + void update_offset(int off); +}; + +// Contour manager +class contour_mgr { +private: + int container_width; + int container_height; + params& user_params; +public: + + std::vector contours; + + contour_mgr(int _container_width, int _container_height, params& _user_params); + + void add_contour(cv::Rect _bounds, std::vector _points); + + // Sort the contours so that the pieces end up being identified based on their position in the original image -- + // i.e., assuming the pieces are arranged in a grid in the image, then number them left to right going + // from the top to the bottom. The point of doing this is to provide some way to correlate hand-written piece + // numbers with the numerical (text) output of this program and is especially helpful if a solution is found + // but a final solution image can't be generated. The piece layout in the image does not need to be exact, + // but the differences in y of each row (or x of each column) must be less than the estimated piece size multiplied + // by the partition factor. If "landscape" is true, then pieces are ordered top to bottom going left to right. + void sort_contours(); + +}; + + +#endif /* CONTOURS_H */ + diff --git a/Src/cxxopts.hpp b/Src/cxxopts.hpp new file mode 100644 index 0000000..1ef1e15 --- /dev/null +++ b/Src/cxxopts.hpp @@ -0,0 +1,2005 @@ +/* + +Copyright (c) 2014, 2015, 2016, 2017 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +#ifndef CXXOPTS_HPP_INCLUDED +#define CXXOPTS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cpp_lib_optional +#include +#define CXXOPTS_HAS_OPTIONAL +#endif + +namespace cxxopts +{ + static constexpr struct { + uint8_t major, minor, patch; + } version = {2, 1, 0}; +} + +//when we ask cxxopts to use Unicode, help strings are processed using ICU, +//which results in the correct lengths being computed for strings when they +//are formatted for the help output +//it is necessary to make sure that can be found by the +//compiler, and that icu-uc is linked in to the binary. + +#ifdef CXXOPTS_USE_UNICODE +#include + +namespace cxxopts +{ + typedef icu::UnicodeString String; + + inline + String + toLocalString(std::string s) + { + return icu::UnicodeString::fromUTF8(std::move(s)); + } + + class UnicodeStringIterator : public + std::iterator + { + public: + + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) + { + } + + value_type + operator*() const + { + return s->char32At(i); + } + + bool + operator==(const UnicodeStringIterator& rhs) const + { + return s == rhs.s && i == rhs.i; + } + + bool + operator!=(const UnicodeStringIterator& rhs) const + { + return !(*this == rhs); + } + + UnicodeStringIterator& + operator++() + { + ++i; + return *this; + } + + UnicodeStringIterator + operator+(int32_t v) + { + return UnicodeStringIterator(s, i + v); + } + + private: + const icu::UnicodeString* s; + int32_t i; + }; + + inline + String& + stringAppend(String&s, String a) + { + return s.append(std::move(a)); + } + + inline + String& + stringAppend(String& s, int n, UChar32 c) + { + for (int i = 0; i != n; ++i) + { + s.append(c); + } + + return s; + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + while (begin != end) + { + s.append(*begin); + ++begin; + } + + return s; + } + + inline + size_t + stringLength(const String& s) + { + return s.length(); + } + + inline + std::string + toUTF8String(const String& s) + { + std::string result; + s.toUTF8String(result); + + return result; + } + + inline + bool + empty(const String& s) + { + return s.isEmpty(); + } +} + +namespace std +{ + inline + cxxopts::UnicodeStringIterator + begin(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, 0); + } + + inline + cxxopts::UnicodeStringIterator + end(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, s.length()); + } +} + +//ifdef CXXOPTS_USE_UNICODE +#else + +namespace cxxopts +{ + typedef std::string String; + + template + T + toLocalString(T&& t) + { + return t; + } + + inline + size_t + stringLength(const String& s) + { + return s.length(); + } + + inline + String& + stringAppend(String&s, String a) + { + return s.append(std::move(a)); + } + + inline + String& + stringAppend(String& s, size_t n, char c) + { + return s.append(n, c); + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + return s.append(begin, end); + } + + template + std::string + toUTF8String(T&& t) + { + return std::forward(t); + } + + inline + bool + empty(const std::string& s) + { + return s.empty(); + } +} + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts +{ + namespace + { +#ifdef _WIN32 + const std::string LQUOTE("\'"); + const std::string RQUOTE("\'"); +#else + const std::string LQUOTE("‘"); + const std::string RQUOTE("’"); +#endif + } + + class Value : public std::enable_shared_from_this + { + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual bool + is_boolean() const = 0; + }; + + class OptionException : public std::exception + { + public: + OptionException(const std::string& message) + : m_message(message) + { + } + + virtual const char* + what() const noexcept + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; + + class OptionSpecException : public OptionException + { + public: + + OptionSpecException(const std::string& message) + : OptionException(message) + { + } + }; + + class OptionParseException : public OptionException + { + public: + OptionParseException(const std::string& message) + : OptionException(message) + { + } + }; + + class option_exists_error : public OptionSpecException + { + public: + option_exists_error(const std::string& option) + : OptionSpecException(u8"Option " + LQUOTE + option + RQUOTE + u8" already exists") + { + } + }; + + class invalid_option_format_error : public OptionSpecException + { + public: + invalid_option_format_error(const std::string& format) + : OptionSpecException(u8"Invalid option format " + LQUOTE + format + RQUOTE) + { + } + }; + + class option_not_exists_exception : public OptionParseException + { + public: + option_not_exists_exception(const std::string& option) + : OptionParseException(u8"Option " + LQUOTE + option + RQUOTE + u8" does not exist") + { + } + }; + + class missing_argument_exception : public OptionParseException + { + public: + missing_argument_exception(const std::string& option) + : OptionParseException( + u8"Option " + LQUOTE + option + RQUOTE + u8" is missing an argument" + ) + { + } + }; + + class option_requires_argument_exception : public OptionParseException + { + public: + option_requires_argument_exception(const std::string& option) + : OptionParseException( + u8"Option " + LQUOTE + option + RQUOTE + u8" requires an argument" + ) + { + } + }; + + class option_not_has_argument_exception : public OptionParseException + { + public: + option_not_has_argument_exception + ( + const std::string& option, + const std::string& arg + ) + : OptionParseException( + u8"Option " + LQUOTE + option + RQUOTE + + u8" does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } + }; + + class option_not_present_exception : public OptionParseException + { + public: + option_not_present_exception(const std::string& option) + : OptionParseException(u8"Option " + LQUOTE + option + RQUOTE + u8" not present") + { + } + }; + + class argument_incorrect_type : public OptionParseException + { + public: + argument_incorrect_type + ( + const std::string& arg + ) + : OptionParseException( + u8"Argument " + LQUOTE + arg + RQUOTE + u8" failed to parse" + ) + { + } + }; + + class option_required_exception : public OptionParseException + { + public: + option_required_exception(const std::string& option) + : OptionParseException( + u8"Option " + LQUOTE + option + RQUOTE + u8" is required but not present" + ) + { + } + }; + + namespace values + { + namespace + { + std::basic_regex integer_pattern + ("(-)?(0x)?([1-9a-zA-Z][0-9a-zA-Z]*)|((0x)?0)"); + std::basic_regex truthy_pattern + ("(t|T)(rue)?"); + std::basic_regex falsy_pattern + ("((f|F)(alse)?)?"); + } + + namespace detail + { + template + struct SignedCheck; + + template + struct SignedCheck + { + template + void + operator()(bool negative, U u, const std::string& text) + { + if (negative) + { + if (u > static_cast(-std::numeric_limits::min())) + { + throw argument_incorrect_type(text); + } + } + else + { + if (u > static_cast(std::numeric_limits::max())) + { + throw argument_incorrect_type(text); + } + } + } + }; + + template + struct SignedCheck + { + template + void + operator()(bool, U, const std::string&) {} + }; + + template + void + check_signed_range(bool negative, U value, const std::string& text) + { + SignedCheck::is_signed>()(negative, value, text); + } + } + + template + R + checked_negate(T&& t, const std::string&, std::true_type) + { + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + return -static_cast(t); + } + + template + T + checked_negate(T&&, const std::string& text, std::false_type) + { + throw argument_incorrect_type(text); + } + + template + void + integer_parser(const std::string& text, T& value) + { + std::smatch match; + std::regex_match(text, match, integer_pattern); + + if (match.length() == 0) + { + throw argument_incorrect_type(text); + } + + if (match.length(4) > 0) + { + value = 0; + return; + } + + using US = typename std::make_unsigned::type; + + constexpr auto umax = std::numeric_limits::max(); + constexpr bool is_signed = std::numeric_limits::is_signed; + const bool negative = match.length(1) > 0; + const uint8_t base = match.length(2) > 0 ? 16 : 10; + + auto value_match = match[3]; + + US result = 0; + + for (auto iter = value_match.first; iter != value_match.second; ++iter) + { + size_t digit = 0; + + if (*iter >= '0' && *iter <= '9') + { + digit = *iter - '0'; + } + else if (base == 16 && *iter >= 'a' && *iter <= 'f') + { + digit = *iter - 'a' + 10; + } + else if (base == 16 && *iter >= 'A' && *iter <= 'F') + { + digit = *iter - 'A' + 10; + } + else + { + throw argument_incorrect_type(text); + } + + if (umax - digit < result * base) + { + throw argument_incorrect_type(text); + } + + result = result * base + digit; + } + + detail::check_signed_range(negative, result, text); + + if (negative) + { + value = checked_negate(result, + text, + std::integral_constant()); + } + else + { + value = result; + } + } + + template + void stringstream_parser(const std::string& text, T& value) + { + std::stringstream in(text); + in >> value; + if (!in) { + throw argument_incorrect_type(text); + } + } + + inline + void + parse_value(const std::string& text, uint8_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int8_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, uint16_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int16_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, uint32_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int32_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, uint64_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int64_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, bool& value) + { + std::smatch result; + std::regex_match(text, result, truthy_pattern); + + if (!result.empty()) + { + value = true; + return; + } + + std::regex_match(text, result, falsy_pattern); + if (!result.empty()) + { + value = false; + return; + } + + throw argument_incorrect_type(text); + } + + inline + void + parse_value(const std::string& text, std::string& value) + { + value = text; + } + + // The fallback parser. It uses the stringstream parser to parse all types + // that have not been overloaded explicitly. It has to be placed in the + // source code before all other more specialized templates. + template + void + parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); + } + + template + void + parse_value(const std::string& text, std::vector& value) + { + T v; + parse_value(text, v); + value.push_back(v); + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + void + parse_value(const std::string& text, std::optional& value) + { + T result; + parse_value(text, result); + value = std::move(result); + } +#endif + + template + struct type_is_container + { + static constexpr bool value = false; + }; + + template + struct type_is_container> + { + static constexpr bool value = true; + }; + + template + class abstract_value : public Value + { + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + abstract_value(T* t) + : m_store(t) + { + } + + virtual ~abstract_value() = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) + { + m_result = std::make_shared(); + m_store = m_result.get(); + } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + parse(const std::string& text) const + { + parse_value(text, *m_store); + } + + bool + is_container() const + { + return type_is_container::value; + } + + void + parse() const + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const + { + return m_default; + } + + bool + has_implicit() const + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::string + get_default_value() const + { + return m_default_value; + } + + std::string + get_implicit_value() const + { + return m_implicit_value; + } + + bool + is_boolean() const + { + return std::is_same::value; + } + + const T& + get() const + { + if (m_store == nullptr) + { + return *m_result; + } + else + { + return *m_store; + } + } + + protected: + std::shared_ptr m_result; + T* m_store; + + bool m_default = false; + bool m_implicit = false; + + std::string m_default_value; + std::string m_implicit_value; + }; + + template + class standard_value : public abstract_value + { + public: + using abstract_value::abstract_value; + + std::shared_ptr + clone() const + { + return std::make_shared>(*this); + } + }; + + template <> + class standard_value : public abstract_value + { + public: + ~standard_value() = default; + + standard_value() + { + set_default_and_implicit(); + } + + standard_value(bool* b) + : abstract_value(b) + { + set_default_and_implicit(); + } + + std::shared_ptr + clone() const + { + return std::make_shared>(*this); + } + + private: + + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } + }; + } + + template + std::shared_ptr + value() + { + return std::make_shared>(); + } + + template + std::shared_ptr + value(T& t) + { + return std::make_shared>(&t); + } + + class OptionAdder; + + class OptionDetails + { + public: + OptionDetails + ( + const std::string& short_, + const std::string& long_, + const String& desc, + std::shared_ptr val + ) + : m_short(short_) + , m_long(long_) + , m_desc(desc) + , m_value(val) + , m_count(0) + { + } + + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_count(rhs.m_count) + { + m_value = rhs.m_value->clone(); + } + + OptionDetails(OptionDetails&& rhs) = default; + + const String& + description() const + { + return m_desc; + } + + const Value& value() const { + return *m_value; + } + + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } + + const std::string& + short_name() const + { + return m_short; + } + + const std::string& + long_name() const + { + return m_long; + } + + private: + std::string m_short; + std::string m_long; + String m_desc; + std::shared_ptr m_value; + int m_count; + }; + + struct HelpOptionDetails + { + std::string s; + std::string l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; + }; + + struct HelpGroupDetails + { + std::string name; + std::string description; + std::vector options; + }; + + class OptionValue + { + public: + void + parse + ( + std::shared_ptr details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + } + + void + parse_default(std::shared_ptr details) + { + ensure_value(details); + m_value->parse(); + } + + size_t + count() const + { + return m_count; + } + + template + const T& + as() const + { +#ifdef CXXOPTS_NO_RTTI + return static_cast&>(*m_value).get(); +#else + return dynamic_cast&>(*m_value).get(); +#endif + } + + private: + void + ensure_value(std::shared_ptr details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } + + std::shared_ptr m_value; + size_t m_count = 0; + }; + + class KeyValue + { + public: + KeyValue(std::string key_, std::string value_) + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } + + const + std::string& + key() const + { + return m_key; + } + + const std::string + value() const + { + return m_value; + } + + template + T + as() const + { + T result; + values::parse_value(m_value, result); + return result; + } + + private: + std::string m_key; + std::string m_value; + }; + + class ParseResult + { + public: + + ParseResult( + const std::unordered_map>&, + std::vector, + int&, char**&); + + size_t + count(const std::string& o) const + { + auto iter = m_options.find(o); + if (iter == m_options.end()) + { + return 0; + } + + auto riter = m_results.find(iter->second); + + return riter->second.count(); + } + + const OptionValue& + operator[](const std::string& option) const + { + auto iter = m_options.find(option); + + if (iter == m_options.end()) + { + throw option_not_present_exception(option); + } + + auto riter = m_results.find(iter->second); + + return riter->second; + } + + const std::vector& + arguments() const + { + return m_sequential; + } + + private: + + OptionValue& + get_option(std::shared_ptr); + + void + parse(int& argc, char**& argv); + + void + add_to_option(const std::string& option, const std::string& arg); + + bool + consume_positional(std::string a); + + void + parse_option + ( + std::shared_ptr value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(std::shared_ptr details); + + void + checked_parse_arg + ( + int argc, + char* argv[], + int& current, + std::shared_ptr value, + const std::string& name + ); + + const std::unordered_map> + &m_options; + std::vector m_positional; + std::vector::iterator m_next_positional; + std::unordered_set m_positional_set; + std::unordered_map, OptionValue> m_results; + + std::vector m_sequential; + }; + + class Options + { + public: + + Options(std::string program, std::string help_string = "") + : m_program(std::move(program)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_next_positional(m_positional.end()) + { + } + + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } + + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } + + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } + + ParseResult + parse(int& argc, char**& argv); + + OptionAdder + add_options(std::string group = ""); + + void + add_option + ( + const std::string& group, + const std::string& s, + const std::string& l, + std::string desc, + std::shared_ptr value, + std::string arg_help + ); + + //parse positional arguments into the given option + void + parse_positional(std::string option); + + void + parse_positional(std::vector options); + + void + parse_positional(std::initializer_list options); + + std::string + help(const std::vector& groups = {""}) const; + + const std::vector + groups() const; + + const HelpGroupDetails& + group_help(const std::string& group) const; + + private: + + void + add_one_option + ( + const std::string& option, + std::shared_ptr details + ); + + String + help_one_group(const std::string& group) const; + + void + generate_group_help + ( + String& result, + const std::vector& groups + ) const; + + void + generate_all_groups_help(String& result) const; + + std::string m_program; + String m_help_string; + std::string m_custom_help; + std::string m_positional_help; + bool m_show_positional; + + std::unordered_map> m_options; + std::vector m_positional; + std::vector::iterator m_next_positional; + std::unordered_set m_positional_set; + + //mapping from groups to help options + std::map m_help; + }; + + class OptionAdder + { + public: + + OptionAdder(Options& options, std::string group) + : m_options(options), m_group(std::move(group)) + { + } + + OptionAdder& + operator() + ( + const std::string& opts, + const std::string& desc, + std::shared_ptr value + = ::cxxopts::value(), + std::string arg_help = "" + ); + + private: + Options& m_options; + std::string m_group; + }; + + namespace + { + constexpr int OPTION_LONGEST = 30; + constexpr int OPTION_DESC_GAP = 2; + + std::basic_regex option_matcher + ("--([[:alnum:]][-_[:alnum:]]+)(=(.*))?|-([[:alnum:]]+)"); + + std::basic_regex option_specifier + ("(([[:alnum:]]),)?[ ]*([[:alnum:]][-_[:alnum:]]*)?"); + + String + format_option + ( + const HelpOptionDetails& o + ) + { + auto& s = o.s; + auto& l = o.l; + + String result = " "; + + if (s.size() > 0) + { + result += "-" + toLocalString(s) + ","; + } + else + { + result += " "; + } + + if (l.size() > 0) + { + result += " --" + toLocalString(l); + } + + auto arg = o.arg_help.size() > 0 ? toLocalString(o.arg_help) : "arg"; + + if (!o.is_boolean) + { + if (o.has_implicit) + { + result += " [=" + arg + "(=" + toLocalString(o.implicit_value) + ")]"; + } + else + { + result += " " + arg; + } + } + + return result; + } + + String + format_description + ( + const HelpOptionDetails& o, + size_t start, + size_t width + ) + { + auto desc = o.desc; + + if (o.has_default && (!o.is_boolean || o.default_value != "false")) + { + desc += toLocalString(" (default: " + o.default_value + ")"); + } + + String result; + + auto current = std::begin(desc); + auto startLine = current; + auto lastSpace = current; + + auto size = size_t{}; + + while (current != std::end(desc)) + { + if (*current == ' ') + { + lastSpace = current; + } + + if (size > width) + { + if (lastSpace == startLine) + { + stringAppend(result, startLine, current + 1); + stringAppend(result, "\n"); + stringAppend(result, start, ' '); + startLine = current + 1; + lastSpace = startLine; + } + else + { + stringAppend(result, startLine, lastSpace); + stringAppend(result, "\n"); + stringAppend(result, start, ' '); + startLine = lastSpace + 1; + } + size = 0; + } + else + { + ++size; + } + + ++current; + } + + //append whatever is left + stringAppend(result, startLine, current); + + return result; + } + } + +inline +ParseResult::ParseResult +( + const std::unordered_map>& options, + std::vector positional, + int& argc, char**& argv +) +: m_options(options) +, m_positional(std::move(positional)) +, m_next_positional(m_positional.begin()) +{ + parse(argc, argv); +} + +inline +OptionAdder +Options::add_options(std::string group) +{ + return OptionAdder(*this, std::move(group)); +} + +inline +OptionAdder& +OptionAdder::operator() +( + const std::string& opts, + const std::string& desc, + std::shared_ptr value, + std::string arg_help +) +{ + std::match_results result; + std::regex_match(opts.c_str(), result, option_specifier); + + if (result.empty()) + { + throw invalid_option_format_error(opts); + } + + const auto& short_match = result[2]; + const auto& long_match = result[3]; + + if (!short_match.length() && !long_match.length()) + { + throw invalid_option_format_error(opts); + } else if (long_match.length() == 1 && short_match.length()) + { + throw invalid_option_format_error(opts); + } + + auto option_names = [] + ( + const std::sub_match& short_, + const std::sub_match& long_ + ) + { + if (long_.length() == 1) + { + return std::make_tuple(long_.str(), short_.str()); + } + else + { + return std::make_tuple(short_.str(), long_.str()); + } + }(short_match, long_match); + + m_options.add_option + ( + m_group, + std::get<0>(option_names), + std::get<1>(option_names), + desc, + value, + std::move(arg_help) + ); + + return *this; +} + +inline +void +ParseResult::parse_default(std::shared_ptr details) +{ + m_results[details].parse_default(details); +} + +inline +void +ParseResult::parse_option +( + std::shared_ptr value, + const std::string& /*name*/, + const std::string& arg +) +{ + auto& result = m_results[value]; + result.parse(value, arg); + + m_sequential.emplace_back(value->long_name(), arg); +} + +inline +void +ParseResult::checked_parse_arg +( + int argc, + char* argv[], + int& current, + std::shared_ptr value, + const std::string& name +) +{ + if (current + 1 >= argc) + { + if (value->value().has_implicit()) + { + parse_option(value, name, value->value().get_implicit_value()); + } + else + { + throw missing_argument_exception(name); + } + } + else + { + if (value->value().has_implicit()) + { + parse_option(value, name, value->value().get_implicit_value()); + } + else + { + parse_option(value, name, argv[current + 1]); + ++current; + } + } +} + +inline +void +ParseResult::add_to_option(const std::string& option, const std::string& arg) +{ + auto iter = m_options.find(option); + + if (iter == m_options.end()) + { + throw option_not_exists_exception(option); + } + + parse_option(iter->second, option, arg); +} + +inline +bool +ParseResult::consume_positional(std::string a) +{ + while (m_next_positional != m_positional.end()) + { + auto iter = m_options.find(*m_next_positional); + if (iter != m_options.end()) + { + auto& result = m_results[iter->second]; + if (!iter->second->value().is_container()) + { + if (result.count() == 0) + { + add_to_option(*m_next_positional, a); + ++m_next_positional; + return true; + } + else + { + ++m_next_positional; + continue; + } + } + else + { + add_to_option(*m_next_positional, a); + return true; + } + } + ++m_next_positional; + } + + return false; +} + +inline +void +Options::parse_positional(std::string option) +{ + parse_positional(std::vector{std::move(option)}); +} + +inline +void +Options::parse_positional(std::vector options) +{ + m_positional = std::move(options); + m_next_positional = m_positional.begin(); + + m_positional_set.insert(m_positional.begin(), m_positional.end()); +} + +inline +void +Options::parse_positional(std::initializer_list options) +{ + parse_positional(std::vector(std::move(options))); +} + +inline +ParseResult +Options::parse(int& argc, char**& argv) +{ + ParseResult result(m_options, m_positional, argc, argv); + return result; +} + +inline +void +ParseResult::parse(int& argc, char**& argv) +{ + int current = 1; + + int nextKeep = 1; + + bool consume_remaining = false; + + while (current != argc) + { + if (strcmp(argv[current], "--") == 0) + { + consume_remaining = true; + ++current; + break; + } + + std::match_results result; + std::regex_match(argv[current], result, option_matcher); + + if (result.empty()) + { + //not a flag + + //if true is returned here then it was consumed, otherwise it is + //ignored + if (consume_positional(argv[current])) + { + } + else + { + argv[nextKeep] = argv[current]; + ++nextKeep; + } + //if we return from here then it was parsed successfully, so continue + } + else + { + //short or long option? + if (result[4].length() != 0) + { + const std::string& s = result[4]; + + for (std::size_t i = 0; i != s.size(); ++i) + { + std::string name(1, s[i]); + auto iter = m_options.find(name); + + if (iter == m_options.end()) + { + throw option_not_exists_exception(name); + } + + auto value = iter->second; + + if (i + 1 == s.size()) + { + //it must be the last argument + checked_parse_arg(argc, argv, current, value, name); + } + else if (value->value().has_implicit()) + { + parse_option(value, name, value->value().get_implicit_value()); + } + else + { + //error + throw option_requires_argument_exception(name); + } + } + } + else if (result[1].length() != 0) + { + const std::string& name = result[1]; + + auto iter = m_options.find(name); + + if (iter == m_options.end()) + { + throw option_not_exists_exception(name); + } + + auto opt = iter->second; + + //equals provided for long option? + if (result[2].length() != 0) + { + //parse the option given + + parse_option(opt, name, result[3]); + } + else + { + //parse the next argument + checked_parse_arg(argc, argv, current, opt, name); + } + } + + } + + ++current; + } + + for (auto& opt : m_options) + { + auto& detail = opt.second; + auto& value = detail->value(); + + auto& store = m_results[detail]; + + if(!store.count() && value.has_default()){ + parse_default(detail); + } + } + + if (consume_remaining) + { + while (current < argc) + { + if (!consume_positional(argv[current])) { + break; + } + ++current; + } + + //adjust argv for any that couldn't be swallowed + while (current != argc) { + argv[nextKeep] = argv[current]; + ++nextKeep; + ++current; + } + } + + argc = nextKeep; + +} + +inline +void +Options::add_option +( + const std::string& group, + const std::string& s, + const std::string& l, + std::string desc, + std::shared_ptr value, + std::string arg_help +) +{ + auto stringDesc = toLocalString(std::move(desc)); + auto option = std::make_shared(s, l, stringDesc, value); + + if (s.size() > 0) + { + add_one_option(s, option); + } + + if (l.size() > 0) + { + add_one_option(l, option); + } + + //add the help details + auto& options = m_help[group]; + + options.options.emplace_back(HelpOptionDetails{s, l, stringDesc, + value->has_default(), value->get_default_value(), + value->has_implicit(), value->get_implicit_value(), + std::move(arg_help), + value->is_container(), + value->is_boolean()}); +} + +inline +void +Options::add_one_option +( + const std::string& option, + std::shared_ptr details +) +{ + auto in = m_options.emplace(option, details); + + if (!in.second) + { + throw option_exists_error(option); + } +} + +inline +String +Options::help_one_group(const std::string& g) const +{ + typedef std::vector> OptionHelp; + + auto group = m_help.find(g); + if (group == m_help.end()) + { + return ""; + } + + OptionHelp format; + + size_t longest = 0; + + String result; + + if (!g.empty()) + { + result += toLocalString(" " + g + " options:\n"); + } + + for (const auto& o : group->second.options) + { + if (o.is_container && + m_positional_set.find(o.l) != m_positional_set.end() && + !m_show_positional) + { + continue; + } + + auto s = format_option(o); + longest = std::max(longest, stringLength(s)); + format.push_back(std::make_pair(s, String())); + } + + longest = std::min(longest, static_cast(OPTION_LONGEST)); + + //widest allowed description + auto allowed = size_t{76} - longest - OPTION_DESC_GAP; + + auto fiter = format.begin(); + for (const auto& o : group->second.options) + { + if (o.is_container && + m_positional_set.find(o.l) != m_positional_set.end() && + !m_show_positional) + { + continue; + } + + auto d = format_description(o, longest + OPTION_DESC_GAP, allowed); + + result += fiter->first; + if (stringLength(fiter->first) > longest) + { + result += '\n'; + result += toLocalString(std::string(longest + OPTION_DESC_GAP, ' ')); + } + else + { + result += toLocalString(std::string(longest + OPTION_DESC_GAP - + stringLength(fiter->first), + ' ')); + } + result += d; + result += '\n'; + + ++fiter; + } + + return result; +} + +inline +void +Options::generate_group_help +( + String& result, + const std::vector& print_groups +) const +{ + for (size_t i = 0; i != print_groups.size(); ++i) + { + const String& group_help_text = help_one_group(print_groups[i]); + if (empty(group_help_text)) + { + continue; + } + result += group_help_text; + if (i < print_groups.size() - 1) + { + result += '\n'; + } + } +} + +inline +void +Options::generate_all_groups_help(String& result) const +{ + std::vector all_groups; + all_groups.reserve(m_help.size()); + + for (auto& group : m_help) + { + all_groups.push_back(group.first); + } + + generate_group_help(result, all_groups); +} + +inline +std::string +Options::help(const std::vector& help_groups) const +{ + String result = m_help_string + "\nUsage:\n " + + toLocalString(m_program) + " " + toLocalString(m_custom_help); + + if (m_positional.size() > 0 && m_positional_help.size() > 0) { + result += " " + toLocalString(m_positional_help); + } + + result += "\n\n"; + + if (help_groups.size() == 0) + { + generate_all_groups_help(result); + } + else + { + generate_group_help(result, help_groups); + } + + return toUTF8String(result); +} + +inline +const std::vector +Options::groups() const +{ + std::vector g; + + std::transform( + m_help.begin(), + m_help.end(), + std::back_inserter(g), + [] (const std::map::value_type& pair) + { + return pair.first; + } + ); + + return g; +} + +inline +const HelpGroupDetails& +Options::group_help(const std::string& group) const +{ + return m_help.at(group); +} + +} + +#endif //CXXOPTS_HPP_INCLUDED diff --git a/PuzzleSolver/edge.cpp b/Src/edge.cpp similarity index 65% rename from PuzzleSolver/edge.cpp rename to Src/edge.cpp index 1e9482f..eb15b78 100644 --- a/PuzzleSolver/edge.cpp +++ b/Src/edge.cpp @@ -6,14 +6,20 @@ // Copyright (c) 2013 Joe Zeimen. All rights reserved. // + + #include "edge.h" + #include #include + #include "utils.h" + edge::edge(std::vector edge){ //original contour = edge; - + arc_length = cv::arcLength(contour, false); + corner_distance = utils::distance(edge[0], edge[edge.size()-1]); //Normalized contours are used for comparisons normalized_contour = normalize(contour); std::vector copy(contour.begin(),contour.end()); @@ -23,13 +29,16 @@ edge::edge(std::vector edge){ classify(); } +std::vector edge::get_contour() { + return contour; +} //Trying OpenCV's match shapes, hasn't worked as well as my compare2 function. double edge::compare(edge that){ //Return large numbers if we know that these shapes simply wont match... - if(type == OUTER_EDGE || that.type == OUTER_EDGE) return 1000000; - if(type == that.type) return 10000000; - return cv::matchShapes(contour, that.contour, CV_CONTOURS_MATCH_I2, 0); + if(type == OUTER_EDGE || that.type == OUTER_EDGE) return DBL_MAX; + if(type == that.type) return DBL_MAX; + return cv::matchShapes(contour, that.contour, COMPAT_CV_CONTOURS_MATCH_I2, 0); } @@ -38,10 +47,10 @@ double edge::compare(edge that){ //The end result is the sum divided by length of the 2 contours double edge::compare2(edge that){ //Return large number if an impossible situation is happening - if(type == OUTER_EDGE || that.type == OUTER_EDGE) return 100000000; - if(type == that.type) return 100000000; + if(type == OUTER_EDGE || that.type == OUTER_EDGE) return DBL_MAX; + if(type == that.type) return DBL_MAX; double cost=0; - double total_length = cv::arcLength(normalized_contour, false) + cv::arcLength(that.reverse_normalized_contour, false); + double total_length = this->arc_length + that.arc_length; for(std::vector::iterator i = normalized_contour.begin(); i!=normalized_contour.end(); i++){ double min = 10000000; @@ -55,7 +64,43 @@ double edge::compare2(edge that){ return cost/total_length; } +double edge::compare3(edge that, double& cscore, double& escore) { + //Return large number if an impossible situation is happening + if(type == OUTER_EDGE || that.type == OUTER_EDGE || type == that.type) { + cscore = 0.0; + escore = DBL_MAX; + return DBL_MAX; + } + + double corners_diff = this->corner_distance - that.corner_distance; + corners_diff *= corners_diff; + cscore = corners_diff; + + double cost = 0.0; + for(std::vector::iterator i = normalized_contour.begin(); i!=normalized_contour.end(); i++){ + double min = DBL_MAX; + for(std::vector::iterator j = that.reverse_normalized_contour.begin(); j!=that.reverse_normalized_contour.end(); j++) { + double dist = utils::distance(*i,*j); + if(dist normalized_contour[i].x) minx = normalized_contour[i].x; if(maxx < normalized_contour[i].x) maxx = normalized_contour[i].x; } @@ -88,11 +133,11 @@ void edge::classify(){ std::vector edge::get_translated_contour(int offset_x, int offset_y){ - return translate_contour(normalized_contour, offset_x, offset_y); + return utils::translate_contour(normalized_contour, offset_x, offset_y); }; std::vector edge::get_translated_contour_reverse(int offset_x, int offset_y){ - return translate_contour(reverse_normalized_contour, offset_x, offset_y); + return utils::translate_contour(reverse_normalized_contour, offset_x, offset_y); }; @@ -127,6 +172,7 @@ std::vector edge::normalize(std::vector cont){ //TODO: get rid of this default constructor so it isn't accedentally used. edge::edge(){ + type = OUTER_EDGE; } edgeType edge::get_type(){ diff --git a/PuzzleSolver/edge.h b/Src/edge.h similarity index 81% rename from PuzzleSolver/edge.h rename to Src/edge.h index 71c6d02..2c9d8e8 100644 --- a/PuzzleSolver/edge.h +++ b/Src/edge.h @@ -10,8 +10,7 @@ #define __PuzzleSolver__edge__ #include -#include - +#include "compat_opencv.h" enum edgeType { OUTER_EDGE, TAB, HOLE }; @@ -27,17 +26,22 @@ class edge{ //to classify the piece. std::vector normalized_contour; std::vector reverse_normalized_contour; + double arc_length; // length of the edge contour + double corner_distance; // straight-line distance between start and end of edge contour template std::vector normalize(std::vector); void classify(); edgeType type; public: edge(); edge(std::vector edge); + std::vector get_contour(); std::vector get_translated_contour(int,int); std::vector get_translated_contour_reverse(int,int); edgeType get_type(); double compare(edge); double compare2(edge); + double compare3(edge); + double compare3(edge, double& cscore, double& escore); std::string edge_type_to_s(); }; diff --git a/Src/guided_match.cpp b/Src/guided_match.cpp new file mode 100644 index 0000000..432712e --- /dev/null +++ b/Src/guided_match.cpp @@ -0,0 +1,414 @@ +#include "opencv2/opencv.hpp" + +#include +#include "compat_opencv.h" +#include "guided_match.h" +#include "utils.h" + +void gm_mouse_callback(int event, int x, int y, int flags, void* userdata); + +std::vector rotate(cv::Point2f center, std::vector cont, double angle) { + + std::vector ret_contour; + angle = angle * M_PI / 180.0; + + for(std::vector::iterator i = cont.begin(); i != cont.end(); i++ ) { + + cv::Point2f t(i->x - center.x, i->y - center.y); + double cosa = std::cos(angle); + double sina = std::sin(angle); + double new_x = cosa * t.x - sina * t.y; + double new_y = sina * t.x + cosa * t.y; + ret_contour.push_back(cv::Point2f((float)(new_x + center.x), (float)(new_y + center.y))); + } + return ret_contour; +} + +class guided_matcher { +public: + piece& p1; + piece& p2; + int e1; + int e2; + bool debug; + + bool pieces; + bool edges; + bool color; + int margin; + + int p2xoff = 0; + int p2yoff = 0; + + + params& user_params; + + float scale_factor; + cv::Mat rendered; + std::string window_name; + + + guided_matcher(piece& p1, piece& p2, int e1, int e2, params& user_params) + : p1(p1), p2(p2), e1(e1), e2(e2), user_params(user_params) + { + debug = false; + color = true; + edges = false; + pieces = true; + margin = 25; + + scale_factor = user_params.getGuiScale(); + + std::stringstream wns; + wns << p1.get_id() << "-" << e1 << " __ " << p2.get_id() << "-" << e2; + window_name = wns.str(); + + + } + + cv::Mat bw_to_color(cv::Mat bw) { + std::vector channels(3); + channels.at(0) = bw; + channels.at(1) = bw; + channels.at(2) = bw; + + cv::Mat color; + cv::merge(channels,color); + return color; + } + + template + cv::Point_ get_top(std::vector>& contour, cv::Point_* bottom = NULL) { + cv::Point f = contour.front(); + cv::Point b = contour.back(); + if (f.y < b.y) { + if (bottom != NULL) { + bottom->x = b.x; + bottom->y = b.y; + } + return f; + } + else { + if (bottom != NULL) { + bottom->x = f.x; + bottom->y = f.y; + } + return b; + } + } + + template + double compute_angle(std::vector> contour) { + cv::Point_ b; + cv::Point_ a = get_top(contour, &b); + cv::Point_ c = cv::Point_(b.x, a.y); + return utils::compute_rotation_angle(a, b, c); + } + + + std::vector render_piece(piece& p, int edge, int extra_rotation, cv::Mat& dst, int xoffset, int yoffset, int padded_dim, cv::Point* align_to) { + cv::Mat orig; + cv::Mat padded; + cv::Mat aligned; + cv::Mat resized; + + int alignx = 0; + int aligny = 0; + + orig = color ? p.full_color : bw_to_color(p.bw); + + + // Add alpha channel and initialize it using the bw image + cv::Mat channels[4]; + cv::split(orig, channels); + p.bw.convertTo(channels[3], channels[0].type()); + cv::merge(channels, 4, orig); + + + // Calculate the translation into the padded area + cv::Point tx((padded_dim - orig.size().width) / 2, (padded_dim - orig.size().height) / 2); + + // Calculate the center of the original + cv::Point center(orig.cols/2.0, orig.rows/2.0); + + + // Compute the rotation required to align the pieces + + // Angle to straighten edge 0 + double angle = compute_angle(p.edges[0].get_contour()); + + // Update the angle to get the selected edge facing the correct direction + // extra_rotation = 0 is used for the piece on the left, extra_rotation=2 is used on the right + angle -= 90.0 * ((edge + extra_rotation) % 4 - 2); + + // Rotate the selected edge contour + std::vector edge_contour = rotate(center, p.edges[edge].get_contour(), -angle); + + // Fine tune the angle + angle += compute_angle(edge_contour); + + + // Rotate and translate the selected edge + edge_contour = rotate(center, p.edges[edge].get_contour(), -angle); + std::vector edge_contour_tx; + utils::translate_contour(edge_contour, edge_contour_tx, tx.x, tx.y, 1.0); + + + // Determine the translation required for alignment + cv::Point top = get_top(edge_contour_tx); + if (align_to != NULL) { + alignx = align_to->x - top.x; + aligny = align_to->y - top.y; + } + + alignx += xoffset; + aligny += yoffset; + + if (pieces) { + // Copy the original into the padded image + padded = cv::Mat::zeros( padded_dim, padded_dim, CV_8UC4); + orig.copyTo(padded(cv::Rect(tx.x, tx.y, orig.size().width, orig.size().height))); + + // Rotate the padded image + cv::Mat r = cv::getRotationMatrix2D(cv::Point2f(padded.cols/2.0, padded.rows/2.0), angle, 1.0); + cv::warpAffine(padded, aligned, r, padded.size()); + + if (aligny != 0) { + cv::Mat out = cv::Mat::zeros(aligned.size(), aligned.type()); + + if (aligny > 0) { + aligned(cv::Rect(0,0, aligned.cols,aligned.rows-aligny)) + .copyTo(out(cv::Rect(0,aligny,aligned.cols,aligned.rows-aligny))); + } else { + aligned(cv::Rect(0,-aligny, aligned.cols,aligned.rows+aligny)) + .copyTo(out(cv::Rect(0,0,aligned.cols,aligned.rows+aligny))); + } + aligned = out; + } + + // Resize + cv::resize(aligned, resized, cv::Size(), scale_factor, scale_factor, cv::INTER_CUBIC); + + // Split to gain access to the padded, rotated, and resized alpha channel + cv::split(resized, channels); + + // Copy using the alpha channel as the mask + cv::Rect crect(alignx*scale_factor, 0, std::min(rendered.size().width, resized.size().width), std::min(rendered.size().height, resized.size().height)); + resized.copyTo(rendered(cv::Rect(crect)), channels[3]); + } + + // Render edge contour in red + if (edges) { + std::vector> edge_contours; + edge_contours.push_back(utils::translate_contour(edge_contour_tx, alignx, aligny, scale_factor)); + cv::polylines(rendered, edge_contours, false, cv::Scalar(0, 0, 255), 2); + } + + return edge_contour_tx; + } + + + void render() { + + int maxdim = std::max(std::max(std::max( + p1.full_color.size().width, + p1.full_color.size().height), + p2.full_color.size().width), + p2.full_color.size().height); + + int width = maxdim * 2 + margin * 4; + + int height = maxdim + margin * 2; + + int padded_dim = maxdim + margin * 2; + + width *= scale_factor; + width += (width % 2) + 2; + height *= scale_factor; + height += (height % 2) + 2; + + rendered = cv::Mat::zeros(height, width, CV_8UC4 ); + + std::vector txc = render_piece(p1, e1, 0, rendered, 0, 0, padded_dim, NULL); + + cv::Point top = get_top(txc); + render_piece(p2, e2, 2, rendered, p2xoff, p2yoff, padded_dim, &top); + + std::stringstream title_stream; + title_stream << "Does piece " << p1.get_number() + << " fit to " << p2.get_number() + << " ? "; + cv::putText(rendered, title_stream.str(), cv::Point(25,25), + cv::FONT_HERSHEY_COMPLEX_SMALL, 0.8, cv::Scalar(0, 255, 255), 1, COMPAT_CV_LINE_AA); + + + cv::imshow(window_name, rendered); + } + + void adjust_scale( float adjustment) { + float new_scale = scale_factor + adjustment; + if (new_scale < 0.25) { + new_scale = 0.25; + } + if (new_scale == scale_factor) { + return; + } + scale_factor = new_scale; + render(); +// std::cout << "Scale factor is now " << scale_factor << std::endl; + } + + std::string edit() { + + + cv::namedWindow(window_name); + cv::setMouseCallback(window_name, gm_mouse_callback, this); + + render(); + + std::string command; + + bool done = false; + do { + int c = cv::waitKey(0); + // std::cout << c << std::endl; + switch (c) { + case -1: // User probably clicked on the "x" in the window title bar + done = true; + command = GM_COMMAND_X_CLOSE; + break; + case 'n': + done = true; + command = GM_COMMAND_NO; + break; + case 'y': + done = true; + command = GM_COMMAND_YES; + break; + case 's': + done = true; + command = GM_COMMAND_SHOW_SET; + break; + case 'r': + done = true; + command = GM_COMMAND_SHOW_ROTATION; + break; + case 'w': + done = true; + command = GM_COMMAND_WORK_ON_SET; + break; + case 'b': + done = true; + command = GM_COMMAND_MARK_BOUNDARY; + break; + case 'c': + color = !color; + render(); + break; + case 'e': + case 'p': + pieces = !pieces; + edges = !pieces; + render(); + break; + case 'x': + p2xoff += 1; + render(); + break; + case 'X': + p2xoff -= 1; + render(); + break; + case 'z': + p2yoff -= 1; + render(); + break; + case 'Z': + p2yoff += 1; + render(); + break; + case '-': + adjust_scale(-0.25); + break; + case '+': + case '=': + adjust_scale(0.25); + break; + case 'h': + case '?': + std::cout << "Keyboard commands:" << std::endl + << "KEY MEANING" << std::endl + << "n no, this is not a valid match" << std::endl + << "y yes, this is a valid match" << std::endl + << "c toggle between color and black and white" << std::endl + << "e toggle between showing the pieces and the matched edge outlines" << std::endl + << "p same as pressing key 'e'" << std::endl + << "-/= decrease/increase the rendering scale of the pieces" << std::endl + << "x/X adjust the x-offset between the matched pieces" << std::endl + << "z/Z adjust the y-offset between the matched pieces" << std::endl + << "s show the pieces in the current collection set" << std::endl + << "r show the piece rotations of the current collection set" << std::endl + << "w work on a different collection set" << std::endl + << "b mark the right edge of the left-hand piece as a boundary" << std::endl + ; + break; + default: + break; + } + } + while (!done); + try { + cv::destroyWindow(window_name); + } + catch (cv::Exception x) { + // Ignore + } + + return command; + } + + + void mouse_down(int x, int y) { + } + + void mouse_move(int x, int y) { + } + + void mouse_up(int x, int y) { + + } + +private: + +}; + +void gm_mouse_callback(int event, int x, int y, int flags, void* userdata) { + guided_matcher* data = (guided_matcher*)userdata; + + if ( event == cv::EVENT_LBUTTONDOWN ) + { + if (data->debug) { + std::cout << "Left mouse down - position (" << x << ", " << y << ")" << std::endl; + } + data->mouse_down(x, y); + } + else if ( event == cv::EVENT_LBUTTONUP ) + { + if (data->debug) { + std::cout << "Left mouse up - position (" << x << ", " << y << ")" << std::endl; + } + data->mouse_up(x, y); + } + else if ( event == cv::EVENT_MOUSEMOVE ) + { + data->mouse_move(x, y); + // std::cout << "Mouse move - position (" << x << ", " << y << ")" << std::endl; + } +} + + +std::string guided_match(piece& p1, piece& p2, int e1, int e2, params& user_params) { + guided_matcher editor(p1, p2, e1, e2, user_params); + return editor.edit(); +} + diff --git a/Src/guided_match.h b/Src/guided_match.h new file mode 100644 index 0000000..65e9202 --- /dev/null +++ b/Src/guided_match.h @@ -0,0 +1,29 @@ +#ifndef GUIDED_MATCH_H +#define GUIDED_MATCH_H + +#include "piece.h" +#include "params.h" + +#define GM_COMMAND_X_CLOSE "x_close" +#define GM_COMMAND_NO "no" +#define GM_COMMAND_YES "yes" +#define GM_COMMAND_SHOW_SET "show_set" +#define GM_COMMAND_SHOW_ROTATION "show_rotation" +#define GM_COMMAND_WORK_ON_SET "work_on_set" +#define GM_COMMAND_MARK_BOUNDARY "mark_boundary" + +/** @brief Display the guided match GUI for the given pieces/edges and waits for user input from the keyboard. + * + * Returns a string value depending on which key the user pressed, see the #define's above for the possible values. + * If the user presses the 'h' or '?' key, then usage information is displayed in the console output. + +@param p1 A puzzle piece object +@param p2 Another puzzle piece object +@param e1 The edge number of p1 +@param e2 The edge number of p2 +@param user_params The user params object +*/ +std::string guided_match(piece& p1, piece& p2, int e1, int e2, params& user_params); + +#endif /* GUIDED_MATCH_H */ + diff --git a/Src/image_viewer.cpp b/Src/image_viewer.cpp new file mode 100644 index 0000000..6f35feb --- /dev/null +++ b/Src/image_viewer.cpp @@ -0,0 +1,127 @@ +#include "image_viewer.h" + + +#if OPENCV_VERSION_MAJOR == 2 +// Mimic cv::rotate(src,dest,rotation_code) which is available starting in OpenCV 3.x +void cv_rotate(const cv::Mat& image, cv::Mat& dest, int rotation_code) +{ + switch (rotation_code) { + case ROTATE_90_CLOCKWISE: + cv::flip(image.t(), dest, 1); + break; + case ROTATE_180: + cv::flip(image, dest, -1); + break; + case ROTATE_90_COUNTERCLOCKWISE: + cv::flip(image.t(), dest, 0); + break; + default: + dest = image.clone(); + break; + } +} +#endif + +// Initial width, the window is resizable +#define INITIAL_IMAGE_WINDOW_WIDTH 768 + +// Show one or more images in a GUI window +class image_viewer { +public: + std::string window_name; + std::vector images; + cv::Mat rendered; + int rotation_code; + int img_index; + + + image_viewer(std::string window_name, std::vector images) { + this->window_name = window_name; + this->images = images; + this->rotation_code = 3; // 0 = 90, 1 = 180, 2 = 270, 3 = 0 + this->img_index = 0; + } + + void render() { + if (rotation_code == 3) { + rendered = images[img_index].clone(); + } else { + compat_cv_rotate(images[img_index], rendered, rotation_code); + } + cv::imshow(window_name, rendered); + } + + void view() { + + float aspect = (float)images[0].size().width / images[0].size().height; + + int height = INITIAL_IMAGE_WINDOW_WIDTH / aspect; + + cv::namedWindow(window_name, cv::WINDOW_NORMAL); + cv::resizeWindow(window_name, INITIAL_IMAGE_WINDOW_WIDTH, height); + + render(); + + bool done = false; + do { + int c = cv::waitKey(0); + // std::cout << c << std::endl; + switch (c) { + case -1: // User probably clicked on the "x" in the window title bar + case 13: // Return key + case 141: // Enter key + case 'n': // "next" + case 'q': // "quit" + done = true; + break; + case 'r': // "rotate" + rotation_code = (rotation_code + 1) % 4; + render(); + break; + case 'R': // "rotate" + rotation_code = (rotation_code + 3) % 4; + render(); + break; + case 't': // "toggle" + img_index = (img_index + 1) % images.size(); + render(); + break; + case 'T': // "toggle" + img_index = (img_index + images.size() - 1) % images.size(); + render(); + default: + break; + } + } + while (!done); + try { + cv::destroyWindow(window_name); + } + catch (cv::Exception x) { + // Ignore + } + + + } + +}; + +void show_image(std::string window_name, cv::Mat& image1) { + + std::vector images; + images.push_back(image1); + image_viewer viewer(window_name, images); + viewer.view(); +} + +void show_images(std::string window_name, cv::Mat& image1, cv::Mat& image2) { + std::vector images; + images.push_back(image1); + images.push_back(image2); + image_viewer viewer(window_name, images); + viewer.view(); +} + + + + diff --git a/Src/image_viewer.h b/Src/image_viewer.h new file mode 100644 index 0000000..5fcfc1f --- /dev/null +++ b/Src/image_viewer.h @@ -0,0 +1,29 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +/* + * File: image_viewer.h + * Author: kellinwood + * + * Created on April 4, 2019, 6:43 PM + */ + +#ifndef IMAGE_VIEWER_H +#define IMAGE_VIEWER_H + + +#include +#include "compat_opencv.h" + +// Show the image in a GUI window +void show_image(std::string window_name, cv::Mat& image1); + +// Show and allow the user to switch between two images. +void show_images(std::string window_name, cv::Mat& image1, cv::Mat& image2); + + +#endif /* IMAGE_VIEWER_H */ + diff --git a/Src/logger.cpp b/Src/logger.cpp new file mode 100755 index 0000000..aa35bd3 --- /dev/null +++ b/Src/logger.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include "logger.h" + +logger::logger() { + ofstream = NULL; +} + +void logger::set_filename(std::string filename) { + logfilename = filename; + ofstream = new std::ofstream(); + ofstream->open(filename, std::ofstream::out | std::ofstream::app); + if (ofstream->fail()) { + std::cerr << "Failed to open " << filename << " for writing" << std::endl; + ofstream = NULL; + } +} + +std::ostringstream& logger::_stream() { + return os; +} + +void logger::_flush() { + std::string message = os.str(); + std::cout << message << std::flush; + if (ofstream != NULL) { + *ofstream << message << std::flush; + } + os.str(""); + os.clear(); +} + +logger& logger::get_instance() +{ + static logger instance; + return instance; +} + +void logger::filename(std::string filename) { + get_instance().set_filename(filename); +} + +std::ostringstream& logger::stream() { + return get_instance()._stream(); +} + +void logger::flush() { + get_instance()._flush(); +} + +logger::~logger() { + _flush(); + if (ofstream != NULL) { + ofstream->close(); + } +} + +/* +int main(int argc, char* argv[]) { + logger::stream() << "Hello, world!" << std::endl; + logger::flush(); + logger::filename("output.log"); + logger::stream() << "Foo, bar!" << std::endl; +} +*/ diff --git a/Src/logger.h b/Src/logger.h new file mode 100755 index 0000000..a184b98 --- /dev/null +++ b/Src/logger.h @@ -0,0 +1,28 @@ +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include + +class logger +{ +private: + std::ostringstream os; + std::string logfilename; + std::ofstream* ofstream; + logger(); + void set_filename(std::string filename); + std::ostringstream& _stream(); + void _flush(); +public: + static logger& get_instance(); + static void filename(std::string filename); + static std::ostringstream& stream(); + static void flush(); + logger(logger const&) = delete; + void operator=(logger const&) = delete; + virtual ~logger(); +}; + +#endif /* LOGGER_H */ diff --git a/Src/main.cpp b/Src/main.cpp new file mode 100644 index 0000000..43fccab --- /dev/null +++ b/Src/main.cpp @@ -0,0 +1,271 @@ +// +// main.cpp +// PuzzleSolver +// +// Created by Joe Zeimen on 4/4/13. +// Copyright (c) 2013 Joe Zeimen. All rights reserved. +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cxxopts.hpp" +#include "logger.h" +#include "params.h" +#include "puzzle.h" +#include "utils.h" +#include "contours.h" +#include "config.h" + + +class demo { +public: + std::string name; + std::string inputDir; + int estimated_piece_size; + int threshold; + bool filter; + std::string comment; + + demo(std::string name, std::string inputDir, int estimated_piece_size, int threshold, bool filter, std::string comment) : + name(name), inputDir(inputDir), estimated_piece_size(estimated_piece_size), + threshold(threshold), filter(filter), comment(comment) + { + } + +}; + +void register_demo(std::map &demos, std::string name, std::string inputDir, int estimated_piece_size, int threshold, bool filter, std::string comment) +{ + demo* demoptr = new demo(name, inputDir, estimated_piece_size, threshold, filter, comment); + demos[demoptr->name] = demoptr; +} + +void demo_help(std::map &demos) +{ + std::cout << std::setw(24) << std::left << "DEMO NAME" << " COMMENT" << std::endl; + for (std::map::iterator it=demos.begin(); it!=demos.end(); ++it) { + demo* d = it->second; + std::cout << std::setw(24) << std::left << d->name << " : " << d->comment << std::endl; + } +} + +int main(int argc, char * argv[]) +{ + params user_params; + + std::map demos; + register_demo(demos, "toy-story-color", "Toy Story", 200, 22, true, "48 pieces, --estimated-size 200 --threshold 22 --filter"); + register_demo(demos, "toy-story-back", "Toy Story back", 200, 50, false, "48 pieces, --estimated-size 200 --threshold 50"); + register_demo(demos, "angry-birds-color", "Angry Birds/color", 300, 30, false, "24 pieces, --estimated-size 300 --threshold 30"); + register_demo(demos, "angry-birds-scanner-open", "Angry Birds/Scanner Open", 300, 30, false, "24 pieces, --estimated-size 300 --threshold 30"); + register_demo(demos, "horses", "horses", 380, 50, false, "104 pieces, --estimated-size 380 --threshold 50"); + register_demo(demos, "horses-numbered", "horses numbered", 380, 50, false, "104 pieces, --estimated-size 380 --threshold 50"); + + cxxopts::Options options("PuzzleSolver", std::string("Version: ") + std::string(VERSION) + std::string("\nSolve jigsaw puzzles using the shapes of the piece edges.\n")); + + options.add_options() + ("v,verbose", "Verbose output", cxxopts::value()->default_value("false")) + ("h,help", "Display this help message") + ("s,solve", "Solve the puzzle after processing the input images and extracting pieces and edges", cxxopts::value()->default_value("false")) + ("g,guided", "Enable interactive solution mode", cxxopts::value()->default_value("false")) + ("w,work-on", "In guided solution mode, start solving with this piece number", cxxopts::value()->default_value("-1")) + ("dont-solve", "Skip finding the solution (e.g., for a demo which normally implies --solve)", cxxopts::value()->default_value("false")) + ("n,solution-name", "Basename for solution text/image/log files written to the output directory", cxxopts::value()->default_value("solution")) + ("e,estimated-size", "Estimated piece size", cxxopts::value()->default_value("200")) + ("t,threshold", "Threshold value used when converting color images to b&w. Min: 0, max: 255.", cxxopts::value()->default_value("30")) + ("f,filter", "Use filter() instead of median_filter()", cxxopts::value()->default_value("false")) + ("m,median-blur-ksize", "Median blur ksize value. Must be odd and greater than 1, e.g.: 3, 5, 7 ...", cxxopts::value()->default_value("5")) + ("r,verify-contours", "Show the contours found in each input image", cxxopts::value()->default_value("false")) + ("i,initial-piece-id", "Identify pieces starting with this number", cxxopts::value()->default_value("1")) + ("o,order", "Order of pieces in the input images", cxxopts::value()->default_value("lrtb")) + ("p,partition", "Piece-ordering partition factor for adjusting behavior of --order", cxxopts::value()->default_value("1.0")) + ("b,corners-blocksize", "Block size to use when finding corners", cxxopts::value()->default_value("25")) + ("c,corners-quality", "Corner quality warning threshold", cxxopts::value()->default_value("300")) + ("a,adjust-corners","Show GUI corner adjuster for each piece where its corner quality exceeds the corners quality threshold", cxxopts::value()->default_value("false")) + ("l,scale","Scale factor for images shown in GUI windows", cxxopts::value()->default_value("1.0")) + ("cscore-limit","Limit of cscore values auto accepted as matches", cxxopts::value()->default_value("125.0")) + ("escore-limit","Limit of escore values auto accepted as matches", cxxopts::value()->default_value("4000.0")) + ("save-all", "Save all images (originals, contours, b&w, color, corners, edges)", cxxopts::value()->default_value("false")) + ("save-originals", "Save original images", cxxopts::value()->default_value("false")) + ("save-contours", "Save contour images", cxxopts::value()->default_value("false")) + ("save-bw", "Save black&white piece images", cxxopts::value()->default_value("false")) + ("save-color", "Save color piece images", cxxopts::value()->default_value("false")) + ("save-corners", "Save piece images showing corner locations", cxxopts::value()->default_value("false")) + ("save-edges", "Save images for each piece edge", cxxopts::value()->default_value("false")) + ("save-matches", "Save images for each pair of matched edges (in auto solve mode only)", cxxopts::value()->default_value("false")) + ("d,demo","Solve a named demo puzzle. See below for a list of demos.", cxxopts::value()) + + ("positional","Positional parameters", cxxopts::value>()) + ; + + options.parse_positional({"positional"}); + options.positional_help(" "); + auto result = options.parse(argc, argv); + + if (result.count("help")) + { + std::cout << options.help({"", "Group"}) << std::endl; + demo_help(demos); + exit(0); + } + + bool is_demo = result.count("demo") > 0; + if (is_demo) { + std::string demo_name = result["demo"].as(); + std::map::iterator it = demos.find(demo_name); + if (it == demos.end()) { + std::cout << "ERROR: Unknown demo name: " << demo_name << std::endl; + std::cout << "Use --help to get the list of demo names" << std::endl; + exit(1); + } + demo* demoptr = it->second; + + // Resolve the PuzzleSolver home dir (parent of 'Scans'), by assuming the PuzzleSolver exe file is in the source directory. + char* dirnamebuf = realpath(argv[0], NULL); + char* puzzleSolverHome=dirname(dirname(dirnamebuf)); + user_params.setInputDir(std::string(puzzleSolverHome) + "/Scans/" + demoptr->inputDir); + free(dirnamebuf); + user_params.setOutputDir("/tmp/"+demoptr->name); + user_params.setSolving(true); + user_params.setEstimatedPieceSize(demoptr->estimated_piece_size); + user_params.setThreshold(demoptr->threshold); + user_params.setUsingMedianFilter(!demoptr->filter); + + // Allow some demo default values to be explicity overridden + if (result.count("estimated-size")) { + user_params.setEstimatedPieceSize(result["estimated-size"].as()); + } + if (result.count("threshold")) { + user_params.setThreshold(result["threshold"].as()); + } + if (result.count("filter")) { + user_params.setUsingMedianFilter(!result["filter"].as()); + } + } + else { + if (result.count("positional") != 2) + { + std::cout << "ERROR: check positional args" << std::endl << std::endl; + std::cout << options.help({"", "Group"}) << std::endl; + exit(1); + } + auto& positional = result["positional"].as>(); + user_params.setInputDir(positional[0]); + user_params.setOutputDir(positional[1]); + user_params.setSolving(result["solve"].as()); + user_params.setEstimatedPieceSize(result["estimated-size"].as()); + user_params.setThreshold(result["threshold"].as()); + user_params.setUsingMedianFilter(!result["filter"].as()); + } + + std::string order = result["order"].as(); + if (piece_order::lookup(order) == NULL) { + std::cout << "ERROR: Order '" << order << "' is invalid, expected one of: "; + std::vector nv; + piece_order::names(nv); + for (int i = 0; i < nv.size(); i++) { + std::cout << nv[i]; + if (i < (nv.size() - 1)) { + std::cout << ", "; + } + } + std::cout << std::endl; + + exit(1); + } + + bool guided = result["guided"].as(); + user_params.setGuidedSolution(guided); + if (guided) { + user_params.setSolving(true); + } + if (result.count("dont-solve")) { + user_params.setSolving(false); + } + user_params.setVerbose(result["verbose"].as()); + user_params.setSolutionFileBasename(result["solution-name"].as()); + user_params.setWorkOnPiece(result["work-on"].as()); + user_params.setMedianBlurKSize(result["median-blur-ksize"].as()); + user_params.setPieceOrder(result["order"].as()); + user_params.setInitialPieceId(result["initial-piece-id"].as()); + user_params.setPartitionFactor(result["partition"].as()); + user_params.setFindCornersBlockSize(result["corners-blocksize"].as()); + user_params.setMinCornersQuality(result["corners-quality"].as()); + user_params.setAdjustingCorners(result["adjust-corners"].as()); + user_params.setGuiScale(result["scale"].as()); + user_params.setCscoreLimit(result["cscore-limit"].as()); + user_params.setEscoreLimit(result["escore-limit"].as()); + user_params.setVerifyingContours(result["verify-contours"].as()); + user_params.setSaveAll(result["save-all"].as()); + user_params.setSavingOriginals(result["save-originals"].as()); + user_params.setSavingContours(result["save-contours"].as()); + user_params.setSavingBlackWhite(result["save-bw"].as()); + user_params.setSavingColor(result["save-color"].as()); + user_params.setSavingCorners(result["save-corners"].as()); + user_params.setSavingEdges(result["save-edges"].as()); + user_params.setSavingMatches(result["save-matches"].as()); + + + mkdir(user_params.getOutputDir().c_str(), 0775); + + std::stringstream logfilename; + logfilename << user_params.getOutputDir() << user_params.getSolutionFileBasename() << ".log"; + logger::filename(logfilename.str()); + + logger::stream() << user_params.to_string() << std::endl; logger::flush(); + logger::stream() << "Starting..." << std::endl; logger::flush(); + timeval time; + gettimeofday(&time, NULL); + long millis = (time.tv_sec * 1000) + (time.tv_usec / 1000); + long inbetween_millis = millis; + + + puzzle puzzle(user_params); + + gettimeofday(&time, NULL); + logger::stream() << std::endl << "time to initialize:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-inbetween_millis)/1000.0 << std::endl; + logger::flush(); + inbetween_millis = ((time.tv_sec * 1000) + (time.tv_usec / 1000)); + + if (!user_params.isSolving()) { + return 0; + } + + logger::stream() << "Finding edge costs..." << std::endl; + logger::flush(); + puzzle.fill_costs(); + gettimeofday(&time, NULL); + logger::stream() << std::endl << "time to fill edge costs:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-inbetween_millis)/1000.0 << std::endl; + logger::flush(); + inbetween_millis = ((time.tv_sec * 1000) + (time.tv_usec / 1000)); + + puzzle.solve(); + gettimeofday(&time, NULL); + logger::stream() << std::endl << "time to solve:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-inbetween_millis)/1000.0 << std::endl; + logger::flush(); + inbetween_millis = ((time.tv_sec * 1000) + (time.tv_usec / 1000)); + puzzle.save_solution_text(); + puzzle.save_solution_image(); + gettimeofday(&time, NULL); + logger::stream() << std::endl << "Time to draw:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-inbetween_millis)/1000.0 << std::endl; + logger::flush(); + + + gettimeofday(&time, NULL); + logger::stream() << std::endl << "total time:" << (((time.tv_sec * 1000) + (time.tv_usec / 1000))-millis)/1000.0 << std::endl; + logger::flush(); + + puzzle.show_solution_image(); + + return 0; +} + + diff --git a/Src/params.cpp b/Src/params.cpp new file mode 100644 index 0000000..e2d1ecf --- /dev/null +++ b/Src/params.cpp @@ -0,0 +1,284 @@ +/* + * params.cpp + * + */ + +#include "params.h" + +#include + +params::params() { +} + +bool params::isSolving() const { + return solving; +} + +void params::setSolving(bool solving) { + this->solving = solving; +} + +bool params::isGuidedSolution() const { + return guidedSolution; +} + +void params::setGuidedSolution(bool guidedSolution) { + this->guidedSolution = guidedSolution; +} + +uint params::getEstimatedPieceSize() const { + return estimatedPieceSize; +} + +void params::setEstimatedPieceSize(uint estimatedPieceSize) { + this->estimatedPieceSize = estimatedPieceSize; +} + +std::string params::getPieceOrder() const { + return pieceOrder; +} + +void params::setPieceOrder(std::string pieceOrder) { + this->pieceOrder = pieceOrder; +} + +uint params::getInitialPieceId() const { + return initialPieceId; +} + +void params::setInitialPieceId(uint initialPieceId) { + this->initialPieceId = initialPieceId; +} + +void params::setSaveAll(bool saveAll) +{ + this->saveAll = saveAll; +} + +bool params::isSavingOriginals() const { + return savingOriginals || saveAll; +} + +void params::setSavingOriginals(bool savingOriginals) { + this->savingOriginals = savingOriginals; +} + +bool params::isSavingBlackWhite() const { + return savingBlackWhite || saveAll; +} + +void params::setSavingBlackWhite(bool savingBlackWhite) { + this->savingBlackWhite = savingBlackWhite; +} + +bool params::isSavingColor() const { + return savingColor || saveAll; +} + +void params::setSavingColor(bool savingColor) { + this->savingColor = savingColor; +} + +bool params::isSavingContours() const { + return savingContours || saveAll; +} + +void params::setSavingContours(bool savingContours) { + this->savingContours = savingContours; +} + +bool params::isSavingCorners() const { + return savingCorners || saveAll; +} + +void params::setSavingCorners(bool savingCorners) { + this->savingCorners = savingCorners; +} + +bool params::isSavingEdges() const { + return savingEdges || saveAll; +} + +void params::setSavingEdges(bool savingEdges) { + this->savingEdges = savingEdges; +} + +bool params::isSavingMatches() const { + return savingMatches || saveAll; +} + +void params::setSavingMatches(bool savingMatches) { + this->savingMatches = savingMatches; +} + +std::string params::getInputDir() const { + return inputDir; +} + +void params::setInputDir(std::string inputDir) { + if (inputDir.back() != '/') inputDir = inputDir + "/"; + this->inputDir = inputDir; +} + +bool params::isVerbose() const { + return verbose; +} + +void params::setVerbose(bool Verbose) { + verbose = Verbose; +} + +std::string params::getOutputDir() const { + return outputDir; +} + +void params::setOutputDir(std::string outputDir) { + if (outputDir.back() != '/') outputDir = outputDir + "/"; + this->outputDir = outputDir; +} + +float params::getPartitionFactor() const { + return partitionFactor; +} + +void params::setPartitionFactor(float partitionFactor) { + this->partitionFactor = partitionFactor; +} + +std::string params::getSolutionFileBasename() const { + return solutionFileBasename; +} + +void params::setSolutionFileBasename(std::string solutionImageFilename) { + this->solutionFileBasename = solutionImageFilename; +} + +uint params::getThreshold() const { + return threshold; +} + +void params::setThreshold(uint threshold) { + this->threshold = threshold; +} + +bool params::isUsingMedianFilter() const { + return useMedianFilter; +} + +void params::setUsingMedianFilter(bool useMedianFilter) { + this->useMedianFilter = useMedianFilter; +} + +uint params::getMedianBlurKSize() const { + return medianBlurKSize; +} + +void params::setMedianBlurKSize(uint medianBlurKSize) { + this->medianBlurKSize = medianBlurKSize; +} + +uint params::getFindCornersBlockSize() const { + return findCornersBlockSize; +} + +void params::setFindCornersBlockSize(uint findCornersBlockSize) { + this->findCornersBlockSize = findCornersBlockSize; +} + +uint params::getMinCornersQuality() const { + return minCornersQuality; +} + +void params::setMinCornersQuality(uint minCornersQuality) { + this->minCornersQuality = minCornersQuality; +} + +float params::getGuiScale() const { + return guiScale; +} + +void params::setGuiScale(float cornerEditorScale) { + this->guiScale = cornerEditorScale; +} + +bool params::isAdjustingCorners() const { + return editingCorners; +} + +void params::setAdjustingCorners(bool editingCorners) { + this->editingCorners = editingCorners; +} + +float params::getCscoreLimit() const { + return cscoreLimit; +} + +void params::setCscoreLimit(float cscoreLimit) { + this->cscoreLimit = cscoreLimit; +} + +float params::getEscoreLimit() const { + return escoreLimit; +} + +void params::setEscoreLimit(float escoreLimit) { + this->escoreLimit = escoreLimit; +} + +int params::getWorkOnPiece() const { + return workOnPiece; +} + +void params::setWorkOnPiece(int workOnPiece) { + this->workOnPiece = workOnPiece; +} + +bool params::isVerifyingContours() const { + return verifyingContours; +} + +void params::setVerifyingContours(bool verifyingContours) { + this->verifyingContours = verifyingContours; +} + +inline const char * const bool_to_string(bool b) +{ + return b ? "true" : "false"; +} + +std::string params::to_string() const { + std::stringstream stream; + stream << "verbose ................ " << bool_to_string(this->isVerbose()) << std::endl; + stream << "input images dir ....... " << this->getInputDir() << std::endl; + stream << "output dir ............. " << this->getOutputDir() << std::endl; + stream << "solution name .......... " << this->getSolutionFileBasename() << std::endl; + stream << "solve puzzle ........... " << bool_to_string(this->isSolving()) << std::endl; + stream << "guided solution mode ... " << bool_to_string(this->isGuidedSolution()) << std::endl; + stream << "work on piece .......... " << this->getWorkOnPiece() << std::endl; + stream << "estimated piece size ... " << this->getEstimatedPieceSize() << std::endl; + stream << "threshold .............. " << this->getThreshold() << std::endl; + stream << "median filter .......... " << bool_to_string(this->isUsingMedianFilter()) << std::endl; + stream << "median blur ksize ...... " << (this->getMedianBlurKSize()) << std::endl; + stream << "piece order ............ " << this->getPieceOrder() << std::endl; + stream << "partition factor ....... " << this->getPartitionFactor() << std::endl; + stream << "find corners block size " << this->getFindCornersBlockSize() << std::endl; + stream << "min corners quality .... " << this->getMinCornersQuality() << std::endl; + stream << "adjust corners ......... " << bool_to_string(this->isAdjustingCorners()) << std::endl; + stream << "gui scale .............. " << this->getGuiScale() << std::endl; + stream << "cscore limit ........... " << this->getCscoreLimit() << std::endl; + stream << "escore limit ........... " << this->getEscoreLimit() << std::endl; + stream << "verify contours ........ " << bool_to_string(this->isVerifyingContours()) << std::endl; + stream << "save original images ... " << bool_to_string(this->isSavingOriginals()) << std::endl; + stream << "save contour images .... " << bool_to_string(this->isSavingContours()) << std::endl; + stream << "save b&w images ........ " << bool_to_string(this->isSavingBlackWhite()) << std::endl; + stream << "save color images ...... " << bool_to_string(this->isSavingColor()) << std::endl; + stream << "save corner images ..... " << bool_to_string(this->isSavingCorners()) << std::endl; + stream << "save edge images ....... " << bool_to_string(this->isSavingEdges()) << std::endl; + stream << "save matched edges ..... " << bool_to_string(this->isSavingMatches()) << std::endl; + return stream.str(); +} + +params::~params() { +} + + diff --git a/Src/params.h b/Src/params.h new file mode 100644 index 0000000..fe69539 --- /dev/null +++ b/Src/params.h @@ -0,0 +1,169 @@ +/* + * params.hpp + * + */ + +#ifndef PARAMS_H_ +#define PARAMS_H_ + +#include +#include + +class params { +private: + bool solving; + bool guidedSolution; + bool verbose; + std::string inputDir; + std::string outputDir; + std::string solutionFileBasename; + std::string pieceOrder; + uint initialPieceId; + uint estimatedPieceSize; + uint threshold; + bool useMedianFilter; + uint medianBlurKSize; + float partitionFactor; + uint minCornersQuality; + bool saveAll; + bool savingOriginals; + bool savingContours; + bool savingBlackWhite; + bool savingColor; + bool savingCorners; + bool savingEdges; + bool savingMatches; + uint findCornersBlockSize; + bool editingCorners; + float guiScale; + float cscoreLimit; + float escoreLimit; + int workOnPiece; + bool verifyingContours; + +public: + params(); + + bool isSolving() const; + + void setSolving(bool solving); + + bool isGuidedSolution() const; + + void setGuidedSolution(bool guidedSolution); + + bool isVerbose() const; + + void setVerbose(bool Verbose); + + std::string getInputDir() const; + + void setInputDir(std::string inputDir); + + std::string getOutputDir() const; + + void setOutputDir(std::string outputDir); + + std::string getSolutionFileBasename() const; + + void setSolutionFileBasename(std::string solutionImageFilename); + + uint getInitialPieceId() const; + + void setInitialPieceId(uint initialPieceId); + + std::string getPieceOrder() const; + + void setPieceOrder(std::string pieceOrder); + + uint getEstimatedPieceSize() const; + + void setEstimatedPieceSize(uint estimatedPieceSize); + + float getPartitionFactor() const; + + void setPartitionFactor(float partitionFactor); + + uint getThreshold() const; + + void setThreshold(uint threshold); + + bool isUsingMedianFilter() const; + + void setUsingMedianFilter(bool useMedianFilter); + + uint getMedianBlurKSize() const; + + void setMedianBlurKSize(uint medianBlurKSize); + + uint getMinCornersQuality() const; + + void setMinCornersQuality(uint minCornersQuality); + + void setSaveAll(bool writeAll); + + bool isSavingOriginals() const; + + void setSavingOriginals(bool savingOriginals); + + bool isSavingBlackWhite() const; + + void setSavingBlackWhite(bool savingBlackWhite); + + bool isSavingColor() const; + + void setSavingColor(bool savingColor); + + bool isSavingContours() const; + + void setSavingContours(bool savingContours); + + bool isSavingCorners() const; + + void setSavingCorners(bool savingCorners); + + bool isSavingEdges() const; + + void setSavingEdges(bool savingEdges); + + bool isSavingMatches() const; + + void setSavingMatches(bool savingMatches); + + uint getFindCornersBlockSize() const; + + void setFindCornersBlockSize(uint findCornersBlockSize); + + float getGuiScale() const; + + void setGuiScale(float cornerEditorScale); + + bool isAdjustingCorners() const; + + void setAdjustingCorners(bool editingCorners); + + float getCscoreLimit() const; + + void setCscoreLimit(float cscoreLimit); + + float getEscoreLimit() const; + + void setEscoreLimit(float escoreLimit); + + int getWorkOnPiece() const; + + void setWorkOnPiece(int workOnPiece); + + bool isVerifyingContours() const; + + void setVerifyingContours(bool verifyingContours); + + std::string to_string() const; + + virtual ~params(); + + + +}; + +#endif /* PARAMS_H_ */ diff --git a/Src/piece.cpp b/Src/piece.cpp new file mode 100644 index 0000000..4fa598f --- /dev/null +++ b/Src/piece.cpp @@ -0,0 +1,383 @@ +// +// piece.cpp +// PuzzleSolver +// +// Created by Joe Zeimen on 4/5/13. +// Copyright (c) 2013 Joe Zeimen. All rights reserved. +// + +#include "piece.h" +#include "adjust_corners.h" + +#include +#include +#include + +#include "compat_opencv.h" +#include "edge.h" +#include "utils.h" +#include "logger.h" + +//This function takes in the beginning and ending of one vector, and returns +//an iterator representing the point where the first item in the second vector is. +std::vector::iterator find_first_in(std::vector::iterator begin, std::vector::iterator end, const std::vector &v){ + for(; begin != end; begin++){ + for(std::vector::const_iterator i = v.begin(); i!=v.end(); i++){ + if(begin->x == i->x && begin->y == i->y) return begin; + } + } + return end; +} + +//This returns iterators from the first vector where the value is equal places in the second vector. +std::vector::iterator> find_all_in(std::vector::iterator begin, std::vector::iterator end, const std::vector &v){ + + std::vector::iterator> places; + for(; begin != end; begin++){ + for(std::vector::const_iterator i = v.begin(); i!=v.end(); i++){ + if(begin->x == i->x && begin->y == i->y) places.push_back(begin); + } + } + return places; +} + + + +piece::piece(uint piece_number, std::string id, cv::Mat color, cv::Mat black_and_white, params& _user_params) : user_params(_user_params) { + this->piece_number = piece_number; + this->id = id; + this->full_color = color; + this->bw = black_and_white; +} + +uint piece::get_number() { + return piece_number; +} + +std::string piece::get_id() { + return id; +} + +void piece::process(){ + find_corners(); + extract_edges(); + classify(); +} + + +// compute the total distance traveling from 0 to index1, index2, index3 +template +double ts_distance(std::vector> corners, uint index1, uint index2, uint index3) { + + double result = utils::distance(corners, 0, index1); + result += utils::distance(corners, index1, index2); + result += utils::distance(corners, index2, index3); + result += utils::distance(corners, index3, 0); + return result; +} + +// Produce a metric that determines the quality of the piece corners. This function +// assumes 4 corners and returns 0 for a perfect rectangle, and higher values for +// shapes that are less like a rectangle. +template +double compute_corners_quality(std::vector> corners) { + + if (corners.size() != 4) { + return 2000.0 * std::fabs(corners.size() - 4); + } + + // order the corners using a simplified shortest path algorithm. We just + // need the points in clockwise or counter-clockwise order starting anywhere. + std::vector> cpoints; // corners ordered by shortest path + cpoints.push_back(corners[0]); + + double tsd0 = ts_distance( corners, 1, 2, 3); + double tsd1 = ts_distance( corners, 2, 1, 3); + double tsd2 = ts_distance( corners, 1, 3, 2); + + if (tsd0 < tsd1 && tsd0 < tsd2) { + cpoints.push_back(corners[1]); + cpoints.push_back(corners[2]); + cpoints.push_back(corners[3]); + } else if (tsd1 < tsd2) { + cpoints.push_back(corners[2]); + cpoints.push_back(corners[1]); + cpoints.push_back(corners[3]); + } else { + cpoints.push_back(corners[1]); + cpoints.push_back(corners[3]); + cpoints.push_back(corners[2]); + } + + // quality will be determined by comparing the interior angles to 90 degrees + // and by comparing the lengths of opposite sides. + double quality = 0.0; + + double side_length[4] = {0, 0, 0, 0}; + + for (uint i = 0; i < 4; i++) { + double angle_diff = utils::compute_angle(cpoints, i) - 90.0; + double corner_quality = angle_diff * angle_diff; + quality += corner_quality; + + side_length[i] = utils::distance(cpoints, i, i+1); + } + + // sldiff is the percent difference between opposite side lengths. + double sldiff = 100.0 * (side_length[0] - side_length[2]) / std::min(side_length[0], side_length[2]); + quality += (sldiff * sldiff); + sldiff = sldiff = (side_length[1] - side_length[3]) / std::min(side_length[1], side_length[3]); + quality += (sldiff * sldiff); + + return quality; +} + +std::string piece::corners_points_filename() { + std::stringstream filename; + filename << user_params.getOutputDir() << "corners-" << id << ".dat"; + return filename.str(); +} + +void piece::save_corners_points() { + std::string filename = corners_points_filename(); + std::ofstream file; + file.open(filename); + if (file.fail()) { + logger::stream() << "Failed to write " << filename << std::endl; logger::flush(); + return; + } + + for (std::vector::iterator it = corners.begin(); it != corners.end(); it++) { + file << it->x << " " << it->y << std::endl; + } + file.close(); +} + +bool piece::load_corners_points() { + std::string filename = corners_points_filename(); + std::ifstream file; + file.open(filename); + if (!file.fail()) { + corners.clear(); + for (int i = 0; i < 4 && !file.fail(); i++) { + float x; + float y; + file >> x; + file >> y; + corners.push_back(cv::Point2f(x,y)); + } + bool failed = file.fail(); + file.close(); + + if (failed) { + logger::stream() << "Failed to read " << filename << std::endl; logger::flush(); + corners.clear(); + } + else { + return true; + } + } + return false; +} + +void piece::save_corners_image() { + cv::Mat corners_img = full_color.clone(); + for(uint i = 0; i < corners.size(); i++ ) { + circle( corners_img, corners[i], corners_img.size().width / 50, cv::Scalar(0,0,255), 2, 8, 0 ); + } + std::stringstream out_file_name; + out_file_name << user_params.getOutputDir() << "corners-" << id << ".png"; + cv::imwrite(out_file_name.str(), corners_img); +} + +//Gets the piece ready to use. +//This code has been adapted from http://docs.opencv.org/doc/tutorials/features2d/trackingmotion/corner_subpixeles/corner_subpixeles.html +void piece::find_corners(){ + + if (load_corners_points()) { + if (user_params.isSavingCorners()) { + save_corners_image(); + } + return; + } + + //How close can 2 corners be? + double minDistance = user_params.getEstimatedPieceSize(); + //How big of an area to look for the corner in. + int blockSize = user_params.getFindCornersBlockSize(); + bool useHarrisDetector = true; + double k = 0.04; + + double min =0; + double max =1; + int max_iterations = 100; + bool found_all_corners = false; + + //Binary search, altering quality until exactly 4 corners are found. + //Usually done in 1 or 2 iterations + while(0 4){ + //Found too many corners increase quality + min = qualityLevel; + } else if (corners.size() < 4){ + max = qualityLevel; + } else { + //found all corners + found_all_corners = true; + break; + } + + } + + //Find the sub-pixel locations of the corners. + cv::Size winSize = cv::Size( blockSize, blockSize ); + cv::Size zeroZone = cv::Size( -1, -1 ); + cv::TermCriteria criteria = cv::TermCriteria( COMPAT_CV_TERM_CRITERIA_EPS + COMPAT_CV_TERM_CRITERIA_MAX_ITER, 40, 0.001 ); + + /// Calculate the refined corner locations + cv::cornerSubPix( bw, corners, winSize, zeroZone, criteria ); + + double cornersQuality = compute_corners_quality(corners); + if (cornersQuality > user_params.getMinCornersQuality()) { + logger::stream() << "Warning: poor corners for piece " << id << ", quality: " << cornersQuality << std::endl; logger::flush(); + + if (user_params.isAdjustingCorners()) { + std::vector edited_corners; + if (adjust_corners(id, full_color, user_params.getGuiScale(), corners, edited_corners, user_params.isVerbose())) { + corners = edited_corners; + cornersQuality = compute_corners_quality(corners); + logger::stream() << "New corner quality for piece " << id << ", quality: " << cornersQuality << std::endl; logger::flush(); + save_corners_points(); + } + } + } + + // More debug stuff, this will mark the corners with a red circle and save the image + if (user_params.isSavingCorners() /* || user_params.getMinCornersQuality() < cornersQuality */) { + save_corners_image(); + } + + + if (corners.size() < 4) { + logger::stream() << "Only found " << corners.size() << " corners for piece " << id << std::endl; logger::flush(); + exit(2); + } +} + + +void piece::extract_edges(){ + //Extract the contour, + //TODO: probably should have this passed in from the puzzle, since it already does this + //It was done this way b/c the contours don't correspond to the correct pixel locations + //in this cropped version of the image. + std::vector > contours; + std::vector hierarchy; + cv::findContours(bw.clone(), contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_NONE); + assert(corners.size() == 4); + if( 1 != contours.size() ){ + logger::stream() << "Found incorrect number of contours (" << contours.size() << ") for piece" << id << std::endl; logger::flush(); + exit(3); + } + std::vector contour = contours[0]; + + contour = utils::remove_duplicates(contour); + + //out of all of the found corners, find the closest points in the contour, + //these will become the endpoints of the edges + for(uint i = 0; i(corners[i],contour[j]); + if(d::iterator> sections; + sections = find_all_in(contour.begin(), contour.end(), corners); + + //Make corners go in the correct order + for(int i = 0; i<4; i++){ + corners[i]=*sections[i]; + } + + + assert(corners[1]!=corners[0] && corners[0]!=corners[2] && corners[0]!=corners[3] && corners[1]!=corners[2] && + corners[1]!=corners[3] && corners[2]!=corners[3]); + + edges[0] = edge(std::vector(sections[0],sections[1])); + edges[1] = edge(std::vector(sections[1],sections[2])); + edges[2] = edge(std::vector(sections[2],sections[3])); + edges[3] = edge(std::vector(sections[3],contour.end())); + + + +} + + + + + + + +//Classify the type of piece +void piece::classify(){ + int count = 0; + for(int i = 0; i<4; i++){ + if(edges[i].get_type() == OUTER_EDGE) count ++; + } + if(count ==0){ + type = MIDDLE; + } else if (count == 1){ + type = FRAME; + } else if (count == 2){ + type = CORNER; + } else { + logger::stream() << "Problem, found too many outer edges for piece" << id << std:: endl; logger::flush(); + exit(4); + } +} + +pieceType piece::get_type(){ + return type; +} + +//Remember the paradigm is that we go in ccw order +//this rotates it ccw "90 degrees" for each "time" +void piece::rotate(int times){ + int times_to_rotate = times%4; + std::rotate(edges, edges+times_to_rotate, edges+4); + std::rotate(corners.begin(), corners.begin()+times_to_rotate, corners.end()); +} + +cv::Point2f piece::get_corner(int id){ + return corners[id]; +} + + + diff --git a/PuzzleSolver/piece.h b/Src/piece.h similarity index 66% rename from PuzzleSolver/piece.h rename to Src/piece.h index d1854a3..23b7679 100644 --- a/PuzzleSolver/piece.h +++ b/Src/piece.h @@ -9,12 +9,11 @@ #ifndef __PuzzleSolver__piece__ #define __PuzzleSolver__piece__ #include -#include "opencv/cv.h" -#include "opencv/highgui.h" -#include "edge.h" -typedef std::vector imlist; - +#include +#include "compat_opencv.h" +#include "edge.h" +#include "params.h" enum pieceType {CORNER, FRAME, MIDDLE}; @@ -27,19 +26,27 @@ enum pieceType {CORNER, FRAME, MIDDLE}; class piece{ private: + uint piece_number; + std::string id; std::vector corners; pieceType type; - void process(); + void find_corners(); void extract_edges(); void classify(); - int piece_size; + params& user_params; + std::string corners_points_filename(); + void save_corners_points(); + bool load_corners_points(); + void save_corners_image(); public: cv::Mat full_color; cv::Mat bw; - - piece(cv::Mat color,cv::Mat bw, int piece_size); edge edges[4]; + piece(uint piece_number, std::string id, cv::Mat color, cv::Mat bw, params& user_params); + void process(); + uint get_number(); + std::string get_id(); pieceType get_type(); cv::Point2f get_corner(int id); diff --git a/Src/puzzle.cpp b/Src/puzzle.cpp new file mode 100644 index 0000000..e661a5d --- /dev/null +++ b/Src/puzzle.cpp @@ -0,0 +1,810 @@ +// +// puzzle.cpp +// PuzzleSolver +// +// Created by Joe Zeimen on 4/5/13. +// Copyright (c) 2013 Joe Zeimen. All rights reserved. +// + +#include "puzzle.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "omp.h" +#include "compat_opencv.h" + +#include "PuzzleDisjointSet.h" +#include "utils.h" +#include "contours.h" +#include "logger.h" +#include "guided_match.h" +#include "image_viewer.h" + +typedef std::vector imlist; + +/* + _________ _____ + \ \ / / + | / \ / _ + ___/ \____/ |__/ \ + / PUZZLE SOLVER } + \__/\ JOE ___ ZEIMEN ___/ + \ / / / + | | | | + /_____/ \_______\ +*/ + + + +puzzle::puzzle(params& _user_params) : user_params(_user_params) { + pieces = extract_pieces(); + solved = false; + if (user_params.isSavingEdges()) { + print_edges(); + } + logger::stream() << "Extracted " << pieces.size() << " pieces" << std::endl; + logger::flush(); +} + + + + +void puzzle::print_edges(){ + + cv::Scalar color = cv::Scalar(255); + + for(uint i =0; i points = pieces[i].edges[j].get_translated_contour(200, 0); + + for (uint p = 0; p < points.size() -1 ; p++) { + cv::line(m, points[p], points[p+1], color); + } + putText(m, pieces[i].edges[j].edge_type_to_s(), cv::Point(300,300), + cv::FONT_HERSHEY_COMPLEX_SMALL, 0.8, color, 1, COMPAT_CV_LINE_AA); + + utils::write_debug_img(user_params, m, "edge", pieces[i].get_id(), std::to_string(j)); + } + } +} + + +std::vector puzzle::extract_pieces() { + std::vector pieces; + imlist color_images = utils::getImages(user_params.getInputDir()); + + logger::stream() << "Extracting pieces..." << std::endl; + logger::flush(); + + //Threshold the image, anything of intensity greater than the threshold becomes white (255) + //anything below becomes 0 +// imlist blured_images = blur(color_images, 7, 5); + + imlist bw; + if(user_params.isUsingMedianFilter()){ + imlist blured_images = utils::median_blur(color_images, user_params.getMedianBlurKSize()); + bw = utils::color_to_bw(blured_images, user_params.getThreshold()); + } else{ + bw= utils::color_to_bw(color_images, user_params.getThreshold()); + utils::filter(bw,2); + } + + uint piece_number = user_params.getInitialPieceId(); + + + //For each input image + for(uint i = 0; i < color_images.size(); i++){ + + char image_number_buf[80]; + sprintf(image_number_buf, "%03d", i+1); + std::string image_number(image_number_buf); + + if (user_params.isSavingOriginals()) { + utils::write_debug_img(user_params, bw[i],"original-bw", image_number); + utils::write_debug_img(user_params, color_images[i], "original-color", image_number); + } + + std::vector > found_contours; + + + //This isn't used but the opencv function wants it anyways. + std::vector hierarchy; + + //Need to clone b/c it will get modified + cv::findContours(bw[i].clone(), found_contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_NONE); + + + + //For each contour in that image + //TODO: (In anticipation of the other TODO's Re-create the b/w image + // based off of the contour to eliminate noise in the layer mask + + contour_mgr contour_mgr(bw[i].size().width, bw[i].size().height, user_params); + + for(uint j = 0; j < found_contours.size(); j++) { + cv::Rect bounds = cv::boundingRect(found_contours[j]); + if(bounds.width < user_params.getEstimatedPieceSize() || bounds.height < user_params.getEstimatedPieceSize()) continue; + + contour_mgr.add_contour(bounds, utils::remove_duplicates(found_contours[j])); + } + + contour_mgr.sort_contours(); + + if (user_params.isVerifyingContours() || user_params.isSavingContours()) { + std::vector > contours_to_draw; + cv::Mat cmat = cv::Mat::zeros(bw[i].size().height, bw[i].size().width, CV_8UC3); + double font_scale = sqrt(bw[i].size().height * bw[i].size().width) / 1000; + for (uint j = 0; j < contour_mgr.contours.size(); j++) { + cv::Rect bounds = contour_mgr.contours[j].bounds; + contours_to_draw.push_back(contour_mgr.contours[j].points); + // Text indicating contour order within the image + cv::putText(cmat, std::to_string(j+piece_number), cv::Point2f(bounds.x+bounds.width/2-(10.0*font_scale),bounds.y+bounds.height/2+(10.0*font_scale)), + cv::FONT_HERSHEY_COMPLEX_SMALL, font_scale, cv::Scalar(0, 255, 255), 1, COMPAT_CV_LINE_AA); + } + + cv::drawContours(cmat, contours_to_draw, -1, cv::Scalar(255,255,255), 2, 16); + + if (user_params.isVerifyingContours()) { + if (i == 0) { + std::cout << "With focus on the contours image window:" << std::endl; + std::cout << " press 't' to toggle between the contours and original image" << std::endl; + std::cout << " press 'n' to advance to the next image" << std::endl; + } + show_images("contours-" + image_number, cmat, color_images[i]); + } + if (user_params.isSavingContours()) { + utils::write_debug_img(user_params, cmat, "contours", image_number); + } + } + + // Uncomment to save a version of the original with the piece numbers overlayed + /* + if (user_params.isSavingOriginals()) { + cv::Mat cmat = color_images[i].clone(); + double font_scale = sqrt(bw[i].size().height * bw[i].size().width) / 1000; + for (uint j = 0; j < contour_mgr.contours.size(); j++) { + cv::Rect bounds = contour_mgr.contours[j].bounds; + // Text indicating contour order within the image + cv::putText(cmat, std::to_string(j+1), cv::Point2f(bounds.x+bounds.width/2-(10.0*font_scale),bounds.y+bounds.height/2+(10.0*font_scale)), + cv::FONT_HERSHEY_COMPLEX_SMALL, font_scale, cv::Scalar(255,255,255), 2, cv::LINE_AA); + } + + write_debug_img(user_params, cmat, "numbered", image_number); + } + */ + + for (uint j = 0; j < contour_mgr.contours.size(); j++) { + int bordersize = 15; + std::stringstream idstream; + + char id_buffer[80]; + snprintf(id_buffer, 80, "%03d-%03d-%04d", i+1, j+1, piece_number); + std::string piece_id(id_buffer); + + cv::Rect bounds = contour_mgr.contours[j].bounds; + std::vector points = contour_mgr.contours[j].points; + + cv::Mat new_bw = cv::Mat::zeros(bounds.height+2*bordersize,bounds.width+2*bordersize,CV_8UC1); + std::vector > contours_to_draw; + contours_to_draw.push_back(utils::translate_contour(points, bordersize-bounds.x, bordersize-bounds.y)); + cv::drawContours(new_bw, contours_to_draw, -1, cv::Scalar(255), COMPAT_CV_FILLED); + + if (user_params.isSavingBlackWhite()) { + utils::write_debug_img(user_params, new_bw, "bw", piece_id); + } + + cv::Rect b2(bounds.x-3, bounds.y-3, bounds.width+6, bounds.height+6); + cv::Mat color_roi = color_images[i](b2); + cv::Mat mini_color = cv::Mat::zeros(bounds.height+2*bordersize,bounds.width+2*bordersize,CV_8UC3); + color_roi.copyTo(mini_color(cv::Rect(bordersize-3,bordersize-3,b2.width,b2.height))); + + if (user_params.isSavingColor()) { + utils::write_debug_img(user_params, mini_color, "color", piece_id); + } + cv::Mat mini_bw = new_bw; + //Create a copy so it can't conflict. + mini_color = mini_color.clone(); + mini_bw = mini_bw.clone(); + + piece p(piece_number, piece_id, mini_color, mini_bw, user_params); + pieces.push_back(p); + + piece_number += 1; + + } + } + + for (std::vector::iterator i= pieces.begin(); i != pieces.end(); i++) { + i->process(); + } + + return pieces; +} + + + + +void puzzle::fill_costs(){ + + int no_edges = (int) pieces.size()*4; + + //TODO: use openmp to speed up this loop w/o blocking the commented lines below +// omp_set_num_threads(4); +#pragma omp parallel for schedule(dynamic) + for(int i =0; i::iterator i= matches.begin(); + while(!p.in_one_set() && i!=matches.end() ){ + int p1 = i->edge1/4; + int e1 = i->edge1%4; + int p2 = i->edge2/4; + int e2 = i->edge2%4; + + if (user_params.isSavingMatches()) { + cv::Mat m = cv::Mat::zeros(500,500,CV_8UC1); + std::stringstream out_file_name; + out_file_name << user_params.getOutputDir() << "match" << output_id << "_" << pieces[p1].get_id() << "-" << e1 << "_" << pieces[p2].get_id() << "-" < > contours; + contours.push_back(pieces[p1].edges[e1].get_translated_contour(200, 0)); + contours.push_back(pieces[p2].edges[e2].get_translated_contour_reverse(200, 0)); + cv::polylines(m, contours, false, cv::Scalar(255)); + cv::imwrite(out_file_name.str(), m); + } + if (user_params.isVerbose()) { + logger::stream() << "Attempting to merge: " << pieces[p1].get_id() << "-" << (e1+1) << " with: " << + pieces[p2].get_id() << "-" << (e2+1) << ", score:" << i->score << " count: " << output_id <= pieces.size()) { + logger::stream() << "Error, 'work on' piece number " << user_params.getWorkOnPiece() << " is out of range. Expected a value between " + << user_params.getInitialPieceId() << " and " << (pieces.size() + user_params.getInitialPieceId() - 1) << std::endl; + logger::flush(); + exit(1); + } + } + + + while (!p.in_one_set()) { + std::vector::iterator i= matches.begin(); + while(!p.in_one_set() && i!=matches.end() ) { + int p1 = i->edge1/4; + int e1 = i->edge1%4; + int p2 = i->edge2/4; + int e2 = i->edge2%4; + + PuzzleDisjointSet::join_context c; + p.init_join(c, p1, p2, e1, e2); + if (!c.joinable || is_boundary_edge(p1, e1) || is_boundary_edge(p2, e2)) { + i++; + output_id += 1; + continue; + } + + if (work_on != -1) { + if (work_on == c.rep_a) { + // no-op + } + else if (work_on == c.rep_b) { + p.init_join(c, p2, p1, e2, e1); + } + else { + i++; + output_id += 1; + continue; + } + } + // Attempt to join if nothing has been joined yet (collection_set_count == 0), or if + // one of the two rep sets is a collection set and the other is unmatched. + else if (p.collection_set_count() == 0 || (p.is_collection_set(c.rep_a) && p.is_unmatched_set(p2))) { + // no-op + } + else if (p.is_collection_set(c.rep_b) && p.is_unmatched_set(p1)) { + // swap order so that the collection set is in rep_a + p.init_join(c, p2, p1, e2, e1); + } + else { + i++; + output_id += 1; + continue; + } + + p.compute_join(c); + + if (c.joinable) { + std::string response = guide_match(c.a, c.b, c.how_a, c.how_b); + if (response == GM_COMMAND_YES) { + p.complete_join(c); + break; + } + else if (response == GM_COMMAND_SHOW_SET) { + PuzzleDisjointSet::forest f = p.get(c.rep_a); + std::cout << set_to_string(f.locations, user_params.getInitialPieceId()) << std::endl; + continue; + } + else if (response == GM_COMMAND_SHOW_ROTATION) { + PuzzleDisjointSet::forest f = p.get(c.rep_a); + std::cout << set_to_string(f.rotations, 0) << std::endl; + continue; + } + else if (response == GM_COMMAND_MARK_BOUNDARY) { + set_boundary_edge(c.a, c.how_a); + continue; + } + else if (response == GM_COMMAND_WORK_ON_SET) { + + std::cout << "Current matched groups IDs are: "; + for (uint j = 0; j < p.get_collection_sets().size(); j++) { + if (j > 0) { + std::cout << ", "; + } + std::cout << (p.get_collection_sets()[j] + user_params.getInitialPieceId()); + } + std::cout << std::endl; + + int piece_number; + bool read_success = false; + + do { + std::cout << "Enter a piece number: " << std::flush; + std::cin >> piece_number; + + if (std::cin.fail()) { + std::cin.clear(); + std::cin.ignore(999,'\n'); + std::cout << "Invalid input" << std::endl; + continue; + } + + int work_on_id = piece_number - user_params.getInitialPieceId(); + + if (work_on_id < 0 || work_on_id >= pieces.size()) { + std::cout << "Error, " << piece_number << " is out of range. Expected a value between " + << user_params.getInitialPieceId() << " and " << (pieces.size() + user_params.getInitialPieceId() - 1) << std::endl; + } + else { + work_on = p.find(work_on_id); + std::cout << "Working on " << (work_on + user_params.getInitialPieceId()); + if (work_on != work_on_id) { + std::cout << " (matched group for " << piece_number << ")"; + } + std::cout << std::endl; + read_success = true; + } + } while (!read_success); + continue; + } + else if (response == GM_COMMAND_X_CLOSE) { + // Ignore + continue; + } + } + i++; + output_id += 1; + } + } +} + +bool match_check_function(void* data, int p1, int p2, int e1, int e2) { + puzzle* p = (puzzle*)data; + return p->check_match(p1, p2, e1, e2); +} + +bool puzzle::check_match(int p1, int p2, int e1, int e2) { + double cscore; + double escore; + double score = pieces[p1].edges[e1].compare3(pieces[p2].edges[e2], cscore, escore); + if (user_params.isVerbose()) { + std::cout << "check_match(" << (p1+user_params.getInitialPieceId()) << ", " << (p2+user_params.getInitialPieceId()) + << ", " << e1 << ", " << e2 << ")=" << cscore << " / " << escore << std::endl; + } + if (score == DBL_MAX || cscore > user_params.getCscoreLimit() || escore > user_params.getEscoreLimit()) { + return false; + } + return true; +} + +//Solves the puzzle +void puzzle::solve(){ + + load_guided_matches(); + load_boundary_edges(); + + PuzzleDisjointSet p(user_params, pieces.size(), match_check_function, this); + // PuzzleDisjointSet p(user_params, pieces.size(), NULL, NULL); + + if (!user_params.isGuidedSolution()) { + auto_solve(p); + } + else { + guided_solve(p); + } + + p.finish(); + + if(p.in_one_set()){ + logger::stream() << "Possible solution found" << std::endl; + logger::flush(); + solved = true; + PuzzleDisjointSet::forest f = p.get(p.find(1)); + solution = f.locations; + solution_rotations = f.rotations; + + for(int i =0; i> date; + istream >> id; + if (istream.eof()) { + break; + } + + boundary_edges[id] = "yes"; + } + + istream.close(); +} + +std::string get_boundary_edge_id(int initial_piece_id, int p1, int e1) { + int id_p1 = p1 + initial_piece_id; + int id_e1 = (e1+1); + + std::stringstream idstream; + idstream << id_p1 << "-" << id_e1; + return idstream.str(); +} + +void puzzle::set_boundary_edge(int p1, int e1) { + + std::string id = get_boundary_edge_id(user_params.getInitialPieceId(), p1, e1); + boundary_edges[id] = "yes"; + + + std::string filename = get_boundary_edges_filename(user_params); + std::ofstream ostream; + ostream.open(filename, std::ofstream::out | std::ofstream::app); + if (ostream.fail()) { + std::cerr << "Failed to open " << filename << " for writing" << std::endl; + exit(1); + } + + std::time_t time = std::time(NULL); + std::tm tm = *std::localtime(&time); + ostream << std::put_time(&tm, "%a_%F_%T") << " " << id << "\n" << std::flush; + ostream.close(); +} + +bool puzzle::is_boundary_edge(int p1, int e1) { + std::string id = get_boundary_edge_id(user_params.getInitialPieceId(), p1, e1); + std::map::iterator it = boundary_edges.find(id); + return it != boundary_edges.end(); +} + +std::string get_guided_matches_filename(params& user_params) { + return user_params.getOutputDir() + "guided-matches.dat"; +} + +void puzzle::load_guided_matches() { + std::string filename = get_guided_matches_filename(user_params); + std::ifstream istream; + istream.open(filename, std::ifstream::in); + if (istream.fail()) { + return; + } + + std::string dateField; + std::string isMatchField; + std::string piecePairId; + + while (true) { + + istream >> dateField; + istream >> isMatchField; + istream >> piecePairId; + if (istream.eof()) { + break; + } + + guided_matches[piecePairId] = isMatchField; + } + + istream.close(); +} + +// Returns a string identifying the piece-edge paring +std::string get_match_id(int initial_piece_id, int p1, int p2, int e1, int e2) { + // Create the ID with the lower value piece number appearing first. + int id_p1; + int id_e1; + int id_p2; + int id_e2; + + if (p1 < p2) { + id_p1 = p1 + initial_piece_id; + id_e1 = (e1+1); + id_p2 = p2 + initial_piece_id; + id_e2 = (e2+1); + } else { + id_p1 = p2 + initial_piece_id; + id_e1 = (e2+1); + id_p2 = p1 + initial_piece_id; + id_e2 = (e1+1); + } + + std::stringstream idstream; + idstream << id_p1 << "-" << id_e1 << "-" << id_p2 << "-" << id_e2; + return idstream.str(); +} + +std::string puzzle::guide_match(int p1, int p2, int e1, int e2) { + + std::string id = get_match_id(user_params.getInitialPieceId(), p1, p2, e1, e2); + + std::map::iterator it = guided_matches.find(id); + if (it != guided_matches.end()) { + return it->second; + } + + if (!user_params.isGuidedSolution()) { + return "yes"; // "yes" results in the default automatic solution behavior + } + + double cscore; + double escore; + double score = pieces[p1].edges[e1].compare3(pieces[p2].edges[e2], cscore, escore); + + std::string response; + if (score == DBL_MAX || cscore > user_params.getCscoreLimit() || escore > user_params.getEscoreLimit()) { + response = "no"; + } else { + std::cout << "Does piece " << (p1 + user_params.getInitialPieceId()) + << " fit to " << (p2 + user_params.getInitialPieceId()) + << " (scores: " << cscore << " / " << escore << ")" + << " ? " << std::flush; + + response = guided_match(pieces[p1], pieces[p2], e1, e2, user_params); + std::cout << response << std::endl; + } + + if (response != "yes" && response != "no") { + return response; + } + guided_matches[id] = response; + + + std::string filename = get_guided_matches_filename(user_params); + std::ofstream ostream; + ostream.open(filename, std::ofstream::out | std::ofstream::app); + if (ostream.fail()) { + std::cerr << "Failed to open " << filename << " for writing" << std::endl; + exit(1); + } + + std::time_t time = std::time(NULL); + std::tm tm = *std::localtime(&time); + ostream << std::put_time(&tm, "%a_%F_%T") << " " << response << " " << id << "\n" << std::flush; + ostream.close(); + return response; +} + +std::string puzzle::set_to_string(cv::Mat_ set, int offset) { + std::stringstream stream; + + int width = 2; + int max_id = pieces.size() + user_params.getInitialPieceId() -1; + if (max_id > 99) { + width = 3; + } + else if (max_id > 999) { + width = 4; + } + + for(int row = 0; row < set.rows; ++row) { + int* p = (int*)set.ptr(row); + for(int col = 0; col < set.cols; ++col) { + int value = *p++; + if (value >= 0) { + stream << std::setw(width) << (value + offset); + } + else { + stream << std::setw(width) << ""; + } + stream << ", "; + } + stream << std::endl; + } + + return stream.str(); +} + +// Generates and displays the solution as text (grid of piece IDs). The text is also saved to +// .txt in the output directory. The numbers are 1-based instead of zero-based so +// that they correspond to the piece IDs. +void puzzle::save_solution_text() { + if(!solved) solve(); + + std::string solution_text = set_to_string(solution, user_params.getInitialPieceId()); + logger::stream() << solution_text << std::endl; + logger::flush(); + + logger::stream() << "\nrotations:\n" << set_to_string(solution_rotations, 0) << std::endl; + +} + +std::string puzzle::get_solution_image_pathname() { + return user_params.getOutputDir() + user_params.getSolutionFileBasename() + ".png"; +} + + + +//Saves an image of the representation of the puzzle. +//only really works when there are no holes +//TODO: fail when puzzle is in configurations that are not possible i.e. holes +void puzzle::save_solution_image(){ + if(!solved) solve(); + + + //Use get affine to map points... + int out_image_size = 6000; + cv::Mat out_image(out_image_size,out_image_size,CV_8UC3, cv::Scalar(200,50,3)); + int border = 10; + + cv::Point2f ** points = new cv::Point2f*[solution.size[0]+1]; + for(int i = 0; i < solution.size[0]+1; ++i) + points[i] = new cv::Point2f[solution.size[1]+1]; + bool failed=false; + + logger::stream() << "Saving image..." << std::endl; + logger::flush(); + for(int i=0; i src; + std::vector dst; + + if(i==0 && j==0){ + points[i][j] = cv::Point2f(border,border); + } + if(i==0){ + points[i][j+1] = cv::Point2f(points[i][j].x+border+x_dist,border); + } + if(j==0){ + points[i+1][j] = cv::Point2f(border,points[i][j].y+border+y_dist); + } + + dst.push_back(points[i][j]); + dst.push_back(points[i+1][j]); + dst.push_back(points[i][j+1]); + src.push_back(pieces[piece_number].get_corner(0)); + src.push_back(pieces[piece_number].get_corner(1)); + src.push_back(pieces[piece_number].get_corner(3)); + + //true means use affine transform + cv::Mat a_trans_mat = cv::estimateRigidTransform(src, dst,true); + cv::Mat_ A = a_trans_mat; + + //Lower right corner of each piece + cv::Point2f l_r_c = pieces[piece_number].get_corner(2); + + //Doing my own matrix multiplication + points[i+1][j+1] = cv::Point2f((float)(A(0,0)*l_r_c.x+A(0,1)*l_r_c.y+A(0,2)),(float)(A(1,0)*l_r_c.x+A(1,1)*l_r_c.y+A(1,2))); + + + + cv::Mat layer; + cv::Mat layer_mask; + + int layer_size = out_image_size; + + cv::warpAffine(pieces[piece_number].full_color, layer, a_trans_mat, cv::Size2i(layer_size,layer_size),cv::INTER_LINEAR,cv::BORDER_TRANSPARENT); + cv::warpAffine(pieces[piece_number].bw, layer_mask, a_trans_mat, cv::Size2i(layer_size,layer_size),cv::INTER_NEAREST,cv::BORDER_TRANSPARENT); + + layer.copyTo(out_image(cv::Rect(0,0,layer_size,layer_size)), layer_mask); + + } + logger::stream() << std::endl; logger::flush(); + + } + if(failed){ + logger::stream() << "Failed, only partial image generated" << std::endl; logger::flush(); + } + + cv::Mat final_out_image; + + utils::autocrop(out_image, final_out_image); + cv::imwrite(get_solution_image_pathname(), final_out_image); + + + + for(int i = 0; i < solution.size[0]+1; ++i) + delete points[i]; + delete[] points; + +} + + +void puzzle::show_solution_image() { + cv::Mat solution_image = cv::imread(get_solution_image_pathname()); + std::cout << "With focus on the solution image window:" << std::endl; + std::cout << " press 'r' one or more times to rotate the solution image by 90 degrees" << std::endl; + std::cout << " press 'q' to quit/exit" << std::endl; + show_image("solution", solution_image); +} diff --git a/Src/puzzle.h b/Src/puzzle.h new file mode 100644 index 0000000..ee19b5b --- /dev/null +++ b/Src/puzzle.h @@ -0,0 +1,65 @@ +// +// puzzle.h +// PuzzleSolver +// +// Created by Joe Zeimen on 4/5/13. +// Copyright (c) 2013 Joe Zeimen. All rights reserved. +// + +#ifndef __PuzzleSolver__puzzle__ +#define __PuzzleSolver__puzzle__ + +#include +#include +#include + +#include "compat_opencv.h" + +#include "edge.h" +#include "params.h" +#include "piece.h" +#include "PuzzleDisjointSet.h" + + +class puzzle{ +private: + struct match_score{ + uint16_t edge1, edge2; + double score; + static bool compare(match_score a, match_score b){ + return a.score matches; + std::vector pieces; + std::map guided_matches; + std::map boundary_edges; + cv::Mat_ solution; + cv::Mat_ solution_rotations; + std::vector extract_pieces(); + void print_edges(); + std::string edgeType_to_s(edgeType e); + void auto_solve(PuzzleDisjointSet& p); + void load_guided_matches(); + void load_boundary_edges(); + void set_boundary_edge(int p1, int e1); + bool is_boundary_edge(int p1, int e1); + void guided_solve(PuzzleDisjointSet& p); + std::string set_to_string(cv::Mat_ set, int offset); +public: + puzzle(params& userParams); + std::string guide_match(int p1, int e1, int p2, int e2); + bool check_match(int p1, int e1, int p2, int e2); + void fill_costs(); + void solve(); + void save_solution_text(); + std::string get_solution_image_pathname(); + void save_solution_image(); + void show_solution_image(); +}; + + + +#endif /* defined(__PuzzleSolver__puzzle__) */ diff --git a/Src/utils.cpp b/Src/utils.cpp new file mode 100644 index 0000000..e4fdfef --- /dev/null +++ b/Src/utils.cpp @@ -0,0 +1,259 @@ +// +// Utils.cpp +// PuzzleSolver +// +// Created by Joe Zeimen on 4/9/13. +// Copyright (c) 2013 Joe Zeimen. All rights reserved. +// + +#include +#include +#include "utils.h" + +#include "compat_opencv.h" +#include "logger.h" + + +void utils::line(cv::Mat mat, std::vector points, int index1, int index2, cv::Scalar color) { + cv::line(mat, points[wrap_index(points, index1)], points[wrap_index(points, index2)], color); +} + +//This function takes a directory, and returns a vector of every image opencv could extract from it. +imlist utils::getImages(std::string path){ + imlist v; + + DIR *dp; + struct dirent *ep; + dp = opendir (path.c_str()); + + std::vector filenames; + + if (dp == NULL) { + logger::stream() << "Couldn't open the directory: " << path << std::endl; + logger::flush(); + exit(1); + } + + while ((ep = readdir(dp))) { + if (strcmp(".", ep->d_name) != 0 && strcmp("..", ep->d_name) != 0) { + filenames.push_back(path+ep->d_name); + } + } + closedir(dp); + + std::sort(filenames.begin(), filenames.end()); + + int id = 0; + for (std::vector::iterator i = filenames.begin(); i != filenames.end(); i++) { + std::string filename = *i; + cv::Mat image = cv::imread(filename); + if (image.data != NULL) { + id += 1; + logger::stream() << "Loaded " << filename << " as image " << std::setfill('0') << std::setw(3) << id << std::endl; + logger::flush(); + v.push_back(image); + } + + + } + + return v; +} + + + +//Easy way to take a list of images and create a bw image at a specified threshold. +imlist utils::color_to_bw(imlist color, int threshold){ + imlist black_and_white; + for(imlist::iterator i = color.begin(); i != color.end(); i++){ + cv::Mat bw; + cv::cvtColor(*i, bw, COMPAT_CV_BGR2GRAY); + cv::threshold(bw, bw, threshold, 255, cv::THRESH_BINARY); + black_and_white.push_back(bw); + } + return black_and_white; +} + +//Performs a open then a close operation in order to remove small anomolies. +void utils::filter(imlist to_filter, int size){ + cv::Mat k = cv::getStructuringElement(cv::MORPH_ELLIPSE,cv::Size(size,size)); + for(imlist::iterator i = to_filter.begin(); i != to_filter.end(); i++){ + cv::Mat bw; + //Opening and closing removes anything smaller than size + cv::morphologyEx(*i, bw, COMPAT_CV_MORPH_TYPE_OPEN, k); + cv::morphologyEx(bw, *i, COMPAT_CV_MORPH_TYPE_CLOSE, k); + } +} + +//Performs a open then a close operation in order to remove small anomolies. +imlist utils::blur(imlist to_filter, int size, double sigma){ + imlist ret; + for(imlist::iterator i = to_filter.begin(); i != to_filter.end(); i++){ + cv::Mat m; + cv::GaussianBlur(*i, m, cv::Size(size,size), sigma); + ret.push_back(m); + } + return ret; +} + + +//Performs a open then a close operation in order to remove small anomolies. +imlist utils::median_blur(imlist to_filter, int k){ + imlist ret; + for(imlist::iterator i = to_filter.begin(); i != to_filter.end(); i++){ + cv::Mat m; + cv::medianBlur(*i, m, k); + ret.push_back(m); + } + return ret; +} + +imlist utils::bilateral_blur(imlist to_blur){ + imlist ret; + for(imlist::iterator i = to_blur.begin(); i != to_blur.end(); i++){ + cv::Mat m; + cv::bilateralFilter(*i, m, 5, 152, 5); + + cv::imwrite("/tmp/final/bilat.png", m); + cv::imwrite("/tmp/final/before_bilat.png", *i); + ret.push_back(m); + } + return ret; +} +std::vector utils::remove_duplicates(std::vector vec){ + bool dupes_found = true; + while(dupes_found){ + dupes_found=false; + int dup_at=-1; + for(uint i =0; i(0,i)); + + return res; +} + +/** + * Function to auto-cropping image + * + * Parameters: + * src The source image + * dst The destination image + */ +void utils::autocrop(cv::Mat& src, cv::Mat& dst) +{ + cv::Rect win(0, 0, src.cols, src.rows); + + std::vector edges; + edges.push_back(cv::Rect(0, 0, src.cols, 1)); + edges.push_back(cv::Rect(src.cols-2, 0, 1, src.rows)); + edges.push_back(cv::Rect(0, src.rows-2, src.cols, 1)); + edges.push_back(cv::Rect(0, 0, 1, src.rows)); + + cv::Mat edge; + int nborder = 0; + cv::Vec3b color = src.at(src.cols-1,src.rows-1); + + for (int i = 0; i < edges.size(); ++i) + { + edge = src(edges[i]); + nborder += is_border(edge, color); + } + + if (nborder == 0) + { + src.copyTo(dst); + return; + } + + bool next; + + do { + edge = src(cv::Rect(win.x, win.height-2, win.width, 1)); + if ((next = is_border(edge, color))) + win.height--; + } + while (next && win.height > 0); + + do { + edge = src(cv::Rect(win.width-2, win.y, 1, win.height)); + if ((next = is_border(edge, color))) + win.width--; + } + while (next && win.width > 0); + + do { + edge = src(cv::Rect(win.x, win.y, win.width, 1)); + if ((next = is_border(edge, color))) + win.y++, win.height--; + } + while (next && win.y <= src.rows); + + do { + edge = src(cv::Rect(win.x, win.y, 1, win.height)); + if ((next = is_border(edge, color))) + win.x++, win.width--; + } + while (next && win.x <= src.cols); + + dst = src(win); +} + + diff --git a/Src/utils.h b/Src/utils.h new file mode 100644 index 0000000..509e870 --- /dev/null +++ b/Src/utils.h @@ -0,0 +1,137 @@ +// +// Utils.h +// PuzzleSolver +// +// Created by Joe Zeimen on 4/9/13. +// Copyright (c) 2013 Joe Zeimen. All rights reserved. +// + +#ifndef __PuzzleSolver__Utils__ +#define __PuzzleSolver__Utils__ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include "compat_opencv.h" +#include "params.h" +typedef std::vector imlist; + +class utils { +public: + + // Assuming Point represents a vector, return its magnitude. + template + static double magnitude(cv::Point_ ab) { + return sqrt(ab.x * ab.x + ab.y * ab.y); + } + + // Calculate the distance from a to b. + template + static double distance(cv::Point_ a, cv::Point_ b) { + return magnitude(b-a); + } + + // wrap the index value so that it satisfies 0 >= index > points.size(). The index value will "wrap around" so that + // -1 will result in points.size()-1, and index=points.size() results in 0, etc. + template + static uint wrap_index(std::vector> points, int index) { + return (points.size() + index) % points.size(); + } + + // Returns the distance between the points at the given indices + template + static double distance(std::vector> points, int index1, int index2) { + return distance( + points[wrap_index(points, index1)], + points[wrap_index(points, index2)] + ); + } + + // Calculate the distance from points[index] to p with index wrapping. + template + static double distance(std::vector> points, int index, cv::Point_ p) { + return distance(points[wrap_index(points, index)],p); + } + + // Given points, a, b, and c, compute the angle at b (in degrees). + template + static double compute_angle(cv::Point_ a, cv::Point_ b, cv::Point_ c) { + + cv::Point_ ab = a - b; // b - a; + cv::Point_ bc = c - b; + return 180.0 * acos(ab.ddot(bc) / (magnitude(ab) * magnitude(bc))) / M_PI; + } + + // Given points, a, b, and c, compute the angle at b (in degrees). Returns a negative angle if a.x < b.x + template + static double compute_rotation_angle(cv::Point_ a, cv::Point_ b, cv::Point_ c) { + + double angle = compute_angle(a,b,c); + if (a.x < b.x) { + return -angle; + } + return angle; + } + + // Computes the angle in degrees at the point with the specified index + template + static double compute_angle(std::vector> points, int index) { + return compute_angle( + points[wrap_index(points, index - 1)], + points[wrap_index(points, index)], + points[wrap_index(points, index + 1)] + ); + } + + // compute the angle in degrees at the point with the given index. Uses index wrapping + // to determine the point before and after the given index. + static double compute_angle(std::vector points, int index); + + // draw a line from the point at index1 to the point at index2. Applies index wrapping to the + // specified indices. + static void line(cv::Mat mat, std::vector points, int index1, int index2, cv::Scalar color); + + static void filter(imlist to_filter, int size); + static imlist color_to_bw(imlist color, int threshold); + static imlist getImages(std::string path); + static imlist blur(imlist to_blur, int size, double sigma); + static imlist median_blur(imlist to_blur, int size); + static imlist bilateral_blur(imlist to_blur); + + //template std::vector translate_contour(std::vector in , int offset_x, int offset_y); + static std::vector remove_duplicates(std::vector vec); + //Return a contour that is translated + template + static void translate_contour(std::vector in, std::vector& ret_contour, int offset_x, int offset_y, float scale = 1.0){ + cv::Point2f offset(offset_x,offset_y); + for(uint i = 0; i + static std::vector translate_contour(std::vector in , int offset_x, int offset_y, float scale = 1.0){ + std::vector ret_contour; + translate_contour(in, ret_contour, offset_x, offset_y, scale); + return ret_contour; + } + + static void write_img(params& user_params, cv::Mat& img, std::string filename); + static void write_debug_img(params& user_params, cv::Mat& img, std::string prefix, std::string index); + static void write_debug_img(params& user_params, cv::Mat& img, std::string prefix, std::string index1, std::string index2); + static void write_debug_img(params& user_params, cv::Mat& img, std::string prefix, uint index); + static void write_debug_img(params& user_params, cv::Mat& img, std::string prefix, uint index1, uint index2); + + static void autocrop(cv::Mat& src, cv::Mat& dst); + + +}; +#endif /* defined(__PuzzleSolver__Utils__) */