pax_global_header00006660000000000000000000000064141423043710014511gustar00rootroot0000000000000052 comment=1d5898d819b3429fe2b02b0069cc086887f50cb3 mrgingham-1.20/000077500000000000000000000000001414230437100134045ustar00rootroot00000000000000mrgingham-1.20/.gitignore000066400000000000000000000005211414230437100153720ustar00rootroot00000000000000*.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.20/ChESS.c000066400000000000000000000110611414230437100144540ustar00rootroot00000000000000/* 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.20/ChESS.h000066400000000000000000000021511414230437100144610ustar00rootroot00000000000000#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.20/ChESS_response_5.docstring000066400000000000000000000010341414230437100203670ustar00rootroot00000000000000Runs the ChESS detector to compute a "cornerness" response Synopsis: response = mrgingham.ChESS_response_5(image) The input is expected to be a numpy array containing unsigned 8-bit integers. The response has the same dimensions as the input, but contains signed 16-bit integers. Broadcasting is fully supported. 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 mrgingham-1.20/Makefile000066400000000000000000000075121414230437100150510ustar00rootroot00000000000000PYTHON_VERSION_FOR_EXTENSIONS := 3 include Makefile.common.header PROJECT_NAME := mrgingham ABI_VERSION := 2 TAIL_VERSION := 1 DIST_BIN := mrgingham mrgingham-observe-pixel-uncertainty 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 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 lives in a POD # 2. This documentation is stripped out here with 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 $^ $@ mrgingham-observe-pixel-uncertainty.pod: %.pod: % ./make-pod.pl $< > $@ cat footer.pod >> $@ EXTRA_CLEAN += *.1 mrgingham-observe-pixel-uncertainty.pod README.org ########## python stuff # In the python api I have to cast a PyCFunctionWithKeywords to a PyCFunction, # and the compiler complains. But that's how Python does it! So I tell the # compiler to chill mrgingham_pywrap.o: CFLAGS += -Wno-cast-function-type 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 Makefile.common.footer mrgingham-1.20/Makefile.common.footer000066400000000000000000000356021414230437100176360ustar00rootroot00000000000000# -*- Makefile -*- # Copied from mrbuild at git hash 9224398 # This is a part of the mrbuild project: https://github.com/dkogan/mrbuild # # Released under an MIT-style license. Modify and distribute as you like: # # Copyright 2016-2019 California Institute of Technology # # 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. # This is a common Makefile that can be used as the core buildsystem for # projects providing a library and some executables using this library. Please # see README.build.org for the documentation. # There are two ways to pass variables to make: # # make CFLAGS=-foo # and # CFLAGS=-foo make # # The former creates a "command line" variable and the latter an # "environment variable". In order to be able to modify a "command line" # variable (to add other flags, say), one MUST use 'override'. So one would have to do # # override CFLAGS += -bar # # without the "override" nothing would happen. I want to avoid this rabbithole # entirely, so I disallow "command line" variables for things that I modify. # # I only do this for xxxFLAGS becuase cross-building and installing in Debian # uses Make in this way. Hopefully this is safe enough $(foreach v,$(filter %FLAGS,$(.VARIABLES)),$(if $(filter command line,$(origin $v)), $(error '$v' not allowed as a make parameter. Please do "$v=xxxx make yyyy" instead of "make yyyy $v=xxxx"))) # Make sure I have the variables that must be defined. Libraries need an ABI and # TAIL version, while other need a plain VERSION MUST_DEF_VARIABLES := PROJECT_NAME $(if $(LIB_SOURCES),ABI_VERSION TAIL_VERSION,VERSION) $(foreach v,$(MUST_DEF_VARIABLES),$(if $($v),,$(error User MUST specify $v))) # The default VERSION string that appears as a #define to each source file, and # to any generated documentation (gengetopt and so on). The user can set this to # whatever they like VERSION ?= $(ABI_VERSION).$(TAIL_VERSION) # Default compilers. By default, we use g++ as a linker CC ?= gcc CXX ?= g++ NVCC ?= nvcc CC_LINKER ?= $(CXX) # used to make gcc output header dependency information. All source # files generated .d dependency definitions that are included at the # bottom of this file CCXXFLAGS += -MMD -MP # always building with debug information. This is stripped into the # -dbg/-debuginfo packages by debhelper/rpm later CCXXFLAGS += -g # I want the frame pointer. Makes walking the stack WAY easier CCXXFLAGS += -fno-omit-frame-pointer # I look through my LIB_SOURCES and BIN_SOURCES. Anything that isn't a wildcard # (has * or ?) should exist. If it doesn't, the user messed up and I flag it get_no_wildcards = $(foreach v,$1,$(if $(findstring ?,$v)$(findstring *,$v),,$v)) complain_if_nonempty = $(if $(strip $1),$(error $2: $1)) complain_unless_all_exist = $(call complain_if_nonempty,$(call get_no_wildcards,$(filter-out $(wildcard $1),$1)),File not found: ) $(call complain_unless_all_exist,$(LIB_SOURCES) $(BIN_SOURCES)) LIB_SOURCES := $(wildcard $(LIB_SOURCES)) BIN_SOURCES := $(wildcard $(BIN_SOURCES)) LIB_OBJECTS := $(addsuffix .o,$(basename $(LIB_SOURCES))) BIN_OBJECTS := $(addsuffix .o,$(basename $(BIN_SOURCES))) SOURCE_DIRS := $(sort ./ $(dir $(LIB_SOURCES) $(BIN_SOURCES))) # if the PROJECT_NAME is libxxx then LIB_NAME is libxxx # if the PROJECT_NAME is xxx then LIB_NAME is libxxx LIB_NAME := $(or $(filter lib%,$(PROJECT_NAME)),lib$(PROJECT_NAME)) LIB_TARGET_SO_BARE := $(LIB_NAME).so LIB_TARGET_SO_ABI := $(LIB_TARGET_SO_BARE).$(ABI_VERSION) LIB_TARGET_SO_FULL := $(LIB_TARGET_SO_ABI).$(TAIL_VERSION) LIB_TARGET_SO_ALL := $(LIB_TARGET_SO_BARE) $(LIB_TARGET_SO_ABI) $(LIB_TARGET_SO_FULL) BIN_TARGETS := $(basename $(BIN_SOURCES)) # all objects built for inclusion in shared libraries get -fPIC. We don't build # static libraries, so this is 100% correct $(LIB_OBJECTS): CCXXFLAGS += -fPIC CCXXFLAGS += -DVERSION='"$(VERSION)"' # These are here to process the options separately for each file being built. # This allows per-target options to be set # # if no explicit optimization flags are given, optimize define massageopts $1 $(if $(filter -O%,$1),,-O3) endef # If no C++ standard requested, I default to c++0x define massageopts_cxx $(call massageopts,$1 $(if $(filter -std=%,$1),,-std=c++0x)) endef define massageopts_c $(call massageopts,$1) endef # define the compile rules. I need to redefine the rules here because my # C..FLAGS variables are simple (immediately evaluated), but the user # could have specified per-target flags that ALWAYS evaluate deferred-ly # # I add the warning options AT THE START of the flag list so that the user can # override these cc_build_rule = $(strip $(CXX) $(call massageopts_cxx,-Wall -Wextra $(CXXFLAGS) $(CCXXFLAGS) $(CPPFLAGS))) -c -o $@ $< c_build_rule = $(strip $(CC) $(call massageopts_c, -Wall -Wextra $(CFLAGS) $(CCXXFLAGS) $(CPPFLAGS))) -c -o $@ $< cu_build_rule = $(strip $(NVCC) $(call massageopts_c, -Wall -Wextra $(CUFLAGS) $(CPPFLAGS))) -c -o $@ $< cu_build_rule += --compiler-options "-Wall -Wextra $(CCXXFLAGS)" %.o:%.C $(cc_build_rule) %.o:%.cc $(cc_build_rule) %.o:%.cpp $(cc_build_rule) %.o:%.c $(c_build_rule) %.o:%.cu $(cu_build_rule) %.o: %.S $(CC) $(ASFLAGS) $(CPPFLAGS) -c -o $@ $< # gengetopt rule. If we have GENGETOPT_OPTIONS, use those; otherwise use some # sensible defaults. If we don't have -F set an --output-dir also %.h %.c: %.ggo gengetopt -i $< \ $(if $(GENGETOPT_OPTIONS),$(if $(filter -F%,$(GENGETOPT_OPTIONS)),,--output-dir $(dir $<))) \ $(or $(GENGETOPT_OPTIONS), -C -u -g $(VERSION) -F $* args -f $(notdir $*) -a $(notdir $*)) # this is how you build QT4 UIs. There's a tool to generate headers from # UI definitions. There's also a tool to generate metadata from QT # classes. This must be compiled and linked in. QT4_MOCS is a list of # all these extra objects needed by QT. Simply add $(QT4_MOCS) to your # objects list and magic happens. Similar with QT4_UI_HEADERS ui_%.h: %.ui uic-qt4 $< > $@ moc_%.cpp: %.h moc-qt4 $< > $@ ui_%.hh: %.ui uic-qt4 $< > $@ moc_%.cpp: %.hh moc-qt4 $< > $@ # Python docstring rules. I construct these from plain ASCII files to handle # line wrapping %.docstring.h: %.docstring < $^ sed 's/\\/\\\\/g; s/"/\\"/g; s/^/"/; s/$$/\\n"/;' > $@ EXTRA_CLEAN += *.docstring.h # by default I build shared libraries only. We known how to build static # libraries too, but I don't do it unless asked all: $(if $(strip $(LIB_SOURCES)),$(LIB_TARGET_SO_ALL)) $(if $(strip $(BIN_SOURCES)),$(BIN_TARGETS)) .PHONY: all .DEFAULT_GOAL := all # use --default-symver if we've got it. *BSDs do not LD_DEFAULT_SYMVER := $(shell ld --default-symver --version 1>/dev/null 2>/dev/null && echo -Wl,--default-symver) $(LIB_TARGET_SO_FULL): LDFLAGS += -shared $(LD_DEFAULT_SYMVER) -fPIC -Wl,-soname,$(notdir $(LIB_TARGET_SO_BARE)).$(ABI_VERSION) $(LIB_TARGET_SO_BARE) $(LIB_TARGET_SO_ABI): $(LIB_TARGET_SO_FULL) ln -fs $(notdir $(LIB_TARGET_SO_FULL)) $@ # Here instead of specifying $^, I do just the %.o parts and then the # others. This is required to make the linker happy to see the dependent # objects first and the dependency objects last. Same as for BIN_TARGETS $(LIB_TARGET_SO_FULL): $(LIB_OBJECTS) $(CC_LINKER) $(LDFLAGS) $(filter %.o, $^) $(filter-out %.o, $^) $(LDLIBS) -o $@ # I make sure to give the .o to the linker before the .so and everything else. # The .o may depend on the other stuff. # # The binaries get an RPATH (removed at install time). I use a relative RPATH # (using $ORIGIN) to make the build reproducible: chrpath doesn't remove the # actual RPATH string from the binary, and the local build directory gets # embedded in the output # # Here $^ contains two flavors of LIB_TARGET (see next stanza), so I manually # remove one SPACE := SPACE := $(SPACE) $(SPACE) dirs_to_dotdot = $(subst $(SPACE),/,$(patsubst %,..,$(subst /, ,$1))) get_parentdir_relative_to_childdir = /$(call dirs_to_dotdot,$(patsubst $1%,%,$2)) $(BIN_TARGETS): %: %.o $(CC_LINKER) -Wl,-rpath='$$ORIGIN'$(call get_parentdir_relative_to_childdir,$(abspath .),$(dir $(abspath $@))) $(LDFLAGS) $(filter %.o, $^) $(filter-out $(LIB_TARGET_SO_ABI),$(filter-out %.o, $^)) $(LDLIBS) -o $@ # The binaries link with the DSO, if there is one. I need the libxxx.so to build # the binary, and I need the libxxx.so.abi to run it. $(BIN_TARGETS): $(if $(strip $(LIB_SOURCES)),$(LIB_TARGET_SO_BARE) $(LIB_TARGET_SO_ABI)) clean: rm -rf $(foreach d,$(SOURCE_DIRS),$(addprefix $d,*.a *.o *.so *.so.* *.d moc_* ui_*.h*)) $(BIN_TARGETS) $(foreach s,.c .h,$(addsuffix $s,$(basename $(shell find . -name '*.ggo')))) $(EXTRA_CLEAN) distclean: clean .PHONY: distclean clean ########################### installation business ifneq (,$(filter install,$(MAKECMDGOALS))) ifeq ($(strip $(DESTDIR)),) $(error Tried to make install without having DESTDIR defined \ "make install" is ONLY for package building. \ What are you trying to do?) endif ifneq (,$(strip $(DIST_PY2_MODULES))) PY2_MODULE_PATH := $(shell python2 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") $(if $(PY2_MODULE_PATH),,$(error "Couldn't find the python2 module path!")) endif ifneq (,$(strip $(DIST_PY3_MODULES))) PY3_MODULE_PATH := $(shell python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") $(if $(PY3_MODULE_PATH),,$(error "Couldn't find the python3 module path!")) endif USE_DEBIAN_PATHS := $(wildcard /etc/debian_version) ifneq (,$(USE_DEBIAN_PATHS)) # we're a debian-ish box, use the multiarch dir DEB_HOST_MULTIARCH := $(shell dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null) USRLIB := usr/lib/$(DEB_HOST_MULTIARCH) else # we're something else. Do what CentOS does. # If /usr/lib64 exists, use that. Otherwise /usr/lib USRLIB := $(if $(wildcard /usr/lib64),usr/lib64,usr/lib) endif endif # I process the simple wildcard exceptions on DIST_BIN and DIST_INCLUDE in a # deferred fashion. The reason is that I wand $(wildcard) to run at install # time, i.e. after stuff is built, and the files $(wildcard) is looking at # already exist DIST_BIN_ORIG := $(DIST_BIN) DIST_INCLUDE_ORIG := $(DIST_INCLUDE) DIST_BIN = $(filter-out $(wildcard $(DIST_BIN_EXCEPT)), \ $(wildcard $(or $(DIST_BIN_ORIG), $(BIN_TARGETS)))) DIST_INCLUDE = $(filter-out $(wildcard $(DIST_INCLUDE_EXCEPT) *.docstring.h), \ $(wildcard $(DIST_INCLUDE_ORIG))) MANDIR := usr/share/man # Generates the install rules. Arguments: # 1. variable containing the being installed # 2. target path they're being installed to define install_rule $(if $(strip $($1)), mkdir -p $2 && \ cp -r $($1) $2 && \ $(if $($(1)_EXCEPT_FINDSPEC),find $2 $($(1)_EXCEPT_FINDSPEC) -delete, true) ) endef # Redhat wants the various manpage section get their own subdir for some reason define move_manpages_for_redhat mkdir -p $(DESTDIR)/$(MANDIR)/man$1 && \ (mv $(DESTDIR)/$(MANDIR)/*.$1 $(DESTDIR)/$(MANDIR)/man$1 2>/dev/null || true) && \ (mv $(DESTDIR)/$(MANDIR)/*.$1.gz $(DESTDIR)/$(MANDIR)/man$1 2>/dev/null || true) && \ (rmdir $(DESTDIR)/$(MANDIR)/man$1 2>/dev/null || true) endef ifneq ($(strip $(LIB_SOURCES)),) install: $(LIB_TARGET_SO_ALL) endif install: $(BIN_TARGETS) $(DIST_DOC) $(DIST_MAN) $(DIST_DATA) # using 'cp -P' instead of 'install' because the latter follows links unconditionally ifneq ($(strip $(LIB_SOURCES)),) mkdir -p $(DESTDIR)/$(USRLIB) cp -P $(LIB_TARGET_SO_FULL) $(DESTDIR)/$(USRLIB) ln -fs $(notdir $(LIB_TARGET_SO_FULL)) $(DESTDIR)/$(USRLIB)/$(notdir $(LIB_TARGET_SO_ABI)) ln -fs $(notdir $(LIB_TARGET_SO_FULL)) $(DESTDIR)/$(USRLIB)/$(notdir $(LIB_TARGET_SO_BARE)) endif $(call install_rule,DIST_BIN, $(DESTDIR)/usr/bin) $(call install_rule,DIST_INCLUDE, $(DESTDIR)/usr/include/$(PROJECT_NAME)) $(call install_rule,DIST_DOC, $(DESTDIR)/usr/share/doc/$(PROJECT_NAME)) # I install the manpages normally. On Redhat I need to shuffle them into # different subdirectories. This is incomplete: sections that aren't simply # digits 1-9 will not be moved. "3perl" is an example I can think of that won't # work here. Good-enough for now $(call install_rule,DIST_MAN, $(DESTDIR)/$(MANDIR)) ifeq (,$(USE_DEBIAN_PATHS)) $(foreach s,1 2 3 4 5 6 7 8 9,$(call move_manpages_for_redhat,$s) && ) true endif $(call install_rule,DIST_DATA, $(DESTDIR)/usr/share/$(PROJECT_NAME)) $(call install_rule,DIST_PERL_MODULES,$(DESTDIR)/usr/share/perl5) $(call install_rule,DIST_PY2_MODULES, $(DESTDIR)/$(PY2_MODULE_PATH)) $(call install_rule,DIST_PY3_MODULES, $(DESTDIR)/$(PY3_MODULE_PATH)) # In filenames I rename __colon__ -> : # This is required because Make can't deal with : in rules for fil in `find $(DESTDIR) -name '*__colon__*'`; do mv $$fil `echo $$fil | sed s/__colon__/:/g`; done # Remove rpaths from everything. /usr/bin is allowed to fail because # some of those executables aren't ELFs. On the other hand, any .so we # find IS en ELF. Those could live in a number of places, since they # could be extension modules for the various languages, and I thus look # for those EVERYWHERE ifneq ($(strip $(DIST_BIN)),) chrpath -d $(DESTDIR)/usr/bin/* 2>/dev/null || true endif find $(DESTDIR) -name '*.so' | xargs chrpath -d # Any perl programs need their binary path stuff stripped out. This # exists to let these run in-tree, but needs to be removed at # install-time (similar to an RPATH) ifneq ($(strip $(DIST_BIN)),) for fil in `find $(DESTDIR)/usr/bin -type f`; do head -n1 $$fil | grep -q '^#!.*/perl$$' && perl -n -i -e 'print unless /^\s* use \s+ lib \b/x' $$fil || true; done endif test -e $(DESTDIR)/usr/share/perl5 && find $(DESTDIR)/usr/share/perl5 -type f | xargs perl -n -i -e 'print unless /^\s* use \s+ lib \b/x' || true ifneq ($(strip $(DIST_BIN)),) # Everything executable needs specific permission bits chmod 0755 $(DESTDIR)/usr/bin/* endif .PHONY: install # I want to keep all the intermediate files always .SECONDARY: # the header dependencies -include $(addsuffix *.d,$(SOURCE_DIRS)) mrgingham-1.20/Makefile.common.header000066400000000000000000000135761414230437100175760ustar00rootroot00000000000000# -*- Makefile -*- # Copied from mrbuild at git hash 9224398 # This is a part of the mrbuild project: https://github.com/dkogan/mrbuild # # Released under an MIT-style license. Modify and distribute as you like: # # Copyright 2016-2019 California Institute of Technology # # 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. # This stuff defines variables (PY_EXT_SUFFIX) that could be used by the user # Makefile at parsing time. So this must be included BEFORE the rest of the user # Makefile PYTHON_VERSION_FOR_EXTENSIONS ?= 3 # 2 or 3 # Flags for python extension modules. See # http://notes.secretsauce.net/notes/2017/11/14_python-extension-modules-without-setuptools-or-distutils.html # # I build the python extension module without any setuptools or anything. # Instead I ask python about the build flags it likes, and build the DSO # normally using those flags. # # There's some sillyness in Make I need to work around. First, I produce a # python script to query the various build flags, but replacing all whitespace # with __whitespace__. The string I get when running this script will then have # a number of whitespace-separated tokens, each setting ONE variable # # I set up a number of variables: # # These come from Python queries. I ask Python about XXX and store the result # into PY_XXX # # PY_CC # PY_CFLAGS # PY_CCSHARED # PY_INCLUDEPY # PY_BLDSHARED # PY_LDFLAGS # PY_EXT_SUFFIX # PY_MULTIARCH # # These process the above into a single set of CFLAGS: # # PY_MRBUILD_CFLAGS # # These process the above into a single set of LDFLAGS: # # PY_MRBUILD_LDFLAGS # # These process the above into a DSO-building linker command # # PY_MRBUILD_LINKER # # When the user Makefile evaluates ANY of these variables I query python, and # memoize the results. So the python is invoked at MOST one time. Any Makefiles # that don't touch the PY_... variables will not end up invoking the python # thing at all # # Variables to ask Python about _PYVARS_LIST := CC CFLAGS CCSHARED INCLUDEPY BLDSHARED BLDLIBRARY LDFLAGS EXT_SUFFIX MULTIARCH # Python script to query those variables define _PYVARS_SCRIPT from __future__ import print_function import sysconfig import re conf = sysconfig.get_config_vars() for v in ($(foreach v,$(_PYVARS_LIST),"$v",)): if v in conf: print(re.sub("[\t ]+", "__whitespace__", "_PY_{}:={}".format(v, conf[v]))) endef # I eval this to actually invoke the Python and to ingest its results. I only # eval this ONLY when necessary. define query_python_extension_building_flags _PYVARS = $(shell python$(PYTHON_VERSION_FOR_EXTENSIONS) -c '$(_PYVARS_SCRIPT)') # I then $(eval) these tokens one at a time, restoring the whitespace $(foreach setvarcmd,$(_PYVARS),$(eval $(subst __whitespace__, ,$(setvarcmd)))) # pull out flags from CC, throw out the compiler itself, since I know better _FLAGS_FROM_PYCC = $(wordlist 2,$(words $(_PY_CC)),$(_PY_CC)) _PY_MRBUILD_CFLAGS = $(filter-out -O%,$(_FLAGS_FROM_PYCC) $(_PY_CFLAGS) $(_PY_CCSHARED) -I$(_PY_INCLUDEPY)) SPACE := SPACE := $(SPACE) $(SPACE) dirs_to_dotdot = $(subst $(SPACE),/,$(patsubst %,..,$(subst /, ,$1))) get_parentdir_relative_to_childdir = /$(call dirs_to_dotdot,$(patsubst $1%,%,$2)) _PY_MRBUILD_LDFLAGS = $(_PY_LDFLAGS) -L$(abspath .) -Wl,-rpath='$$ORIGIN'$(call get_parentdir_relative_to_childdir,$(abspath .),$(dir $(abspath $@))) _PY_MRBUILD_LINKER = $(_PY_BLDSHARED) $(_PY_BLDLIBRARY) endef # List of variables a user Makefile could touch. These are all PY_... _PYVARS_API := $(foreach v,$(_PYVARS_LIST),PY_$v) PY_MRBUILD_CFLAGS PY_MRBUILD_LDFLAGS PY_MRBUILD_LINKER # The first time the user touches these variables, ask Python. Each subsequent # time, use the previously-returned value. So we query Python at most once. If a # project isn't using the Python extension modules, we will not query Python at # all # # I handle all the Python API variables identically, except for PY_EXT_SUFFIX. # If Python gives me a suffix, I use it (this is available in python3; it has # ABI, architecture details). Otherwise, I try the multiarch suffix, or if even # THAT isn't available, just do .so. I need to handle it specially to make the # self-referential logic work with the memoization logic define _PY_DEFINE_API_VAR $1 = $$(or $$(_$1),$$(eval $$(value query_python_extension_building_flags))$$(_$1)) endef define _PY_DEFINE_API_VAR_EXTSUFFIX $1 = $$(or $$(_$1),$$(eval $$(value query_python_extension_building_flags))$$(or $$(_$1),$$(if $$(PY_MULTIARCH),.$$(PY_MULTIARCH)).so)) endef $(foreach v,$(filter-out PY_EXT_SUFFIX,$(_PYVARS_API)),$(eval $(call _PY_DEFINE_API_VAR,$v))) $(eval $(call _PY_DEFINE_API_VAR_EXTSUFFIX,PY_EXT_SUFFIX)) # Useful to pull in a local build of some library. For testing. Sets the # compiler and linker (runtime and build-time) flags. Invoke like this: # $(eval $(call add_local_library_path,/home/user/library)) define add_local_library_path CFLAGS += -I$1 CXXFLAGS += -I$1 LDFLAGS += -L$1 -Wl,-rpath=$1 endef mrgingham-1.20/README.org000066400000000000000000000420361414230437100150570ustar00rootroot00000000000000* 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 * DESCRIPTION Both chessboard and a non-offset 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, but only for some core functionality. Currently mrgingham looks for a square grid of points, with some user-requestable width. The default is a 10x10 grid. ** 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". *** 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. *** 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. ** 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. =mrgingham-...= are distributed, and their manpages appear below. - =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 - =test-find-grid-from-points= takes in 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. The =mrgingham...= tools are distributed in the package, while the others are internal. ** 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 /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 ] DESCRIPTION This tool uses the "mrgingham" library to detect chessboard corners from images stored on disk. 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 containing the filename, coordinates of the chessboard corners and the decimation level used to compute each corner. For diagnostics, pass in "--debug". This produces a number of self-plotting files that describe the results of the intermediate steps. Each diagnostic file is reported on the console when it is written. Both chessboard and a non-offset grid of circles are supported. Chessboard are the *strongly* preferred choice; the circle detector is mostly here for compatibility. Both are 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. The default is a 10x10 grid. Approach This tool works in two passes: * 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) * 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". Chessboards This is based on the feature detector described in this paper: The authors provide a simple MIT-licensed implementation here: 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. A recommended pattern is available in "chessboard.10x10.pdf" and "chessboard.14fx14.pdf" in the "mrgingham" sources. 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: * Find a reasonable intensity threshold * Threshold the image * Find blobs * 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. ARGUMENTS The general usage is mrgingham [--debug] [--jobs N] [--noclahe] [--blur radius] [--level l] [--blobs] imageglobs imageglobs ... By default we look for a chessboard. By default we apply adaptive histogram equalization, then blur with a radius of 1. We then use an adaptive level of downsampling when looking for the chessboard. The arguments are "--noclahe" Optional argument to control image preprocessing. Unless given we will apply adaptive histogram equalization (CLAHE algorithm) to the images. This is *extremely* helpful if the images aren't lit evenly; which is most of them. "--blur RADIUS" Optional argument to control image preprocessing. This will apply a gaussian blur to the image (after the histogram equalization). A light blurring is very helpful with CLAHE, since that makes noisy images. By default we will blur with radius = 1. Set to <= 0 to disable "--level L" Optional argument to control 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. "--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. The images are given as (multiple) globs. The output is a vnlog with columns "filename","x","y". All filenames matched in the glob will appear in the output. Images for which no chessboard pattern was found appear as a single record with null "x" and "y". "--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 "--blobs" Find circle centers instead of chessboard corners. Not recommended #+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 * 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.20/README.template.org000066400000000000000000000176541414230437100167010ustar00rootroot00000000000000* 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 * DESCRIPTION Both chessboard and a non-offset 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, but only for some core functionality. Currently mrgingham looks for a square grid of points, with some user-requestable width. The default is a 10x10 grid. ** 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". *** 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. *** 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. ** 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. =mrgingham-...= are distributed, and their manpages appear below. - =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 - =test-find-grid-from-points= takes in 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. The =mrgingham...= tools are distributed in the package, while the others are internal. ** 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 * 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.20/chessboard.10x10.fig000066400000000000000000000127601414230437100167660ustar00rootroot00000000000000#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.20/chessboard.10x10.pdf000066400000000000000000000052671414230437100167760ustar00rootroot00000000000000%PDF-1.5 %Çì¢ 5 0 obj <> stream xœ•KNÆ0 „÷=EÖ,LœÄy®€à_±î/‘V¶é‚Nª."5ŸìÉxÚ|…Hâþèúö¹=¿¶ðøÙbxlÜb¤ÁG*RÏsi¾~¿oOnTj˜tÒ­ì«"£Ê "Ö«p')W½¬g¡8®j™"H¥Tõ´èèÔ-æ[ÙilÂR'’H¢\NL}ý ¡˜²3âŽ]"‰ðB½ÌRq”»µ°zΫBs—)õ…(§° J”:‚“Õs¡–WÉ2 &˨… :Á>åÕ4®2£ÉZP»ë±!cʆ,ÓtnõjÈÑ¥qÇ”gA©¥X}æ9<ž,©õ“Wx€æú÷U`XÌñ›½`<½ì8¯„yòQ°”~Z˜:LX ªý_ʇ‚ºtÝ̉;½,LX6¤ÜqJUjv§>§vG"È/,Ù¿ç4”n!Ž¡ýü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.20/chessboard.14x14.fig000066400000000000000000000244111414230437100167720ustar00rootroot00000000000000#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.20/chessboard.14x14.pdf000066400000000000000000000053461414230437100170040ustar00rootroot00000000000000%PDF-1.7 %Çì¢ %%Invocation: gs -q -dSAFER -sAutoRotatePages=? -sDEVICE=pdfwrite -dPDFSETTINGS=/prepress ? ? - 5 0 obj <> stream xœW»nÄ0 Ûó™;~ÛùŒ~CÑvêÐþÿPÛ'êzÅ… :¸"͈–ò½{v?ÿäùöµùýsëÅ»RžŽÚ]{ìÉå’ôùó¾}¼l¥6ç¼§Ú‹KUþÛôyCi­ìƒ;ÂI­’Uhhì©L>QUjý[ð?Jøž¢]Ž%ºx§äãá((…Ð]8 ŸÖ±,3—Üjy „ÔÌÅTŠ¡])(äÐ\®†öpôìR³PÍûY™ŸPkñv.ŠŠq¼Ä¡‡3ÆÑ.vC½¢8cOÙµÄM…,Å+ aélöåX‹'„tBïlOC:¡Ý'…N¯¥Q†*øY4®ŠBpø%©H‚ÃŃªxRZ‚⦢c ”$ÇxÓ’B%Œ\=jq' ^ (~FÄÂpbÅbŠ«é¢ÝLQž6#t:áfWäb¿žÙ´ª¬»¨dCôE®8zKý>ñœQ#¸„ó”‚£`åó,cTÒ"€ÄL‰;C??êP+UGIÔ BAàÚ Ý«7#í^ÜŒ¼Å1á©,Ü@¬ËåÊ`áâFi!jº*¢(Ü=œQwê¶ r@…pQrÕ¢°'pQ¸\i¿è˜á(ŒÞ03' .}Þíè‹(:@°×Ó÷,Ÿ~°ëÚ¶à]?†»úÇÅþ ¯‘£Ð…)a0ÎÑuMºñtóÝðJ^¡q>i% hl0hSº3‘ï9kë÷êD×c€èv Ðùü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.20/find_blobs.cc000066400000000000000000000036271414230437100160240ustar00rootroot00000000000000#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 = 40; blobDetectorParams.maxArea = 80000; blobDetectorParams.minDistBetweenBlobs = 15; 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.20/find_blobs.hh000066400000000000000000000011021414230437100160200ustar00rootroot00000000000000#pragma once #include #include #include "point.hh" // I look for white-on-black dots 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.20/find_chessboard.docstring000066400000000000000000000015611414230437100204420ustar00rootroot00000000000000Runs the full mrgingham chessboard detector on an image Synopsis: chessboard_points = mrgingham.find_chessboard(image) The input is the image, as a numpy array. The output is a numpy array of shape (N*N,2) containing ordered pixel coordinates of the chessboard. If no chessboard was found, None is returned. An optional argument "image_pyramid_level" 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 means "start with a downsampled image, and then refine the results by repeatedly reducing the downsampling. This is the default. We detect an NxN grid of corners, where N defaults to 10. To select a different value, pass a kwarg gridn=N No broadcasting is supported by this function mrgingham-1.20/find_chessboard_corners.cc000066400000000000000000000577371414230437100206060ustar00rootroot00000000000000#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.20/find_chessboard_corners.docstring000066400000000000000000000013131414230437100221700ustar00rootroot00000000000000Finds corner features in an image Synopsis: corners = mrgingham.find_chessboard_corners(image) The input is the image, as a numpy array. The output is a numpy array of shape (...,2) containing pixel coordinates of corners we found. This is part of the full mrgingham processing. A full run would then take these corners and try to find a set of corners that comprise a chessboard grid. An optional argument "image_pyramid_level" 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. The default is 0. No broadcasting is supported by this function mrgingham-1.20/find_chessboard_corners.hh000066400000000000000000000073431414230437100206040ustar00rootroot00000000000000#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.20/find_grid.cc000066400000000000000000001661421414230437100156520ustar00rootroot00000000000000#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 type 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.20/make-pod.pl000077500000000000000000000077631414230437100154560ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use feature ':5.10'; my $usagemessage = "Usage: $0 frobnicate > frobnicate.pod"; if(@ARGV != 1) { die "Need exactly one argument on the cmdline.\n$usagemessage"; } my $path = $ARGV[0]; if( ! (-r $path && -x $path && -f $path) ) { die "Commandline argument must be an executable file\n$usagemessage"; } # for proper --help formatting $ENV{COLUMNS}=80; # prepend ./ if no path given. I'm going to run this thing, so we need that $path = "./$path" unless $path =~ m{^/}; my $helpstring = `$path --help`; # I assume the following stucture. If the --help string doesn't fit this # structure, I barf # # usage: frobnicator [-h] [--xxx XXX] # a [b ...] # # Does thing # # Synopsis: # # $ frobnicator xxx # ... blah blah # # $ more examples ... # # long description that talks about stuff # long description that talks about stuff # long description that talks about stuff # # long description that talks about stuff # # long description that talks about stuff # # positional arguments: # a Does what a does # # optional arguments: # b Does what b does # ... # usage is the thing up to the first blank line my ($usage) = $helpstring =~ m/(^usage.*?)\n\n/msp or die "Couldn't parse out the usage"; $helpstring = ${^POSTMATCH}; # Then we have a one-line summary my ($summary) = $helpstring =~ m/(^.*?)\n\n/p or die "Couldn't parse out the summary"; $helpstring = ${^POSTMATCH}; # Then the synopsis my ($synopsis) = $helpstring =~ m/ ^synopsis.*?\n\n # synopsis title ( # capture stuff (?:(?:[ \t] .+?)? \n)+ # a bunch of lines: empty or beginning with whitespace ) # That's all I want /xpi or die "Couldn't parse out the synopsis"; $helpstring = ${^POSTMATCH}; $synopsis =~ s/\n*$//g; # cull trailing whitespace # Now a description: everything until 'xxxx arguments'. I might not have a # description at all my ($description, $post) = $helpstring =~ /(^.*?)(?:\n\n)?(\w+ arguments:?\n)/ips or die "Couldn't parse description"; $helpstring = $post . ${^POSTMATCH}; # Now the arguments my @args; while($helpstring !~ /^\s*$/) { my ($argument_kind) = $helpstring =~ /(^\w+ arguments):?\n\n?/pi or die "Couldn't parse out argument kind"; $helpstring = ${^POSTMATCH}; my ($argument_what) = $helpstring =~ /(^.*?)(?:\n\n|\n$)/pis or die "Couldn't parse out argument what"; $helpstring = ${^POSTMATCH}; # I really should parse the table argparse puts out, but let's just finish # this as is for now push @args, [uc($argument_kind), $argument_what]; } # Alrighty. I can now write out this thing say "=head1 NAME\n"; my ($programname) = $path =~ m{([^/]+$)} or die "Couldn't parse out the program name"; say "$programname - $summary\n"; say "=head1 SYNOPSIS\n"; say "$synopsis\n"; if( $description ) { say "=head1 DESCRIPTION\n"; say "$description\n"; } say "=head1 OPTIONS\n"; for my $arg (@args) { my ($kind,$what) = @$arg; say "=head2 $kind\n"; say "$what\n"; } __END__ =head1 NAME make-pod.pl - creates POD documentation from a python commandline tool =head1 SYNOPSIS $ ./make-pod.pl frobnicate > frobnicate.pod =head1 DESCRIPTION Python has a culture of ignoring standard ways of doing things, and building their own everything from scratch for some reason. And as a result I need to write this tool to give me manpages. This tool is a hack that works for THIS project. A core assumption is that running the tool with C<--help> produces fairly complete documentation, and I just need to massage it. My tools all have a manpage-like docstring, use C to document the options, and tell C to use the docstring in the C<--help> description. This tool generates a POD, which can then be made into a manpage with C. =head1 REQUIRED ARGUMENTS =over =item Tool that we're making a manpage for =back =head1 AUTHOR Dima Kogan, C<< >> mrgingham-1.20/mrgingham-from-image.cc000066400000000000000000000320621414230437100177100ustar00rootroot00000000000000#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 image = cv::imread(filename, cv::IMREAD_IGNORE_ORIENTATION | cv::IMREAD_GRAYSCALE); if( image.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; } if( ctx.doclahe ) { // CLAHE doesn't by itself use the full dynamic range all the time. // I explicitly apply histogram equalization and then CLAHE cv::equalizeHist(image, image); clahe->apply(image, image); } if( ctx.blur_radius > 0 ) { cv::blur( image, image, 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, image); 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, image, 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, image, 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 = "Usage: %s [--debug] [--debug-sequence x,y]\n" " [--jobs N] [--noclahe] [--blur radius]\n" " [--level l] [--blobs] imageglobs imageglobs ...\n" "\n" " By default we look for a chessboard. By default we apply adaptive histogram\n" " equalization, then blur with a radius of 1. We then use an adaptive level of\n" " downsampling when looking for the chessboard.\n" "\n" " We will look for chessboards, unless --blobs is given\n" "\n" " --noclahe is optional: unless given, we will pre-process the image with an\n" " adaptive histogram equalization step (using the CLAHE algorithm). This is\n" " useful if the calibration board has a lighting gradient across it.\n" "\n" " --blur radius applies a blur (after CLAHE) to the image before processing.\n" " By default we will blur with a radius of 1. To disable, set the radius to <= 0\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" " --no-refine By default, the coordinates of reported corners are re-detected at\n" " less-downsampled zoom levels to improve their accuracy. If we do not want to do\n" " that, pass --no-refine\n" "\n" " --jobs N will parallelize the processing N-ways. -j is a synonym. This is like\n" " GNU make, except you're required to explicitly specify a job count.\n" "\n" " We detect an NxN grid of corners, where N defaults to 10. To select a different\n" " value, pass --gridn N\n" "\n" " The images are given as (multiple) globs. The output is a vnlog with columns\n" " filename,x,y. All filenames matched in the glob will appear in the output.\n" " Images for which no chessboard pattern was found appear as a single record\n" " with null x and y.\n" "\n" " For debugging, pass in --debug. This will dump the various intermediate results\n" " into /tmp and it will report more stuff on the console.\n" " For debugging sequence candidates, pass in --debug-sequence x,y where 'x,y' are the\n" " approximate image coordinates of the start of a given sequence (corner on the edge of\n" " a chessboard. This doesn't need to be exact; mrgingham will report on the nearest\n" " corner\n"; 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.20/mrgingham.cc000066400000000000000000000170661414230437100156760ustar00rootroot00000000000000#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.20/mrgingham.hh000066400000000000000000000105711414230437100157020ustar00rootroot00000000000000#pragma once #include #include #include "point.hh" // I look for white-on-black dots namespace mrgingham { struct debug_sequence_t { bool dodebug; PointInt pt; debug_sequence_t() : dodebug(false), pt() {} }; 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.20/mrgingham.pod000066400000000000000000000145501414230437100160660ustar00rootroot00000000000000=head1 NAME mrgingham - Extract chessboard corners from a set of images =head1 SYNOPSIS $ 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 ] =head1 DESCRIPTION This tool uses the C library to detect chessboard corners from images stored on disk. Images are given on the commandline, as globs. Each glob is expanded, and each image is processed (possibly in parallel if C<-j> was given). The output is a L containing the filename, coordinates of the chessboard corners and the decimation level used to compute each corner. For diagnostics, pass in C<--debug>. This produces a number of self-plotting files that describe the results of the intermediate steps. Each diagnostic file is reported on the console when it is written. Both chessboard and a non-offset grid of circles are supported. Chessboard are the I preferred choice; the circle detector is mostly here for compatibility. Both are 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 I use OpenCV here, but only for some core functionality. Currently mrgingham looks for a square grid of points, with some user-requestable width. The default is a 10x10 grid. =head2 Approach This tool works in two passes: =over =item * 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 =over =item * there will be many outliers =item * 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) =back =item * 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 =back If we return I 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". =head3 Chessboards This is based on the feature detector described in this paper: L The authors provide a simple MIT-licensed implementation here: L This produces an image of detector response. I 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 I, meaning a chessboard with 11 I per side. A recommended pattern is available in C and C in the C sources. =head3 Circles B The circle finder does mostly what the first stage of the OpenCV circle detector does: =over =item * Find a reasonable intensity threshold =item * Threshold the image =item * Find blobs =item * Return centroid of the blobs =back 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. =head1 ARGUMENTS The general usage is mrgingham [--debug] [--jobs N] [--noclahe] [--blur radius] [--level l] [--blobs] imageglobs imageglobs ... By default we look for a chessboard. By default we apply adaptive histogram equalization, then blur with a radius of 1. We then use an adaptive level of downsampling when looking for the chessboard. The arguments are =over =item C<--noclahe> Optional argument to control image preprocessing. Unless given we will apply adaptive histogram equalization (CLAHE algorithm) to the images. This is I helpful if the images aren't lit evenly; which is most of them. =item C<--blur RADIUS> Optional argument to control image preprocessing. This will apply a gaussian blur to the image (after the histogram equalization). A light blurring is very helpful with CLAHE, since that makes noisy images. By default we will blur with radius = 1. Set to <= 0 to disable =item C<--level L> Optional argument to control image preprocessing. Applies a downsampling to the image (after CLAHE and C<--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. =item C<--jobs N> Parallelizes the processing N-ways. C<-j> is a synonym. This is just like GNU make, except you're required to explicitly specify a job count. The images are given as (multiple) globs. The output is a vnlog with columns C,C,C. All filenames matched in the glob will appear in the output. Images for which no chessboard pattern was found appear as a single record with null C and C. =item C<--debug> If given, C will dump various intermediate results into C and it will report more stuff on the console. The output is self-documenting =item C<--blobs> Find circle centers instead of chessboard corners. Not recommended =back =head1 REPOSITORY L =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) =cut mrgingham-1.20/mrgingham_pywrap.c000066400000000000000000000257321414230437100171340ustar00rootroot00000000000000#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(function_prefix, name, args) {#name, \ (PyCFunction)function_prefix ## name, \ args, \ function_prefix ## 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 reponse"); 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 PyObject* find_chessboard_corners(PyObject* NPY_UNUSED(self), PyObject* args, PyObject* kwargs) { PyArrayObject* image = NULL; PyObject* result = NULL; int image_pyramid_level = 0; SET_SIGINT(); char* keywords[] = { "image", "image_pyramid_level", NULL }; if(!PyArg_ParseTupleAndKeywords( args, kwargs, "O&|i", keywords, PyArray_Converter, &image, &image_pyramid_level, NULL)) 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; } bool add_points(int* xy, int N, double scale) { 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; } 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, &add_points) ) { 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 PyObject* find_chessboard(PyObject* NPY_UNUSED(self), PyObject* args, PyObject* kwargs) { PyArrayObject* image = NULL; PyObject* result = NULL; int image_pyramid_level = -1; int gridn = 10; SET_SIGINT(); char* keywords[] = { "image", "image_pyramid_level", "gridn", NULL }; if(!PyArg_ParseTupleAndKeywords( args, kwargs, "O&|ii", keywords, PyArray_Converter, &image, &image_pyramid_level, &gridn, NULL)) 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; } bool add_points(double* xy, int N) { 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*sizeof(double)); return true; } 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, &add_points) ) { // 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 py_ChESS_response_5_docstring[] = #include "ChESS_response_5.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(py_,ChESS_response_5, METH_VARARGS), PYMETHODDEF_ENTRY(, find_chessboard_corners, METH_VARARGS | METH_KEYWORDS), PYMETHODDEF_ENTRY(, find_chessboard, 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.20/mrgingham_pywrap_cplusplus_bridge.cc000066400000000000000000000106561414230437100227240ustar00rootroot00000000000000#include "find_chessboard_corners.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 int image_pyramid_level, bool (*add_points)(int* xy, int N, double scale) ) { cv::Mat cvimage(Nrows, Ncols, CV_8UC1, imagebuffer, stride); std::vector out_points; bool result = find_chessboard_corners_from_image_array( &out_points, cvimage, image_pyramid_level ); 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); } 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 (*add_points)(double* xy, int N) ) { cv::Mat cvimage(Nrows, Ncols, CV_8UC1, imagebuffer, stride); std::vector out_points; signed char* refinement_level = NULL; bool result = (find_chessboard_from_image_array( out_points, &refinement_level, gridn, cvimage, image_pyramid_level ) >= 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() ); } mrgingham-1.20/mrgingham_pywrap_cplusplus_bridge.h000066400000000000000000000026201414230437100225560ustar00rootroot00000000000000#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 int image_pyramid_level, bool (*add_pointss)(int* xy, int N, double scale) ); 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 (*add_points)(double* xy, int N) ); #ifdef __cplusplus } #endif mrgingham-1.20/packaging/000077500000000000000000000000001414230437100153305ustar00rootroot00000000000000mrgingham-1.20/packaging/README000066400000000000000000000004151414230437100162100ustar00rootroot00000000000000These 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.20/packaging/mrgingham.spec000066400000000000000000000023201414230437100201520ustar00rootroot00000000000000Name: mrgingham Version: 1.20 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: opencv-devel >= 3.2 BuildRequires: boost-devel BuildRequires: chrpath # 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 %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.20/point.hh000066400000000000000000000004111414230437100150520ustar00rootroot00000000000000#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.20/test-dump-blobs.cc000066400000000000000000000005271414230437100167400ustar00rootroot00000000000000#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.20/test-dump-chessboard-corners.cc000066400000000000000000000066151414230437100214310ustar00rootroot00000000000000#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.20/test-find-grid-from-points.cc000066400000000000000000000054041414230437100210110ustar00rootroot00000000000000#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; }