├── .gitignore ├── BGHMatcher.cpp ├── BGHMatcher.h ├── Knobs.cpp ├── Knobs.h ├── LICENSE ├── README.md ├── bghmatcher.sln ├── bghmatcher.vcxproj ├── bghmatcher.vcxproj.filters ├── data ├── bottle_20perc_top_b_on_w.png ├── circle_b_on_w.png ├── panda_face.png ├── ring_b_on_w.png └── stars_main.png ├── main.cpp ├── util.cpp └── util.h /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /BGHMatcher.cpp: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright(c) 2018 Mark Whitney 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #include 24 | #include 25 | #include 26 | #include "BGHMatcher.h" 27 | #include "opencv2/highgui.hpp" 28 | 29 | 30 | namespace BGHMatcher 31 | { 32 | // custom comparison operator for cv::Point 33 | // it can can be used to sort points by X then by Y 34 | struct cmpCvPoint { 35 | bool operator()(const cv::Point& a, const cv::Point& b) const { 36 | return (a.x < b.x) || ((a.x == b.x) && (a.y < b.y)); 37 | } 38 | }; 39 | 40 | 41 | void create_ghough_table( 42 | const cv::Mat& rgrad, 43 | const double scale, 44 | BGHMatcher::T_ghough_table& rtable) 45 | { 46 | // sanity check scale value 47 | double fac = scale; 48 | if (fac < 0.1) fac = 0.1; 49 | if (fac > 10.0) fac = 10.0; 50 | 51 | // calculate centering offset 52 | int row_offset = rgrad.rows / 2; 53 | int col_offset = rgrad.cols / 2; 54 | 55 | // iterate through the gradient image pixel-by-pixel 56 | // use STL structures to build a lookup table dynamically 57 | uint8_t max_code = 0; 58 | std::map> lookup_table; 59 | for (int i = 0; i < rgrad.rows; i++) 60 | { 61 | const uint8_t * pix = rgrad.ptr(i); 62 | for (int j = 0; j < rgrad.cols; j++) 63 | { 64 | // get the gradient pixel value (key) 65 | // everything non-zero is valid 66 | const uint8_t uu = pix[j]; 67 | if (uu) 68 | { 69 | // the scaling operation can make one point have multiple votes 70 | // so the vote count is mapped to a point and incremented 71 | cv::Point offset_pt = cv::Point(col_offset - j, row_offset - i); 72 | offset_pt.x = static_cast(fac * offset_pt.x); 73 | offset_pt.y = static_cast(fac * offset_pt.y); 74 | lookup_table[uu][offset_pt]++; 75 | max_code = (uu > max_code) ? uu : max_code; 76 | } 77 | } 78 | } 79 | 80 | // add blank entry for any code not in map for codes 0-max 81 | for (uint8_t key = 0; key <= max_code; key++) 82 | { 83 | if (lookup_table.count(key) == 0) 84 | { 85 | lookup_table[key] = {}; 86 | } 87 | } 88 | 89 | // blow away any old data in table 90 | rtable.clear(); 91 | 92 | // then put lookup table into a fixed non-STL structure 93 | // that is much more efficient when running debug code 94 | rtable.img_sz = rgrad.size(); 95 | rtable.elem_ct = lookup_table.size(); 96 | rtable.elems = new T_ghough_elem[rtable.elem_ct]; 97 | for (const auto& r : lookup_table) 98 | { 99 | uint8_t key = r.first; 100 | size_t n = r.second.size(); 101 | if (n > 0) 102 | { 103 | rtable.elems[key].ct = n; 104 | rtable.elems[key].pt_votes = new T_pt_votes[n]; 105 | size_t k = 0; 106 | for (const auto& rr : r.second) 107 | { 108 | cv::Point pt = rr.first; 109 | rtable.elems[key].pt_votes[k++] = { pt, rr.second }; 110 | rtable.total_votes += rr.second; 111 | rtable.total_entries++; 112 | } 113 | } 114 | } 115 | } 116 | 117 | 118 | void create_masked_gradient_orientation_img( 119 | const cv::Mat& rimg, 120 | cv::Mat& rmgo, 121 | const BGHMatcher::T_ghough_params& rparams) 122 | { 123 | double qmax; 124 | double qmin; 125 | double ang_step = rparams.ang_step; 126 | cv::Mat temp_dx; 127 | cv::Mat temp_dy; 128 | cv::Mat temp_mag; 129 | cv::Mat temp_ang; 130 | cv::Mat temp_mask; 131 | const int SOBEL_DEPTH = CV_32F; 132 | 133 | // calculate X and Y gradients for input image 134 | cv::Sobel(rimg, temp_dx, SOBEL_DEPTH, 1, 0, rparams.ksobel); 135 | cv::Sobel(rimg, temp_dy, SOBEL_DEPTH, 0, 1, rparams.ksobel); 136 | 137 | // convert X-Y gradients to magnitude and angle 138 | cartToPolar(temp_dx, temp_dy, temp_mag, temp_ang); 139 | 140 | // create mask for pixels that exceed gradient magnitude threshold 141 | minMaxLoc(temp_mag, nullptr, &qmax); 142 | temp_mask = (temp_mag > (qmax * rparams.mag_thr)); 143 | 144 | // scale, offset, and convert the angle image so 0-2pi becomes integers 1 to (ANG_STEP+1) 145 | // note that the angle can sometimes be 2pi which is equivalent to an angle of 0 146 | // for some binary source images not all gradient codes may be generated 147 | ang_step = (ang_step > ANG_STEP_MAX) ? ANG_STEP_MAX : ang_step; 148 | ang_step = (ang_step < ANG_STEP_MIN) ? ANG_STEP_MIN: ang_step; 149 | temp_ang.convertTo(rmgo, CV_8U, ang_step / (CV_2PI), 1.0); 150 | 151 | // apply mask to eliminate pixels 152 | rmgo &= temp_mask; 153 | } 154 | 155 | 156 | void init_ghough_table_from_img( 157 | cv::Mat& rimg, 158 | BGHMatcher::T_ghough_table& rtable, 159 | const BGHMatcher::T_ghough_params& rparams) 160 | { 161 | cv::Mat img_cgrad; 162 | create_masked_gradient_orientation_img(rimg, img_cgrad, rparams); 163 | 164 | cv::Mat img_target; 165 | GaussianBlur(rimg, img_target, { rparams.kblur, rparams.kblur }, 0); 166 | 167 | // create Generalized Hough lookup table from masked gradient image 168 | BGHMatcher::create_ghough_table(img_cgrad, rparams.scale, rtable); 169 | 170 | #if 1 171 | cv::Mat img_display; 172 | normalize(img_cgrad, img_display, 0, 255, cv::NORM_MINMAX); 173 | imshow("GHTemplate", img_display); 174 | #endif 175 | 176 | // save metadata for lookup table 177 | rtable.params = rparams; 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /BGHMatcher.h: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright(c) 2018 Mark Whitney 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #ifndef BGH_MATCHER_H_ 24 | #define BGH_MATCHER_H_ 25 | 26 | #include "opencv2/imgproc.hpp" 27 | 28 | 29 | namespace BGHMatcher 30 | { 31 | constexpr double ANG_STEP_MAX = 254.0; 32 | constexpr double ANG_STEP_MIN = 4.0; 33 | 34 | 35 | // parameters used to create Generalized Hough lookup table 36 | typedef struct _T_ghough_params_struct 37 | { 38 | int kblur; 39 | int ksobel; 40 | double scale; 41 | double mag_thr; 42 | double ang_step; 43 | _T_ghough_params_struct() : 44 | kblur(7), ksobel(7), scale(1.0), mag_thr(1.0), ang_step(8.0) {} 45 | _T_ghough_params_struct(const int kb, const int ks, const double s, const double m, const double a) : 46 | kblur(kb), ksobel(ks), scale(s), mag_thr(m), ang_step(a) {} 47 | } T_ghough_params; 48 | 49 | 50 | // structure that combines an image point and a vote count for that point 51 | typedef struct _T_pt_votes_struct 52 | { 53 | cv::Point pt; 54 | uint16_t votes; 55 | _T_pt_votes_struct() : pt{}, votes(0) {} 56 | _T_pt_votes_struct(const cv::Point& _pt, const uint16_t _v) : pt{_pt}, votes(_v) {} 57 | } T_pt_votes; 58 | 59 | 60 | // one gradient orientation element for the Generalized Hough lookup table 61 | typedef struct _T_ghough_elem_struct 62 | { 63 | size_t ct; 64 | T_pt_votes * pt_votes; 65 | _T_ghough_elem_struct() : ct(0), pt_votes(nullptr) {} 66 | ~_T_ghough_elem_struct() { clear(); } 67 | void clear() { ct = 0; if (pt_votes) { delete[] pt_votes; } pt_votes = nullptr; } 68 | } T_ghough_elem; 69 | 70 | 71 | // Non-STL data structure for Generalized Hough lookup table 72 | typedef struct _T_ghough_table_struct 73 | { 74 | T_ghough_params params; 75 | cv::Size img_sz; 76 | size_t elem_ct; 77 | size_t total_votes; 78 | size_t total_entries; 79 | T_ghough_elem * elems; 80 | 81 | _T_ghough_table_struct() : 82 | params(), img_sz(0, 0), elem_ct(0), total_votes(0), total_entries(0), elems(nullptr) {} 83 | 84 | ~_T_ghough_table_struct() { clear(); } 85 | 86 | void clear() 87 | { 88 | if (elems != nullptr) 89 | { 90 | for (size_t i = 0; i < elem_ct; i++) { elems[i].clear(); } 91 | delete[] elems; 92 | } 93 | img_sz = { 0, 0 }; 94 | total_votes = 0; 95 | total_entries = 0; 96 | elems = nullptr; 97 | elem_ct = 0; 98 | } 99 | } T_ghough_table; 100 | 101 | 102 | // Applies Generalized Hough transform to an encoded gradient image (CV_8U). 103 | // The size of the target image used to generate the table will constrain the results. 104 | // Pixels near border and within half the X or Y dimensions of target image will be 0. 105 | // Template parameters specify output type. Try or . 106 | // Output image is same size as input. Maxima indicate good matches. 107 | template 108 | void apply_ghough_transform( 109 | const cv::Mat& rimg, 110 | cv::Mat& rout, 111 | const BGHMatcher::T_ghough_table& rtable) 112 | { 113 | rout = cv::Mat::zeros(rimg.size(), E); 114 | for (int i = rtable.img_sz.height / 2; i < rimg.rows - rtable.img_sz.height / 2; i++) 115 | { 116 | const uint8_t * pix = rimg.ptr(i); 117 | for (int j = rtable.img_sz.width / 2; j < rimg.cols - rtable.img_sz.width / 2; j++) 118 | { 119 | // look up voting table for pixel 120 | // iterate through the points (if any) and add votes 121 | uint8_t uu = pix[j]; 122 | T_pt_votes * pt_votes = rtable.elems[uu].pt_votes; 123 | const size_t ct = rtable.elems[uu].ct; 124 | for (size_t k = 0; k < ct; k++) 125 | { 126 | const cv::Point& rp = pt_votes[k].pt; 127 | int mx = (j + rp.x); 128 | int my = (i + rp.y); 129 | T * pix = rout.ptr(my) + mx; 130 | *pix += pt_votes[k].votes; 131 | } 132 | } 133 | } 134 | } 135 | 136 | 137 | // Applies Generalized Hough transform to an input encoded gradient image (CV_8U). 138 | // Each vote is range-checked. Votes that would fall outside the image are discarded. 139 | // Template parameters specify output type. Try or . 140 | // Output image is same size as input. Maxima indicate good matches. 141 | template 142 | void apply_ghough_transform_allpix( 143 | const cv::Mat& rimg, 144 | cv::Mat& rout, 145 | const BGHMatcher::T_ghough_table& rtable) 146 | { 147 | rout = cv::Mat::zeros(rimg.size(), E); 148 | for (int i = 1; i < (rimg.rows - 1); i++) 149 | { 150 | const uint8_t * pix = rimg.ptr(i); 151 | for (int j = 1; j < (rimg.cols - 1); j++) 152 | { 153 | // look up voting table for pixel 154 | // iterate through the points and add votes 155 | uint8_t uu = pix[j]; 156 | T_pt_votes * pt_votes = rtable.elems[uu].pt_votes; 157 | const size_t ct = rtable.elems[uu].ct; 158 | for (size_t k = 0; k < ct; k++) 159 | { 160 | // only vote if pixel is within output image bounds 161 | const cv::Point& rp = pt_votes[k].pt; 162 | int mx = (j + rp.x); 163 | int my = (i + rp.y); 164 | if ((mx >= 0) && (mx < rout.cols) && 165 | (my >= 0) && (my < rout.rows)) 166 | { 167 | T * pix = rout.ptr(my) + mx; 168 | *pix += pt_votes[k].votes; 169 | } 170 | } 171 | } 172 | } 173 | } 174 | 175 | 176 | // This is the preprocessing step for the "classic" Generalized Hough algorithm. 177 | // Calculates Sobel derivatives of input grayscale image. Converts to polar coordinates and 178 | // finds magnitude and angle (orientation). Converts angle to integer with 4 to 254 steps. 179 | // Masks the pixels with gradient magnitudes above a threshold. 180 | void create_masked_gradient_orientation_img( 181 | const cv::Mat& rimg, 182 | cv::Mat& rmgo, 183 | const BGHMatcher::T_ghough_params& rparams); 184 | 185 | 186 | // Creates a Generalized Hough lookup table from encoded gradient input image (CV_8U). 187 | // The scale parameter shrinks or expands the point set. 188 | void create_ghough_table( 189 | const cv::Mat& rgrad, 190 | const double scale, 191 | BGHMatcher::T_ghough_table& rtable); 192 | 193 | 194 | // Helper function for initializing Generalized Hough table from grayscale image. 195 | // Default parameters are good starting point for doing object identification. 196 | // Table must be a newly created object with blank data. 197 | void init_ghough_table_from_img( 198 | cv::Mat& rimg, 199 | BGHMatcher::T_ghough_table& rtable, 200 | const BGHMatcher::T_ghough_params& rparams); 201 | } 202 | 203 | #endif // BGH_MATCHER_H_ 204 | -------------------------------------------------------------------------------- /Knobs.cpp: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright(c) 2018 Mark Whitney 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #include 24 | #include 25 | #include "Knobs.h" 26 | 27 | 28 | Knobs::Knobs() : 29 | is_op_required(false), 30 | is_equ_hist_enabled(false), 31 | is_record_enabled(false), 32 | kpreblur(7), 33 | kcliplimit(4), 34 | nchannel(Knobs::ALL_CHANNELS), 35 | noutmode(Knobs::OUT_COLOR), 36 | op_id(Knobs::OP_NONE), 37 | nimgscale(3), 38 | nksize(4), 39 | vimgscale({ 0.25, 0.325, 0.4, 0.5, 0.625, 0.75, 1.0 }), 40 | vksize({ -1, 1, 3, 5, 7}) 41 | { 42 | } 43 | 44 | 45 | Knobs::~Knobs() 46 | { 47 | } 48 | 49 | 50 | void Knobs::show_help(void) const 51 | { 52 | std::cout << std::endl; 53 | std::cout << "KEYS FUNCTION" << std::endl; 54 | std::cout << "----- ------------------------------------------------------" << std::endl; 55 | std::cout << "Esc Quit" << std::endl; 56 | std::cout << "1,2,3,4 Choose BGR channel (Blue, Green, Red, BGR-to-Gray)" << std::endl; 57 | std::cout << "7,8,9,0 Output mode (raw match, gradients, pre-proc, color)" << std::endl; 58 | std::cout << "- or = Adjust pre-blur (decrease, increase)" << std::endl; 59 | std::cout << "_ or + Adjust CLAHE clip limit (decrease, increase)" << std::endl; 60 | std::cout << "[ or ] Adjust image scale (decrease, increase)" << std::endl; 61 | std::cout << "{ or } Adjust Sobel kernel size (decrease, increase)" << std::endl; 62 | std::cout << "e Toggle histogram equalization" << std::endl; 63 | std::cout << "r Toggle recording mode" << std::endl; 64 | std::cout << "t Select next template from collection" << std::endl; 65 | std::cout << "u Update Hough parameters from current settings" << std::endl; 66 | std::cout << "v Create video from files in movie folder" << std::endl; 67 | std::cout << "? Display this help info" << std::endl; 68 | std::cout << std::endl; 69 | } 70 | 71 | 72 | void Knobs::handle_keypress(const char ckey) 73 | { 74 | bool is_valid = true; 75 | 76 | is_op_required = false; 77 | 78 | switch (ckey) 79 | { 80 | case '1': 81 | case '2': 82 | case '3': 83 | case '4': 84 | { 85 | // convert to channel code 0,1,2,3 86 | set_channel(ckey - '1'); 87 | break; 88 | } 89 | case '7': set_output_mode(Knobs::OUT_RAW); break; 90 | case '8': set_output_mode(Knobs::OUT_GRAD); break; 91 | case '9': set_output_mode(Knobs::OUT_PREP); break; 92 | case '0': set_output_mode(Knobs::OUT_COLOR); break; 93 | case '+': inc_clip_limit(); break; 94 | case '_': dec_clip_limit(); break; 95 | case ']': inc_img_scale(); break; 96 | case '[': dec_img_scale(); break; 97 | case '=': 98 | { 99 | inc_pre_blur(); 100 | break; 101 | } 102 | case '-': 103 | { 104 | dec_pre_blur(); 105 | break; 106 | } 107 | case '}': 108 | { 109 | inc_ksize(); 110 | is_op_required = true; 111 | op_id = Knobs::OP_UPDATE; 112 | break; 113 | } 114 | case '{': 115 | { 116 | dec_ksize(); 117 | is_op_required = true; 118 | op_id = Knobs::OP_UPDATE; 119 | break; 120 | } 121 | case 'e': 122 | { 123 | toggle_equ_hist_enabled(); 124 | break; 125 | } 126 | case 'r': 127 | { 128 | is_op_required = true; 129 | op_id = Knobs::OP_RECORD; 130 | toggle_record_enabled(); 131 | break; 132 | } 133 | case 't': 134 | { 135 | is_op_required = true; 136 | op_id = Knobs::OP_TEMPLATE; 137 | break; 138 | } 139 | case 'u': 140 | { 141 | is_op_required = true; 142 | op_id = Knobs::OP_UPDATE; 143 | break; 144 | } 145 | case 'v': 146 | { 147 | is_op_required = true; 148 | op_id = Knobs::OP_MAKE_VIDEO; 149 | break; 150 | } 151 | case '?': 152 | { 153 | is_valid = false; 154 | show_help(); 155 | break; 156 | } 157 | default: 158 | { 159 | is_valid = false; 160 | break; 161 | } 162 | } 163 | 164 | // display settings whenever valid keypress handled 165 | // except if it's an "op required" keypress 166 | if (is_valid && !is_op_required) 167 | { 168 | const std::vector srgb({ "Blue ", "Green", "Red ", "Gray " }); 169 | const std::vector sout({ "Raw ", "Grad ", "Prep ", "Color" }); 170 | std::cout << "Equ=" << is_equ_hist_enabled; 171 | std::cout << " Clip=" << kcliplimit; 172 | std::cout << " Ch=" << srgb[nchannel]; 173 | std::cout << " Blur=" << kpreblur; 174 | std::cout << " Out=" << sout[noutmode]; 175 | std::cout << " Scale=" << vimgscale[nimgscale]; 176 | std::cout << std::endl; 177 | } 178 | } 179 | 180 | 181 | bool Knobs::get_op_flag(int& ropid) 182 | { 183 | bool result = is_op_required; 184 | ropid = op_id; 185 | is_op_required = false; 186 | return result; 187 | } -------------------------------------------------------------------------------- /Knobs.h: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright(c) 2018 Mark Whitney 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #ifndef KNOBS_H_ 24 | #define KNOBS_H_ 25 | 26 | #include 27 | 28 | class Knobs 29 | { 30 | public: 31 | 32 | enum 33 | { 34 | ALL_CHANNELS = 3, 35 | }; 36 | 37 | enum 38 | { 39 | OUT_RAW = 0, 40 | OUT_GRAD, 41 | OUT_PREP, 42 | OUT_COLOR, 43 | }; 44 | 45 | enum 46 | { 47 | OP_NONE = 0, 48 | OP_TEMPLATE, 49 | OP_UPDATE, 50 | OP_RECORD, 51 | OP_MAKE_VIDEO, 52 | }; 53 | 54 | Knobs(); 55 | virtual ~Knobs(); 56 | 57 | void show_help(void) const; 58 | 59 | bool get_op_flag(int& ropid); 60 | 61 | bool get_equ_hist_enabled(void) const { return is_equ_hist_enabled; } 62 | void toggle_equ_hist_enabled(void) { is_equ_hist_enabled = !is_equ_hist_enabled; } 63 | 64 | bool get_record_enabled(void) const { return is_record_enabled; } 65 | void toggle_record_enabled(void) { is_record_enabled = !is_record_enabled; } 66 | 67 | int get_pre_blur(void) const { return kpreblur; } 68 | void inc_pre_blur(void) { kpreblur = (kpreblur < 35) ? kpreblur + 2 : kpreblur; } 69 | void dec_pre_blur(void) { kpreblur = (kpreblur > 1) ? kpreblur - 2 : kpreblur; }; 70 | 71 | int get_clip_limit(void) const { return kcliplimit; } 72 | void inc_clip_limit(void) { kcliplimit = (kcliplimit < 20) ? kcliplimit + 1 : kcliplimit; } 73 | void dec_clip_limit(void) { kcliplimit = (kcliplimit > 0) ? kcliplimit - 1 : kcliplimit; }; 74 | 75 | int get_channel(void) const { return nchannel; } 76 | void set_channel(const int n) { nchannel = n; } 77 | 78 | int get_output_mode(void) const { return noutmode; } 79 | void set_output_mode(const int n) { noutmode = n; } 80 | 81 | double get_img_scale(void) const { return vimgscale[nimgscale]; } 82 | void inc_img_scale(void) { nimgscale = (nimgscale < (vimgscale.size() - 1)) ? nimgscale + 1 : nimgscale; } 83 | void dec_img_scale(void) { nimgscale = (nimgscale > 0) ? nimgscale - 1 : nimgscale; }; 84 | 85 | double get_ksize(void) const { return vksize[nksize]; } 86 | void inc_ksize(void) { nksize = (nksize < (vksize.size() - 1)) ? nksize + 1 : nksize; } 87 | void dec_ksize(void) { nksize = (nksize > 0) ? nksize - 1 : nksize; }; 88 | 89 | void handle_keypress(const char c); 90 | 91 | private: 92 | 93 | // One-shot flag for signaling when extra operation needs to be done 94 | // before continuing image processing loop 95 | bool is_op_required; 96 | 97 | // Flag for enabling histogram equalization 98 | bool is_equ_hist_enabled; 99 | 100 | // Flag for enabling recording 101 | bool is_record_enabled; 102 | 103 | // Amount of Gaussian blurring in preprocessing step 104 | int kpreblur; 105 | 106 | // Clip limit for CLAHE 107 | int kcliplimit; 108 | 109 | // Channel selection (B,G,R, or Gray) 110 | int nchannel; 111 | 112 | // Output mode (raw, mask, or color) 113 | int noutmode; 114 | 115 | // Type of operation that is required 116 | int op_id; 117 | 118 | // Index of currently selected scale factor 119 | size_t nimgscale; 120 | 121 | // Index of currently selected Sobel kernel size 122 | size_t nksize; 123 | 124 | // Array of supported scale factors 125 | std::vector vimgscale; 126 | 127 | // Array of supported Sobel kernel sizes 128 | std::vector vksize; 129 | }; 130 | 131 | #endif // KNOBS_H_ 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mark Whitney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BGHMatcher 2 | 3 | Experiments with the Generalized Hough Transform. 4 | 5 | This repository has an implementation of the the classic Generalized Hough (GH) algorithm. 6 | Sobel filters calculate the X and Y derivatives. A Cartesian-to-Polar operation converts 7 | the X and Y derivatives to magnitude and angle. Gradients with magnitudes below an 8 | arbitrary threshold are discarded. The angles are converted to 8-bit integer codes 9 | for quick lookup in a voting table. 10 | 11 | Some implementations of the GH algorithm use a Canny Edge Detector as a pre-processing step and 12 | sub-divide an image into voting bins. It can be difficult to tune the Canny detector and bin size. 13 | In this implementation, the gradient calculations are blurry and the edges are "fuzzy". This 14 | creates a lot of redundant votes. In practice, this creates well-defined maxima. The maxima 15 | are located at single pixels rather than at voting bins, which may be associated with multiple 16 | pixels. Blurry gradients might also provide more tolerance to variations in scale and 17 | rotation when finding matches in the target image. 18 | 19 | # Installation 20 | 21 | The project compiles in the Community edition of Visual Studio 2019 (toolset v142). 22 | It uses the Windows pre-built OpenCV 4.5.3 libraries extracted to **c:\opencv-4.5.3**. 23 | I just copied the appropriate OpenCV DLLs to wherever I had my executables. It creates a 24 | command-line Windows executable. I have tested it on a Windows 10 Home (22H2) 64-bit machine. 25 | 26 | ## Camera 27 | 28 | I tested with a Logitech c270. It was the cheapest one I could find that I could purchase locally. 29 | It was plug-and-play. 30 | 31 | ## Video 32 | 33 | Click the image for YouTube demo video: 34 | 35 | [![BGHMatcher Demo Video](http://img.youtube.com/vi/heNQ9mr__L8/0.jpg)](http://www.youtube.com/watch?v=heNQ9mr__L8) 36 | 37 | 38 | -------------------------------------------------------------------------------- /bghmatcher.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "bghmatcher", "bghmatcher.vcxproj", "{8E87C65D-383F-4108-8015-D7BED81B9290}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Debug|x64.ActiveCfg = Debug|x64 17 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Debug|x64.Build.0 = Debug|x64 18 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Debug|x86.ActiveCfg = Debug|Win32 19 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Debug|x86.Build.0 = Debug|Win32 20 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Release|x64.ActiveCfg = Release|x64 21 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Release|x64.Build.0 = Release|x64 22 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Release|x86.ActiveCfg = Release|Win32 23 | {8E87C65D-383F-4108-8015-D7BED81B9290}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /bghmatcher.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {8E87C65D-383F-4108-8015-D7BED81B9290} 23 | Win32Proj 24 | 10.0 25 | 26 | 27 | 28 | Application 29 | true 30 | v142 31 | 32 | 33 | Application 34 | false 35 | v142 36 | 37 | 38 | Application 39 | true 40 | v142 41 | 42 | 43 | Application 44 | false 45 | v142 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | true 67 | 68 | 69 | true 70 | 71 | 72 | 73 | WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) 74 | MultiThreadedDebugDLL 75 | Level3 76 | ProgramDatabase 77 | Disabled 78 | 79 | 80 | MachineX86 81 | true 82 | Windows 83 | 84 | 85 | 86 | 87 | WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) 88 | MultiThreadedDLL 89 | Level3 90 | ProgramDatabase 91 | 92 | 93 | MachineX86 94 | true 95 | Windows 96 | true 97 | true 98 | 99 | 100 | 101 | 102 | C:\opencv-4.5.3\opencv\build\include;%(AdditionalIncludeDirectories) 103 | 104 | 105 | C:\opencv-4.5.3\opencv\build\x64\vc14\lib;%(AdditionalLibraryDirectories) 106 | opencv_world453d.lib;%(AdditionalDependencies) 107 | 108 | 109 | 110 | 111 | C:\opencv-4.5.3\opencv\build\include;%(AdditionalIncludeDirectories) 112 | 113 | 114 | C:\opencv-4.5.3\opencv\build\x64\vc14\lib;%(AdditionalLibraryDirectories) 115 | opencv_world453.lib;%(AdditionalDependencies) 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /bghmatcher.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | Source Files 29 | 30 | 31 | 32 | 33 | Header Files 34 | 35 | 36 | Header Files 37 | 38 | 39 | Header Files 40 | 41 | 42 | -------------------------------------------------------------------------------- /data/bottle_20perc_top_b_on_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwgit00/BGHMatcher/0e9e0b074bdfc371900155dd248897cda5e89016/data/bottle_20perc_top_b_on_w.png -------------------------------------------------------------------------------- /data/circle_b_on_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwgit00/BGHMatcher/0e9e0b074bdfc371900155dd248897cda5e89016/data/circle_b_on_w.png -------------------------------------------------------------------------------- /data/panda_face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwgit00/BGHMatcher/0e9e0b074bdfc371900155dd248897cda5e89016/data/panda_face.png -------------------------------------------------------------------------------- /data/ring_b_on_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwgit00/BGHMatcher/0e9e0b074bdfc371900155dd248897cda5e89016/data/ring_b_on_w.png -------------------------------------------------------------------------------- /data/stars_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwgit00/BGHMatcher/0e9e0b074bdfc371900155dd248897cda5e89016/data/stars_main.png -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright(c) 2018 Mark Whitney 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #include "Windows.h" 24 | #include "opencv2/imgproc.hpp" 25 | #include "opencv2/imgcodecs.hpp" 26 | #include "opencv2/highgui.hpp" 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "BGHMatcher.h" 34 | #include "Knobs.h" 35 | #include "util.h" 36 | 37 | 38 | #define MATCH_DISPLAY_THRESHOLD (0.8) // arbitrary 39 | #define MOVIE_PATH ".\\movie\\" // user may need to create or change this 40 | #define DATA_PATH ".\\data\\" // user may need to change this 41 | 42 | 43 | using namespace cv; 44 | 45 | 46 | #define SCA_BLACK (cv::Scalar(0,0,0)) 47 | #define SCA_RED (cv::Scalar(0,0,255)) 48 | #define SCA_GREEN (cv::Scalar(0,255,0)) 49 | #define SCA_BLUE (cv::Scalar(255,0,0)) 50 | #define SCA_MAGENTA (cv::Scalar(255,0,255)) 51 | #define SCA_YELLOW (cv::Scalar(0,255,255)) 52 | #define SCA_WHITE (cv::Scalar(255,255,255)) 53 | 54 | 55 | Mat template_image; 56 | const char * stitle = "BGHMatcher"; 57 | const double default_mag_thr = 0.2; 58 | int n_record_ctr = 0; 59 | size_t nfile = 0; 60 | 61 | const std::vector vfiles = 62 | { 63 | { default_mag_thr, 1.5, "circle_b_on_w.png" }, 64 | { default_mag_thr, 1.5, "ring_b_on_w.png" }, 65 | { default_mag_thr, 3.0, "bottle_20perc_top_b_on_w.png" }, 66 | { default_mag_thr, 3.5, "panda_face.png" }, 67 | { default_mag_thr, 3.0, "stars_main.png" } 68 | }; 69 | 70 | 71 | bool wait_and_check_keys(Knobs& rknobs) 72 | { 73 | bool result = true; 74 | 75 | int nkey = waitKey(1); 76 | char ckey = static_cast(nkey); 77 | 78 | // check that a keypress has been returned 79 | if (nkey >= 0) 80 | { 81 | if (ckey == 27) 82 | { 83 | // done if ESC has been pressed 84 | result = false; 85 | } 86 | else 87 | { 88 | rknobs.handle_keypress(ckey); 89 | } 90 | } 91 | 92 | return result; 93 | } 94 | 95 | 96 | void image_output( 97 | Mat& rimg, 98 | const double qmax, 99 | const Point& rptmax, 100 | const Knobs& rknobs, 101 | BGHMatcher::T_ghough_table& rtable) 102 | { 103 | const int h_score = 16; 104 | const double scale = rtable.params.scale; 105 | 106 | // determine size of "target" box 107 | // it will vary depending on the scale parameter 108 | Size rsz = rtable.img_sz; 109 | rsz.height *= scale; 110 | rsz.width *= scale; 111 | Point corner = { rptmax.x - rsz.width / 2, rptmax.y - rsz.height / 2 }; 112 | 113 | // format score string for viewer (#.##) 114 | std::ostringstream oss; 115 | oss << std::fixed << std::setprecision(2) << (qmax / rtable.total_votes); 116 | 117 | // draw current template in upper right corner 118 | Mat bgr_template_img; 119 | cvtColor(template_image, bgr_template_img, COLOR_GRAY2BGR); 120 | Size osz = rimg.size(); 121 | Size tsz = template_image.size(); 122 | Rect roi = cv::Rect(osz.width - tsz.width, 0, tsz.width, tsz.height); 123 | bgr_template_img.copyTo(rimg(roi)); 124 | 125 | // draw colored box around template image (magenta if recording) 126 | cv::Scalar box_color = (rknobs.get_record_enabled()) ? SCA_MAGENTA : SCA_BLUE; 127 | rectangle(rimg, { osz.width - tsz.width, 0 }, { osz.width, tsz.height }, box_color, 2); 128 | 129 | // draw black background box then draw text score on top of it 130 | rectangle(rimg, { corner.x,corner.y - h_score, 40, h_score }, SCA_BLACK, -1); 131 | putText(rimg, oss.str(), { corner.x,corner.y - 4 }, FONT_HERSHEY_PLAIN, 1.0, SCA_WHITE, 1); 132 | 133 | // draw rectangle around best match with yellow dot at center 134 | rectangle(rimg, { corner.x, corner.y, rsz.width, rsz.height }, SCA_GREEN, 2); 135 | circle(rimg, rptmax, 2, SCA_YELLOW, -1); 136 | 137 | // save each frame to a file if recording 138 | if (rknobs.get_record_enabled()) 139 | { 140 | std::ostringstream osx; 141 | osx << MOVIE_PATH << "img_" << std::setfill('0') << std::setw(5) << n_record_ctr << ".png"; 142 | imwrite(osx.str(), rimg); 143 | n_record_ctr++; 144 | } 145 | 146 | cv::imshow(stitle, rimg); 147 | } 148 | 149 | 150 | void reload_template( 151 | const Knobs& rknobs, 152 | BGHMatcher::T_ghough_table& rtable, 153 | const T_file_info& rinfo) 154 | { 155 | int kblur = rknobs.get_pre_blur(); 156 | int ksobel = rknobs.get_ksize(); 157 | std::string spath = DATA_PATH + rinfo.sname; 158 | template_image = imread(spath, IMREAD_GRAYSCALE); 159 | 160 | if (template_image.size().empty()) 161 | { 162 | std::cout << "** FAILED TO LOAD TEMPLATE FILE: " << spath << std::endl; 163 | std::cout << "** PLEASE CHECK YOUR FILE PATH!!!" << std::endl; 164 | exit(-1); 165 | } 166 | 167 | BGHMatcher::init_ghough_table_from_img( 168 | template_image, rtable, { kblur, ksobel, rinfo.img_scale, rinfo.mag_thr, 8.0 }); 169 | 170 | std::cout << "Loaded template (blur,sobel) = " << kblur << "," << ksobel << "): "; 171 | std::cout << rinfo.sname << " " << rtable.total_votes << std::endl; 172 | } 173 | 174 | 175 | void loop(void) 176 | { 177 | Knobs theKnobs; 178 | int op_id; 179 | 180 | double qmax; 181 | Size capture_size; 182 | Point ptmax; 183 | 184 | Mat img; 185 | Mat img_viewer; 186 | Mat img_gray; 187 | Mat img_grad; 188 | Mat img_channels[3]; 189 | Mat img_match; 190 | 191 | BGHMatcher::T_ghough_table theGHData; 192 | Ptr pCLAHE = createCLAHE(); 193 | 194 | // need a 0 as argument 195 | VideoCapture vcap(0); 196 | if (!vcap.isOpened()) 197 | { 198 | std::cout << "Failed to open VideoCapture device!" << std::endl; 199 | /////// 200 | return; 201 | /////// 202 | } 203 | 204 | // camera is ready so grab a first image to determine its full size 205 | vcap >> img; 206 | capture_size = img.size(); 207 | 208 | // use dummy operation to print initial Knobs settings message 209 | // and force template to be loaded at start of loop 210 | theKnobs.handle_keypress('0'); 211 | 212 | // initialize lookup table 213 | reload_template(theKnobs, theGHData, vfiles[nfile]); 214 | 215 | // and the image processing loop is running... 216 | bool is_running = true; 217 | 218 | while (is_running) 219 | { 220 | int kblur = theKnobs.get_pre_blur(); 221 | int ksobel = theKnobs.get_ksize(); 222 | 223 | // check for any operations that 224 | // might halt or reset the image processing loop 225 | if (theKnobs.get_op_flag(op_id)) 226 | { 227 | if (op_id == Knobs::OP_TEMPLATE || op_id == Knobs::OP_UPDATE) 228 | { 229 | // changing the template will advance the file index 230 | if (op_id == Knobs::OP_TEMPLATE) 231 | { 232 | nfile = (nfile + 1) % vfiles.size(); 233 | } 234 | reload_template(theKnobs, theGHData, vfiles[nfile]); 235 | } 236 | else if (op_id == Knobs::OP_RECORD) 237 | { 238 | if (theKnobs.get_record_enabled()) 239 | { 240 | // reset recording frame counter 241 | std::cout << "RECORDING STARTED" << std::endl; 242 | n_record_ctr = 0; 243 | } 244 | else 245 | { 246 | std::cout << "RECORDING STOPPED" << std::endl; 247 | } 248 | } 249 | else if (op_id == Knobs::OP_MAKE_VIDEO) 250 | { 251 | std::cout << "CREATING VIDEO FILE..." << std::endl; 252 | std::list listOfPNG; 253 | get_dir_list(MOVIE_PATH, "*.png", listOfPNG); 254 | bool is_ok = make_video(15.0, MOVIE_PATH, 255 | "movie.mov", 256 | VideoWriter::fourcc('M', 'P', '4', 'V'), 257 | listOfPNG); 258 | std::cout << ((is_ok) ? "SUCCESS!" : "FAILURE!") << std::endl; 259 | } 260 | } 261 | 262 | // grab image 263 | vcap >> img; 264 | 265 | // apply the current image scale setting 266 | double img_scale = theKnobs.get_img_scale(); 267 | Size viewer_size = Size( 268 | static_cast(capture_size.width * img_scale), 269 | static_cast(capture_size.height * img_scale)); 270 | resize(img, img_viewer, viewer_size); 271 | 272 | // apply the current channel setting 273 | int nchan = theKnobs.get_channel(); 274 | if (nchan == Knobs::ALL_CHANNELS) 275 | { 276 | // combine all channels into grayscale 277 | cvtColor(img_viewer, img_gray, COLOR_BGR2GRAY); 278 | } 279 | else 280 | { 281 | // select only one BGR channel 282 | split(img_viewer, img_channels); 283 | img_gray = img_channels[nchan]; 284 | } 285 | 286 | // apply the current histogram equalization setting 287 | if (theKnobs.get_equ_hist_enabled()) 288 | { 289 | double c = theKnobs.get_clip_limit(); 290 | pCLAHE->setClipLimit(c); 291 | pCLAHE->apply(img_gray, img_gray); 292 | } 293 | 294 | // apply the current blur setting 295 | if (kblur > 1) 296 | { 297 | GaussianBlur(img_gray, img_gray, { kblur, kblur }, 0); 298 | } 299 | 300 | // create image of encoded Sobel gradient orientations from blurred input image 301 | // then apply Generalized Hough transform and locate maximum (best match) 302 | BGHMatcher::create_masked_gradient_orientation_img(img_gray, img_grad, theGHData.params); 303 | BGHMatcher::apply_ghough_transform_allpix(img_grad, img_match, theGHData); 304 | 305 | minMaxLoc(img_match, nullptr, &qmax, nullptr, &ptmax); 306 | 307 | // apply the current output mode 308 | // content varies but all final output images are BGR 309 | switch (theKnobs.get_output_mode()) 310 | { 311 | case Knobs::OUT_RAW: 312 | { 313 | // show the raw match result 314 | Mat temp_8U; 315 | normalize(img_match, img_match, 0, 255, cv::NORM_MINMAX); 316 | img_match.convertTo(temp_8U, CV_8U); 317 | cvtColor(temp_8U, img_viewer, COLOR_GRAY2BGR); 318 | break; 319 | } 320 | case Knobs::OUT_GRAD: 321 | { 322 | // display encoded gradient image 323 | // show red overlay of any matches that exceed arbitrary threshold 324 | Mat match_mask; 325 | std::vector> contours; 326 | normalize(img_grad, img_grad, 0, 255, cv::NORM_MINMAX); 327 | cvtColor(img_grad, img_viewer, COLOR_GRAY2BGR); 328 | normalize(img_match, img_match, 0, 1, cv::NORM_MINMAX); 329 | match_mask = (img_match > MATCH_DISPLAY_THRESHOLD); 330 | findContours(match_mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); 331 | drawContours(img_viewer, contours, -1, SCA_RED, -1, LINE_8, noArray(), INT_MAX); 332 | break; 333 | } 334 | case Knobs::OUT_PREP: 335 | { 336 | cvtColor(img_gray, img_viewer, COLOR_GRAY2BGR); 337 | break; 338 | } 339 | case Knobs::OUT_COLOR: 340 | default: 341 | { 342 | // no extra output processing 343 | break; 344 | } 345 | } 346 | 347 | // always show best match contour and target dot on BGR image 348 | image_output(img_viewer, qmax, ptmax, theKnobs, theGHData); 349 | 350 | // handle keyboard events and end when ESC is pressed 351 | is_running = wait_and_check_keys(theKnobs); 352 | } 353 | 354 | // when everything is done, release the capture device and windows 355 | vcap.release(); 356 | destroyAllWindows(); 357 | } 358 | 359 | 360 | int main(int argc, char** argv) 361 | { 362 | loop(); 363 | return 0; 364 | } 365 | -------------------------------------------------------------------------------- /util.cpp: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright(c) 2018 Mark Whitney 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #include "Windows.h" 24 | 25 | #include "opencv2/highgui.hpp" 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | #include "util.h" 32 | 33 | 34 | void get_dir_list( 35 | const std::string& rsdir, 36 | const std::string& rspattern, 37 | std::list& listOfFiles) 38 | { 39 | std::string s = rsdir + "\\" + rspattern; 40 | 41 | WIN32_FIND_DATA search_data; 42 | memset(&search_data, 0, sizeof(WIN32_FIND_DATA)); 43 | 44 | HANDLE handle = FindFirstFile(s.data(), &search_data); 45 | 46 | while (handle != INVALID_HANDLE_VALUE) 47 | { 48 | std::string sfile(search_data.cFileName); 49 | listOfFiles.push_back(rsdir + "\\" + sfile); 50 | if (FindNextFile(handle, &search_data) == FALSE) 51 | { 52 | break; 53 | } 54 | } 55 | 56 | FindClose(handle); 57 | } 58 | 59 | 60 | bool make_video( 61 | const double fps, 62 | const std::string& rspath, 63 | const std::string& rsname, 64 | const int iFOURCC, 65 | const std::list& rListOfPNG) 66 | { 67 | bool result = false; 68 | 69 | // determine size of frames from first image in list 70 | // they should all be the same size 71 | const std::string& rs = rListOfPNG.front(); 72 | cv::Mat img = cv::imread(rs); 73 | cv::Size img_sz = img.size(); 74 | 75 | std::string sname = rspath + "\\" + rsname; 76 | 77 | // build movie from separate frames 78 | cv::VideoWriter vw = cv::VideoWriter(sname, iFOURCC, fps, img_sz); 79 | 80 | if (vw.isOpened()) 81 | { 82 | for (const auto& r : rListOfPNG) 83 | { 84 | cv::Mat img = cv::imread(r); 85 | vw.write(img); 86 | } 87 | 88 | result = true; 89 | } 90 | 91 | return result; 92 | } 93 | -------------------------------------------------------------------------------- /util.h: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright(c) 2018 Mark Whitney 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #ifndef UTIL_H_ 24 | #define UTIL_H_ 25 | 26 | #include 27 | #include 28 | 29 | typedef struct 30 | { 31 | double mag_thr; 32 | double img_scale; 33 | std::string sname; 34 | } T_file_info; 35 | 36 | // Get list of all files in a directory that match a pattern 37 | void get_dir_list( 38 | const std::string& rsdir, 39 | const std::string& rspattern, 40 | std::list& listOfFiles); 41 | 42 | // Use OpenCV routine to make video from a list of files. 43 | // Here are some extension and FOURCC combos that should work in Windows: 44 | // "movie.wmv", CV_FOURCC('W', 'M', 'V', '2') 45 | // "movie.avi", CV_FOURCC('M', 'J', 'P', 'G') 46 | // "movie.avi", CV_FOURCC('M', 'P', '4', '2') 47 | // "movie.avi", CV_FOURCC('M', 'P', '4', 'V') -- error messages but VLC can play it 48 | // "movie.mov", CV_FOURCC('M', 'P', '4', 'V') -- error messages but VLC can play it, iMovie can import it 49 | // "movie.mov", CV_FOURCC('M', 'J', 'P', 'G') -- error messages but VLC can play it, iMovie can import it 50 | bool make_video( 51 | const double fps, 52 | const std::string& rspath, 53 | const std::string& rsname, 54 | const int iFOURCC, 55 | const std::list& rListOfPNG); 56 | 57 | #endif // UTIL_H_ 58 | --------------------------------------------------------------------------------