pax_global_header00006660000000000000000000000064147666026540014532gustar00rootroot0000000000000052 comment=03596d88bf839ec6a799b3f8a6648a722e3f786e mrgingham-1.26/000077500000000000000000000000001476660265400134335ustar00rootroot00000000000000mrgingham-1.26/.gitignore000066400000000000000000000005211476660265400154210ustar00rootroot00000000000000*.o *.so* *.d *~ mrgingham test-find-grid-from-points test-dump-chessboard-corners test-dump-blobs mrgingham-observe-pixel-uncertainty.pod mrgingham-from-image core cscope.* analyses/ debian/.debhelper/ debian/files debian/*.log debian/*.substvars debian/libmrgingham-dev/ debian/libmrgingham1/ debian/tmp/ test/data/ *.docstring.h mrgingham-1.26/ChESS.c000066400000000000000000000110611476660265400145030ustar00rootroot00000000000000/* This is the reference implementation from this paper: https://arxiv.org/abs/1301.5491 Dima made a few modifications. Obtained from here: http://www-sigproc.eng.cam.ac.uk/Main/SB476Chess There's a more full-featured GPL-licensed implementation on that page */ /** * The ChESS corner detection algorithm * * Copyright 2010-2012 Stuart Bennett * * 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. */ #include #include /** * Perform the ChESS corner detection algorithm with a 5 px sampling radius * * @param response output response image. Densely-packed * signed-16-bits-per-pixel image if size (w,h). Densely- * packed means the stride doesn't apply * @param image input image. Assumed 8 bits (1 byte) per pixel. Not * densely-packed: the stride applies * @param w image width * @param h image height * @param stride the length (in bytes) of each row in memory of the input * image. If stored densely, w == stride */ __attribute__((visibility("default"))) void mrgingham_ChESS_response_5( int16_t* restrict response, const uint8_t* restrict image, int w, int h, int stride ) { int x, y; // funny bounds due to sampling ring radius (5) and border of previously applied blur (2) for (y = 7; y < h - 7; y++) for (x = 7; x < w - 7; x++) { const unsigned offset_input = x + y * stride; const unsigned offset_response = x + y * w; uint8_t circular_sample[16]; circular_sample[2] = image[offset_input - 2 - 5 * stride]; circular_sample[1] = image[offset_input - 5 * stride]; circular_sample[0] = image[offset_input + 2 - 5 * stride]; circular_sample[8] = image[offset_input - 2 + 5 * stride]; circular_sample[9] = image[offset_input + 5 * stride]; circular_sample[10] = image[offset_input + 2 + 5 * stride]; circular_sample[3] = image[offset_input - 4 - 4 * stride]; circular_sample[15] = image[offset_input + 4 - 4 * stride]; circular_sample[7] = image[offset_input - 4 + 4 * stride]; circular_sample[11] = image[offset_input + 4 + 4 * stride]; circular_sample[4] = image[offset_input - 5 - 2 * stride]; circular_sample[14] = image[offset_input + 5 - 2 * stride]; circular_sample[6] = image[offset_input - 5 + 2 * stride]; circular_sample[12] = image[offset_input + 5 + 2 * stride]; circular_sample[5] = image[offset_input - 5]; circular_sample[13] = image[offset_input + 5]; // purely horizontal local_mean samples uint16_t local_mean = (image[offset_input - 1] + image[offset_input] + image[offset_input + 1]) * 16 / 3; uint16_t sum_response = 0; uint16_t diff_response = 0; uint16_t mean = 0; int sub_idx; for (sub_idx = 0; sub_idx < 4; ++sub_idx) { uint8_t a = circular_sample[sub_idx]; uint8_t b = circular_sample[sub_idx + 4]; uint8_t c = circular_sample[sub_idx + 8]; uint8_t d = circular_sample[sub_idx + 12]; sum_response += abs(a - b + c - d); diff_response += abs(a - c) + abs(b - d); mean += a + b + c + d; } response[offset_response] = sum_response - diff_response - abs(mean - local_mean); } } mrgingham-1.26/ChESS.h000066400000000000000000000021511476660265400145100ustar00rootroot00000000000000#pragma once /* This is the reference implementation from this paper: https://arxiv.org/abs/1301.5491 Dima made a few modifications. Obtained from here: http://www-sigproc.eng.cam.ac.uk/Main/SB476Chess There's a more full-featured GPL-licensed implementation on that page */ /** * Perform the ChESS corner detection algorithm with a 5 px sampling radius * * @param response output response image. Densely-packed * signed-16-bits-per-pixel image if size (w,h). Densely- * packed means the stride doesn't apply * @param image input image. Assumed 8 bits (1 byte) per pixel. Not * densely-packed: the stride applies * @param w image width * @param h image height * @param stride the length (in bytes) of each row in memory of the input * image. If stored densely, w == stride */ void mrgingham_ChESS_response_5( int16_t* response, const uint8_t* image, int w, int h, int stride); mrgingham-1.26/ChESS_response_5.docstring000066400000000000000000000012071476660265400204200ustar00rootroot00000000000000Runs the ChESS detector to compute a "cornerness" response SYNOPSIS response = mrgingham.ChESS_response_5(image) This wraps the reference implementation described in this paper: https://arxiv.org/abs/1301.5491 Implementation obtained here: http://www-sigproc.eng.cam.ac.uk/Main/SB476Chess Copyright 2010-2012 Stuart Bennett This function supports broadcasting fully. ARGMENTS - image: numpy array of shape (H,W) and dtype np.uint8. This is the image we're processing. Must be densely-stored, grayscale image RETURNED VALUE A numpy array of shape (H,W): the same shape as the input. Unlike the input, this has dtype np.uint16 mrgingham-1.26/Makefile000066400000000000000000000102011476660265400150650ustar00rootroot00000000000000include choose_mrbuild.mk include $(MRBUILD_MK)/Makefile.common.header PROJECT_NAME := mrgingham ABI_VERSION := 2 TAIL_VERSION := 1 DIST_BIN := mrgingham mrgingham-observe-pixel-uncertainty mrgingham-rotate-corners DIST_MAN := $(addsuffix .1,$(DIST_BIN)) # I want the tool I ship to be called "mrgingham", but I already have # mrgingham.cc: it's a part of the LIBRARY mrgingham: mrgingham-from-image cp $< $@ EXTRA_CLEAN += mrgingham all: mrgingham BIN_SOURCES := mrgingham-from-image.cc BIN_SOURCES += test-dump-chessboard-corners.cc test-dump-blobs.cc test-find-grid-from-points.cc LIB_SOURCES := find_grid.cc find_blobs.cc find_chessboard_corners.cc mrgingham.cc ChESS.c # The opencv people (or maybe the Debian people?) have renamed the opencv.pc # file in opencv 4. So now I look for both version 4 and the default. What will # happen with opencv5? We'll see! CXXFLAGS_CV := $(shell pkg-config --cflags opencv4 2>/dev/null || pkg-config --cflags opencv 2>/dev/null) LDLIBS_CV := $(shell pkg-config --libs opencv4 2>/dev/null || pkg-config --libs opencv 2>/dev/null) CCXXFLAGS += $(CXXFLAGS_CV) LDLIBS += $(LDLIBS_CV) -lpthread CCXXFLAGS += -fvisibility=hidden CFLAGS += -std=gnu99 CCXXFLAGS += -Wno-unused-function -Wno-missing-field-initializers -Wno-unused-parameter -Wno-strict-aliasing -Wno-int-to-pointer-cast -Wno-unused-variable # On opencv4 I do this: ifneq ($(wildcard /usr/include/opencv4),) CCXXFLAGS += -D CV_LOAD_IMAGE_GRAYSCALE=cv::IMREAD_GRAYSCALE endif test: test/test--mrgingham-rotate-corners .PHONY: test DIST_INCLUDE := mrgingham.hh point.hh # I construct the README.org from the template. The only thing I do is to insert # the manpages. Note that this is more complicated than it looks: # # 1. The documentation is extracted into a .pod by make-pod-from-help # 2. This documentation is stripped out here wih pod2text, and included in the # README. This README is an org-mode file, and the README.template.org # container included the manpage text inside a #+BEGIN_EXAMPLE/#+END_EXAMPLE. # So the manpages are treated as a verbatim, unformatted text blob # 3. Further down, the same POD is converted to a manpage via pod2man define MAKE_README = BEGIN \ { \ for $$a (@ARGV) \ { \ $$base = $$a =~ s/\.pod$$//r; \ $$c{$$base} = `pod2text $$a | mawk "/REPOSITORY/{exit} {print}"`; \ } \ } \ \ while() \ { \ print s/xxx-manpage-(.*?)-xxx/$$c{$$1}/gr; \ } endef README.org: README.template.org $(DIST_BIN:%=%.pod) < $(filter README%,$^) perl -e '$(MAKE_README)' $(filter-out README%,$^) > $@ all: README.org %.1: %.pod pod2man --center="mrgingham: chessboard corner finder" --name=MRGINGHAM --release="mrgingham $(VERSION)" --section=1 $^ $@ %.pod: % $(MRBUILD_BIN)/make-pod-from-help $< > $@.tmp cat footer.pod >> $@.tmp mv $@.tmp $@ EXTRA_CLEAN += *.1 $(addsuffix .pod,$(DIST_BIN)) README.org %.usage.h: %.usage < $^ sed 's/\\/\\\\/g; s/"/\\"/g; s/^/"/; s/$$/\\n"/;' > $@ EXTRA_CLEAN += *.usage.h mrgingham-from-image.o: mrgingham.usage.h ########## chessboard-generating rules ########## I can "make chessboard.10x14.pdf" and it'll do the right thing chessboard.%.fig: generate-chessboard-fig.py ./$< $(subst x, ,$*) > $@.tmp mv $@.tmp $@ %.pdf: %.fig fig2dev -L pdf $< > $@.tmp mv $@.tmp $@ ########## python stuff mrgingham_pywrap.o: CCXXFLAGS += $(PY_MRBUILD_CFLAGS) mrgingham_pywrap_cplusplus_bridge.o: CCXXFLAGS += -fPIC mrgingham_pywrap.o: $(addsuffix .h,$(wildcard *.docstring)) # The python library is called "mrgingham.$(SO)". This is confusing, but is the # best I could do. Because I want to be able to "import mrgingham"; and the # normal way of creating a "mrgingham" subdirectory for all the python stuff # doesn't work here: I already have a directory entry called "mrgingham"; it's # the main commandline tool. mrgingham$(PY_EXT_SUFFIX): mrgingham_pywrap.o mrgingham_pywrap_cplusplus_bridge.o libmrgingham.$(SO) $(PY_MRBUILD_LINKER) $(PY_MRBUILD_LDFLAGS) $^ -o $@ DIST_PY3_MODULES := mrgingham$(PY_EXT_SUFFIX) all: mrgingham$(PY_EXT_SUFFIX) include $(MRBUILD_MK)/Makefile.common.footer mrgingham-1.26/README.org000066400000000000000000000510441476660265400151050ustar00rootroot00000000000000* SYNOPSIS Detect calibration boards in observed camera images #+BEGIN_EXAMPLE $ mrgingham /tmp/image*.jpg # filename x y level /tmp/image1.jpg - - /tmp/image2.jpg 1385.433000 1471.719000 0 /tmp/image2.jpg 1483.597000 1469.825000 0 /tmp/image2.jpg 1582.086000 1467.561000 1 ... $ mrgingham /tmp/image.jpg | vnl-filter -p x,y | feedgnuplot --domain --lines --points --image /tmp/image.jpg [ image pops up with the detected grid plotted on top ] $ mrgingham /tmp/image.jpg | vnl-filter -p x,y,level | feedgnuplot --domain --with 'linespoints pt 7 ps 2 palette' --tuplesizeall 3 --image /tmp/image.jpg [ fancy image pops up with the detected grid plotted on top, detections colored by their decimation level ] #+END_EXAMPLE * INSTALLATION As of today (2022-02-27), mrgingham is included in the bleeding-edge versions of Debian and Ubuntu. So if you're running at least Debian/testing or the not-yet-published Debian 12 (bookworm) or Ubuntu 22.04 (jammy), you can simply #+begin_src sh apt install mrgingham libmrgingham-dev #+end_src to install the standalone tool and the development library respectively. For older Debian and Ubuntu distros, packages are available in the mrcal repositories. Please see the [[http://mrcal.secretsauce.net/install.html][mrcal installation page.]] If running any other distro, you should build mrgingham from source. First, make sure all the build-time dependencies are installed. These are listed in the [[https://salsa.debian.org/science-team/mrgingham/-/blob/master/debian/control][=debian/control=]] file: #+begin_example Build-Depends: ..., libopencv-dev, libboost-dev, pkg-config, mawk, perl, python3-all, python3-all-dev, python3-numpy #+end_example On a Debian-based distro you can: #+begin_src sh sudo apt install \ libopencv-dev \ libboost-dev \ pkg-config \ mawk \ perl \ python3-all \ python3-all-dev \ python3-numpy #+end_src Replace the package names with their analogues on other distros. Once the dependencies are installed, you #+begin_src sh make #+end_src and then the tool can be invoked with #+begin_src sh ./mrgingham #+end_src * DESCRIPTION Both chessboard and a square grid of circles are supported. Chessboard are the /strongly/ preferred choice, since the circles cannot produce accurate results: we care about the center point, which we are not directly observing. Thus with closeup and oblique views, the reported circle center and the real circle center could be very far away from each other. Because of this, more work was put into the chessboard detector. Use that one. Really. These are both nominally supported by OpenCV, but those implementations are slow and not at all robust, in my experience. The implementations here are much faster and work much better. I /do/ use OpenCV here, but only for some core functionality. Currently mrgingham looks for a /square/ grid of points, with some user-requestable width. Rectangular grids are /not/ supported at this time. ** Approach These tools work in two passes: 1. Look for "interesting" points in the image. The goal is to find all the points we care about, in any order. It is assumed that - there will be many outliers - there will be no outliers interspersed throughout the points we do care about (this isn't an unreasonable requirement: areas between chessboard corners have a solid color) 2. Run a geometric analysis to find a grid in this set of "interesting" points. This will throw out the outliers and it will order the output If we return /any/ data, that means we found a full grid. The geometric search is fairly anal, so if we found a full grid, it's extremely likely that it is "right". Once again: *the current implementation of mrgingham detects only complete chessboards*. *** Chessboards This is based on the feature detector described in this paper: https://arxiv.org/abs/1301.5491 The authors provide a simple MIT-licensed implementation here: http://www-sigproc.eng.cam.ac.uk/Main/SB476Chess This produces an image of detector response. /This/ library then aggregates these responses by looking at local neighborhoods of high responses, and computing the mean of the position of the points in each candidate neighborhood, weighted by the detector response. As noted earlier, I look for a square grid, 10x10 points by default. Here that means 10x10 /internal corners/, meaning a chessboard with 11 /squares/ per side. To ensure robust detections, it is recommended to make the outer squares of the chessboard wider than the inner squares. This would ensure that we see exactly 10 points in a row with the expected spacing. If the outer squares have the same size, the edge of the board might be picked up, and we would see 11 or 12 points instead. A recommended 10x10 pattern can be printed from this file: [[chessboard.10x10.pdf]]. And a recommended 14x14 pattern can be printed from this file: [[chessboard.14x14.pdf]]. The denser chessboard containts more data, so fewer observations will be required for convergence of the calibration algorithm. But a higher-res camera is required to reliably detect the corners. A simple tool to generate these =.pdf= files is included in the mrgingham repository. To make an NxM chessboard figure do =make chessboard.NxM.pdf= in the mrgingham source tree. =N= and =M= must be even integers. Note: today mrgingham requires that =N == M=, but eventually this may be lifted, so this tool does not have this requirement. *** Circles *This isn't recommended, and exists for legacy compatibility only* The circle finder does mostly what the first stage of the OpenCV circle detector does: 1. Find a reasonable intensity threshold 2. Threshold the image 3. Find blobs 4. Return centroid of the blobs This is relatively slow, can get confused by uneven lighting (although CLAHE can take care of that), and is inaccurate: nothing says that the centroid of a blob came from the center of the circle on the calibration board. ** Output representation The =mrgingham= tool produces its output in a [[https://github.com/dkogan/vnlog/][vnlog]] text table. The columns are: - =filename=: path to the image on disk - =x=, =y=: detected pixel coordinates of the chessboard corner - =level=: image level used in detecting this corner. Level 0 means "full-resolution". Level 1 means "half-resolution" and that the noise on this detection has double the standard deviation. Level 2 means "quarter-resolution" and so on. If no chessboard was found in an image, a single record is output: #+begin_example filename - - - #+end_example The corners are output in a consistent order: starting at the top-left, traversing the grid, in the horizontal direction first. Usually, the chessboard is observed by multiple cameras mounted at a similar orientation, so this consistent order is consistent across cameras. However, if some cameras in the set are rotated, their observed chessboard corners will not be consistent anymore: the first corner will be "top-left" in pixel coordinates for both, which is at the top of the chessboard for rightside-up cameras, but the bottom of the chessboard for upside-down cameras. This situation is resolved with the =mrgingham-rotate-corners= tool. It post-processes =mrgingham= output to reorder detections from rotated cameras. See the manpage of that tool for more detail. Eventually the implementation could be extended to be able to uniquely identify each corner, obviating the need for =mrgingham-rotate-corners=, but we're not there today. ** API The user-facing functions live in =mrgingham.hh=. Everything is in C++, mostly because some of the underlying libraries are in C++. All functions return a =bool= to indicate success/failure. All functions put the destination arguments /first/. All functions return the output points in =std::vector=, an ordered list of found points. The inputs are one of - An image filename - An OpenCV matrix: =cv::Mat& image= - A set of detected points, that are unordered, and are a superset of the points we're seeking The prototypes: #+BEGIN_SRC C++ namespace mrgingham { bool find_circle_grid_from_image_array( std::vector& points_out, const cv::Mat& image ); bool find_circle_grid_from_image_file( std::vector& points_out, const char* filename ); bool find_chessboard_from_image_array( std::vector& points_out, const cv::Mat& image, int image_pyramid_level = -1 ); int find_chessboard_from_image_file( std::vector& points_out, const char* filename, int image_pyramid_level = -1 ); bool find_grid_from_points( std::vector& points_out, const std::vector& points ); }; #+END_SRC The arguments should be clear. The only one that needs an explanation is =image_pyramid_level=: - if =image_pyramid_level= is 0 then we just use the image as is. - if =image_pyramid_level= > 0 then we cut down the image by a factor of 2 that many times. So for example, level 3 means each dimension is cut down by a factor of 2^3 = 8 - if =image_pyramid_level= < 0 then we try several levels, taking the first one that produces results ** Applications There're several included applications that exercise the library. The =mrgingham-...= tools are distributed, and their manpages appear below. The =test-...= tools are internal. - =mrgingham= takes in images as globs (with some optional manipulation given on the cmdline), finds the grids, and returns them on stdout, as a vnlog - =mrgingham-observe-pixel-uncertainty= evaluates the distribution of corner detections from repeated observations of a stationary scene - =mrgingham-rotate-corners= corrects chessboard detections produced by rotated cameras by reordering the points in the detection stream - =test-find-grid-from-points= ingests a file that contains an unordered set of points with outliers. It the finds the grid, and returns it on stdout - =test-dump-chessboard-corners= is a lower-level tool that just finds the chessboard corner features and returns them on stdout. No geometric search is done. - =test-dump-chessboard-corners= similarly is a lower-level tool that just finds the blob center features and returns them on stdout. No geometric search is done. ** Tests There's a test suite in =test/test.sh=. It checks all images in =test/data/*=, and reports which ones produced no data. Currently I don't ship any actual data. I will at some point. * MANPAGES ** mrgingham #+BEGIN_EXAMPLE NAME mrgingham - Extract chessboard corners from a set of images SYNOPSIS $ mrgingham image*.jpg # filename x y level image1.jpg - - image2.jpg 1385.433000 1471.719000 0 image2.jpg 1483.597000 1469.825000 0 image2.jpg 1582.086000 1467.561000 1 ... $ mrgingham image.jpg | vnl-filter -p x,y,level | feedgnuplot --domain \ --with 'linespoints pt 7 ps 2 palette' \ --tuplesizeall 3 \ --image image.jpg [ image pops up with the detected grid plotted on top, detections color-coded by their decimation level ] DESCRIPTION The mrgingham tool detects chessboard corners from images stored on disk. The images are given on the commandline, as globs. Each glob is expanded, and each image is processed, possibly in parallel if -j was given. The output is a vnlog text table ( containing columns: - filename: path to the image on disk - x, y: detected pixel coordinates of the chessboard corner - level: image level used in detecting this corner. Level 0 means "full-resolution". Level 1 means "half-resolution" and that the noise on this detection has double the standard deviation. Level 2 means "quarter-resolution" and so on. If no chessboard was found in an image, a single record is output: filename - - - The corners are output in a consistent order: starting at the top-left, traversing the grid, in the horizontal direction first. Usually, the chessboard is observed by multiple cameras mounted at a similar orientation, so this consistent order is consistent across cameras. By default we look for a CHESSBOARD, not a grid of circles or Apriltags or anything else. By default we apply adaptive histogram equalization (CLAHE), then blur with a radius of 1. We then use an adaptive level of downsampling when looking for the chessboard. These defaults work very well in practice. For debugging, pass in --debug. This will dump the various intermediate results into /tmp and it will report more stuff on the console. Most of the intermediate results are self-plotting data files. Run them. For debugging sequence candidates, pass in --debug-sequence x,y where 'x,y' are the approximate image coordinates of the start of a given sequence (corner on the edge of a chessboard. This doesn't need to be exact; mrgingham will report on the nearest corner See the mrgingham project documentation for more detail: OPTIONS POSITIONAL ARGUMENTS imageglobs Globs specifying the images to process. May be given more than once OPTIONAL ARGUMENTS --blobs Finds circle centers instead of chessboard corners. Not recommended --gridn N Requests detections of an NxN grid of corners. If omitted, N defaults to 10 --noclahe Controls image preprocessing. Unless given, we will apply adaptive histogram equalization (CLAHE algorithm) to the images. This is EXTREMELY helpful if the images aren't illuminated evenly; which applies to most real-world images. --blur RADIUS Controls image preprocessing. Applies a gaussian blur to the image after the histogram equalization. A light blurring is very helpful with CLAHE, since it produces noisy images. By default we will blur with radius = 1. Set to <= 0 to disable --level LEVEL Controls image preprocessing. Applies a downsampling to the image (after CLAHE and --blur, if those are given). Level 0 means 'use the original image'. Level > 0 means downsample by 2**level. Level < 0 means 'try several different levels until we find one that works. This is the default. --no-refine Disables corner refinement. By default, the coordinates of reported corners are re-detected at less-downsampled zoom levels to improve their accuracy. If we do not want to do that, pass --no-refine --jobs N Parallelizes the processing N-ways. -j is a synonym. This is just like GNU make, except you're required to explicitly specify a job count. --debug If given, mrgingham will dump various intermediate results into /tmp and it will report more stuff on the console. The output is self-documenting --debug-sequence If given, we report details about sequence matching. Do this if --debug reports correct-looking corners (all corners detected, no doubled-up detections, no detections inside the chessboard but not on a corner) #+END_EXAMPLE ** mrgingham-observe-pixel-uncertainty #+BEGIN_EXAMPLE NAME mrgingham-observe-pixel-uncertainty - Evaluate observed point distribution from stationary observations SYNOPSIS $ observe-pixel-uncertainty '*.png' Evaluated 49 observations mean 1-sigma for independent x,y: 0.26 $ mrcal-calibrate-cameras --observed-pixel-uncertainty 0.26 ..... [ mrcal computes a camera calibration ] DESCRIPTION mrgingham has finite precision, so repeated observations of the same board will produce slightly different corner coordinates. This tool takes in a set of images (assumed observing a chessboard, with both the camera and board stationary). It then outputs the 1-standard-deviation statistic for the distribution of detected corners. This can then be passed in to mrcal: 'mrcal-calibrate-cameras --observed-pixel-uncertainty ...' The distribution of the detected corners is assumed to be gaussian, and INDEPENDENT in the horizontal and vertical directions. If the x and y distributions are each s, then the LENGTH of the deviation of each pixel is a Rayleigh distribution with expected value s*sqrt(pi/2) ~ s*1.25 THIS TOOL PERFORMS VERY LIGHT OUTLIER REJECTION; IT IS ASSUMED THAT THE SCENE IS STATIONARY OPTIONS POSITIONAL ARGUMENTS input Either 1: A glob that matches images observing a stationary calibration target. This must be a GLOB. So in the shell pass in '*.png' and NOT *.png. These are processed by 'mrgingham' and the arguments passed in with --mrgingham. Or 2: a vnlog representing corner detections from these images. This is assumed to be a file with a filename ending in .vnl, formatted like 'mrgingham' output: 3 columns: filename,x,y OPTIONAL ARGUMENTS -h, --help show this help message and exit --show {geometry,histograms} Visualize something. Arguments can be: "geometry": show the 1-stdev ellipses of the distribution for each chessboard corner separately. "histograms": show the distribution of all the x- and y-deviations off the mean --mrgingham MRGINGHAM If we're processing images, these are the arguments given to mrgingham. If we are reading a pre-computed file, this does nothing --num-corners NUM_CORNERS How many corners to expect in each image. If this is wrong I will throw an error. Defaults to 100 --imagersize IMAGERSIZE IMAGERSIZE Optional imager dimensions: width and height. This is optional. If given, we use it to size the "--show geometry" plot #+END_EXAMPLE ** mrgingham-rotate-corners #+BEGIN_EXAMPLE NAME mrgingham-rotate-corners - Adjust mrgingham corner detections from rotated cameras SYNOPSIS # camera A is rightside-up # camera B is mounted sideways # cameras C,D are upside-down mrgingham --gridn N \ 'frame*-cameraA.jpg' \ 'frame*-cameraB.jpg' \ 'frame*-cameraC.jpg' \ 'frame*-cameraD.jpg' | \ mrgingham-rotate-corners --gridn N \ --90 cameraB --180 'camera[CD]' DESCRIPTION The mrgingham chessboard detector finds a chessboard in an image, but it has no way to know whether the detected chessboard was upside-down or otherwise rotated: the chessboard itself has no detectable marking to make this clear. In the usual case, the cameras as all mounted in the same orientation, so they all detect the same orientation of the chessboard, and there is no problem. However, if some cameras are mounted sideways or upside-down, the sequence of corners will correspond to different corners between the cameras with different orientations. This can be addressed by this tool. This tool ingests mrgingham detections, and outputs them after correcting the chessboard observations produced by rotated cameras. Each rotation option is an awk regular expression used to select images from specific cameras. The regular expression is tested against the image filenames. Each rotation option may be given multiple times. Any files not matched by any rotation option are passed through unrotated. #+END_EXAMPLE * MAINTAINER This is maintained by Dima Kogan . Please let Dima know if something is unclear/broken/missing. * LICENSE AND COPYRIGHT This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. Copyright 2017-2021 California Institute of Technology Copyright 2017-2021 Dima Kogan (=dima@secretsauce.net=) mrgingham-1.26/README.template.org000066400000000000000000000267271476660265400167310ustar00rootroot00000000000000* SYNOPSIS Detect calibration boards in observed camera images #+BEGIN_EXAMPLE $ mrgingham /tmp/image*.jpg # filename x y level /tmp/image1.jpg - - /tmp/image2.jpg 1385.433000 1471.719000 0 /tmp/image2.jpg 1483.597000 1469.825000 0 /tmp/image2.jpg 1582.086000 1467.561000 1 ... $ mrgingham /tmp/image.jpg | vnl-filter -p x,y | feedgnuplot --domain --lines --points --image /tmp/image.jpg [ image pops up with the detected grid plotted on top ] $ mrgingham /tmp/image.jpg | vnl-filter -p x,y,level | feedgnuplot --domain --with 'linespoints pt 7 ps 2 palette' --tuplesizeall 3 --image /tmp/image.jpg [ fancy image pops up with the detected grid plotted on top, detections colored by their decimation level ] #+END_EXAMPLE * INSTALLATION As of today (2022-02-27), mrgingham is included in the bleeding-edge versions of Debian and Ubuntu. So if you're running at least Debian/testing or the not-yet-published Debian 12 (bookworm) or Ubuntu 22.04 (jammy), you can simply #+begin_src sh apt install mrgingham libmrgingham-dev #+end_src to install the standalone tool and the development library respectively. For older Debian and Ubuntu distros, packages are available in the mrcal repositories. Please see the [[http://mrcal.secretsauce.net/install.html][mrcal installation page.]] If running any other distro, you should build mrgingham from source. First, make sure all the build-time dependencies are installed. These are listed in the [[https://salsa.debian.org/science-team/mrgingham/-/blob/master/debian/control][=debian/control=]] file: #+begin_example Build-Depends: ..., libopencv-dev, libboost-dev, pkg-config, mawk, perl, python3-all, python3-all-dev, python3-numpy #+end_example On a Debian-based distro you can: #+begin_src sh sudo apt install \ libopencv-dev \ libboost-dev \ pkg-config \ mawk \ perl \ python3-all \ python3-all-dev \ python3-numpy #+end_src Replace the package names with their analogues on other distros. Once the dependencies are installed, you #+begin_src sh make #+end_src and then the tool can be invoked with #+begin_src sh ./mrgingham #+end_src * DESCRIPTION Both chessboard and a square grid of circles are supported. Chessboard are the /strongly/ preferred choice, since the circles cannot produce accurate results: we care about the center point, which we are not directly observing. Thus with closeup and oblique views, the reported circle center and the real circle center could be very far away from each other. Because of this, more work was put into the chessboard detector. Use that one. Really. These are both nominally supported by OpenCV, but those implementations are slow and not at all robust, in my experience. The implementations here are much faster and work much better. I /do/ use OpenCV here, but only for some core functionality. Currently mrgingham looks for a /square/ grid of points, with some user-requestable width. Rectangular grids are /not/ supported at this time. ** Approach These tools work in two passes: 1. Look for "interesting" points in the image. The goal is to find all the points we care about, in any order. It is assumed that - there will be many outliers - there will be no outliers interspersed throughout the points we do care about (this isn't an unreasonable requirement: areas between chessboard corners have a solid color) 2. Run a geometric analysis to find a grid in this set of "interesting" points. This will throw out the outliers and it will order the output If we return /any/ data, that means we found a full grid. The geometric search is fairly anal, so if we found a full grid, it's extremely likely that it is "right". Once again: *the current implementation of mrgingham detects only complete chessboards*. *** Chessboards This is based on the feature detector described in this paper: https://arxiv.org/abs/1301.5491 The authors provide a simple MIT-licensed implementation here: http://www-sigproc.eng.cam.ac.uk/Main/SB476Chess This produces an image of detector response. /This/ library then aggregates these responses by looking at local neighborhoods of high responses, and computing the mean of the position of the points in each candidate neighborhood, weighted by the detector response. As noted earlier, I look for a square grid, 10x10 points by default. Here that means 10x10 /internal corners/, meaning a chessboard with 11 /squares/ per side. To ensure robust detections, it is recommended to make the outer squares of the chessboard wider than the inner squares. This would ensure that we see exactly 10 points in a row with the expected spacing. If the outer squares have the same size, the edge of the board might be picked up, and we would see 11 or 12 points instead. A recommended 10x10 pattern can be printed from this file: [[chessboard.10x10.pdf]]. And a recommended 14x14 pattern can be printed from this file: [[chessboard.14x14.pdf]]. The denser chessboard containts more data, so fewer observations will be required for convergence of the calibration algorithm. But a higher-res camera is required to reliably detect the corners. A simple tool to generate these =.pdf= files is included in the mrgingham repository. To make an NxM chessboard figure do =make chessboard.NxM.pdf= in the mrgingham source tree. =N= and =M= must be even integers. Note: today mrgingham requires that =N == M=, but eventually this may be lifted, so this tool does not have this requirement. *** Circles *This isn't recommended, and exists for legacy compatibility only* The circle finder does mostly what the first stage of the OpenCV circle detector does: 1. Find a reasonable intensity threshold 2. Threshold the image 3. Find blobs 4. Return centroid of the blobs This is relatively slow, can get confused by uneven lighting (although CLAHE can take care of that), and is inaccurate: nothing says that the centroid of a blob came from the center of the circle on the calibration board. ** Output representation The =mrgingham= tool produces its output in a [[https://github.com/dkogan/vnlog/][vnlog]] text table. The columns are: - =filename=: path to the image on disk - =x=, =y=: detected pixel coordinates of the chessboard corner - =level=: image level used in detecting this corner. Level 0 means "full-resolution". Level 1 means "half-resolution" and that the noise on this detection has double the standard deviation. Level 2 means "quarter-resolution" and so on. If no chessboard was found in an image, a single record is output: #+begin_example filename - - - #+end_example The corners are output in a consistent order: starting at the top-left, traversing the grid, in the horizontal direction first. Usually, the chessboard is observed by multiple cameras mounted at a similar orientation, so this consistent order is consistent across cameras. However, if some cameras in the set are rotated, their observed chessboard corners will not be consistent anymore: the first corner will be "top-left" in pixel coordinates for both, which is at the top of the chessboard for rightside-up cameras, but the bottom of the chessboard for upside-down cameras. This situation is resolved with the =mrgingham-rotate-corners= tool. It post-processes =mrgingham= output to reorder detections from rotated cameras. See the manpage of that tool for more detail. Eventually the implementation could be extended to be able to uniquely identify each corner, obviating the need for =mrgingham-rotate-corners=, but we're not there today. ** API The user-facing functions live in =mrgingham.hh=. Everything is in C++, mostly because some of the underlying libraries are in C++. All functions return a =bool= to indicate success/failure. All functions put the destination arguments /first/. All functions return the output points in =std::vector=, an ordered list of found points. The inputs are one of - An image filename - An OpenCV matrix: =cv::Mat& image= - A set of detected points, that are unordered, and are a superset of the points we're seeking The prototypes: #+BEGIN_SRC C++ namespace mrgingham { bool find_circle_grid_from_image_array( std::vector& points_out, const cv::Mat& image ); bool find_circle_grid_from_image_file( std::vector& points_out, const char* filename ); bool find_chessboard_from_image_array( std::vector& points_out, const cv::Mat& image, int image_pyramid_level = -1 ); int find_chessboard_from_image_file( std::vector& points_out, const char* filename, int image_pyramid_level = -1 ); bool find_grid_from_points( std::vector& points_out, const std::vector& points ); }; #+END_SRC The arguments should be clear. The only one that needs an explanation is =image_pyramid_level=: - if =image_pyramid_level= is 0 then we just use the image as is. - if =image_pyramid_level= > 0 then we cut down the image by a factor of 2 that many times. So for example, level 3 means each dimension is cut down by a factor of 2^3 = 8 - if =image_pyramid_level= < 0 then we try several levels, taking the first one that produces results ** Applications There're several included applications that exercise the library. The =mrgingham-...= tools are distributed, and their manpages appear below. The =test-...= tools are internal. - =mrgingham= takes in images as globs (with some optional manipulation given on the cmdline), finds the grids, and returns them on stdout, as a vnlog - =mrgingham-observe-pixel-uncertainty= evaluates the distribution of corner detections from repeated observations of a stationary scene - =mrgingham-rotate-corners= corrects chessboard detections produced by rotated cameras by reordering the points in the detection stream - =test-find-grid-from-points= ingests a file that contains an unordered set of points with outliers. It the finds the grid, and returns it on stdout - =test-dump-chessboard-corners= is a lower-level tool that just finds the chessboard corner features and returns them on stdout. No geometric search is done. - =test-dump-chessboard-corners= similarly is a lower-level tool that just finds the blob center features and returns them on stdout. No geometric search is done. ** Tests There's a test suite in =test/test.sh=. It checks all images in =test/data/*=, and reports which ones produced no data. Currently I don't ship any actual data. I will at some point. * MANPAGES ** mrgingham #+BEGIN_EXAMPLE xxx-manpage-mrgingham-xxx #+END_EXAMPLE ** mrgingham-observe-pixel-uncertainty #+BEGIN_EXAMPLE xxx-manpage-mrgingham-observe-pixel-uncertainty-xxx #+END_EXAMPLE ** mrgingham-rotate-corners #+BEGIN_EXAMPLE xxx-manpage-mrgingham-rotate-corners-xxx #+END_EXAMPLE * MAINTAINER This is maintained by Dima Kogan . Please let Dima know if something is unclear/broken/missing. * LICENSE AND COPYRIGHT This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. Copyright 2017-2021 California Institute of Technology Copyright 2017-2021 Dima Kogan (=dima@secretsauce.net=) mrgingham-1.26/chessboard.10x10.fig000066400000000000000000000127601476660265400170150ustar00rootroot00000000000000#FIG 3.2 Produced by xfig version 3.2.6a Portrait Center Metric Letter 100.00 Single -2 1200 2 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 4050 6300 4050 6300 4500 5850 4500 5850 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 4500 5850 4500 5850 4950 5400 4950 5400 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 4500 6750 4500 6750 4950 6300 4950 6300 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 4950 6300 4950 6300 5400 5850 5400 5850 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 5400 6750 5400 6750 5850 6300 5850 6300 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 5400 5850 5400 5850 5850 5400 5850 5400 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 4950 7200 4950 7200 5400 6750 5400 6750 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 4050 7200 4050 7200 4500 6750 4500 6750 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 2700 5850 2700 5850 3150 5400 3150 5400 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 2700 6750 2700 6750 3150 6300 3150 6300 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 3150 6300 3150 6300 3600 5850 3600 5850 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 3600 6750 3600 6750 4050 6300 4050 6300 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 3600 5850 3600 5850 4050 5400 4050 5400 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 3150 7200 3150 7200 3600 6750 3600 6750 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 2700 4950 2700 4950 3150 4500 3150 4500 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 3150 4500 3150 4500 3600 4050 3600 4050 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 3600 4950 3600 4950 4050 4500 4050 4500 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 3150 5400 3150 5400 3600 4950 3600 4950 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 2700 7650 2700 7650 3150 7200 3150 7200 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 3600 7650 3600 7650 4050 7200 4050 7200 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 4500 7650 4500 7650 4950 7200 4950 7200 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 5400 7650 5400 7650 5850 7200 5850 7200 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 4950 8100 4950 8100 5400 7650 5400 7650 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 4050 8100 4050 8100 4500 7650 4500 7650 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 3150 8100 3150 8100 3600 7650 3600 7650 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 5850 7200 5850 7200 6300 6750 6300 6750 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 5850 6300 5850 6300 6300 5850 6300 5850 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 5850 5400 5850 5400 6300 4950 6300 4950 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 5850 4500 5850 4500 6300 4050 6300 4050 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 6300 4950 6300 4950 6750 4500 6750 4500 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 6300 5850 6300 5850 6750 5400 6750 5400 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 6300 6750 6300 6750 6750 6300 6750 6300 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 6300 7650 6300 7650 6750 7200 6750 7200 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 5850 8100 5850 8100 6300 7650 6300 7650 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 2700 9000 2700 9000 3150 8100 3150 8100 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 4050 5400 4050 5400 4500 4950 4500 4950 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 4500 4950 4500 4950 4950 4500 4950 4500 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 4950 5400 4950 5400 5400 4950 5400 4950 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 4050 4500 4050 4500 4500 4050 4500 4050 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 5400 4950 5400 4950 5850 4500 5850 4500 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 4950 4500 4950 4500 5400 4050 5400 4050 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 2700 4050 2700 4050 3150 3150 3150 3150 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 3600 4050 3600 4050 4050 3150 4050 3150 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 4500 4050 4500 4050 4950 3150 4950 3150 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 5400 4050 5400 4050 5850 3150 5850 3150 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 3600 9000 3600 9000 4050 8100 4050 8100 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 4500 9000 4500 9000 4950 8100 4950 8100 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 5400 9000 5400 9000 5850 8100 5850 8100 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 6300 9000 6300 9000 6750 8100 6750 8100 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 6300 4050 6300 4050 6750 3150 6750 3150 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 6750 4500 6750 4500 7650 4050 7650 4050 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 6750 5400 6750 5400 7650 4950 7650 4950 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 6750 6300 6750 6300 7650 5850 7650 5850 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 6750 7200 6750 7200 7650 6750 7650 6750 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 6750 8100 6750 8100 7650 7650 7650 7650 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 1800 8100 1800 8100 2700 7650 2700 7650 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 1800 7200 1800 7200 2700 6750 2700 6750 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 1800 6300 1800 6300 2700 5850 2700 5850 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 1800 5400 1800 5400 2700 4950 2700 4950 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 1800 4500 1800 4500 2700 4050 2700 4050 1800 mrgingham-1.26/chessboard.10x10.pdf000066400000000000000000000052671476660265400170250ustar00rootroot00000000000000%PDF-1.5 %쏢 5 0 obj <> stream xKN0 =E,Ly_/VN."5x|H=bxlbG*Rsi~oOnTjtҭ" "֫p')Wg8j"HT-[ilÂR'H\NL}3]"BRqzΫBs)( J:sW2 &˨ :>42ZP!cʆ,tnjqǔgAX}9<,WxU`X`<8yQ~Z:LX _ʇt̉;,LX6qJUjv>vG"/,ٿ4n!0!Ňendstream endobj 6 0 obj 353 endobj 4 0 obj <> /Contents 5 0 R >> endobj 3 0 obj << /Type /Pages /Kids [ 4 0 R ] /Count 1 >> endobj 1 0 obj <> endobj 7 0 obj <>endobj 8 0 obj <> endobj 9 0 obj <>stream 2017-11-03T23:54:52-07:00 2017-11-03T23:54:52-07:00 fig2dev Version 3.2.6a chessboard.fig endstream endobj 2 0 obj <>endobj xref 0 10 0000000000 65535 f 0000000647 00000 n 0000002211 00000 n 0000000588 00000 n 0000000457 00000 n 0000000015 00000 n 0000000438 00000 n 0000000711 00000 n 0000000752 00000 n 0000000781 00000 n trailer << /Size 10 /Root 1 0 R /Info 2 0 R /ID [] >> startxref 2390 %%EOF mrgingham-1.26/chessboard.14x14.fig000066400000000000000000000244111476660265400170210ustar00rootroot00000000000000#FIG 3.2 Produced by xfig version 3.2.8 Portrait Center Metric Letter 100.00 Single -2 1200 2 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 2700 4950 2700 4950 3150 4500 3150 4500 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 3150 4500 3150 4500 3600 4050 3600 4050 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 3600 4950 3600 4950 4050 4500 4050 4500 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 3150 5400 3150 5400 3600 4950 3600 4950 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 2700 4050 2700 4050 3150 3150 3150 3150 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 3600 4050 3600 4050 4050 3150 4050 3150 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 1800 5400 1800 5400 2700 4950 2700 4950 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 1800 4500 1800 4500 2700 4050 2700 4050 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 2700 8550 2700 8550 3150 8100 3150 8100 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9000 2700 9450 2700 9450 3150 9000 3150 9000 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 3150 9000 3150 9000 3600 8550 3600 8550 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 3600 8550 3600 8550 4050 8100 4050 8100 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 1800 9900 1800 9900 2700 9450 2700 9450 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 1800 9000 1800 9000 2700 8550 2700 8550 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 2700 5850 2700 5850 3150 5400 3150 5400 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 2700 6750 2700 6750 3150 6300 3150 6300 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 3150 6300 3150 6300 3600 5850 3600 5850 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 3600 6750 3600 6750 4050 6300 4050 6300 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 3600 5850 3600 5850 4050 5400 4050 5400 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 3150 7200 3150 7200 3600 6750 3600 6750 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 2700 7650 2700 7650 3150 7200 3150 7200 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 3600 7650 3600 7650 4050 7200 4050 7200 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 1800 8100 1800 8100 2700 7650 2700 7650 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 1800 7200 1800 7200 2700 6750 2700 6750 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 1800 6300 1800 6300 2700 5850 2700 5850 1800 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 3150 8100 3150 8100 3600 7650 3600 7650 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 6750 5400 6750 5400 7200 4950 7200 4950 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 7200 4950 7200 4950 7650 4500 7650 4500 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 7650 5400 7650 5400 8100 4950 8100 4950 7650 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 6750 4500 6750 4500 7200 4050 7200 4050 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 7650 4500 7650 4500 8100 4050 8100 4050 7650 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 7200 4050 7200 4050 7650 3150 7650 3150 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 8100 4050 8100 4050 8550 3150 8550 3150 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 7200 8550 7200 8550 7650 8100 7650 8100 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9000 7200 9450 7200 9450 7650 9000 7650 9000 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 7650 9000 7650 9000 8100 8550 8100 8550 7650 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 8100 8550 8100 8550 8550 8100 8550 8100 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 7650 9900 7650 9900 8100 9450 8100 9450 7650 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 6750 9900 6750 9900 7200 9450 7200 9450 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 6750 6300 6750 6300 7200 5850 7200 5850 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 7200 5850 7200 5850 7650 5400 7650 5400 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 7200 6750 7200 6750 7650 6300 7650 6300 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 7650 6300 7650 6300 8100 5850 8100 5850 7650 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 8100 6750 8100 6750 8550 6300 8550 6300 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 7650 7200 7650 7200 8100 6750 8100 6750 7650 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 6750 7200 6750 7200 7200 6750 7200 6750 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 6750 8100 6750 8100 7200 7650 7200 7650 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 5850 5400 5850 5400 6300 4950 6300 4950 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 5850 4500 5850 4500 6300 4050 6300 4050 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 6300 4950 6300 4950 6750 4500 6750 4500 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 4050 5400 4050 5400 4500 4950 4500 4950 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 4500 4950 4500 4950 4950 4500 4950 4500 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 4950 5400 4950 5400 5400 4950 5400 4950 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 4050 4500 4050 4500 4500 4050 4500 4050 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 5400 4950 5400 4950 5850 4500 5850 4500 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 4950 4500 4950 4500 5400 4050 5400 4050 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 4500 4050 4500 4050 4950 3150 4950 3150 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 5400 4050 5400 4050 5850 3150 5850 3150 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 3150 6300 4050 6300 4050 6750 3150 6750 3150 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 4050 9000 4050 9000 4500 8550 4500 8550 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 4500 8550 4500 8550 4950 8100 4950 8100 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9000 4500 9450 4500 9450 4950 9000 4950 9000 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 4950 9000 4950 9000 5400 8550 5400 8550 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9000 5400 9450 5400 9450 5850 9000 5850 9000 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 5400 8550 5400 8550 5850 8100 5850 8100 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 4950 9900 4950 9900 5400 9450 5400 9450 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 4050 9900 4050 9900 4500 9450 4500 9450 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 5850 9900 5850 9900 6300 9450 6300 9450 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8100 6300 8550 6300 8550 6750 8100 6750 8100 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9000 6300 9450 6300 9450 6750 9000 6750 9000 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 4050 6300 4050 6300 4500 5850 4500 5850 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 4500 5850 4500 5850 4950 5400 4950 5400 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 4500 6750 4500 6750 4950 6300 4950 6300 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 4950 6300 4950 6300 5400 5850 5400 5850 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 5400 6750 5400 6750 5850 6300 5850 6300 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 5400 5850 5400 5850 5850 5400 5850 5400 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 4950 7200 4950 7200 5400 6750 5400 6750 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 4050 7200 4050 7200 4500 6750 4500 6750 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 4500 7650 4500 7650 4950 7200 4950 7200 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 5400 7650 5400 7650 5850 7200 5850 7200 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 4950 8100 4950 8100 5400 7650 5400 7650 4950 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 4050 8100 4050 8100 4500 7650 4500 7650 4050 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 5850 7200 5850 7200 6300 6750 6300 6750 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 5850 6300 5850 6300 6300 5850 6300 5850 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 6300 5850 6300 5850 6750 5400 6750 5400 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6300 6300 6750 6300 6750 6750 6300 6750 6300 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 6300 7650 6300 7650 6750 7200 6750 7200 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 5850 8100 5850 8100 6300 7650 6300 7650 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 5850 9000 5850 9000 6300 8550 6300 8550 5850 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 6750 9000 6750 9000 7200 8550 7200 8550 6750 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9000 3600 9450 3600 9450 4050 9000 4050 9000 3600 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4500 8100 4950 8100 4950 8550 4500 8550 4500 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5400 8100 5850 8100 5850 8550 5400 8550 5400 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 8550 9900 8550 9900 9450 9450 9450 9450 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 8550 9900 8550 9900 9450 9450 9450 9450 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 5850 8550 6300 8550 6300 9450 5850 9450 5850 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 6750 8550 7200 8550 7200 9450 6750 9450 6750 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4050 8550 4500 8550 4500 9450 4050 9450 4050 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 4950 8550 5400 8550 5400 9450 4950 9450 4950 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9450 3150 9900 3150 9900 3600 9450 3600 9450 3150 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9000 8100 9450 8100 9450 8550 9000 8550 9000 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 8550 8550 9000 8550 9000 9450 8550 9450 8550 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 8550 8100 8550 8100 9450 7650 9450 7650 8550 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 8100 7650 8100 7650 8550 7200 8550 7200 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7200 7200 7650 7200 7650 7650 7200 7650 7200 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 7650 7650 8100 7650 8100 8100 7650 8100 7650 7650 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9900 2700 10800 2700 10800 3150 9900 3150 9900 2700 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9900 7200 10800 7200 10800 7650 9900 7650 9900 7200 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9900 8100 10800 8100 10800 8550 9900 8550 9900 8100 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9900 4500 10800 4500 10800 4950 9900 4950 9900 4500 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9900 5400 10800 5400 10800 5850 9900 5850 9900 5400 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9900 6300 10800 6300 10800 6750 9900 6750 9900 6300 2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5 9900 3600 10800 3600 10800 4050 9900 4050 9900 3600 mrgingham-1.26/chessboard.14x14.pdf000066400000000000000000000053461476660265400170330ustar00rootroot00000000000000%PDF-1.7 %쏢 %%Invocation: gs -q -dSAFER -sAutoRotatePages=? -sDEVICE=pdfwrite -dPDFSETTINGS=/prepress ? ? - 5 0 obj <> stream xWn0 ;~~CvP'zŅ :"͈{v?sŻRž]{}l6珼ڋKUyCi;IUhhL>QUj[?J]%x((]8 ֱ,3jyT])(\pRPYPkv.qġ3ѝ.vC8cOٵM,+ alX'tBlOC:'NQ*Y4Bp%HŃxRZ⦢c $xӒB%\=jq' ^ (~FpbbLQ6#t:fWbٴdCE8zK>Q#󔂏`,cT"L;C??P+UGI BAڠݫ7#^܌1,@`Fi!j*(=Qw r@pQr'pQ\i(03' .}(:@Ӎ,~ڶ]? )a0uMtJ^q>i% hl0hS39kDcv V*Mo=>~{S.endstream endobj 6 0 obj 583 endobj 4 0 obj <> /Contents 5 0 R >> endobj 3 0 obj << /Type /Pages /Kids [ 4 0 R ] /Count 1 >> endobj 1 0 obj <> endobj 7 0 obj <>stream 2021-09-16T15:04:58-07:00 2021-09-16T15:04:58-07:00 fig2dev Version 3.2.8 chessboard.14x14.fig endstream endobj 2 0 obj <>endobj xref 0 8 0000000000 65535 f 0000000956 00000 n 0000002293 00000 n 0000000897 00000 n 0000000783 00000 n 0000000111 00000 n 0000000764 00000 n 0000001020 00000 n trailer << /Size 8 /Root 1 0 R /Info 2 0 R /ID [] >> startxref 2479 %%EOF mrgingham-1.26/choose_mrbuild.mk000066400000000000000000000014651476660265400167700ustar00rootroot00000000000000# Use the local mrbuild or the system mrbuild or tell the user how to download # it ifneq (,$(wildcard mrbuild/)) MRBUILD_MK=mrbuild MRBUILD_BIN=mrbuild/bin else ifneq (,$(wildcard /usr/include/mrbuild/Makefile.common.header)) MRBUILD_MK=/usr/include/mrbuild MRBUILD_BIN=/usr/bin else V := 1.13 SHA512 := 7a1422026cdbe12cea6882c3b76087dcc4c1d258369ec2abb941a779a539253a37850563f801049d570e8ad342722030fd918114388b5155a89644491a221f16 URL := https://github.com/dkogan/mrbuild/archive/refs/tags/v$V.tar.gz TARGZ := mrbuild-$V.tar.gz cmd := wget -O $(TARGZ) ${URL} && sha512sum --quiet --strict -c <(echo $(SHA512) $(TARGZ)) && tar xvfz $(TARGZ) && ln -fs mrbuild-$V mrbuild $(error mrbuild not found. Either 'apt install mrbuild', or if not possible, get it locally like this: '${cmd}') endif mrgingham-1.26/find_blobs.cc000066400000000000000000000036261476660265400160520ustar00rootroot00000000000000#include #include #include "point.hh" #include "mrgingham-internal.h" using namespace mrgingham; namespace mrgingham { __attribute__((visibility("default"))) bool find_blobs_from_image_array( std::vector* points, const cv::Mat& image, bool dodump ) { cv::SimpleBlobDetector::Params blobDetectorParams; blobDetectorParams.minArea = 20; blobDetectorParams.maxArea = 80000; blobDetectorParams.minDistBetweenBlobs = 5; blobDetectorParams.blobColor = 0; // black-on-white dots std::vector keypoints; cv::Ptr blobDetector = cv::SimpleBlobDetector::create(blobDetectorParams); blobDetector->detect(image, keypoints); for(std::vector::iterator it = keypoints.begin(); it != keypoints.end(); it++) { if( dodump ) { printf("%f %f\n", it->pt.x, it->pt.y); } else { points->push_back( PointInt((int)(it->pt.x * FIND_GRID_SCALE + 0.5), (int)(it->pt.y * FIND_GRID_SCALE + 0.5))); } } return true; } __attribute__((visibility("default"))) bool find_blobs_from_image_file( std::vector* points, const char* filename, bool dodump ) { cv::Mat image = cv::imread(filename, cv::IMREAD_IGNORE_ORIENTATION | cv::IMREAD_GRAYSCALE); if( image.data == NULL ) { fprintf(stderr, "%s:%d in %s(): Couldn't open image '%s'." " Sorry.\n", __FILE__, __LINE__, __func__, filename); return false; } return find_blobs_from_image_array( points, image, dodump ); } } mrgingham-1.26/find_blobs.hh000066400000000000000000000012041476660265400160520ustar00rootroot00000000000000#pragma once #include #include #include "point.hh" // This is currently hard-coded to find black-on-white blobs (look at // blobColor in find_blobs.cc) namespace mrgingham { // these all output the points scaled by FIND_GRID_SCALE bool find_blobs_from_image_array( std::vector* points, const cv::Mat& image, bool dodump = false); bool find_blobs_from_image_file( std::vector* points, const char* filename, bool dodump = false); } mrgingham-1.26/find_board.docstring000066400000000000000000000043361476660265400174460ustar00rootroot00000000000000Runs the full mrgingham chessboard detector on an image SYNOPSIS points = mrgingham.find_board(image) [ points is an array of shape (N*N,2), the ordered list of ] [ pixel coordinates in the grid ] This function runs the full mrgingham sequence: - pre-process image - find chessboard corners (or blobs) - find an NxN grid in the set of corners - refine No broadcasting is supported by this function ARGUMENTS - image: numpy array of shape (H,W) and dtype np.uint8. This is the image we're processing. Must be densely-stored, grayscale image - blobs: optional boolean, defaulting to False. If True, we look for black-on-white blobs, instead of chessboard corners. If blobs: we MUST have image_pyramid_level==0 - image_pyramid_level: optional integer defaulting to -1. This can be given to operate on a downsampled version of the image. 0 means "original image", 1 means "downsample by a factor of 2 in each dimension", 2 means "downsample by a factor of 4 in each dimension" and so on. image_pyramid_level < 0 (the default) means "start with a downsampled image, and then refine the results by repeatedly reducing the downsampling. If blobs: image_pyramid_level==0 is the only allowed option - gridn: optional integer defaulting to 10. We detect an NxN grid of corners, where N is given by this argument - debug: optional boolean, defaulting to False. If True, this will dump various intermediate results into /tmp and it will report more stuff on standard out. Most of the intermediate results are self-plotting data files. Run them. This does the same thing as "mrgingham --debug"; the Python output is unaffected. - debug_sequence: optional string "X,Y" where X and Y are integers. In addition to "debug", this produces diagnostics when searching for sequence candidates. X,Y are the approximate image coordinates of the start of a given sequence (corner on the edge of a chessboard. This doesn't need to be exact; mrgingham will report on the nearest corner. This does the same thing as "mrgingham --debug-sequence"; the Python output is unaffected. RETURNED VALUE A numpy array of shape (N*N,2) containing ordered pixel coordinates of the grid found in the image. If no grid was found, None is returned. mrgingham-1.26/find_chessboard.docstring000066400000000000000000000002341476660265400204650ustar00rootroot00000000000000Runs the full mrgingham detector on an image This is a compatibility alias for mrgingham.find_board(). See the documentation for that function for details mrgingham-1.26/find_chessboard_corners.cc000066400000000000000000000577371476660265400206350ustar00rootroot00000000000000#include #include #include #include #include "point.hh" #include "mrgingham-internal.h" extern "C" { #include "ChESS.h" } // The various tunable parameters // When we find a connected component of high-enough corner responses, the peak // must have a response at least this strong for the component to be accepted #define RESPONSE_MIN_PEAK_THRESHOLD 120 // Corner responses must be at least this strong to be included into our // connected component #define RESPONSE_MIN_THRESHOLD 15 // Corner responses must be at least this strong to be included into our // connected component. This is based on the maximum response we have so far // encountered in our component #define RESPONSE_MIN_THRESHOLD_RATIO_OF_MAX(response_max) (((uint16_t)(response_max)) >> 4) #define CONNECTED_COMPONENT_MIN_SIZE 2 // When looking at a candidate corner (peak of a connected component), we look // at the variance of the intensities of the pixels in a region around the // candidate corner. This has to be "relatively high". If we somehow end up // looking at a region inside a chessboard square instead of on a corner, then // the region will be relatively flat (same color), and the variance will be too // low. These parameters set the size of this search window and the threshold // for the standard deviation (sqrt(variance)) #define CONSTANCY_WINDOW_R 10 #define STDEV_THRESHOLD 20 #define VARIANCE_THRESHOLD (STDEV_THRESHOLD*STDEV_THRESHOLD) using namespace mrgingham; namespace mrgingham { static bool high_variance( int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t* image ) { if(x-CONSTANCY_WINDOW_R < 0 || x+CONSTANCY_WINDOW_R >= w || y-CONSTANCY_WINDOW_R < 0 || y+CONSTANCY_WINDOW_R >= h ) { // I give up on edges return false; } // I should be able to do this with opencv, but it's way too much of a pain // in my ass, so I do it myself int32_t sum = 0; for(int dy = -CONSTANCY_WINDOW_R; dy <=CONSTANCY_WINDOW_R; dy++) for(int dx = -CONSTANCY_WINDOW_R; dx <=CONSTANCY_WINDOW_R; dx++) { uint8_t val = image[ x+dx + (y+dy)*w ]; sum += (int32_t)val; } int32_t mean = sum / ((1 + 2*CONSTANCY_WINDOW_R)* (1 + 2*CONSTANCY_WINDOW_R)); int32_t sum_deviation_sq = 0; for(int dy = -CONSTANCY_WINDOW_R; dy <=CONSTANCY_WINDOW_R; dy++) for(int dx = -CONSTANCY_WINDOW_R; dx <=CONSTANCY_WINDOW_R; dx++) { uint8_t val = image[ x+dx + (y+dy)*w ]; int32_t deviation = (int32_t)val - mean; sum_deviation_sq += deviation*deviation; } int32_t var = sum_deviation_sq / ((1 + 2*CONSTANCY_WINDOW_R)* (1 + 2*CONSTANCY_WINDOW_R)); // // to show the variances and empirically find the threshold // printf("%d %d %d\n", x, y, var); // return false; return var > VARIANCE_THRESHOLD; } // The point-list data structure for the connected-component traversal struct xy_t { int16_t x, y; }; struct xylist_t { struct xy_t* xy; int N; }; static struct xylist_t xylist_alloc() { struct xylist_t l = {}; // start out large-enough for most use cases (should have connected // components with <10 pixels generally). Will realloc if really needed l.xy = (struct xy_t*)malloc( 128 * sizeof(struct xy_t) ); return l; } static void xylist_reset(struct xylist_t* l) { l->N = 0; } static void xylist_reset_with(struct xylist_t* l, int16_t x, int16_t y) { l->N = 1; l->xy[0].x = x; l->xy[0].y = y; } static void xylist_free(struct xylist_t* l) { free(l->xy); l->xy = NULL; l->N = -1; } static void xylist_push(struct xylist_t* l, int16_t x, int16_t y) { l->N++; l->xy = (struct xy_t*)realloc(l->xy, l->N * sizeof(struct xy_t)); // no-op most of the time l->xy[l->N-1].x = x; l->xy[l->N-1].y = y; } static bool xylist_pop(struct xylist_t* l, int16_t *x, int16_t *y) { if(l->N <= 0) return false; *x = l->xy[ l->N-1 ].x; *y = l->xy[ l->N-1 ].y; l->N--; return true; } typedef struct { uint64_t sum_w_x, sum_w_y, sum_w; int N; // I keep track of the position and magnitude of the peak, and I reject all // points whose response is smaller than some (small) ratio of the max. Note // that the max is updated as I go, so it's possible to accumulate some // points that have become invalid (because the is_valid threshold has moved // with the max). However, since I weigh everything by the response value, // this won't be a strong effect: the incorrectly-accumulated points will // have a small weight uint16_t x_peak, y_peak; int16_t response_max; } connected_component_t; static bool is_valid(int16_t x, int16_t y, int16_t w, int16_t h, const int16_t* d, const connected_component_t* c) { if(x<0 || x>=w || y<0 || y>=h) return false; int16_t response = d[x+y*w]; return response > RESPONSE_MIN_THRESHOLD && (c == NULL || response > RESPONSE_MIN_THRESHOLD_RATIO_OF_MAX(c->response_max)); } static void accumulate(int16_t x, int16_t y, int16_t w, int16_t h, const int16_t* d, connected_component_t* c) { int16_t response = d[x+y*w]; if( response > c->response_max) { c->response_max = response; c->x_peak = x; c->y_peak = y; } c->sum_w_x += response * x; c->sum_w_y += response * y; c->sum_w += response; c->N++; // // to show the responses and empirically find the threshold // printf("%d %d %d\n", x, y, response); } static bool connected_component_is_valid(const connected_component_t* c, int16_t w, int16_t h, const uint8_t* image) { // We're looking at a candidate peak. I don't want to find anything // inside a chessboard square, which the detector does sometimes. I // can detect this condition by looking at a local variance of the // original image at this point. The image will be relatively // constant in a chessboard square, and I throw out this candidate // then return c->N >= CONNECTED_COMPONENT_MIN_SIZE && c->response_max > RESPONSE_MIN_PEAK_THRESHOLD && high_variance(c->x_peak, c->y_peak, w,h, image); } static void check_and_push_candidate(struct xylist_t* l, bool* touched_margin, int16_t x, int16_t y, int16_t w, int16_t h, const int16_t* d, int margin) { if( !(x >= margin && x < w-margin && y >= margin && y < h-margin )) { *touched_margin = true; return; } if( d[x+y*w] <= 0 ) return; xylist_push(l, x, y); } static bool follow_connected_component(PointDouble* out, struct xylist_t* l, int16_t w, int16_t h, int16_t* d, const uint8_t* image, int margin) { connected_component_t c = {}; bool touched_margin = false; int16_t x, y; while( xylist_pop(l, &x, &y)) { if(!is_valid(x,y,w,h,d, &c)) { d[x + y*w] = 0; // mark invalid; just in case continue; } accumulate (x,y,w,h,d, &c); d[x + y*w] = 0; // mark invalid check_and_push_candidate(l, &touched_margin, x+1, y, w,h,d,margin); check_and_push_candidate(l, &touched_margin, x-1, y, w,h,d,margin); check_and_push_candidate(l, &touched_margin, x, y+1, w,h,d,margin); check_and_push_candidate(l, &touched_margin, x, y-1, w,h,d,margin); } // If I touched the margin, this connected component is NOT valid if( !touched_margin && connected_component_is_valid(&c, w,h,image) ) { out->x = (double)c.sum_w_x / (double)c.sum_w; out->y = (double)c.sum_w_y / (double)c.sum_w; return true; } return false; } static PointDouble scale_image_coord(const PointDouble* pt, double scale) { // My (x,y) coords here are based on a downsampled image, and I want to // up-sample them. An NxN image consists of a grid of NxN cells. The MIDDLE // of each cell is indexed by integer coords. Thus the top-left corner of // the image is at the top-left corner of the top-left cell at coords // (-0.5,-0.5) at ANY resolution. So (-0.5,-0.5) is a fixed point of the // scaling, not (0,0). Thus to change the scaling, I translate to a coord // system with its origin at (-0.5,-0.5), scale, and then translate back return PointDouble( (pt->x + 0.5) * scale - 0.5, (pt->y + 0.5) * scale - 0.5 ); } #define DUMP_FILENAME_CORNERS_BASE "/tmp/mrgingham-1-corners" #define DUMP_FILENAME_CORNERS DUMP_FILENAME_CORNERS_BASE ".vnl" static int process_connected_components(int w, int h, int16_t* d, const uint8_t* image, std::vector* points_scaled_out, std::vector* points_refinement, signed char* level_refinement, bool debug, const char* debug_image_filename, int image_pyramid_level, int margin) { FILE* debugfp = NULL; const char* debug_filename = NULL; if(debug) { if(points_refinement == NULL) debug_filename = DUMP_FILENAME_CORNERS; else { char filename[256]; sprintf(filename, DUMP_FILENAME_CORNERS_BASE "-refinement-level%d.vnl", image_pyramid_level); debug_filename = filename; } fprintf(stderr, "Writing self-plotting corner dump to %s\n", debug_filename); debugfp = fopen(debug_filename, "w"); assert(debugfp); if(debug_image_filename != NULL) fprintf(debugfp, "#!/usr/bin/feedgnuplot --dom --with 'points pt 7 ps 2' --square --image %s\n", debug_image_filename); else fprintf(debugfp, "#!/usr/bin/feedgnuplot --dom --square --set 'yr [:] rev'\n"); fprintf(debugfp, "# x y\n"); } uint16_t coord_scale = 1U << image_pyramid_level; struct xylist_t l = xylist_alloc(); int N = 0; // I assume that points_scaled_out and points_refinement aren't both non-NULL // I loop through all the pixels in the image. For each one I expand it into // the connected component that contains it. If I'm refining, I only look // for the connected component around the points I'm interested in if(points_scaled_out != NULL) { for(int16_t y = margin+1; ypush_back(PointInt((int)(0.5 + pt.x * FIND_GRID_SCALE), (int)(0.5 + pt.y * FIND_GRID_SCALE))); } } N = points_scaled_out->size(); } else if(points_refinement != NULL) { for(unsigned i=0; isize(); i++) { // I can only refine the current estimate if it was computed at one // level higher than what I'm at now if( level_refinement[i] != image_pyramid_level+1 ) continue; PointDouble& pt_full = (*points_refinement)[i]; // The point pt indexes the full-size image, while the // connected-component stuff looks at a downsampled image. I convert PointDouble pt_downsampled = scale_image_coord(&pt_full, 1.0 / coord_scale); int x = (int)(pt_downsampled.x + 0.5); int y = (int)(pt_downsampled.y + 0.5); // I seed my refinement from the 3x3 neighborhood around the // previous center point. It is possible for the ChESS response // right in the center to be invalid, and I'll then not be able to // refine the point at all xylist_reset(&l); for(int dx = -1; dx<=1; dx++) for(int dy = -1; dy<=1; dy++) if( is_valid(x+dx,y+dy,w,h,d, NULL)) xylist_push(&l,x+dx,y+dy); PointDouble pt; if(follow_connected_component(&pt, &l, w,h,d, image, margin)) { pt_full = scale_image_coord(&pt, (double)coord_scale); if( debugfp ) fprintf(debugfp, "%f %f\n", pt_full.x, pt_full.y); level_refinement[i] = image_pyramid_level; N++; } } } xylist_free(&l); if(debug) { fclose(debugfp); chmod(debug_filename, S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IWGRP | S_IXUSR | S_IXGRP | S_IXOTH); } return N; } // returns a scaled image, or NULL on failure #define SCALED_PROCESSED_IMAGE_FILENAME "/tmp/mrgingham-scaled-processed-level%d.png" static const cv::Mat* apply_image_pyramid_scaling(// out // This MAY be used for the output image. The // pointer returned by this function is what the // caller should use. The caller should provide a // cv::Mat object that this function can use for its // purposes. When the caller is done with the scaled // image, they may free this object cv::Mat& image_buffer_output, // in const cv::Mat& image_input, // set to 0 to just use the image int image_pyramid_level, bool debug ) { if( image_pyramid_level < 0 || // 10 is an arbitrary high number image_pyramid_level > 10 ) { fprintf(stderr, "%s:%d in %s(): Got an unreasonable image_pyramid_level = %d." " Sorry.\n", __FILE__, __LINE__, __func__, image_pyramid_level); return NULL; } const cv::Mat* image; if(image_pyramid_level == 0) image = &image_input; else { double scale = 1.0 / ((double)(1 << image_pyramid_level)); cv::resize( image_input, image_buffer_output, cv::Size(), scale, scale, cv::INTER_LINEAR ); image = &image_buffer_output; } if( debug ) { char filename[256]; sprintf(filename, SCALED_PROCESSED_IMAGE_FILENAME, image_pyramid_level); cv::imwrite(filename, *image); fprintf(stderr, "Wrote scaled,processed image to %s\n", filename); } if( !image->isContinuous() ) { fprintf(stderr, "%s:%d in %s(): I can only handle continuous arrays (stride == width) currently." " Sorry.\n", __FILE__, __LINE__, __func__); return NULL; } if( image->type() != CV_8U ) { fprintf(stderr, "%s:%d in %s(): I can only handle CV_8U arrays currently." " Sorry.\n", __FILE__, __LINE__, __func__); return NULL; } return image; } #define CHESS_RESPONSE_FILENAME "/tmp/mrgingham-chess-response%s-level%d.png" #define CHESS_RESPONSE_POSITIVE_FILENAME "/tmp/mrgingham-chess-response%s-level%d-positive.png" static int _find_or_refine_chessboard_corners_from_image_array ( // out std::vector* points_scaled_out, std::vector* points_refinement, signed char* level_refinement, // in const cv::Mat& image_input, int image_pyramid_level, bool debug, const char* debug_image_filename) { cv::Mat _image; const cv::Mat* image = apply_image_pyramid_scaling(_image, image_input, image_pyramid_level, debug); if( image == NULL ) return 0; const int w = image->cols; const int h = image->rows; // I don't NEED to zero this out, but it makes the debugging easier. // Otherwise the edges will contain uninitialized garbage, and the actual // data will be hard to see in the debug images cv::Mat response = cv::Mat::zeros( cv::Size(w, h), CV_16S ); uint8_t* imageData = image->data; int16_t* responseData = (int16_t*)response.data; mrgingham_ChESS_response_5( responseData, imageData, w, h, w ); if(debug) { cv::Mat out; cv::normalize(response, out, 0, 255, cv::NORM_MINMAX); char filename[256]; sprintf(filename, CHESS_RESPONSE_FILENAME, (points_refinement==NULL) ? "" : "-refinement", image_pyramid_level); cv::imwrite(filename, out); fprintf(stderr, "Wrote a normalized ChESS response to %s\n", filename); } // I set all responses <0 to "0". These are not valid as candidates, and // I'll use "0" to mean "visited" in the upcoming connectivity search for( int xy = 0; xy < w*h; xy++ ) if(responseData[xy] < 0) responseData[xy] = 0; if(debug) { cv::Mat out; cv::normalize(response, out, 0, 255, cv::NORM_MINMAX); char filename[256]; sprintf(filename, CHESS_RESPONSE_POSITIVE_FILENAME, (points_refinement==NULL) ? "" : "-refinement", image_pyramid_level); cv::imwrite(filename, out); fprintf(stderr, "Wrote positive-only, normalized ChESS response to %s\n", filename); } // I have responses. I // // - Find local peaks // - Ignore invalid local peaks // - Find center-of-mass of the region around the local peak // This serves both to throw away duplicate nearby points at the same corner // and to provide sub-pixel-interpolation for the corner location return process_connected_components(w, h, responseData, (uint8_t*)image->data, points_scaled_out, points_refinement, level_refinement, debug, debug_image_filename, image_pyramid_level, // The ChESS response is invalid at a 7-pixel // margin around the image. This is a property // of the ChESS implementation. Anything that // needs to touch pixels in this 7-pixel-wide // ring is invalid 7); } __attribute__((visibility("default"))) bool find_chessboard_corners_from_image_array( // out // integers scaled up by // FIND_GRID_SCALE to get more // resolution std::vector* points_scaled_out, // in const cv::Mat& image_input, // set to 0 to just use the image int image_pyramid_level, bool debug, const char* debug_image_filename) { return _find_or_refine_chessboard_corners_from_image_array(points_scaled_out, NULL, NULL, image_input, image_pyramid_level, debug, debug_image_filename) > 0; } // Returns how many points were refined __attribute__((visibility("default"))) int refine_chessboard_corners_from_image_array( // out/int // initial coordinates on input, // refined coordinates on output std::vector* points, // level[ipoint] is the // decimation level used to // compute that point. // if(level[ipoint] == // image_pyramid_level+1) then // that point could be refined. // If I successfully refine a // point, I update level[ipoint] signed char* level, // in const cv::Mat& image_input, int image_pyramid_level, bool debug, const char* debug_image_filename) { return _find_or_refine_chessboard_corners_from_image_array( NULL, points, level, image_input, image_pyramid_level, debug, debug_image_filename); } __attribute__((visibility("default"))) bool find_chessboard_corners_from_image_file( // out // integers scaled up by // FIND_GRID_SCALE to get more // resolution std::vector* points, // in const char* filename, // set to 0 to just use the image int image_pyramid_level, bool debug ) { cv::Mat image = cv::imread(filename, cv::IMREAD_IGNORE_ORIENTATION | cv::IMREAD_GRAYSCALE); if( image.data == NULL ) { fprintf(stderr, "%s:%d in %s(): Couldn't open image '%s'." " Sorry.\n", __FILE__, __LINE__, __func__, filename); return false; } return find_chessboard_corners_from_image_array( points, image, image_pyramid_level, debug, filename ); } } mrgingham-1.26/find_chessboard_corners.docstring000066400000000000000000000002221476660265400222150ustar00rootroot00000000000000Finds discrete points in an image This is a compatibility alias for mrgingham.find_points(). See the documentation for that function for details mrgingham-1.26/find_chessboard_corners.hh000066400000000000000000000073431476660265400206330ustar00rootroot00000000000000#pragma once #include #include #include "point.hh" namespace mrgingham { // these all output the points scaled by FIND_GRID_SCALE in points[]. bool find_chessboard_corners_from_image_array( // out // integers scaled up by // FIND_GRID_SCALE to get more // resolution std::vector* points_scaled_out, // in const cv::Mat& image_input, // set to 0 to just use the // image. Values > 0 cut down the // image by a factor of 2 that // many times. So for example, // level==2 means each dimension // is cut down by a factor of 4 int image_pyramid_level, bool debug = false, const char* debug_image_filename = NULL); bool find_chessboard_corners_from_image_file( // out // integers scaled up by // FIND_GRID_SCALE to get more // resolution std::vector* points, // in const char* filename, // set to 0 to just use the // image. Values > 0 cut down the // image by a factor of 2 that // many times. So for example, // level==2 means each dimension // is cut down by a factor of 4 int image_pyramid_level, bool debug = false); int refine_chessboard_corners_from_image_array( // out/int // initial coordinates on input, // refined coordinates on output std::vector* points, // level[ipoint] is the // decimation level used to // compute that point. // if(level[ipoint] == // image_pyramid_level+1) then // that point could be refined. // If I successfully refine a // point, I update level[ipoint] signed char* level, // in const cv::Mat& image_input, int image_pyramid_level, bool debug = false, const char* debug_image_filename = NULL); }; mrgingham-1.26/find_grid.cc000066400000000000000000001661351476660265400157030ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include "point.hh" #include "mrgingham.hh" #include "mrgingham-internal.h" using namespace mrgingham; // From the boost voronoi-diagram tutorial. Don't ask. using boost::polygon::voronoi_diagram; namespace boost { namespace polygon { template <> struct geometry_concept { typedef point_concept type; }; template <> struct point_traits { typedef int coordinate_type; static inline coordinate_type get(const PointInt& point, orientation_2d orient) { return (orient == HORIZONTAL) ? point.x : point.y; } }; }} typedef voronoi_diagram VORONOI; /* Voronoi library terminology. Each "cell" c has an edge = c->incident_edge(). The edge e points FROM the cell e->cell(). The edges are a linked list. e->next() is the next edge from c, moving clockwise. e->prev() moves counterclockwise(). An edge e A->B has an inverse e->twin() B->A. For any voronoi vertex ("cell" in the terminology of this voronoi library) I want look at all the neighboring cells. The simplest thing to do is to look at all the vertices directly connected to this vertex with an edge. This works most of the time. But for very skewed views of a chessboard this sometimes isn't enough, and I look at the in-between vertices too. From vertex A below I would previously consider E and B and C as neighbors, but I now also want to consider D E--B-----D | / \ / |/ \ / A-----C How do I find D? Let e be A->B. Then D = e->twin()->prev()->prev()->twin()->cell() There're a few topological wrinkles that this scheme creates. First off, at the boundary of the graph, there may not exist an "inbetween" vertex. In the example above there's no vertex between C and E from A (looking clockwise). I check by making sure that e->twin()->prev()->twin()->cell() == e->next()->twin()->cell(). In the "good" example of D being between B and C, both should point to C. I can also have more complex topologies, which would create duplicate neighbors. For instance if I have this: ---F -/ /| / / | A---H | \ \ | -\ \| ---G Looking from A, vertex G lies between F,H. But G is directly connected to A, so I would already use this as a neighbor. There's a whole class of such issues, and I avoid them by only selecting vertices that are monotonic in angle, moving around A. So in the above example the midpoint MUST lie to the right of F and to the left of H. But G lies to the right of H, so I do not report an in-between vertex between F and H. This is all a bit heuristic, but is sufficient for the purposes of chessboard grid finding. */ #define FOR_ALL_ADJACENT_CELLS(c) do { \ const PointInt* pt = &points[c->source_index()]; \ const VORONOI::edge_type* const __e0 = c->incident_edge(); \ bool __first = true; \ for(const VORONOI::edge_type* __e = __e0; \ __e0 != NULL && (__e != __e0 || __first); \ __e = __e->next(), __first=false) \ { \ /* Look for two neighbors using this edge. The edge itself, */ \ /* and the vertex between this edge and the next edge */ \ for(int __i=0; __i<2; __i++) \ { \ const VORONOI::cell_type* c_adjacent = __e->twin()->cell(); \ const PointInt* pt_adjacent = &points[c_adjacent->source_index()]; \ if(__i == 0) \ ; /* use c_adjacent, pt_adjacent above */ \ else \ { \ const VORONOI::cell_type* c0 = c_adjacent; \ const PointInt* pt0 = pt_adjacent; \ const VORONOI::cell_type* c1 = __e->next()->twin()->cell(); \ const PointInt* pt1 = &points[c1->source_index()]; \ PointInt v0( pt0->x - pt->x, pt0->y - pt->y ); \ PointInt v1( pt1->x - pt->x, pt1->y - pt->y ); \ \ /* check for out-of-bounds midpoints in two ways */ \ /* just one method is probably enough, but I want to make sure */ \ \ /* if this edge and the next edge don't form an acute angle, */ \ /* then we're at a graph boundary, and there's no midpoint */ \ if((long)v1.x*(long)v0.y > (long)v0.x*(long)v1.y) \ continue; \ \ /* if this and the next edge don't form a triangle, */ \ /* then we're at a graph boundary, and there's no midpoint */ \ if(__e->twin()->prev()->twin()->cell() != __e->next()->twin()->cell() ) \ continue; \ \ /* get the midpoint vertex. It must lie angularly between its two neighbors */ \ c_adjacent = __e->twin()->prev()->prev()->twin()->cell(); \ const PointInt* ptmid = &points[c_adjacent->source_index()]; \ PointInt vmid( ptmid->x - pt->x, ptmid->y - pt->y ); \ if((long)v1 .x*(long)vmid.y > (long)vmid.x*(long)v1 .y) \ continue; \ if((long)vmid.x*(long)v0 .y > (long)v0 .x*(long)vmid.y) \ continue; \ pt_adjacent = &points[c_adjacent->source_index()]; \ } \ \ PointInt delta( pt_adjacent->x - pt->x, \ pt_adjacent->y - pt->y ); #define FOR_ALL_ADJACENT_CELLS_END() }}} while(0) // for integers #define ABS(x) ((x) > 0 ? (x) : -(x)) struct CandidateSequence { // First two cells and the last cell. The rest of the sequence is // constructed by following the best path from these two cells. The rules // that define this "best" path are consistent, so we don't store the path // itself, but recompute it each time it is needed const VORONOI::cell_type* c0; const VORONOI::cell_type* c1; const VORONOI::cell_type* clast; PointDouble delta_mean; double spacing_angle; double spacing_length; }; struct HypothesisStatistics { PointInt delta_last; double length_ratio_sum; int length_ratio_N; }; static void fill_initial_hypothesis_statistics(// out HypothesisStatistics* stats, // in const PointInt* delta0) { stats->delta_last = *delta0; stats->length_ratio_sum = 0.0; stats->length_ratio_N = 0; } // need delta, N_remaining, c, points #define FOR_MATCHING_ADJACENT_CELLS(debug_sequence_pointscale) do { \ HypothesisStatistics stats; \ fill_initial_hypothesis_statistics(&stats, delta); \ for(int i=0; i& points, int debug_sequence_pointscale /* <=0 means "no debugging" */ ) { // We're given a voronoi cell, and some properties that a potential next // cell in the sequence should match. I look through all the voronoi // neighbors of THIS cell, and return the first one that matches all my // requirements. Multiple neighboring cells COULD match, but I'm assuming // clean data, so this possibility is ignored. // // A matching next cell should have the following properties, all // established by the previous cells in the sequence. The next cell should // match all of these: // // - located along an expected direction (tight bound on angle) // // - should be an expected distance away (loose bound on absolute distance) // // - the ratio of successive distances in a sequence should be constant-ish // // The points in a sequence come from projections of equidistantly-spaced // points in a straight line, so the projected DIRECTIONS should match very // well. The distances may not, if the observed object is tilted and is // relatively close to the camera, but each successive distance will vary ~ // geometrically due to perspective effects, or it the distances will all be // roughly constant, which is still geometric, technically const PointInt& delta_last = stats->delta_last; double delta_last_length = hypot((double)delta_last.x, (double)delta_last.y); FOR_ALL_ADJACENT_CELLS(c) { if(debug_sequence_pointscale > 0) fprintf(stderr, "Considering connection in sequence from (%d,%d) -> (%d,%d); delta (%d,%d) ..... \n", pt->x / debug_sequence_pointscale, pt->y / debug_sequence_pointscale, pt_adjacent->x / debug_sequence_pointscale, pt_adjacent->y / debug_sequence_pointscale, delta.x / debug_sequence_pointscale, delta.y / debug_sequence_pointscale); double delta_length = hypot( (double)delta.x, (double)delta.y ); double cos_err = ((double)delta_last.x * (double)delta.x + (double)delta_last.y * (double)delta.y) / (delta_last_length * delta_length); if( cos_err < THRESHOLD_SPACING_COS ) { if(debug_sequence_pointscale > 0) fprintf(stderr, "..... rejecting. Angle is wrong. I wanted cos_err>=threshold, but saw %f<%f\n", cos_err, THRESHOLD_SPACING_COS); continue; } double length_ratio = delta_length / delta_last_length; if( length_ratio < THRESHOLD_SPACING_LENGTH_RATIO_MIN || length_ratio > THRESHOLD_SPACING_LENGTH_RATIO_MAX ) { if(debug_sequence_pointscale > 0) fprintf(stderr, "..... rejecting. Lengths are wrong. I wanted abs(length_ratio)<=threshold, but saw %f<%f or %f>%f\n", length_ratio, THRESHOLD_SPACING_LENGTH_RATIO_MIN, length_ratio, THRESHOLD_SPACING_LENGTH_RATIO_MAX); continue; } // I compute the mean and look at the deviation from the CURRENT mean. I // ignore the first few points, since the mean is unstable then. This is // OK, however, since I'm going to find and analyze the same sequence in // the reverse order, and this will cover the other end if( stats->length_ratio_N > 2 ) { double length_ratio_mean = stats->length_ratio_sum / (double)stats->length_ratio_N; double length_ratio_deviation = length_ratio - length_ratio_mean; if( length_ratio_deviation < -THRESHOLD_SPACING_LENGTH_RATIO_DEVIATION || length_ratio_deviation > THRESHOLD_SPACING_LENGTH_RATIO_DEVIATION ) { if(debug_sequence_pointscale > 0) fprintf(stderr, "..... rejecting. Lengths are wrong. I wanted abs(length_ratio_deviation)<=threshold, but saw %f>%f\n", fabs(length_ratio_deviation), THRESHOLD_SPACING_LENGTH_RATIO_DEVIATION); continue; } } stats->length_ratio_sum += length_ratio; stats->length_ratio_N++; stats->delta_last = delta; if(debug_sequence_pointscale > 0) fprintf(stderr, "..... accepting!\n\n"); return c_adjacent; } FOR_ALL_ADJACENT_CELLS_END(); return NULL; } static const VORONOI::cell_type* search_along_sequence( // out PointDouble* delta_mean, // in const PointInt* delta, const VORONOI::cell_type* c, int N_remaining, const std::vector& points, int debug_sequence_pointscale ) { delta_mean->x = (double)delta->x; delta_mean->y = (double)delta->y; const VORONOI::cell_type* clast = NULL; FOR_MATCHING_ADJACENT_CELLS(debug_sequence_pointscale) { if( c_adjacent == NULL ) return NULL; delta_mean->x += (double)stats.delta_last.x; delta_mean->y += (double)stats.delta_last.y; if(i == N_remaining-1) clast = c_adjacent; } FOR_MATCHING_ADJACENT_CELLS_END(); delta_mean->x /= (double)(N_remaining+1); delta_mean->y /= (double)(N_remaining+1); return clast; } static void output_point( std::vector& points_out, const VORONOI::cell_type* c, const std::vector& points ) { const PointInt* pt = &points[c->source_index()]; points_out.push_back( PointDouble( (double)pt->x / (double)FIND_GRID_SCALE, (double)pt->y / (double)FIND_GRID_SCALE) ); } static void output_points_along_sequence( std::vector& points_out, const PointInt* delta, const VORONOI::cell_type* c, int N_remaining, const std::vector& points) { FOR_MATCHING_ADJACENT_CELLS(-1) { output_point(points_out, c_adjacent, points); } FOR_MATCHING_ADJACENT_CELLS_END(); } static void output_row( std::vector& points_out, const CandidateSequence& row, const std::vector& points, const int gridn) { output_point(points_out, row.c0, points); output_point(points_out, row.c1, points); const PointInt* pt0 = &points[row.c0->source_index()]; const PointInt* pt1 = &points[row.c1->source_index()]; PointInt delta({ pt1->x - pt0->x, pt1->y - pt0->y}); output_points_along_sequence( points_out, &delta, row.c1, gridn-2, points); } // dumps the voronoi diagram to a self-plotting vnlog #define DUMP_FILENAME_VORONOI "/tmp/mrgingham-2-voronoi.vnl" static void dump_voronoi( const VORONOI* voronoi, const std::vector& points ) { FILE* fp = fopen(DUMP_FILENAME_VORONOI, "w"); assert(fp); // the kernel limits the #! line to 127 characters, so I abbreviate fprintf(fp, "#!/usr/bin/feedgnuplot --domain --dataid --with 'lines linecolor 0' --square --maxcurves 100000 --set 'yrange [:] rev'\n"); fprintf(fp, "# x id_edge y\n"); int i_edge = 0; for (auto it = voronoi->cells().begin(); it != voronoi->cells().end(); it++ ) { const VORONOI::cell_type* c = &(*it); const PointInt* pt0 = &points[c->source_index()]; const VORONOI::edge_type* const e0 = c->incident_edge(); bool first = true; for(const VORONOI::edge_type* e = e0; e0 != NULL && (e != e0 || first); e = e->next(), first=false) { const VORONOI::cell_type* c_adjacent = e->twin()->cell(); const PointInt* pt1 = &points[c_adjacent->source_index()]; fprintf(fp, "%f %d %f\n", pt0->x/(double)FIND_GRID_SCALE, i_edge, pt0->y/(double)FIND_GRID_SCALE); fprintf(fp, "%f %d %f\n", pt1->x/(double)FIND_GRID_SCALE, i_edge, pt1->y/(double)FIND_GRID_SCALE); i_edge++; } } fclose(fp); chmod(DUMP_FILENAME_VORONOI, S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IWGRP | S_IXUSR | S_IXGRP | S_IXOTH); fprintf(stderr, "Wrote self-plotting voronoi diagram to " DUMP_FILENAME_VORONOI "\n"); } static void dump_interval( FILE* fp, const int i_candidate, const int i_pt, const VORONOI::cell_type* c0, const VORONOI::cell_type* c1, const std::vector& points ) { const PointInt* pt0 = &points[c0->source_index()]; if( c1 == NULL ) { fprintf(fp, "%d %d %f %f - - - - - -\n", i_candidate, i_pt, (double)pt0->x / (double)FIND_GRID_SCALE, (double)pt0->y / (double)FIND_GRID_SCALE); return; } const PointInt* pt1 = &points[c1->source_index()]; double dx = (double)(pt1->x - pt0->x) / (double)FIND_GRID_SCALE; double dy = (double)(pt1->y - pt0->y) / (double)FIND_GRID_SCALE; double length = hypot(dx,dy); double angle = atan2(dy,dx) * 180.0 / M_PI; fprintf(fp, "%d %d %f %f %f %f %f %f %f %f\n", i_candidate, i_pt, (double)pt0->x / (double)FIND_GRID_SCALE, (double)pt0->y / (double)FIND_GRID_SCALE, (double)pt1->x / (double)FIND_GRID_SCALE, (double)pt1->y / (double)FIND_GRID_SCALE, dx, dy, length, angle); } static void dump_intervals_along_sequence( FILE* fp, const CandidateSequence* cs, int i_candidate, const std::vector& points, const int gridn) { int N_remaining = gridn-1; dump_interval(fp, i_candidate, 0, cs->c0, cs->c1, points); const PointInt* pt0 = &points[cs->c0->source_index()]; const PointInt* pt1 = &points[cs->c1->source_index()]; PointInt _delta({ pt1->x - pt0->x, pt1->y - pt0->y}); const PointInt* delta = &_delta; const VORONOI::cell_type* c = cs->c1; FOR_MATCHING_ADJACENT_CELLS(-1) { dump_interval(fp, i_candidate, i+1, c, i+1 == gridn-1 ? NULL : c_adjacent, points); } FOR_MATCHING_ADJACENT_CELLS_END(); } static double get_spacing_angle( double y, double x ) { double angle = 180.0/M_PI * atan2(y,x); if( angle < 0 ) angle += 180.0; return angle; } typedef std::vector v_CS; typedef std::map > SequenceIndicesFromPoint; struct outer_cycle { short e[4]; }; static void get_sequence_candidates( // out v_CS* sequence_candidates, // in const VORONOI* voronoi, const std::vector& points, // for debugging const debug_sequence_t& debug_sequence, const int gridn) { const VORONOI::cell_type* tracing_c = NULL; int debug_sequence_pointscale = -1; if(debug_sequence.dodebug) { // we're tracing some point. I find the nearest voronoi vertex, and // debug_sequence that unsigned long d2 = (unsigned long)(-1L); // max at first debug_sequence_pointscale = FIND_GRID_SCALE; for (auto it = voronoi->cells().begin(); it != voronoi->cells().end(); it++ ) { const VORONOI::cell_type* c = &(*it); const PointInt* pt = &points[c->source_index()]; long dx = (long)(pt->x - debug_sequence_pointscale*debug_sequence.pt.x); long dy = (long)(pt->y - debug_sequence_pointscale*debug_sequence.pt.y); unsigned long d2_here = (unsigned long)(dx*dx + dy*dy); if(d2_here < d2) { d2 = d2_here; tracing_c = c; } } const PointInt* pt = &points[tracing_c->source_index()]; fprintf(stderr, "============== Looking at sequences from (%d,%d)\n", pt->x / debug_sequence_pointscale, pt->y / debug_sequence_pointscale); } for (auto it = voronoi->cells().begin(); it != voronoi->cells().end(); it++ ) { const VORONOI::cell_type* c = &(*it); bool debug_sequence = ( c == tracing_c ); FOR_ALL_ADJACENT_CELLS(c) { if(c == tracing_c) fprintf(stderr, "\n\n====== Looking at adjacent point (%d,%d)\n", pt_adjacent->x / debug_sequence_pointscale, pt_adjacent->y / debug_sequence_pointscale); PointDouble delta_mean; const VORONOI::cell_type* clast = search_along_sequence( &delta_mean, &delta, c_adjacent, gridn-2, points, (c == tracing_c) ? debug_sequence_pointscale : -1 ); if( clast ) { double spacing_angle = get_spacing_angle(delta_mean.y, delta_mean.x); double spacing_length = hypot(delta_mean.x, delta_mean.y); sequence_candidates->push_back( CandidateSequence({c, c_adjacent, clast, delta_mean, spacing_angle, spacing_length}) ); } } FOR_ALL_ADJACENT_CELLS_END(); } } static void get_candidate_point( unsigned int* cs_point, const VORONOI::cell_type* c ) { *cs_point = c->source_index(); } static void get_candidate_points_along_sequence( unsigned int* cs_points, const PointInt* delta, const VORONOI::cell_type* c, int N_remaining, const std::vector& points) { FOR_MATCHING_ADJACENT_CELLS(-1) { get_candidate_point(cs_points, c_adjacent); cs_points++; } FOR_MATCHING_ADJACENT_CELLS_END(); } static void get_candidate_points( unsigned int* cs_points, const CandidateSequence* cs, const std::vector& points, const int gridn) { get_candidate_point( &cs_points[0], cs->c0 ); get_candidate_point( &cs_points[1], cs->c1 ); const PointInt* pt0 = &points[cs->c0->source_index()]; const PointInt* pt1 = &points[cs->c1->source_index()]; PointInt delta({ pt1->x - pt0->x, pt1->y - pt0->y}); get_candidate_points_along_sequence(&cs_points[2], &delta, cs->c1, gridn-2, points); } // dumps a terse self-plotting vnlog visualization of sequence candidates, and a // more detailed vnlog containing more data #define DUMP_BASENAME_ALL_SEQUENCE_CANDIDATES "/tmp/mrgingham-3-candidates" #define DUMP_BASENAME_OUTER_EDGES "/tmp/mrgingham-4-outer-edges" #define DUMP_BASENAME_OUTER_EDGE_CYCLES "/tmp/mrgingham-5-outer-edge-cycles" #define DUMP_BASENAME_IDENTIFIED_OUTER_EDGE_CYCLE "/tmp/mrgingham-6-identified-outer-edge-cycle" #define dump_candidates(basename, sequence_candidates, outer_edges, points, gridn) \ _dump_candidates( basename".vnl", basename"-detailed.vnl", \ sequence_candidates, outer_edges, points, gridn ) static void _dump_candidates(const char* filename_sparse, const char* filename_dense, const v_CS* sequence_candidates, const std::vector* outer_edges, const std::vector& points, const int gridn) { FILE* fp = fopen(filename_sparse, "w"); assert(fp); // the kernel limits the #! line to 127 characters, so I abbreviate fprintf(fp, "#!/usr/bin/feedgnuplot --dom --aut --square --rangesizea 3 --w 'vec size screen 0.01,20 fixed fill' --set 'yr [:] rev'\n"); fprintf(fp, "# fromx fromy deltax deltay\n"); if(outer_edges == NULL) for( auto it = sequence_candidates->begin(); it != sequence_candidates->end(); it++ ) { const CandidateSequence* cs = &(*it); const PointInt* pt = &points[cs->c0->source_index()]; fprintf(fp, "%f %f %f %f\n", (double)(pt->x) / (double)FIND_GRID_SCALE, (double)(pt->y) / (double)FIND_GRID_SCALE, cs->delta_mean.x / (double)FIND_GRID_SCALE, cs->delta_mean.y / (double)FIND_GRID_SCALE); } else for( auto it = outer_edges->begin(); it != outer_edges->end(); it++ ) { const CandidateSequence* cs = &((*sequence_candidates)[*it]); const PointInt* pt = &points[cs->c0->source_index()]; fprintf(fp, "%f %f %f %f\n", (double)(pt->x) / (double)FIND_GRID_SCALE, (double)(pt->y) / (double)FIND_GRID_SCALE, cs->delta_mean.x / (double)FIND_GRID_SCALE, cs->delta_mean.y / (double)FIND_GRID_SCALE); } fclose(fp); chmod(filename_sparse, S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IWGRP | S_IXUSR | S_IXGRP | S_IXOTH); fprintf(stderr, "Wrote self-plotting sequence-candidate dump to %s\n", filename_sparse); // detailed fp = fopen(filename_dense, "w"); assert(fp); fprintf(fp, "# candidateid pointid fromx fromy tox toy deltax deltay len angle\n"); if(outer_edges == NULL) { int N = sequence_candidates->size(); for( int i=0; isize(); for( int i=0; i& outer_cycles, const std::vector& outer_edges, const v_CS& sequence_candidates, const std::vector& points) { FILE* fp = fopen(DUMP_BASENAME_OUTER_EDGE_CYCLES, "w"); assert(fp); // the kernel limits the #! line to 127 characters, so I abbreviate fprintf(fp, "#!/usr/bin/feedgnuplot --datai --dom --aut --square --rangesizea 3 --w 'vec size screen 0.01,20 fixed fill' --set 'yr [:] rev'\n"); fprintf(fp, "# fromx type fromy deltax deltay\n"); for( int i_cycle=0; i_cycle<(int)outer_cycles.size(); i_cycle++ ) { for(int i_edge = 0; i_edge<4; i_edge++ ) { const CandidateSequence* cs = &sequence_candidates[outer_edges[ outer_cycles[i_cycle].e[i_edge] ]]; const PointInt* pt = &points[cs->c0->source_index()]; fprintf(fp, "%f %d %f %f %f\n", (double)(pt->x) / (double)FIND_GRID_SCALE, i_cycle, (double)(pt->y) / (double)FIND_GRID_SCALE, cs->delta_mean.x / (double)FIND_GRID_SCALE, cs->delta_mean.y / (double)FIND_GRID_SCALE); } } fclose(fp); chmod(DUMP_BASENAME_OUTER_EDGE_CYCLES, S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IWGRP | S_IXUSR | S_IXGRP | S_IXOTH); fprintf(stderr, "Wrote outer edge cycle dump to %s\n", DUMP_BASENAME_OUTER_EDGE_CYCLES); } static void dump_outer_edge_cycles_identified(const std::vector& outer_cycles, const std::vector& outer_edges, const v_CS& sequence_candidates, const std::vector& points, const int* outer_cycle_pair, int iclockwise, const int* iedge_top) { FILE* fp = fopen(DUMP_BASENAME_IDENTIFIED_OUTER_EDGE_CYCLE, "w"); assert(fp); // the kernel limits the #! line to 127 characters, so I abbreviate fprintf(fp, "#!/usr/bin/feedgnuplot --datai --dom --aut --square --rangesizea 3 --w 'vec size screen 0.01,20 fixed fill' --set 'yr [:] rev'\n"); fprintf(fp, "# fromx type fromy deltax deltay\n"); for( int i_cycle=0; i_cycle<2; i_cycle++ ) { for(int i_edge = 0; i_edge<4; i_edge++ ) { const CandidateSequence* cs = &sequence_candidates[outer_edges[ outer_cycles[outer_cycle_pair[i_cycle]].e[i_edge] ]]; const PointInt* pt = &points[cs->c0->source_index()]; char what[128]; sprintf(what, "%s%s", (i_cycle == iclockwise) ? "clockwise" : "counterclockwise", iedge_top[i_cycle] == i_edge ? "-top" : ""); fprintf(fp, "%f %s %f %f %f\n", (double)(pt->x) / (double)FIND_GRID_SCALE, what, (double)(pt->y) / (double)FIND_GRID_SCALE, cs->delta_mean.x / (double)FIND_GRID_SCALE, cs->delta_mean.y / (double)FIND_GRID_SCALE); } } fclose(fp); chmod(DUMP_BASENAME_IDENTIFIED_OUTER_EDGE_CYCLE, S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IWGRP | S_IXUSR | S_IXGRP | S_IXOTH); fprintf(stderr, "Wrote outer edge cycle dump to %s\n", DUMP_BASENAME_IDENTIFIED_OUTER_EDGE_CYCLE); } static bool is_crossing( unsigned int line0_pt0, unsigned int line0_pt1, unsigned int line1_pt0, unsigned int line1_pt1, const std::vector& points) { // First I translate everything so that line0_pt0 is at the origin float l0pt1[2] = { (float)(points[line0_pt1].x - points[line0_pt0].x), (float)(points[line0_pt1].y - points[line0_pt0].y) }; float l1pt0[2] = { (float)(points[line1_pt0].x - points[line0_pt0].x), (float)(points[line1_pt0].y - points[line0_pt0].y) }; float l1pt1[2] = { (float)(points[line1_pt1].x - points[line0_pt0].x), (float)(points[line1_pt1].y - points[line0_pt0].y) }; // Now I rotate the 3 points such that l0pt1 ends up aligned on the x axis. // I don't bother to divide by the hypotenuse, so l0pt1 is at (d^2, 0) float d2 = l0pt1[0]*l0pt1[0] + l0pt1[1]*l0pt1[1]; float l1pt0_rotated[2] = { l1pt0[0]*l0pt1[0] + l1pt0[1]*l0pt1[1], -l1pt0[0]*l0pt1[1] + l1pt0[1]*l0pt1[0] }; float l1pt1_rotated[2] = { l1pt1[0]*l0pt1[0] + l1pt1[1]*l0pt1[1], -l1pt1[0]*l0pt1[1] + l1pt1[1]*l0pt1[0] }; // In this rotated space, the two points must have opposite-sign y (lie on // both sides of the line). if( l1pt0_rotated[1]*l1pt1_rotated[1] > 0 ) return false; // To cross the line, both x coords can't be off the edge if( (l1pt0_rotated[0] < 0 && l1pt1_rotated[0] < 0) || (l1pt0_rotated[0] > d2 && l1pt1_rotated[0] > d2) ) return false; // OK, maybe they do cross. I actually compute the intersection // a + k(b-a) = [..., 0] -> k = a1/(a1-b1) float k = l1pt0_rotated[1] / (l1pt0_rotated[1] - l1pt1_rotated[1]); float x = l1pt0_rotated[0] + k * (l1pt1_rotated[0] - l1pt0_rotated[0]); return x >= 0.0f && x <= d2; } // recursively search for 4-cycle sequences of outer edges. On success, returns // true, with the cycle reported in edges static bool next_outer_edge(// this iteration outer_cycle* edges, // edge sequence being // evaluated. Output // returned here int edge_count, // how many edges we've got, // including this one // context unsigned int point_initial, const std::vector& outer_edges, const v_CS& sequence_candidates, const SequenceIndicesFromPoint& outer_edges_from_point, const std::vector& points, bool debug) { bool found_cycle = false; outer_cycle outer_cycle_found = {}; int i_edge = (int)edges->e[edge_count-1]; unsigned int first_point_this_edge = sequence_candidates[outer_edges[i_edge]].c0 ->source_index(); unsigned int last_point_this_edge = sequence_candidates[outer_edges[i_edge]].clast->source_index(); const std::vector* next_edges; try { next_edges = &outer_edges_from_point.at(last_point_this_edge); } catch(...) { if(debug) fprintf(stderr, "No opposing outer edge\n"); return false; } int Nedges_from_here = next_edges->size(); for(int i=0; isource_index(); if( last_point_next_edge == first_point_this_edge ) // This next edge is an inverse of this edge. It's not a part of my // 4-cycle continue; // I need to ignore X structures. In the below, ACBDA is valid, // but ABCDA is NOT valid. // // A C // |\/| // |/\| // D B // // Thus I make sure that // edge 3 does not cross edge 1 and that // edge 4 does not cross edge 2 if(edge_count != 3) { // This is not the last edge. It may not go to the start if( last_point_next_edge == point_initial ) continue; if(edge_count == 2) { if( is_crossing(sequence_candidates[outer_edges[ edges->e[0] ]].c0 ->source_index(), sequence_candidates[outer_edges[ edges->e[0] ]].clast->source_index(), sequence_candidates[outer_edges[ (*next_edges)[i] ]].c0 ->source_index(), sequence_candidates[outer_edges[ (*next_edges)[i] ]].clast->source_index(), points )) continue; } edges->e[edge_count] = (short)(*next_edges)[i]; if(!next_outer_edge( edges, edge_count+1, point_initial, outer_edges, sequence_candidates, outer_edges_from_point, points, debug)) continue; // Got a successful result. It must be unique if(found_cycle) { if(debug) fprintf(stderr, "Found non-unique 4-cycle\n"); return false; } found_cycle = true; outer_cycle_found = *edges; } else { // Last edge. May only go back to the start if( last_point_next_edge != point_initial ) continue; if( is_crossing(sequence_candidates[outer_edges[ edges->e[1] ]].c0 ->source_index(), sequence_candidates[outer_edges[ edges->e[1] ]].clast->source_index(), sequence_candidates[outer_edges[ (*next_edges)[i] ]].c0 ->source_index(), sequence_candidates[outer_edges[ (*next_edges)[i] ]].clast->source_index(), points )) { // I already found the last edge, but it's crossing itself. I // know there aren't any more solutions here, so I exit return false; } edges->e[3] = (short)(*next_edges)[i]; return true; } } if(!found_cycle) return false; *edges = outer_cycle_found; return true; } static bool is_equalAndOpposite_cycle(const outer_cycle& cycle0, const outer_cycle& cycle1, // context const std::vector& outer_edges, const v_CS& sequence_candidates, bool debug) { // Pick an arbitrary starting point: initial point of the first edge of the // first cycle int iedge0 = 0; unsigned int ipt0 = sequence_candidates[outer_edges[cycle0.e[iedge0]]].c0->source_index(); // find the edge in the potentially-opposite cycle that ends at this point int iedge1 = -1; for(int _iedge1 = 0; _iedge1 < 4; _iedge1++) if(ipt0 == sequence_candidates[outer_edges[ cycle1.e[_iedge1] ]].clast->source_index()) { iedge1 = _iedge1; break; } if( iedge1 < 0) { if(debug) fprintf(stderr, "Given outer cycles are NOT equal and opposite: couldn't find a corresponding point in the two cycles\n"); return false; } // Now I traverse the two cycles, and compare. I traverse the second cycle // backwards, and compare the back/front and front/back for(int i=0; i<4; i++) { unsigned int cycle0_points[2] = { (unsigned int)sequence_candidates[outer_edges[cycle0.e[iedge0]]].c0 ->source_index(), (unsigned int)sequence_candidates[outer_edges[cycle0.e[iedge0]]].clast->source_index()}; unsigned int cycle1_points[2] = { (unsigned int)sequence_candidates[outer_edges[cycle1.e[iedge1]]].c0 ->source_index(), (unsigned int)sequence_candidates[outer_edges[cycle1.e[iedge1]]].clast->source_index() }; if(cycle0_points[0] != cycle1_points[1] || cycle0_points[1] != cycle1_points[0] ) { if(debug) fprintf(stderr, "Given outer cycles are NOT equal and opposite\n"); return false; } iedge0 = (iedge0+1) % 4; iedge1 = (iedge1+3) % 4; } return true; } // assumes the two given cycles are equal and opposite // // returns // 0 if cycle0 is clockwise // 1 if cycle1 is clockwise // <0 if the cycle is not convex // // Convexity is determined by connecting the corners with straight lines. I // guess maybe a lens could be so distorted that you get nonconvex polygon, but // I can't quite imagine that. The polygon would have to look like this: /* /\ / \ / \ / \ / /\ \ / -- -- \ /__/ \__\ */ static int select_clockwise_cycle_and_find_top(// out int iedge_top[2], const outer_cycle& cycle0, const outer_cycle& cycle1, // context const std::vector& outer_edges, const v_CS& sequence_candidates, const std::vector& points, bool debug) { // I pick a cycle, and look at the sign of the cross-product of each // consecutive direction. The sign should be constant for all consecutive // directions. Non-convexity would generate a different sign. And the sign // tells me if the thing is clockwise or not // 4 segments, each direction has an x and a y int v[4][2]; for(int i=0; i<4; i++) { unsigned int ipt0 = sequence_candidates[outer_edges[cycle0.e[i]]].c0 ->source_index(); unsigned int ipt1 = sequence_candidates[outer_edges[cycle0.e[i]]].clast->source_index(); v[i][0] = (points[ipt1].x - points[ipt0].x) / FIND_GRID_SCALE_APPROX_POWER2 ; v[i][1] = (points[ipt1].y - points[ipt0].y) / FIND_GRID_SCALE_APPROX_POWER2 ; } bool sign[4]; for(int i0=0; i0<4; i0++) { int i1 = (i0+1)%4; sign[i0] = v[i1][0]*v[i0][1] < v[i0][0]*v[i1][1]; } int i_clockwise; if( sign[0] && sign[1] && sign[2] && sign[3]) i_clockwise = 0; else if(!sign[0] && !sign[1] && !sign[2] && !sign[3]) i_clockwise = 1; else { if(debug) fprintf(stderr, "The outer edge cycles aren't convex!\n"); return -1; } const outer_cycle* cycles[2] = {&cycle0, &cycle1}; for( int icycle=0; icycle<2; icycle++) { // To find the "top", I find the pick the two edges sharing the lowest y // coord vertex, and pick the most horizontal one of those. // // In all of these [0] is the "lowest-y-coord" edge. [1] is the // second-lowest-y-coord edge. These top-2 edges will be tied for the // lowest y coord, which is fine: I have extra logic to pick between // them int y_min[2] = {INT_MAX, INT_MAX}; // lowest y coord int iedge_min[2] = {-1, -1}; // edge index int ipt_miny[2] = {}; // index of the point with the lower y coord in this edge int ipt_maxy[2] = {}; // index of the point with the higher y coord in this edge for(int i=0; i<4; i++) { unsigned int ipt0 = sequence_candidates[outer_edges[cycles[icycle]->e[i]]].c0 ->source_index(); unsigned int ipt1 = sequence_candidates[outer_edges[cycles[icycle]->e[i]]].clast->source_index(); int y_min_this, ipt_miny_this, ipt_maxy_this; if(points[ipt0].y < points[ipt1].y) { y_min_this = points[ipt0].y; ipt_miny_this = ipt0; ipt_maxy_this = ipt1; } else { y_min_this = points[ipt1].y; ipt_miny_this = ipt1; ipt_maxy_this = ipt0; } if(y_min_this < y_min[0]) { // Highest one seen so far. Push back the previous highest y_min [1] = y_min [0]; iedge_min[1] = iedge_min[0]; ipt_miny [1] = ipt_miny [0]; ipt_maxy [1] = ipt_maxy [0]; y_min [0] = y_min_this; iedge_min[0] = i; ipt_miny [0] = ipt_miny_this; ipt_maxy [0] = ipt_maxy_this; } else if(y_min_this < y_min[1]) { // Second-highest y_min [1] = y_min_this; iedge_min[1] = i; ipt_miny [1] = ipt_miny_this; ipt_maxy [1] = ipt_maxy_this; } } // I have the two edges, both of which MAY be the "top" edge. This edge // should be more the more horizontal of the two. I check for an angle // difference, and if it's too small return nothing: I shouldn't return // low-confidence results. I want the angle difference off horizontal, // so in this scenario, the angle difference I want is 0, even though // diff(theta) is high: // // - // | | // - - // | | // // So I use abs(dx) before looking at the angle difference // // cos(dtheta) = inner(v0,v1) / sqrt(norm2(v0)*norm2(v1)) -> // cos(dtheta)^2 = (v0x*v1x + v0y*v1y)^2 / ((v0x*v0x + v0y*v0y)*(v1x*v1x + v1y*v1y)) -> // sin(dtheta)^2 = 1 - cos(dtheta)^2 = (v0x*v1y - v0y*v1x)^2 / ((v0x*v0x + v0y*v0y)*(v1x*v1x + v1y*v1y)) int64_t v0y = (points[ipt_maxy[0]].y - points[ipt_miny[0]].y) / FIND_GRID_SCALE_APPROX_POWER2; int64_t v0x = (points[ipt_maxy[0]].x - points[ipt_miny[0]].x) / FIND_GRID_SCALE_APPROX_POWER2; int64_t v1y = (points[ipt_maxy[1]].y - points[ipt_miny[1]].y) / FIND_GRID_SCALE_APPROX_POWER2; int64_t v1x = (points[ipt_maxy[1]].x - points[ipt_miny[1]].x) / FIND_GRID_SCALE_APPROX_POWER2; v0x = ABS(v0x); v1x = ABS(v1x); int64_t cross = (v0x*v1y - v0y*v1x)*(v0x*v1y - v0y*v1x); int64_t denom = (v0x*v0x + v0y*v0y)*(v1x*v1x + v1y*v1y); #define SINTHSQ_THRESHOLD_NUMERATOR 1 /* 20.7 deg */ #define SINTHSQ_THRESHOLD_DENOMINATOR 8 if( ABS(cross)*SINTHSQ_THRESHOLD_DENOMINATOR < denom*SINTHSQ_THRESHOLD_NUMERATOR ) { if(debug) { fprintf(stderr, "Highest 2 edges have a similar orientation. I can't tell clearly which is the more horizontal one\n"); fprintf(stderr, " Highest edge: (%.2f,%.2f) - (%.2f,%.2f). Highest vertex: (%.2f,%.2f)\n", (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[0]]]].c0 ->source_index()].x / (double)FIND_GRID_SCALE, (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[0]]]].c0 ->source_index()].y / (double)FIND_GRID_SCALE, (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[0]]]].clast->source_index()].x / (double)FIND_GRID_SCALE, (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[0]]]].clast->source_index()].y / (double)FIND_GRID_SCALE, (double)points[ipt_miny[0] ].x / (double)FIND_GRID_SCALE, (double)points[ipt_miny[0] ].y / (double)FIND_GRID_SCALE); fprintf(stderr, " Second-highest edge: (%.2f,%.2f) - (%.2f,%.2f). Highest vertex: (%.2f,%.2f)\n", (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[1]]]].c0 ->source_index()].x / (double)FIND_GRID_SCALE, (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[1]]]].c0 ->source_index()].y / (double)FIND_GRID_SCALE, (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[1]]]].clast->source_index()].x / (double)FIND_GRID_SCALE, (double)points[sequence_candidates[outer_edges[cycles[icycle]->e[iedge_min[1]]]].clast->source_index()].y / (double)FIND_GRID_SCALE, (double)points[ipt_miny[1] ].x / (double)FIND_GRID_SCALE, (double)points[ipt_miny[1] ].y / (double)FIND_GRID_SCALE); fprintf(stderr, " sin(angle difference) as computed here: %f. Threshold: %f\n", sqrt((double)ABS(cross)/(double)denom), sqrt((double)SINTHSQ_THRESHOLD_NUMERATOR/(double)SINTHSQ_THRESHOLD_DENOMINATOR)); } return -1; } // Which is more horizontal? I want the lower abs(dy/dx), but I don't // want to do any division. // abs(v0y)/abs(v0x) < abs(v1y)/abs(v1x) -> // abs(v0y*v1x) < abs(v1y*v0x) if( ABS(v0y*v1x) < ABS(v1y*v0x) ) iedge_top[icycle] = iedge_min[0]; else iedge_top[icycle] = iedge_min[1]; } return i_clockwise; } static int find_sequence_from_to( // inputs unsigned int from, unsigned int to, // context const v_CS& sequence_candidates, const SequenceIndicesFromPoint& sequences_from_point ) { try { const std::vector& sequences = sequences_from_point.at(from); for(int i=0; i<(int)sequences.size(); i++) { const CandidateSequence* cs = &sequence_candidates[sequences[i]]; if(cs->clast->source_index() == to) return sequences[i]; } return -1; } catch(...) { return -1; } } __attribute__((visibility("default"))) bool mrgingham::find_grid_from_points( // out std::vector& points_out, // in const std::vector& points, const int gridn, bool debug, const debug_sequence_t& debug_sequence) { VORONOI voronoi; construct_voronoi(points.begin(), points.end(), &voronoi); if(debug) dump_voronoi(&voronoi, points); v_CS sequence_candidates; get_sequence_candidates(&sequence_candidates, &voronoi, points, debug_sequence, gridn); if(debug) { dump_candidates(DUMP_BASENAME_ALL_SEQUENCE_CANDIDATES, &sequence_candidates, NULL, points, gridn); fprintf(stderr, "got %zd points\n", points.size()); fprintf(stderr, "got %zd sequence candidates\n", sequence_candidates.size()); } // I have all the sequence candidates. I find all the sequences that could // be edges of my grid: each one begins at a cell that's the start of at // least two sequences std::vector outer_edges; // I likely only need 8, but I don't want to ever reallocate this thing outer_edges.reserve(20); std::map sequences_initiated_count; int Ncs = sequence_candidates.size(); for( int i=0; ic0->source_index()]++; } for( int i=0; ic0->source_index()] >= 2) outer_edges.push_back(i); } // I now have potential outer edges: all sequences that begin with a cell // that initiates at least two sequences (this one and at least one other) if( outer_edges.size() < 8 ) { // need at least 8 outer edges: 4 in each direction if(debug) { fprintf(stderr, "Too few candidates for an outer edge of the grid. Needed at least 8, got %d\n", (int)outer_edges.size()); } return false; } if(debug) dump_candidates(DUMP_BASENAME_OUTER_EDGES, &sequence_candidates, &outer_edges, points, gridn); // I won't have very many of these outer edges, so I don't worry too much // about efficient algorithms here. // // I look for 2 cyclical sequences of length 4 each. Equal and opposite to // each other int Nouter_edges = outer_edges.size(); SequenceIndicesFromPoint outer_edges_from_point; for( int i=0; ic0->source_index()].push_back(i); } std::vector outer_cycles; std::set outer_edges_in_found_cycles; for( int i=0; isource_index(), outer_edges, sequence_candidates, outer_edges_from_point, points, debug)) continue; outer_cycles.push_back( outer_cycle_found ); for(int i=0; i<4; i++) outer_edges_in_found_cycles.insert(outer_cycle_found.e[i]); } if(debug && outer_cycles.size()) dump_outer_edge_cycles(outer_cycles, outer_edges, sequence_candidates, points); if(outer_cycles.size() < 2) { if(debug) fprintf(stderr, "Found too few 4-cycles. Needed at least 2, got %d\n", (int)outer_cycles.size()); return false; } // I should have exactly one set of an equal/opposite cycles int outer_cycle_pair[2] = {-1,-1}; for(int i0=0; i0<(int)outer_cycles.size(); i0++) for(int i1=i0+1; i1<(int)outer_cycles.size(); i1++) { if(is_equalAndOpposite_cycle(outer_cycles[i0], outer_cycles[i1], outer_edges, sequence_candidates, debug)) { if(outer_cycle_pair[0] >= 0) { if(debug) fprintf(stderr, "Found more than one equal-and-opposite pair of outer-edge cycles. Giving up\n"); return false; } outer_cycle_pair[0] = i0; outer_cycle_pair[1] = i1; } } if(outer_cycle_pair[0] < 0) { if(debug) fprintf(stderr, "Didn't find any equal-and-opposite pairs of outer-edge cycles. Giving up\n"); return false; } // I have my equal-and-opposite pair of cycles. I find the clockwise one. It // contains the top edge, which is the first in the sequence I'm going to // end up reporting int iedge_top[2]; int iclockwise = select_clockwise_cycle_and_find_top(// out iedge_top, // cycles I'm looking at outer_cycles[outer_cycle_pair[0]], outer_cycles[outer_cycle_pair[1]], // context outer_edges, sequence_candidates, points, debug); if(iclockwise < 0) return false; if(debug) dump_outer_edge_cycles_identified(outer_cycles, outer_edges, sequence_candidates, points, outer_cycle_pair, iclockwise, iedge_top); // All done with the outer edges of the board. I now fill-in the internal // grid SequenceIndicesFromPoint sequences_from_point; for( int i=0; i<(int)sequence_candidates.size(); i++ ) { const CandidateSequence* cs = &sequence_candidates[i]; sequences_from_point[cs->c0->source_index()].push_back(i); } // sequences in sequence_candidates[] int horizontal_rows[gridn]; int vertical_left, vertical_right; horizontal_rows[0] = outer_edges[outer_cycles[outer_cycle_pair[ iclockwise]].e[ iedge_top[ iclockwise] ]]; vertical_left = outer_edges[outer_cycles[outer_cycle_pair[1-iclockwise]].e[ (iedge_top[1-iclockwise] + 1) % 4 ]]; vertical_right = outer_edges[outer_cycles[outer_cycle_pair[ iclockwise]].e[ (iedge_top[ iclockwise] + 1) % 4 ]]; unsigned int vertical_left_points [gridn]; unsigned int vertical_right_points[gridn]; get_candidate_points( vertical_left_points, &sequence_candidates[vertical_left ], points, gridn ); get_candidate_points( vertical_right_points, &sequence_candidates[vertical_right], points, gridn ); for(int i=1; i =head1 AUTHOR Dima Kogan, C<< >> =head1 LICENSE AND COPYRIGHT This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. Copyright 2017-2018 California Institute of Technology Copyright 2017-2018 Dima Kogan (C) mrgingham-1.26/generate-chessboard-fig.py000077500000000000000000000074201476660265400204630ustar00rootroot00000000000000#!/usr/bin/python3 r'''Generate a .fig file with a chessboard with a given geometry SYNOPSIS ./generate-chessboard-fig.py 10 14 > board-10-14.fig fig2dev -L pdf board-10-14.fig > board-10-14.pdf ''' import sys import argparse import re import os def parse_args(): parser = \ argparse.ArgumentParser(description = __doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('--blobs', action = 'store_true', help='''If given, we generate a grid of circles, not a chessboard''') parser.add_argument('--circle-radius-cells', type=float, default = 0.2, help='''Applies only if --blobs. Specifies the radius of each circle, in the units of grid spacing: radius == 0.5 would create circles that are large enough such that adjacent ones touch.''') parser.add_argument('Wcorners', type=int, help='''The number of chessboard corners in the horizontal direction''') parser.add_argument('Hcorners', type=int, help='''The number of chessboard corners in the vertical direction''') args = parser.parse_args() return args args = parse_args() import numpy as np import numpysane as nps # The fig format is documented in many places. For instance: # https://web.archive.org/web/20070920204655/http://epb.lbl.gov/xfig/fig-format.html # # Here I simply took the chessboards I drew in xfig, and extended those .fig # files using this script. The header and chessboard-square definitions come # directly from the .fig files I made in xfig header = fr'''#FIG 3.2 Produced by the mrgingham chessboard generator: {sys.argv[0]} Portrait Center Metric Letter 100.00 Single -2 1200 2''' # Arbitrary units. fig2dev appears to compute the bounding box in integer units, # so if this is too small, the bounding box will be off square_size = 1000 def black_cell(x,y, *, double_width = False, double_height = False): X0 = (x+0)*square_size X1 = (x+1)*square_size Y0 = (y+0)*square_size Y1 = (y+1)*square_size if double_width: X1 += square_size if double_height: Y1 += square_size print("2 2 0 0 0 0 50 -1 20 0.000 0 0 -1 0 0 5") print(f" {X0} {Y0} {X1} {Y0} {X1} {Y1} {X0} {Y1} {X0} {Y0}") def black_circle(x,y,R): '''Units of all x,y is "cells". Units of R is "fig units"''' X = x * square_size Y = y * square_size print(f"1 3 0 0 0 0 50 -1 20 0 1 0 {X} {Y} {R} {R} {X} {Y} {X+R} {Y}") Wcells = args.Wcorners + 3 Hcells = args.Hcorners + 3 print(header) if not args.blobs: if (args.Wcorners // 2) * 2 != args.Wcorners or \ (args.Hcorners // 2) * 2 != args.Hcorners: print("Wcorners and Hcorners are assumed to be even", file = sys.stderr) sys.exit(1) # top/bottom edges for y in (0,Hcells-2): for x in range(2,Wcells-2,2): black_cell(x,y, double_height = True) # left/right edges for x in (0,Wcells-2): for y in range(2,Hcells-2,2): black_cell(x,y, double_width = True) # middle for y in range(2,Hcells-2,2): for x in range(3,Wcells-2,2): black_cell(x,y) for y in range(3,Hcells-2,2): for x in range(2,Wcells-2,2): black_cell(x,y) else: R = int(round(square_size * args.circle_radius_cells)) for y in range(0,args.Hcorners): for x in range(0,args.Wcorners): black_circle(x,y,R) mrgingham-1.26/mrgingham-from-image.cc000066400000000000000000000267741476660265400177540ustar00rootroot00000000000000#include "mrgingham.hh" #include #include #include #include #include #include #include using namespace mrgingham; struct mrgingham_thread_context_t { const glob_t* _glob; int Njobs; bool doclahe; int blur_radius; bool doblobs; bool do_refine; int gridn; bool debug; debug_sequence_t debug_sequence; int image_pyramid_level; } ctx; static void* worker( void* _ijob ) { // Worker thread. Processes images from the glob. Writes point detections // back out on the other end. int ijob = *(int*)(&_ijob); cv::Ptr clahe; if(ctx.doclahe) { clahe = cv::createCLAHE(); clahe->setClipLimit(8); } // The buffer. I'll realloc() this as I go. MUST free at the end signed char* refinement_level = NULL; for(int i_image=ijob; i_image<(int)ctx._glob->gl_pathc; i_image += ctx.Njobs) { const char* filename = ctx._glob->gl_pathv[i_image]; cv::Mat image0 = cv::imread(filename, cv::IMREAD_IGNORE_ORIENTATION | cv::IMREAD_GRAYSCALE | cv::IMREAD_ANYDEPTH); if( image0.data == NULL ) { fprintf(stderr, "Couldn't open image '%s'\n", filename); flockfile(stdout); { printf("## Couldn't open image '%s'\n", filename); printf("%s - - -\n", filename); } funlockfile(stdout); break; } cv::Mat image1; if(image0.depth() == CV_8U) { if( ctx.doclahe ) { // CLAHE doesn't by itself use the full dynamic range all the time, // so I explicitly normalize the image and then CLAHE cv::normalize(image0, image0, 0, 255, cv::NORM_MINMAX); clahe->apply(image0, image1); } else image1 = image0; } else if(image0.depth() == CV_16U) { if( ctx.doclahe ) { // CLAHE doesn't by itself use the full dynamic range all the time, // so I explicitly normalize the image and then CLAHE cv::normalize(image0, image0, 0, 65535, cv::NORM_MINMAX); clahe->apply(image0, image0); } image0.convertTo(image1, CV_8U, 255./65535.); } else { fprintf(stderr, "Couldn't process image '%s', I only know how to handle 8-bit and 16-bit unsigned images\n", filename); flockfile(stdout); { printf("## Couldn't process image '%s', I only know how to handle 8-bit and 16-bit unsigned images\n", filename); printf("%s - - -\n", filename); } funlockfile(stdout); break; } if( ctx.blur_radius > 0 ) { cv::blur( image1, image1, cv::Size(1 + 2*ctx.blur_radius, 1 + 2*ctx.blur_radius)); } if( ctx.debug ) { do { char basename[1024]; const char* last_slash = strrchr(filename, '/'); const char* basename_start = last_slash ? &last_slash[1] : filename; char* basename_start_end = stpncpy(basename, basename_start, sizeof(basename)); if(&basename[sizeof(basename)] == basename_start_end) { fprintf(stderr, "--debug file dump overran filename buffer! Not dumping files\n"); break; } // basename is now just the FILENAME with no directory. It still has // an extension char* last_dot = strrchr(basename, '.'); if(last_dot) *last_dot = '\0'; char filename_out[1024]; if(snprintf(filename_out, sizeof(filename_out), "/tmp/%s_preprocessed.png", basename) >= (int)sizeof(filename_out)) { fprintf(stderr, "--debug file dump overran filename buffer! Not dumping files\n"); break; } cv::imwrite(filename_out, image1); fprintf(stderr, "Wrote preprocessed image to %s\n", filename_out); } while(0); } std::vector points_out; bool result; int found_pyramid_level; // need this because ctx.image_pyramid_level could be -1 if(ctx.doblobs) { result = find_circle_grid_from_image_array(points_out, image1, ctx.gridn, ctx.debug, ctx.debug_sequence); // ctx.image_pyramid_level == 0 here. cmdline parser makes sure. found_pyramid_level = 0; } else { found_pyramid_level = find_chessboard_from_image_array (points_out, ctx.do_refine ? &refinement_level : NULL, ctx.gridn, image1, ctx.image_pyramid_level, ctx.debug, ctx.debug_sequence, filename); result = (found_pyramid_level >= 0); } flockfile(stdout); { if( result ) { for(int i=0; i<(int)points_out.size(); i++) printf( "%s %f %f %d\n", filename, points_out[i].x, points_out[i].y, (refinement_level == NULL) ? found_pyramid_level : (int)refinement_level[i]); } else printf("%s - - -\n", filename); } funlockfile(stdout); } free(refinement_level); return NULL; } int main(int argc, char* argv[]) { const char* usage = #include "mrgingham.usage.h" ; struct option opts[] = { { "blobs", no_argument, NULL, 'B' }, { "blur", required_argument, NULL, 'b' }, { "noclahe", no_argument, NULL, 'C' }, { "level", required_argument, NULL, 'l' }, { "no-refine", no_argument, NULL, 'R' }, { "jobs", required_argument, NULL, 'j' }, { "gridn", required_argument, NULL, 'N' }, { "debug", no_argument, NULL, 'd' }, { "debug-sequence", required_argument, NULL, 'D' }, { "help", no_argument, NULL, 'h' }, {} }; bool doblobs = false; bool doclahe = true; bool do_refine = true; bool debug = false; bool debug_sequence = false; PointInt debug_sequence_pt; int blur_radius = 1; int image_pyramid_level = -1; int jobs = 1; int gridn = 10; int opt; do { // "h" means -h does something opt = getopt_long(argc, argv, "hj:b:l:", opts, NULL); switch(opt) { case -1: break; case 'h': printf(usage, argv[0]); return 0; case 'B': doblobs = true; break; case 'C': doclahe = false; break; case 'R': do_refine = false; break; case 'd': debug = true; break; case 'D': debug_sequence = true; if( 2 != sscanf(optarg, "%d,%d", &debug_sequence_pt.x, &debug_sequence_pt.y) ) { fprintf(stderr, "I could not parse 'x,y' from --debug-sequence '%s'. Giving up\n", optarg); fprintf(stderr, usage, argv[0]); return -1; } break; case 'N': gridn = atoi(optarg); break; case 'b': blur_radius = atoi(optarg); break; case 'l': image_pyramid_level = atoi(optarg); break; case 'j': jobs = atoi(optarg); break; case '?': fprintf(stderr, "Unknown option\n"); fprintf(stderr, usage, argv[0]); return 1; } } while( opt != -1 ); if( optind > argc-1) { fprintf(stderr, "Not enough arguments: need image globs\n"); fprintf(stderr, usage, argv[0]); return 1; } if( jobs <= 0 ) { fprintf(stderr, "The job count must be a positive integer\n"); fprintf(stderr, usage, argv[0]); return 1; } if( doblobs && image_pyramid_level >= 0) { fprintf(stderr, "ERROR: 'image_pyramid_level' only implemented for chessboards.\n"); return 1; } if(gridn < 2) { fprintf(stderr, "--gridn value must be >= 2\n"); return 1; } glob_t _glob; int doappend = 0; for( int iopt_glob = optind; iopt_glob l[1]: # ...0 is the major axis # ...1 is the minor axis r0 = l[0] r1 = l[1] v0 = v[:,0] v1 = v[:,1] else: # ...0 is the minor axis # ...1 is the major axis r1 = l[0] r0 = l[1] v1 = v[:,0] v0 = v[:,1] # angle between x axis and major axis th = np.arctan2(v0[1], v0[0]) rx,ry = np.sqrt(M[0,0]),np.sqrt(M[1,1]) return np.array((r0,r1,rx,ry,th)) points_mean = np.mean(points, axis=0) points_centered = points - points_mean all_dxy = nps.clump(points_centered, n=2) # shape: (N,2) sigma_dxy = np.std(all_dxy,axis=-2) # I throw out outliers before reporting the statistics. I do it in discrete # points: any point that has either an outliery x or an outliery y is thrown out idx_in = np.max(np.abs(all_dxy) - 4.0*sigma_dxy, axis=-1) < 0.0 all_dxy = all_dxy[idx_in, :] all_dxy -= np.mean(all_dxy, axis=-2) sigma_dxy = np.std(all_dxy,axis=-2) title = "Have {} observations, separate x,y stdev: ({:.2f},{:.2f}), joint x,y stdev: {:.2f}". \ format(points.shape[0], np.std(all_dxy[:,0]), np.std(all_dxy[:,1]), np.std(all_dxy.ravel())) print(title) if not args.show: sys.exit(0) import gnuplotlib as gp if args.show == 'geometry': C = np.mean( nps.outer(points_centered, points_centered), axis=0 ) rad_major,rad_minor,rad_x,rad_y,angle = nps.transpose(ellipse_stats(C)) _xrange = None _yrange = None _yinv = None if args.imagersize is not None: _xrange = [0, args.imagersize[0]-1] _yrange = [args.imagersize[1]-1, 0] else: _yinv = True gp.plot((points_mean[:,0], points_mean[:,1], 2*rad_major,2*rad_minor, angle*180.0/np.pi, dict(_with = 'ellipses', tuplesize=5, _legend='1-sigma: dependent x,y')), (points_mean[:,0], points_mean[:,1], 2*rad_x, 2*rad_y, dict(_with = 'ellipses', tuplesize=4, _legend='1-sigma: independent x,y')), (points[...,0].ravel(), points[...,1].ravel(), dict(_with = 'points')), square = 1, _xrange = _xrange, _yrange = _yrange, _yinv = _yinv, wait = 1) elif args.show == 'histograms': var_xy = np.var(all_dxy, axis=-2) binwidth = 0.02 from scipy.special import erf equations = [ '{k}*exp(-(x)*(x)/(2.*{var})) / sqrt(2.*pi*{var}) title "{what}-distribution: gaussian fit" with lines lw 2'. \ format(what = 'x' if i==0 else 'y', var = var_xy[i], k = all_dxy.shape[-2]*erf(binwidth/(2.*np.sqrt(2)*np.sqrt(var_xy[i]))) * np.sqrt(2.*np.pi*var_xy[i])) \ for i in range(2) ] gp.plot( nps.transpose(all_dxy), _legend=np.array(('x-distribution: observed ', 'y-distribution: observed')), histogram = 1, binwidth = binwidth, _with = np.array(('boxes fill solid border lt -1', 'boxes fill transparent pattern 1', )), equation_above = equations, title = title, wait = 1) mrgingham-1.26/mrgingham-rotate-corners000077500000000000000000000107311476660265400203010ustar00rootroot00000000000000#!/bin/bash # Takes a processed corners.vnl result, and rotates some of the chessboard # observations. Used if some of the cameras in the calibrated set were mounted # sideways or upside-down # Option parsing is the adapted sample in # /usr/share/doc/util-linux/examples/getopt-example.bash usage=" < corners.vnl \\ mrgingham-rotate-corners [--gridn N] \\ --90 REGEX_CAM_90deg \\ --180 REGEX_CAM_180deg \\ --270 REGEX_CAM_270deg \\ [... more rotation selections ...] \\ > corners-rotated.vnl Adjust mrgingham corner detections from rotated cameras Synopsis: # camera A is rightside-up # camera B is mounted sideways # cameras C,D are upside-down mrgingham --gridn N \\ 'frame*-cameraA.jpg' \\ 'frame*-cameraB.jpg' \\ 'frame*-cameraC.jpg' \\ 'frame*-cameraD.jpg' | \\ mrgingham-rotate-corners --gridn N \\ --90 cameraB --180 'camera[CD]' The mrgingham chessboard detector finds a chessboard in an image, but it has no way to know whether the detected chessboard was upside-down or otherwise rotated: the chessboard itself has no detectable marking to make this clear. In the usual case, the cameras as all mounted in the same orientation, so they all detect the same orientation of the chessboard, and there is no problem. However, if some cameras are mounted sideways or upside-down, the sequence of corners will correspond to different corners between the cameras with different orientations. This can be addressed by this tool. This tool ingests mrgingham detections, and outputs them after correcting the chessboard observations produced by rotated cameras. Each rotation option is an awk regular expression used to select images from specific cameras. The regular expression is tested against the image filenames. Each rotation option may be given multiple times. Any files not matched by any rotation option are passed through unrotated. " ARGS=$(getopt -n $0 -l help,gridn:,90:,180:,270: -o "h" -- "$@") if [ $? -ne 0 ]; then echo "Usage: $usage" > /dev/stderr exit 1 fi eval set -- "$ARGS" unset ARGS gridn=10 cam_rotate90=() cam_rotate180=() cam_rotate270=() while true; do case "$1" in '--help'|'-h') echo "Usage: $usage" exit 0 ;; '--gridn') gridn=$2 shift 2 continue ;; '--90') cam_rotate90+=($2) shift 2 ;; '--180') cam_rotate180+=($2) shift 2 ;; '--270') cam_rotate270+=($2) shift 2 ;; '--') shift break ;; *) echo "Error parsing options!" > /dev/stderr exit 1 ;; esac done if (( $# )); then echo "Extra arguments given" > /dev/stderr echo "Usage: $usage" > /dev/stderr exit 1 fi cam_rotate90_select_expression="0" for f in ${cam_rotate90[@]}; do cam_rotate90_select_expression+=" || filename ~ \"$f\"" done cam_rotate180_select_expression="0" for f in ${cam_rotate180[@]}; do cam_rotate180_select_expression+=" || filename ~ \"$f\"" done cam_rotate270_select_expression="0" for f in ${cam_rotate270[@]}; do cam_rotate270_select_expression+=" || filename ~ \"$f\"" done Nx=$gridn Ny=$gridn Nxy=$((Nx*Ny)) ICORNER_FUNCTION=" if(filename!=filename_prev) { N=i+1 if(!(N == 0 || N == 1 || N == $Nxy)) { print \"# File '\"filename\"': expected \"$Nxy\" points but received \"N > \"/dev/stderr\"; failed=1; exit 1 } i =0; ix=0; iy=0; filename_prev=filename; } else { i++; ix++; if(ix == $Nx) { ix = 0; iy++; } } if($cam_rotate90_select_expression) return ($Nx-1-ix)*$Ny + iy; if($cam_rotate270_select_expression) return ix*$Ny + ($Ny-1-iy); if($cam_rotate180_select_expression) return ($Nx*$Ny-1 - i); return i; " END_EXPRESSION=" N=i+1 if(!failed && !(N == 0 || N == 1 || N == $Nxy)) { print \"# The last file in the data file expected \"$Nxy\" points but received \"N > \"/dev/stderr\"; exit 1 } " vnl-filter \ --skipcomments \ --end "$END_EXPRESSION" \ --sub "icorner() { $ICORNER_FUNCTION }" \ -p icorner='icorner()',. \ | vnl-sort -k filename -k icorner.n \ | vnl-filter -p '!icorner' exit ${PIPESTATUS[0]} mrgingham-1.26/mrgingham.cc000066400000000000000000000170661476660265400157250ustar00rootroot00000000000000#include "mrgingham.hh" #include "find_blobs.hh" #include "find_chessboard_corners.hh" #include namespace mrgingham { __attribute__((visibility("default"))) bool find_circle_grid_from_image_array( std::vector& points_out, const cv::Mat& image, const int gridn, bool debug, debug_sequence_t debug_sequence) { std::vector points; find_blobs_from_image_array(&points, image); return find_grid_from_points(points_out, points, gridn, debug, debug_sequence); } __attribute__((visibility("default"))) bool find_circle_grid_from_image_file( std::vector& points_out, const char* filename, const int gridn, bool debug, debug_sequence_t debug_sequence) { std::vector points; find_blobs_from_image_file(&points, filename); return find_grid_from_points(points_out, points, gridn, debug, debug_sequence); } // *refinement_level is managed by realloc(). IT IS THE CALLER'S // *RESPONSIBILITY TO free() IT static bool _find_chessboard_from_image_array( std::vector& points_out, signed char** refinement_level, const cv::Mat& image, int image_pyramid_level, const int gridn, bool debug, debug_sequence_t debug_sequence, const char* debug_image_filename) { const bool do_refine = (refinement_level != NULL); std::vector points; find_chessboard_corners_from_image_array(&points, image, image_pyramid_level, debug, debug_image_filename); if(!find_grid_from_points(points_out, points, gridn, debug, debug_sequence)) return false; // we found a grid! If we're not trying to refine the locations, or if // we can't refine them, we're done if(!do_refine || image_pyramid_level == 0) return true; // Alright, I need to refine each intersection. Big-picture logic: // // for(points) // { // zoom = next_from_current; // while(update corner coord using zoom) // zoom = next_from_current; // } // // It would be more efficient to loop through the zoom levels once, so I // move that to the outer loop // // for(zoom) // { // for(points) // if(this point is refinable) // refine(); // if( no points remain refinable ) // break; // } int N = points_out.size(); *refinement_level = (signed char*)realloc((void*)*refinement_level, N*sizeof(**refinement_level)); assert(*refinement_level); for(int i=0; i& points_out, signed char** refinement_level, const int gridn, const cv::Mat& image, int image_pyramid_level, bool debug, debug_sequence_t debug_sequence, const char* debug_image_filename) { if( image_pyramid_level >= 0) return _find_chessboard_from_image_array( points_out, refinement_level, image, image_pyramid_level, gridn, debug, debug_sequence, debug_image_filename) ? image_pyramid_level : -1; for( image_pyramid_level=3; image_pyramid_level>=0; image_pyramid_level--) { int result = _find_chessboard_from_image_array( points_out, refinement_level, image, image_pyramid_level, gridn, debug, debug_sequence, debug_image_filename) ? image_pyramid_level : -1; if(result >= 0) return result; } return -1; } // *refinement_level is managed by realloc(). IT IS THE CALLER'S // *RESPONSIBILITY TO free() IT __attribute__((visibility("default"))) int find_chessboard_from_image_file( std::vector& points_out, signed char** refinement_level, const int gridn, const char* filename, int image_pyramid_level, bool debug, debug_sequence_t debug_sequence) { cv::Mat image = cv::imread(filename, cv::IMREAD_IGNORE_ORIENTATION | cv::IMREAD_GRAYSCALE); if( image.data == NULL ) { fprintf(stderr, "%s:%d in %s(): Couldn't open image '%s'." " Sorry.\n", __FILE__, __LINE__, __func__, filename); return -1; } std::vector points; return find_chessboard_from_image_array(points_out, refinement_level, gridn, image, image_pyramid_level, debug, debug_sequence, filename); } }; mrgingham-1.26/mrgingham.hh000066400000000000000000000107021476660265400157250ustar00rootroot00000000000000#pragma once #include #include #include "point.hh" namespace mrgingham { struct debug_sequence_t { bool dodebug; PointInt pt; debug_sequence_t() : dodebug(false), pt() {} }; // This is currently hard-coded to find black-on-white blobs (look at // blobColor in find_blobs.cc) bool find_circle_grid_from_image_array( std::vector& points_out, const cv::Mat& image, const int gridn, bool debug = false, debug_sequence_t debug_sequence = debug_sequence_t()); bool find_circle_grid_from_image_file( std::vector& points_out, const char* filename, const int gridn, bool debug = false, debug_sequence_t debug_sequence = debug_sequence_t()); // set image_pyramid_level=0 to just use the image as is. // // image_pyramid_level > 0 cut down the image by a factor of 2 that many // times. So for example, level==2 means each dimension is cut down by a // factor of 4. // // image_pyramid_level < 0 means we try several levels, taking the first one // that produces results // // If we want to refine each reported point, pass a pointer to a buffer into // refinement_level. I'll realloc() the buffer as needed, and I'll return // the pyramid level of each point on exit. // // *refinement_level is managed by realloc(). IT IS THE CALLER'S // *RESPONSIBILITY TO free() IT // // Returns the pyramid level where we found the grid, or <0 on failure int find_chessboard_from_image_array( std::vector& points_out, signed char** refinement_level, const int gridn, const cv::Mat& image, int image_pyramid_level = -1, bool debug = false, debug_sequence_t debug_sequence = debug_sequence_t(), const char* debug_image_filename = NULL); // set image_pyramid_level=0 to just use the image as is. // // image_pyramid_level > 0 cut down the image by a factor of 2 that many // times. So for example, level==2 means each dimension is cut down by a // factor of 4. // // image_pyramid_level < 0 means we try several levels, taking the first one // that produces results // // If we want to refine each reported point, pass a pointer to a buffer into // refinement_level. I'll realloc() the buffer as needed, and I'll return // the pyramid level of each point on exit. // // *refinement_level is managed by realloc(). IT IS THE CALLER'S // *RESPONSIBILITY TO free() IT // // Returns the pyramid level where we found the grid, or <0 on failure int find_chessboard_from_image_file( std::vector& points_out, signed char** refinement_level, const int gridn, const char* filename, int image_pyramid_level = -1, bool debug = false, debug_sequence_t debug_sequence = debug_sequence_t()); bool find_grid_from_points( std::vector& points_out, const std::vector& points, const int gridn, bool debug = false, const debug_sequence_t& debug_sequence = debug_sequence_t()); }; mrgingham-1.26/mrgingham.usage000066400000000000000000000107561476660265400164430ustar00rootroot00000000000000Usage: %s \ [--blobs] [--gridn N] [--noclahe] [--blur radius] \ [--level l] [--no-refine] [--jobs N] \ [--debug] [--debug-sequence x,y] \ imageglobs imageglobs ... Extract chessboard corners from a set of images SYNOPSIS $ mrgingham image*.jpg # filename x y level image1.jpg - - image2.jpg 1385.433000 1471.719000 0 image2.jpg 1483.597000 1469.825000 0 image2.jpg 1582.086000 1467.561000 1 ... $ mrgingham image.jpg | vnl-filter -p x,y,level | feedgnuplot --domain \ --with 'linespoints pt 7 ps 2 palette' \ --tuplesizeall 3 \ --image image.jpg [ image pops up with the detected grid plotted on top, detections color-coded by their decimation level ] The mrgingham tool detects chessboard corners from images stored on disk. The images are given on the commandline, as globs. Each glob is expanded, and each image is processed, possibly in parallel if -j was given. The output is a vnlog text table (https://www.github.com/dkogan/vnlog) containing columns: - filename: path to the image on disk - x, y: detected pixel coordinates of the chessboard corner - level: image level used in detecting this corner. Level 0 means "full-resolution". Level 1 means "half-resolution" and that the noise on this detection has double the standard deviation. Level 2 means "quarter-resolution" and so on. If no chessboard was found in an image, a single record is output: filename - - - The corners are output in a consistent order: starting at the top-left, traversing the grid, in the horizontal direction first. Usually, the chessboard is observed by multiple cameras mounted at a similar orientation, so this consistent order is consistent across cameras. By default we look for a CHESSBOARD, not a grid of circles or Apriltags or anything else. By default we apply adaptive histogram equalization (CLAHE), then blur with a radius of 1. We then use an adaptive level of downsampling when looking for the chessboard. These defaults work very well in practice. For debugging, pass in --debug. This will dump the various intermediate results into /tmp and it will report more stuff on the console. Most of the intermediate results are self-plotting data files. Run them. For debugging sequence candidates, pass in --debug-sequence x,y where 'x,y' are the approximate image coordinates of the start of a given sequence (corner on the edge of a chessboard. This doesn't need to be exact; mrgingham will report on the nearest corner See the mrgingham project documentation for more detail: https://github.com/dkogan/mrgingham/ POSITIONAL ARGUMENTS imageglobs Globs specifying the images to process. May be given more than once OPTIONAL ARGUMENTS --blobs Finds circle centers instead of chessboard corners. Not recommended --gridn N Requests detections of an NxN grid of corners. If omitted, N defaults to 10 --noclahe Controls image preprocessing. Unless given, we will apply adaptive histogram equalization (CLAHE algorithm) to the images. This is EXTREMELY helpful if the images aren't illuminated evenly; which applies to most real-world images. --blur RADIUS Controls image preprocessing. Applies a gaussian blur to the image after the histogram equalization. A light blurring is very helpful with CLAHE, since it produces noisy images. By default we will blur with radius = 1. Set to <= 0 to disable --level LEVEL Controls image preprocessing. Applies a downsampling to the image (after CLAHE and --blur, if those are given). Level 0 means 'use the original image'. Level > 0 means downsample by 2**level. Level < 0 means 'try several different levels until we find one that works. This is the default. --no-refine Disables corner refinement. By default, the coordinates of reported corners are re-detected at less-downsampled zoom levels to improve their accuracy. If we do not want to do that, pass --no-refine --jobs N Parallelizes the processing N-ways. -j is a synonym. This is just like GNU make, except you're required to explicitly specify a job count. --debug If given, mrgingham will dump various intermediate results into /tmp and it will report more stuff on the console. The output is self-documenting --debug-sequence If given, we report details about sequence matching. Do this if --debug reports correct-looking corners (all corners detected, no doubled-up detections, no detections inside the chessboard but not on a corner) mrgingham-1.26/mrgingham_pywrap.c000066400000000000000000000326641476660265400171650ustar00rootroot00000000000000#define NPY_NO_DEPRECATED_API NPY_API_VERSION #include #include #include #include #include #include "ChESS.h" #include "mrgingham_pywrap_cplusplus_bridge.h" // Python is silly. There's some nuance about signal handling where it sets a // SIGINT (ctrl-c) handler to just set a flag, and the python layer then reads // this flag and does the thing. Here I'm running C code, so SIGINT would set a // flag, but not quit, so I can't interrupt the solver. Thus I reset the SIGINT // handler to the default, and put it back to the python-specific version when // I'm done #define SET_SIGINT() struct sigaction sigaction_old; \ do { \ if( 0 != sigaction(SIGINT, \ &(struct sigaction){ .sa_handler = SIG_DFL }, \ &sigaction_old) ) \ { \ PyErr_SetString(PyExc_RuntimeError, "sigaction() failed"); \ goto done; \ } \ } while(0) #define RESET_SIGINT() do { \ if( 0 != sigaction(SIGINT, \ &sigaction_old, NULL )) \ PyErr_SetString(PyExc_RuntimeError, "sigaction-restore failed"); \ } while(0) #define PYMETHODDEF_ENTRY(name, c_function_name, args) {#name, \ (PyCFunction)c_function_name, \ args, \ name ## _docstring} static PyObject* py_ChESS_response_5(PyObject* NPY_UNUSED(self), PyObject* args) { PyObject* result = NULL; SET_SIGINT(); PyArrayObject* image = NULL; if(!PyArg_ParseTuple( args, "O&", PyArray_Converter, &image )) goto done; npy_intp* dims = PyArray_DIMS (image); npy_intp* strides = PyArray_STRIDES(image); int ndims = PyArray_NDIM (image); if( ndims < 2 ) { PyErr_Format(PyExc_RuntimeError, "The input image array must have at least 2 dims (extra ones will be broadcasted); got %d", PyArray_NDIM(image)); goto done; } if( PyArray_TYPE(image) != NPY_UINT8 ) { PyErr_SetString(PyExc_RuntimeError, "The input image array must contain 8-bit unsigned data"); goto done; } if( strides[ndims-1] != 1 ) { PyErr_SetString(PyExc_RuntimeError, "Image rows must live in contiguous memory"); goto done; } PyArrayObject* response = (PyArrayObject*)PyArray_SimpleNew(ndims, dims, NPY_INT16); if(response == NULL) { PyErr_SetString(PyExc_RuntimeError, "Couldn't allocate response"); goto done; } // broadcast through all the slices { // need a block to pacify the compiler npy_intp islice[ndims]; islice[ndims - 1] = 0; islice[ndims - 2] = 0; void loop_dim(int idim) { if(idim < 0) { int16_t* data_response = (int16_t*)PyArray_GetPtr(response, islice); uint8_t* data_image = (uint8_t*)PyArray_GetPtr(image, islice); mrgingham_ChESS_response_5( data_response, data_image, dims[ndims-1], dims[ndims-2], strides[ndims-2]); return; } for(islice[idim]=0; islice[idim] < dims[idim]; islice[idim]++) loop_dim(idim-1); } // The last 2 dimensions index each slice (x,y inside each image). The // dimensions before that are for broadcasting loop_dim(ndims-3); } result = (PyObject*)response; done: Py_XDECREF(image); RESET_SIGINT(); return result; } static bool add_points__find_points(int* xy, int N, double scale, void* cookie) { PyObject** result = (PyObject**)cookie; *result = PyArray_SimpleNew(2, ((npy_intp[]){N, 2}), NPY_DOUBLE); if(*result == NULL) return false; double* out_data = (double*)PyArray_BYTES((PyArrayObject*)*result); for(int i=0; i<2*N; i++) out_data[i] = scale * (double)xy[i]; return true; } static PyObject* find_points(PyObject* NPY_UNUSED(self), PyObject* args, PyObject* kwargs) { PyArrayObject* image = NULL; PyObject* result = NULL; int image_pyramid_level = 0; int blobs = 0; int debug = 0; SET_SIGINT(); char* keywords[] = { "image", "image_pyramid_level", "blobs", "debug", NULL }; if(!PyArg_ParseTupleAndKeywords( args, kwargs, "O&|ipp", keywords, PyArray_Converter, &image, &image_pyramid_level, &blobs, &debug, NULL)) goto done; if(blobs && image_pyramid_level != 0) { PyErr_Format(PyExc_RuntimeError, "blob detector requires that image_pyramid_level == 0"); goto done; } npy_intp* dims = PyArray_DIMS (image); npy_intp* strides = PyArray_STRIDES(image); int ndims = PyArray_NDIM (image); if( ndims != 2 ) { PyErr_Format(PyExc_RuntimeError, "The input image array must have exactly 2 dims (broadcasting not supported here); got %d", PyArray_NDIM(image)); goto done; } if( PyArray_TYPE(image) != NPY_UINT8 ) { PyErr_SetString(PyExc_RuntimeError, "The input image array must contain 8-bit unsigned data"); goto done; } if( strides[ndims-1] != 1 ) { PyErr_SetString(PyExc_RuntimeError, "Image rows must live in contiguous memory"); goto done; } if(! find_chessboard_corners_from_image_array_C(PyArray_DIMS(image)[0], PyArray_DIMS(image)[1], PyArray_STRIDES(image)[0], PyArray_BYTES(image), image_pyramid_level, blobs, debug, &add_points__find_points, &result) ) { if(result == NULL) { // Detector didn't find anything. I don't flag an error, // but simply return a no-data array result = PyArray_SimpleNew(2, ((npy_intp[]){0, 2}), NPY_DOUBLE); } else { // an actual error occured. I complain Py_DECREF(result); result = NULL; PyErr_SetString(PyExc_RuntimeError, "find_chessboard_corners_from_image_array_C() failed"); goto done; } } done: Py_XDECREF(image); RESET_SIGINT(); return result; } static bool add_points__find_board(double* xy, int N, void* cookie) { PyObject** result = (PyObject**)cookie; *result = PyArray_SimpleNew(2, ((npy_intp[]){N, 2}), NPY_DOUBLE); if(*result == NULL) return false; double* out_data = (double*)PyArray_BYTES((PyArrayObject*)*result); memcpy(out_data, xy, 2*N*sizeof(double)); return true; } static PyObject* find_board(PyObject* NPY_UNUSED(self), PyObject* args, PyObject* kwargs) { PyArrayObject* image = NULL; PyObject* result = NULL; int image_pyramid_level = -1; int gridn = 10; int blobs = 0; int debug = 0; const char* debug_sequence_string = NULL; int debug_sequence_x = -1; int debug_sequence_y = -1; SET_SIGINT(); char* keywords[] = { "image", "image_pyramid_level", "gridn", "blobs", "debug", "debug_sequence", NULL }; if(!PyArg_ParseTupleAndKeywords( args, kwargs, "O&|iipps", keywords, PyArray_Converter, &image, &image_pyramid_level, &gridn, &blobs, &debug, &debug_sequence_string, NULL)) goto done; if(blobs && image_pyramid_level != 0) { PyErr_Format(PyExc_RuntimeError, "blob detector requires that image_pyramid_level == 0"); goto done; } if(debug_sequence_string != NULL) { // parse string into INTEGER,INTEGER int nbytesread; int res = sscanf(debug_sequence_string, "%d,%d%n", &debug_sequence_x, &debug_sequence_x, &nbytesread); if(!(res == 2 && debug_sequence_string[nbytesread] == '\0')) { PyErr_Format(PyExc_RuntimeError, "Couldn't parse debug_sequence as an 'INTEGER,INTEGER' string"); goto done; } } npy_intp* dims = PyArray_DIMS (image); npy_intp* strides = PyArray_STRIDES(image); int ndims = PyArray_NDIM (image); if( ndims != 2 ) { PyErr_Format(PyExc_RuntimeError, "The input image array must have exactly 2 dims (broadcasting not supported here); got %d", PyArray_NDIM(image)); goto done; } if( PyArray_TYPE(image) != NPY_UINT8 ) { PyErr_SetString(PyExc_RuntimeError, "The input image array must contain 8-bit unsigned data"); goto done; } if( strides[ndims-1] != 1 ) { PyErr_SetString(PyExc_RuntimeError, "Image rows must live in contiguous memory"); goto done; } if(gridn < 2) { PyErr_SetString(PyExc_RuntimeError, "gridn value must be >= 2"); goto done; } if(! find_chessboard_from_image_array_C(PyArray_DIMS(image)[0], PyArray_DIMS(image)[1], PyArray_STRIDES(image)[0], PyArray_BYTES(image), gridn, image_pyramid_level, blobs, debug, debug_sequence_x, debug_sequence_y, &add_points__find_board, &result) ) { // This is allowed to fail. We possibly found no chessboard. This is // sloppy since it ignore other potential errors, but there shouldn't be // any in this path Py_XDECREF(result); if( result == NULL ) { result = Py_None; Py_INCREF(result); } } done: Py_XDECREF(image); RESET_SIGINT(); return result; } static const char ChESS_response_5_docstring[] = #include "ChESS_response_5.docstring.h" ; static const char find_points_docstring[] = #include "find_points.docstring.h" ; static const char find_board_docstring[] = #include "find_board.docstring.h" ; static const char find_chessboard_corners_docstring[] = #include "find_chessboard_corners.docstring.h" ; static const char find_chessboard_docstring[] = #include "find_chessboard.docstring.h" ; static PyMethodDef methods[] = { PYMETHODDEF_ENTRY(ChESS_response_5, py_ChESS_response_5, METH_VARARGS), PYMETHODDEF_ENTRY(find_points, find_points, METH_VARARGS | METH_KEYWORDS), PYMETHODDEF_ENTRY(find_board, find_board, METH_VARARGS | METH_KEYWORDS), // These are compatibility functions. Same implementation as the above, but // different names. Meant to keep old code running PYMETHODDEF_ENTRY(find_chessboard_corners, find_points, METH_VARARGS | METH_KEYWORDS), PYMETHODDEF_ENTRY(find_chessboard, find_board, METH_VARARGS | METH_KEYWORDS), {} }; #if PY_MAJOR_VERSION == 2 __attribute__((visibility("default"))) PyMODINIT_FUNC initmrgingham(void) { Py_InitModule3("mrgingham", methods, "Chessboard-detection routines"); import_array(); } #else static struct PyModuleDef module_def = { PyModuleDef_HEAD_INIT, "mrgingham", "Chessboard-detection routines", -1, methods }; PyMODINIT_FUNC PyInit_mrgingham(void) { import_array(); return PyModule_Create(&module_def); } #endif mrgingham-1.26/mrgingham_pywrap_cplusplus_bridge.cc000066400000000000000000000143731476660265400227530ustar00rootroot00000000000000#include "find_chessboard_corners.hh" #include "find_blobs.hh" #include "mrgingham.hh" #include "mrgingham_pywrap_cplusplus_bridge.h" #include "mrgingham-internal.h" // These are wrappers. And the reason this file exists at all is because // mrgingham has a c++ api, while the python wrappers are in C. AND there's // another complicating factor: on CentOS7.2 if I build a C++ thing that talks // to numpy it barfs: // // g++ -Wall -Wextra -std=gnu99 -Wno-cast-function-type -I/usr/include/opencv -fvisibility=hidden -Wno-unused-function -Wno-missing-field-initializers -Wno-unused-parameter -Wno-strict-aliasing -Wno-int-to-pointer-cast -Wno-unused-variable -MMD -MP -g -fno-omit-frame-pointer -DVERSION='"1.1"' -pthread -fno-strict-aliasing -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -DNDEBUG -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -fPIC -I/usr/include/python2.7 -O3 -c -o/dev/null mrgingham_pywrap_cplusplus_bridge.cc |& head -n 50 // cc1plus: warning: command line option '-std=gnu99' is valid for C/ObjC but not for C++ [enabled by default] // In file included from /usr/include/numpy/ndarraytypes.h:7:0, // from /usr/include/numpy/ndarrayobject.h:17, // from /usr/include/numpy/arrayobject.h:15, // from mrgingham_pywrap_cplusplus_bridge.cc:5: // /usr/include/numpy/npy_common.h:188:2: error: #error Must use Python with unicode enabled. // #error Must use Python with unicode enabled. // ^ // // As a result, this file doesn't touch Python, and does stuff with callbacks. // Yuck. Callbacks will make stuff slower. With a newer distro, the python // unicode thing maybe isn't a problem. You can revert the commit on 2019/3/25 // if you want to try it again extern "C" bool find_chessboard_corners_from_image_array_C( // in int Nrows, int Ncols, int stride, char* imagebuffer, // this is const // set to 0 to just use the image. Set // to <0 to try automatically find a // good scaling level. Try this first int image_pyramid_level, bool doblobs, bool debug, bool (*add_points)(int* xy, int N, double scale, void* cookie), void* cookie ) { cv::Mat cvimage(Nrows, Ncols, CV_8UC1, imagebuffer, stride); std::vector out_points; bool result; if(doblobs) { if(image_pyramid_level != 0) return false; result = find_blobs_from_image_array( &out_points, cvimage ); } else { result = find_chessboard_corners_from_image_array( &out_points, cvimage, image_pyramid_level, debug ); } if( !result ) return false; static_assert( sizeof(mrgingham::PointInt) == 2*sizeof(int), "add_points() assumes PointInt is simply 2 ints"); return (*add_points)( &out_points[0].x, (int)out_points.size(), 1. / (double)FIND_GRID_SCALE, cookie); } extern "C" bool find_chessboard_from_image_array_C( // in int Nrows, int Ncols, int stride, char* imagebuffer, // this is const const int gridn, // set to 0 to just use the image. Set // to <0 to try automatically find a // good scaling level. Try this first int image_pyramid_level, bool doblobs, bool debug, int debug_sequence_x, int debug_sequence_y, bool (*add_points)(double* xy, int N, void* cookie), void* cookie ) { cv::Mat cvimage(Nrows, Ncols, CV_8UC1, imagebuffer, stride); std::vector out_points; mrgingham::debug_sequence_t debug_sequence = {}; if(debug_sequence_x >= 0 && debug_sequence_y >= 0) { debug_sequence.dodebug = true; debug_sequence.pt.x = debug_sequence_x; debug_sequence.pt.y = debug_sequence_y; } bool result; if(doblobs) { if(image_pyramid_level != 0) return false; result = find_circle_grid_from_image_array(out_points, cvimage, gridn, debug, debug_sequence); } else { signed char* refinement_level = NULL; result = (find_chessboard_from_image_array( out_points, &refinement_level, gridn, cvimage, image_pyramid_level, debug, debug_sequence, NULL ) >= 0); free(refinement_level); } if( !result ) return false; static_assert( sizeof(mrgingham::PointDouble) == 2*sizeof(double), "add_points() assumes PointDouble is simply 2 doubles"); return (*add_points)( &out_points[0].x, (int)out_points.size(), cookie); } mrgingham-1.26/mrgingham_pywrap_cplusplus_bridge.h000066400000000000000000000040601476660265400226050ustar00rootroot00000000000000#pragma once #ifdef __cplusplus extern "C" { #endif // These are wrappers. And the reason this file exists at all is because // mrgingham has a c++ api, while the python wrappers are in C bool find_chessboard_corners_from_image_array_C( // in int Nrows, int Ncols, int stride, char* imagebuffer, // this is const // set to 0 to just use the image. Set // to <0 to try automatically find a // good scaling level. Try this first int image_pyramid_level, bool doblobs, bool debug, bool (*add_points)(int* xy, int N, double scale, void* cookie), void* cookie ); bool find_chessboard_from_image_array_C( // in int Nrows, int Ncols, int stride, char* imagebuffer, // this is const const int gridn, // set to 0 to just use the image. Set // to <0 to try automatically find a // good scaling level. Try this first int image_pyramid_level, bool doblobs, bool debug, int debug_sequence_x, int debug_sequence_y, bool (*add_points)(double* xy, int N, void* cookie), void* cookie ); #ifdef __cplusplus } #endif mrgingham-1.26/packaging/000077500000000000000000000000001476660265400153575ustar00rootroot00000000000000mrgingham-1.26/packaging/README000066400000000000000000000004151476660265400162370ustar00rootroot00000000000000These are sample packaging files that can be used to build your own packages. I'm about to push mrgingham into Debian, and it'll then percolate to Ubuntu. A .spec for rpm-based distros is provided here. Want this to be shipped in some other distro? Send them patches! mrgingham-1.26/packaging/mrgingham.spec000066400000000000000000000025471476660265400202140ustar00rootroot00000000000000Name: mrgingham Version: 1.22 Release: 1%{?dist} Summary: Chessboard corner finder for camera calibrations License: LGPL-2.1+ URL: https://github.com/dkogan/mrgingham/ Source0: https://github.com/dkogan/mrgingham/archive/%{version}.tar.gz#/%{name}-%{version}.tar.gz BuildRequires: mrbuild BuildRequires: opencv-devel >= 3.2 BuildRequires: boost-devel BuildRequires: chrpath # for test--mrgingham-rotate-corners BuildRequires: zsh BuildRequires: vnlog # to build the manpages I need to run 'mrgingham-observe-pixel-uncertainty # --help' BuildRequires: python36 # for the python interface BuildRequires: python36-numpy BuildRequires: python36-devel BuildRequires: python36-libs Conflicts: mrgingham-tools <= 1.10 # for mrgingham-rotate-corners Requires: vnlog %description Library to find a grid of points; used for calibration routines %package devel Requires: %{name}%{_isa} = %{version}-%{release} Summary: Development files for mrgingham %description devel Headers and libraries for building applications using mrgingham %prep %setup -q %build make %{?_smp_mflags} all %install rm -rf $RPM_BUILD_ROOT %make_install %post -p /sbin/ldconfig %postun -p /sbin/ldconfig %files %doc %{_libdir}/*.so.* %{_bindir}/* %{_mandir}/* %{python3_sitelib}/* %files devel %{_libdir}/*.so %{_includedir}/* mrgingham-1.26/point.hh000066400000000000000000000004111476660265400151010ustar00rootroot00000000000000#pragma once namespace mrgingham { struct PointInt { int x,y; PointInt(int _x=0, int _y=0) : x(_x), y(_y) {} }; struct PointDouble { double x,y; PointDouble(double _x=0, double _y=0) : x(_x), y(_y) {} }; }; mrgingham-1.26/test-dump-blobs.cc000066400000000000000000000005271476660265400167670ustar00rootroot00000000000000#include "find_blobs.hh" #include using namespace mrgingham; int main(int argc, char* argv[]) { if( argc != 2 ) { fprintf(stderr, "missing arg: need image filename on the cmdline\n"); return 1; } std::vector points; find_blobs_from_image_file(&points, argv[1], true); return 0; } mrgingham-1.26/test-dump-chessboard-corners.cc000066400000000000000000000066151476660265400214600ustar00rootroot00000000000000#include #include #include #include #include "find_chessboard_corners.hh" using namespace mrgingham; int main(int argc, char* argv[]) { const char* usage = "Usage: %s [--clahe] [--blur radius] [--level l] image\n" "\n" " --clahe is optional: it will pre-process the image with an adaptive histogram\n" " equalization step. This is useful if the calibration board has a lighting\n" " gradient across it.\n" "\n" " --blur radius applies a blur (after --clahe, if given) to the image before\n" " processing\n" "\n" " --level l applies a downsampling to the image before processing it (after\n" " --clahe and --blur, if given) to the image before processing. Level 0 means\n" " 'use the original image'. Level > 0 means downsample by 2**level. Level < 0\n" " means 'try several different levels until we find one that works. This is the.\n" " default.\n" "\n"; struct option opts[] = { { "blur", required_argument, NULL, 'b' }, { "clahe", no_argument, NULL, 'c' }, { "level", required_argument, NULL, 'l' }, { "help", no_argument, NULL, 'h' }, {} }; bool doclahe = false; int blur_radius = -1; int image_pyramid_level = -1; int opt; do { // "h" means -h does something opt = getopt_long(argc, argv, "h", opts, NULL); switch(opt) { case -1: break; case 'h': printf(usage, argv[0]); return 0; case 'c': doclahe = true; break; case 'b': blur_radius = atoi(optarg); if(blur_radius <= 0) { fprintf(stderr, "blur_radius < 0\n"); fprintf(stderr, usage, argv[0]); return 1; } break; case 'l': image_pyramid_level = atoi(optarg); break; case '?': fprintf(stderr, "Unknown option\n"); fprintf(stderr, usage, argv[0]); return 1; } } while( opt != -1 ); if( optind != argc-1) { fprintf(stderr, "Need a single image on the cmdline\n"); fprintf(stderr, usage, argv[0]); return 1; } cv::Ptr clahe; if(doclahe) { clahe = cv::createCLAHE(); clahe->setClipLimit(8); } const char* filename = argv[argc-1]; cv::Mat image = cv::imread(filename, cv::IMREAD_IGNORE_ORIENTATION | cv::IMREAD_GRAYSCALE); if( image.data == NULL ) { fprintf(stderr, "Couldn't open image '%s'\n", filename); return 1; } if( doclahe ) { cv::equalizeHist(image, image); clahe->apply(image, image); } if( blur_radius > 0 ) { cv::blur( image, image, cv::Size(1 + 2*blur_radius, 1 + 2*blur_radius)); } std::vector points; if(!mrgingham::find_chessboard_corners_from_image_array (&points, image, image_pyramid_level, true, filename)) { fprintf(stderr, "Error computing the corners!\n"); return 1; } return 0; } mrgingham-1.26/test-find-grid-from-points.cc000066400000000000000000000054041476660265400210400ustar00rootroot00000000000000#include "mrgingham.hh" #include "mrgingham-internal.h" #include #include using namespace mrgingham; static bool read_points( std::vector* points, const char* file ) { FILE* fp = fopen(file, "r"); if( fp == NULL ) { fprintf(stderr, "couldn't open '%s'\n", file); return false; } char* line = NULL; size_t n = 0; while(getline(&line, &n, fp) >= 0) { double x,y; int Nread = sscanf(line, "%lf %lf", &x, &y); if(Nread != 2) continue; PointInt pt( (int)( x * FIND_GRID_SCALE + 0.5 ), (int)( y * FIND_GRID_SCALE + 0.5 ) ); points->push_back(pt); } fclose(fp); free(line); return true; } int main(int argc, char* argv[]) { const char* usage = "Usage: %s [--debug] points.vnl\n" "\n" "Given a set of pre-detected points, this tool finds a chessboard grid, and returns\n" "the ordered coordinates of this grid on standard output. The pre-detected points\n" "can come from something like test-dump-chessboard-corners.\n" "\n" "We detect an NxN grid of corners, where N defaults to 10. To select a different\n" "value, pass --gridn N\n"; struct option opts[] = { { "gridn", required_argument, NULL, 'N' }, { "help", no_argument, NULL, 'h' }, { "debug", no_argument, NULL, 'd' }, {} }; int gridn = 10; bool debug = false; int opt; do { // "h" means -h does something opt = getopt_long(argc, argv, "h", opts, NULL); switch(opt) { case -1: break; case 'h': printf(usage, argv[0]); return 0; case 'd': debug = true; break; case 'N': gridn = atoi(optarg); break; case '?': fprintf(stderr, "Unknown option\n"); fprintf(stderr, usage, argv[0]); return 1; } } while( opt != -1 ); if( optind != argc-1) { fprintf(stderr, "Need a single points-file on the cmdline\n"); fprintf(stderr, usage, argv[0]); return 1; } if(gridn < 2) { fprintf(stderr, "--gridn value must be >= 2\n"); return 1; } std::vector points; if( !read_points(&points, argv[argc-1]) ) return 1; std::vector points_out; bool result = find_grid_from_points(points_out, points, gridn, debug); printf("# x y\n"); if( result ) { for(int i=0; i<(int)points_out.size(); i++) printf("%f %f\n", points_out[i].x, points_out[i].y); return 0; } return 1; } mrgingham-1.26/test/000077500000000000000000000000001476660265400144125ustar00rootroot00000000000000mrgingham-1.26/test/data/000077500000000000000000000000001476660265400153235ustar00rootroot00000000000000mrgingham-1.26/test/data/data-for-rotate-corners.vnl000066400000000000000000000006061476660265400225100ustar00rootroot00000000000000# filename x y level b0 - - - b1 20 200 0 b1 21 200 0 b1 22 200 0 b1 20 201 0 b1 21 201 0 b1 22 201 0 b1 20 202 0 b1 21 202 0 b1 22 202 0 a0 - - - c1 30 300 0 c1 31 300 0 c1 32 300 0 c1 30 301 0 c1 31 301 0 c1 32 301 0 c1 30 302 0 c1 31 302 0 c1 32 302 0 b2 - - - d0 - - - a1 40 400 0 a1 41 400 0 a1 42 400 0 a1 40 401 0 a1 41 401 0 a1 42 401 0 a1 40 402 0 a1 41 402 0 a1 42 402 0 a2 - - - mrgingham-1.26/test/test--mrgingham-rotate-corners000077500000000000000000000105471476660265400223170ustar00rootroot00000000000000#!/bin/zsh program=${0:h}/../mrgingham-rotate-corners datafile=${0:h}/data/data-for-rotate-corners.vnl numfailed=0 function check { name=$1 data_post_pipe=$2 args=$3 data_ref=$4 [[ -z "$data_post_pipe" ]] && data_post_pipe="cat" data_received=$(< $datafile ${(z)data_post_pipe} | $program ${(z)args} 2>/dev/null) error_code_received=$? if [[ "$data_ref" = "ERROR" ]] { # expected error if (( error_code_received )) { echo "Test OK: $name" } else { echo "Test failed: $name: Expected failure, but program succeeded:" echo "Command: < $datafile $data_post_pipe | $program $args" echo "" numfailed=$((numfailed+1)) } } else { # did NOT expect an error if (( error_code_received )) { echo "Test failed: $name: Expected success, but program failed" echo "Command: < $datafile $data_post_pipe | $program $args" echo "" numfailed=$((numfailed+1)) } else { if [[ "$data_ref" = "$data_received" ]] { echo "Test OK: $name" } else { echo "Test failed: $name:" echo "Command: < $datafile $data_post_pipe | $program $args" echo "" echo "======= expected ========" echo $data_ref echo "======= received ========" echo $data_received echo "=========================" echo "" numfailed=$((numfailed+1)) } } } } check "passthrough" "" "--gridn 3" "$(cat <