pax_global_header00006660000000000000000000000064131746516220014521gustar00rootroot0000000000000052 comment=8991c9d2c2da3c02722f6ce58c6997c1da0366cb uprightdiff-1.3.0/000077500000000000000000000000001317465162200140355ustar00rootroot00000000000000uprightdiff-1.3.0/.gitignore000066400000000000000000000002311317465162200160210ustar00rootroot00000000000000/uprightdiff /test *.o *.log /.deps /Makefile /Makefile.in /aclocal.m4 /autom4te.cache /compile /config.status /configure /depcomp /install-sh /missing uprightdiff-1.3.0/.gitreview000066400000000000000000000001521317465162200160410ustar00rootroot00000000000000[gerrit] host=gerrit.wikimedia.org port=29418 project=integration/uprightdiff.git defaultrebase=0 track=1 uprightdiff-1.3.0/BlockMotionSearch.cpp000066400000000000000000000046671317465162200201240ustar00rootroot00000000000000#include #include "BlockMotionSearch.h" #include "OutwardAlternatingSearch.h" BlockMotionSearch::Mat1i BlockMotionSearch::search() { int yBlockCount = m_source.rows / m_blockSize; int xBlockCount = m_source.cols / m_blockSize; m_blockMotion = Mat1i(yBlockCount, xBlockCount); for (m_yIndex = 0; m_yIndex < yBlockCount; m_yIndex++) { m_y = m_yIndex * m_blockSize; for (m_xIndex = 0; m_xIndex < xBlockCount; m_xIndex++) { m_x = m_xIndex * m_blockSize; cv::Rect sourceRect(m_x, m_y, m_blockSize, m_blockSize); Mat3b sourceBlock = m_source(sourceRect); // Priority 1: exactly constant baseline if (m_xIndex > 0 && m_blockMotion(m_yIndex, m_xIndex - 1) != NOT_FOUND) { if (tryMotion(sourceBlock, m_blockMotion(m_yIndex, m_xIndex - 1))) { continue; } } int searchStart; if (m_yIndex > 0 && m_blockMotion(m_yIndex - 1, m_xIndex) != NOT_FOUND) { // Priority 2: near-constant vertical flow searchStart = m_y + m_blockMotion(m_yIndex - 1, m_xIndex); } else if (m_xIndex > 0 && m_blockMotion(m_yIndex, m_xIndex - 1) != NOT_FOUND) { // Priority 3: near-constant baseline searchStart = m_y + m_blockMotion(m_yIndex, m_xIndex - 1); } else { // Priority 4: source offset searchStart = m_y; } // Check bounds of searchStart if (searchStart > m_dest.rows - m_blockSize) { searchStart = m_dest.rows - m_blockSize; } if (searchStart < 0) { searchStart = 0; } // Make sure the search window includes the no-change case int tempWindowSize = std::max(std::abs(searchStart - m_y), m_windowSize); OutwardAlternatingSearch search(searchStart, m_dest.rows - m_blockSize + 1, tempWindowSize); m_blockMotion(m_yIndex, m_xIndex) = NOT_FOUND; for (; search; ++search) { if (tryMotion(sourceBlock, search.pos() - m_y)) { break; } } } } return m_blockMotion; } bool BlockMotionSearch::tryMotion(const Mat3b & sourceBlock, int dy) { cv::Rect destRect(m_x, m_y + dy, m_blockSize, m_blockSize); Mat3b destBlock = m_dest(destRect); if (blockEqual(sourceBlock, destBlock)) { m_blockMotion(m_yIndex, m_xIndex) = dy; return true; } else { return false; } } bool BlockMotionSearch::blockEqual(const Mat3b & m1, const Mat3b & m2) { if (m1.size() != m2.size()) { return false; } int rowSize = m1.cols * m1.elemSize(); for (int y = 0; y < m1.rows; y++) { if (std::memcmp(m1.ptr(y), m2.ptr(y), rowSize) != 0) { return false; } } return true; } uprightdiff-1.3.0/BlockMotionSearch.h000066400000000000000000000014751317465162200175630ustar00rootroot00000000000000#include class BlockMotionSearch { public: typedef cv::Mat_ Mat3b; typedef cv::Mat_ Mat1i; enum {NOT_FOUND = 0x7fffffff}; static Mat1i Search(const Mat3b & alice, const Mat3b & bob, int blockSize, int windowSize) { BlockMotionSearch obj(alice, bob, blockSize, windowSize); return obj.search(); } private: BlockMotionSearch(const Mat3b & alice, const Mat3b & bob, int blockSize, int windowSize) : m_source(alice), m_dest(bob), m_blockSize(blockSize), m_windowSize(windowSize) {} Mat1i search(); bool tryMotion(const Mat3b & sourceBlock, int dy); bool blockEqual(const Mat3b & m1, const Mat3b & m2); const Mat3b & m_source; const Mat3b & m_dest; Mat1i m_blockMotion; const int m_blockSize; const int m_windowSize; int m_xIndex, m_yIndex, m_x, m_y; }; uprightdiff-1.3.0/LICENSE000066400000000000000000000045421317465162200150470ustar00rootroot00000000000000UprightDiff was written by Tim Starling. Since a small amount of code was copied from OpenCV, UprightDiff is available under the same license as OpenCV. The license is below. By downloading, copying, installing or using the software you agree to this license. If you do not agree to this license, do not download, install, copy or use the software. License Agreement For Open Source Computer Vision Library (3-clause BSD License) Copyright (C) 2000-2015, Intel Corporation, all rights reserved. Copyright (C) 2009-2011, Willow Garage Inc., all rights reserved. Copyright (C) 2009-2015, NVIDIA Corporation, all rights reserved. Copyright (C) 2010-2013, Advanced Micro Devices, Inc., all rights reserved. Copyright (C) 2015, OpenCV Foundation, all rights reserved. Copyright (C) 2015, Itseez Inc., all rights reserved. Third party copyrights are property of their respective owners. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the names of the copyright holders nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission. This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall copyright holders or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. uprightdiff-1.3.0/Logger.h000066400000000000000000000023361317465162200154310ustar00rootroot00000000000000#include #include #include #include class Logger { public: Logger(std::ostream & backend, int level, bool showTimestamp = false) : m_level(level), m_showTimestamp(showTimestamp), m_realStream(backend, true), m_devNull(backend, false) {} enum {TRACE, DEBUG, INFO, WARNING, ERROR, FATAL}; class LogStream { friend class Logger; LogStream(std::ostream & backend, bool enabled) : m_backend(backend), m_enabled(enabled) {} std::ostream & m_backend; bool m_enabled; public: template LogStream & operator<<(const T & x) { if (m_enabled) { m_backend << x; } return *this; } }; LogStream & log(int level) { if (level >= m_level) { if (m_showTimestamp) { m_realStream << timestamp(); } return m_realStream; } else { return m_devNull; } } std::string timestamp() { std::clock_t now = clock(); std::clock_t totalMillis = now / (CLOCKS_PER_SEC / 1000); std::ostringstream buf; buf << std::setw(3) << (totalMillis / 1000) << "." << std::setfill('0') << std::setw(3) << (totalMillis % 1000) << " "; return buf.str(); } private: int m_level; bool m_showTimestamp; LogStream m_realStream; LogStream m_devNull; }; uprightdiff-1.3.0/Makefile.am000066400000000000000000000004131317465162200160670ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in AUTOMAKE_OPTIONS = foreign bin_PROGRAMS = uprightdiff uprightdiff_SOURCES = main.cpp BlockMotionSearch.cpp UprightDiff.cpp test: g++ $(CFLAGS) tests/RollingBlockCounterTest.cpp -lopencv_core -o test ./test uprightdiff-1.3.0/OutwardAlternatingSearch.h000066400000000000000000000017611317465162200211570ustar00rootroot00000000000000 class OutwardAlternatingSearch { public: OutwardAlternatingSearch(int middle, int height, int window) : m_middle(middle), m_height(height), m_window(window), m_negative(true), m_distance(0), m_done(false) {} const OutwardAlternatingSearch & operator++() { if (m_negative) { m_distance++; if (m_middle + m_distance < m_height) { m_negative = false; } else if (m_middle - m_distance < 0) { m_done = true; } } else { if (m_middle - m_distance < 0) { m_distance++; if (m_middle + m_distance >= m_height) { m_done = true; } } else { m_negative = true; } } if (m_distance > m_window) { m_done = true; } return *this; } operator bool() { return !m_done; } int offset() const { return m_negative ? - m_distance : m_distance; } int pos() const { return m_middle + offset(); } private: int m_middle; int m_height; int m_window; bool m_negative; int m_distance; bool m_done; }; uprightdiff-1.3.0/README.md000066400000000000000000000130141317465162200153130ustar00rootroot00000000000000This utility examines the differences between two images. It produces a visual annotation and reports statistics. It is optimised for images which come from browser screenshots. It analyses the image for vertical motion, and annotates connected regions that have the same vertical displacement. Then it highlights any remaining ("residual") differences which are not explained by vertical motion on a pixel-by-pixel basis. ## Usage ``` ./uprightdiff [options] Accepted options are: --help Show help message and exit --block-size arg Block size for initial search (default 16) --window-size arg Initial range for vertical motion detection (default 200) --brush-width arg Brush width when heuristically expanding blocks. A higher value gives smoother motion regions. This should be an odd number. (default 9) --outer-hl-window arg The size of the outer square used for detecting isolated small features to highlight. This size defines what we mean by "isolated". It should be an odd number. (default 21) --inner-hl-window arg The size of the inner square used for detecting isolated small features to highlight. This size defines what we mean by "small". It should be an odd number. (default 5) --intermediate-dir arg A directory where intermediate images should be placed. This is our equivalent of debug or trace output. -v [ --verbose ] Write progress info to stderr. --format arg The output format for statistics, may be text (the default), json or none. -t [ --log-timestamp ] Annotate progress info with elapsed time. ``` If you see an error "libdc1394 error: Failed to initialize libdc1394", this can be ignored. OpenCV links to libdc1394 unconditionally, and libdc1394 unconditionally writes out this error on startup if it does not find any cameras, but this utility does not make use of any camera functions. This is fixed in OpenCV 3.x (which is not released yet): libdc1394 is now optional. ## Algorithm description Motion is detected by first doing an exhaustive search for motion of blocks, 16x16 pixels by default. The block size should be large enough so that a single block by itself contains identifying features, but not so large that regions of motion will be missed. This usually means that it should be similar to the font size. Unlike similar algorithms used by video compression or robotics, we require an exact match for motion search to succeed. Then, starting from the block search results, regions with known motion are expanded into regions of unknown motion. This is done at the full resolution, but with a broad "brush size", defaulting to 9px, which defines a minimum region of action. Using a brush size which is similar to or larger than the font size prevents motion regions from closely hugging the text. Connected regions in the resulting optical flow map are identified by a flood fill algorithm. If a region has an area of at least 50px, it is shown in the annotation as a contour outline, with a labelled arrow showing the direction and magnitude of motion. These labels use a rotating palette of bluish colours. Regions smaller than 50px are simply filled with the bluish colour. A model of the first image is created, with motion applied. Regions where the second image matches the moved image are shown in grey, half faded to white. Residuals are shown by putting the intensity of the moved model in the red channel, and the intensity of the second image in the green channel. If the motion search algorithm failed for a given pixel, then the first image is used, instead of the moved image. Thus, residual colours appear as follows: | First | Second | Result |-------|--------|------- | Dark | Dark | Dark | Dark | Light | Green | Light | Dark | Red | Light | Light | Yellow Since it is hard to spot isolated residual pixels by eye, we finally run a search for isolated residual features, and put a yellow circle around any that are found. An isolated feature is defined as a location where all of the residual pixels in a large outer square are also within a smaller inner square at its centre. The following statistics are provided: * modifiedArea: This is a simple count of the number of pixels for which the source does not match the destination (after they have both been expanded to the same size). * movedArea: The number of pixels for which nonzero motion was detected. * residualArea: The number of pixels which differed between the resulting image and the second input image. By specifying --format=json, these statistics are provided on stdout JSON format, e.g.: {"modifiedArea":5045596,"movedArea":6081096,"residualArea":78707} ## Compilation Install the dependencies. On Debian/Ubuntu this means: `sudo apt-get install build-essential g++ libopencv-highgui-dev libboost-program-options-dev` On Mac OS X with homebrew: `brew install opencv boost` Then compile: ``` autoreconf --verbose --install --symlink ./configure make ``` And optionally install it: `make install PREFIX=/usr/local` All dependencies (C++11, Boost and OpenCV) are cross-platform, and the code is intended to be portable, so it should be possible to compile it on other operating systems if necessary. However, a custom makefile will be required. uprightdiff-1.3.0/RollingBlockCounter.h000066400000000000000000000035561317465162200201400ustar00rootroot00000000000000#include template class RollingBlockCounter { public: RollingBlockCounter(const Mat & mat, int centreX, int window); void purge() { m_valid = false; } int operator ()(int centreY); private: inline static cv::Rect GetBounds(int width, int height, int centreX, int window); int m_halfWindow; Mat m_strip; int m_cy; bool m_valid; int m_count; }; template RollingBlockCounter::RollingBlockCounter(const Mat & mat, int cx, int window) : m_halfWindow((window - 1) / 2), m_strip(mat, GetBounds(mat.cols, mat.rows, cx, window)), m_cy(0), m_valid(false), m_count(0) {} template cv::Rect RollingBlockCounter::GetBounds(int width, int height, int cx, int window) { int halfWindow = (window - 1) / 2; int left = std::max(cx - halfWindow, 0); int right = std::min(cx + halfWindow + 1, width); return cv::Rect(left, 0, right - left, height); } template int RollingBlockCounter::operator ()(int cy) { int delta = 0; if (m_valid && cy == m_cy) { // No update needed } else if (m_valid && cy == m_cy + 1) { // Incremental calculation // Subtract top row (if any) int topY = cy - m_halfWindow - 1; if (topY >= 0) { auto * row = m_strip[topY]; for (int x = 0; x < m_strip.cols; x++) { delta -= row[x]; } } // Add bottom row int bottomY = cy + m_halfWindow; if (bottomY < m_strip.rows) { auto * row = m_strip[bottomY]; for (int x = 0; x < m_strip.cols; x++) { delta += row[x]; } } m_count += delta; } else { // Calculate from scratch int topY = std::max(cy - m_halfWindow, 0); int bottomY = std::min(cy + m_halfWindow, m_strip.rows - 1); for (int y = topY; y <= bottomY; y++) { auto * row = m_strip[y]; for (int x = 0; x < m_strip.cols; x++) { delta += row[x]; } } m_count = delta; } m_valid = true; m_cy = cy; return m_count; } uprightdiff-1.3.0/UprightDiff.cpp000066400000000000000000000334761317465162200167710ustar00rootroot00000000000000#include #include #include #include #include #include #include "UprightDiff.h" #include "BlockMotionSearch.h" #include "RollingBlockCounter.h" typedef UprightDiff::uchar uchar; typedef UprightDiff::Mat3b Mat3b; typedef UprightDiff::Mat1i Mat1i; typedef UprightDiff::Mat1b Mat1b; void UprightDiff::Diff(const cv::Mat & alice, const cv::Mat & bob, const Options & options, Output & output) { UprightDiff uprightDiff(alice, bob, options, output); uprightDiff.execute(); uprightDiff.m_alice.release(); uprightDiff.m_bob.release(); uprightDiff.m_motion.release(); } UprightDiff::UprightDiff( const cv::Mat & alice, const cv::Mat & bob, const Options & options, Output & output) : m_options(options), m_output(output), m_logger(options.logStream ? *options.logStream : std::cerr, options.logLevel, options.logTimestamp) { m_size = cv::Size( std::max(alice.cols, bob.cols), std::max(alice.rows, bob.rows)); info() << "Extending both images to size " << m_size.width << "x" << m_size.height << "\n"; m_alice = ConvertInput("first", alice, m_size); m_bob = ConvertInput("second", bob, m_size); } void UprightDiff::execute() { m_output.totalArea = m_size.area(); calculateMaskArea(); // Calculate block motion by exhaustive search info() << "Searching for motion...\n"; Mat1i blockMotion = BlockMotionSearch::Search(m_bob, m_alice, m_options.blockSize, m_options.windowSize); // Scale up block motion matrix m_motion = ScaleUpMotion(blockMotion, m_options.blockSize, m_size); intermediateOutput("prepaint", m_motion); info() << "Expanding motion blocks\n"; // Expand block motion into sub-block NOT_FOUND regions for (int y = 0; y < m_size.height; y++) { // Paint right paintSubBlockLine(cv::Point(0, y), cv::Point(1, 0)); // Paint left paintSubBlockLine(cv::Point(m_size.width - 1, y), cv::Point(-1, 0)); } for (int x = 0; x < m_size.width; x++) { // Paint down paintSubBlockLine(cv::Point(x, 0), cv::Point(0, 1)); // Paint up paintSubBlockLine(cv::Point(x, m_size.height - 1), cv::Point(0, -1)); } intermediateOutput("postpaint", m_motion); info() << "Calculating residuals\n"; visualizeResidual(); info() << "Annotating motion\n"; // Draw motion annotations annotateMotion(); info() << "Done\n"; } Mat3b UprightDiff::ConvertInput(const char * label, const cv::Mat & input, const cv::Size & size) { if (input.type() != CV_8UC3) { throw std::runtime_error(std::string("The ") + label + " image is invalid or has the wrong pixel type\n"); } Mat3b ret(size, cv::Vec3b(128, 128, 128)); input.copyTo(ret(cv::Rect(cv::Point(), input.size()))); return ret; } void UprightDiff::calculateMaskArea() { Mat1b mask(m_size, 0); for (int y = 0; y < m_size.height; y++) { for (int x = 0; x < m_size.width; x++) { if (m_alice(y, x) != m_bob(y, x)) { mask(y, x) = 255; } } } intermediateOutput("mask", mask); m_output.maskArea = cv::countNonZero(mask); } Mat1i UprightDiff::ScaleUpMotion(Mat1i & blockMotion, int blockSize, const cv::Size & destSize) { Mat1i motion(destSize); Mat1i notFound(1, 1, NOT_FOUND); int x, y, xIndex, yIndex; for (y = 0, yIndex = 0; yIndex < blockMotion.rows; yIndex++, y += blockSize) { for (x = 0, xIndex = 0; xIndex < blockMotion.cols; xIndex++, x += blockSize) { cv::Rect sourceRect(xIndex, yIndex, 1, 1); cv::Rect destRect(xIndex * blockSize, yIndex * blockSize, blockSize, blockSize); cv::repeat(blockMotion(sourceRect), blockSize, blockSize, motion(destRect)); } cv::Rect edgeRect(x, y, destSize.width - x, blockSize); if (edgeRect.width > 0) { cv::repeat(notFound, edgeRect.height, edgeRect.width, motion(edgeRect)); } } { cv::Rect edgeRect(0, y, destSize.width, destSize.height - y); if (edgeRect.height > 0) { cv::repeat(notFound, edgeRect.height, edgeRect.width, motion(edgeRect)); } } return motion; } void UprightDiff::paintSubBlockLine(const cv::Point & start, const cv::Point & step) { int halfWidth = (m_options.brushWidth - 1) / 2; cv::Point brushStep(step.y, step.x); cv::Point halfWidthVector = halfWidth * brushStep; cv::Point pos = start; cv::Rect bounds(cv::Point(), m_motion.size()); int prevConsensus = NOT_FOUND; while (bounds.contains(pos)) { cv::Rect roiRect(pos - halfWidthVector, pos + halfWidthVector + cv::Point(1, 1)); if ((roiRect & bounds) != roiRect) { break; } cv::Mat1i roiBlock = m_motion(roiRect); // Paint the current step if (prevConsensus != NOT_FOUND && prevConsensus != INVALID) { int curConsensus = GetWeakConsensus(roiBlock); if (curConsensus == NOT_FOUND || curConsensus == prevConsensus) { for (int b = -halfWidth; b <= halfWidth; b++) { cv::Point srcPos = pos + b * brushStep; cv::Point destPos = srcPos + cv::Point(0, prevConsensus); if (bounds.contains(destPos) && m_bob(srcPos) == m_alice(destPos)) { m_motion.at(srcPos) = prevConsensus; } } } } prevConsensus = GetStrongConsensus(roiBlock); GetStrongConsensus(roiBlock); pos += step; } } uchar UprightDiff::BgrToGrey(const cv::Vec3b & bgr) { return cv::saturate_cast( 76 * bgr[2] / 255 // Blue + 150 * bgr[1] / 255 // Green + 29 * bgr[0] / 255); // Red } cv::Vec3b UprightDiff::BgrToFadedGreyBgr(const cv::Vec3b & bgr) { uchar value = 127 + BgrToGrey(bgr) / 2; return cv::Vec3b(value, value, value); } /** * Get the value of all elements in the block, or INVALID if they are not all * the same. */ int UprightDiff::GetStrongConsensus(const cv::Mat1i & block) { int consensus = block(0, 0); for (int y = 0; y < block.rows; y++) { for (int x = 0; x < block.cols; x++) { if (block(y, x) != consensus) { return INVALID; } } } return consensus; } /** * Get the value of all elements of the block, or INVALID if they are not all * the same, except for NOT_FOUND elements which are ignored. If all elements * are NOT_FOUND, NOT_FOUND is returned. */ int UprightDiff::GetWeakConsensus(const cv::Mat1i & block) { int consensus = NOT_FOUND; for (int y = 0; y < block.rows; y++) { for (int x = 0; x < block.cols; x++) { int v = block(y, x); if (v != consensus && v != NOT_FOUND) { return INVALID; } if (v != NOT_FOUND) { consensus = v; } } } return consensus; } Mat3b UprightDiff::visualizeResidual() { // Prepare moved image Mat3b moved(m_size, cv::Vec3b(255, 0, 255)); m_output.movedArea = 0; for (int y = 0; y < m_size.height; y++) { for (int x = 0; x < m_size.width; x++) { int dy = m_motion(y, x); if (dy != NOT_FOUND) { if (dy != 0) { m_output.movedArea++; } if (y + dy >= moved.rows || y + dy < 0) { throw std::runtime_error( "Error: out of bounds: (" + std::to_string(x) + ", " + std::to_string(y) + " + " + std::to_string(dy) + ")\n"); } moved(y, x) = m_alice(y + dy, x); } } } intermediateOutput("moved", moved); // Compute residual visualisation m_output.visual = Mat3b(m_size, cv::Vec3b(128, 128, 128)); Mat3b & visual = m_output.visual; m_output.residualArea = 0; Mat1b residualMask(m_size, uchar(0)); for (int y = 0; y < m_size.height; y++) { for (int x = 0; x < m_size.width; x++) { if (moved(y, x) == m_bob(y, x)) { visual(y, x) = BgrToFadedGreyBgr(moved(y, x)); } else if (m_motion(y, x) == NOT_FOUND) { cv::Vec3b ac = m_alice(y, x); cv::Vec3b bc = m_bob(y, x); if (ac == bc) { visual(y, x) = BgrToFadedGreyBgr(ac); } else { visual(y, x) = cv::Vec3b(0, BgrToGrey(bc), BgrToGrey(ac)); m_output.residualArea ++; residualMask(y, x) = 1; } } else { cv::Vec3b mc = moved(y, x); cv::Vec3b bc = m_bob(y, x); visual(y, x) = cv::Vec3b(0, BgrToGrey(bc), BgrToGrey(mc)); m_output.residualArea ++; residualMask(y, x) = 1; } } } intermediateOutput("residual-mask", residualMask); intermediateOutput("plain-residual", visual); // Highlight isolated residual pixels // This is done by maintaining a count of the number of residual pixels in // two concentric blocks. As the block moves, we subtract the row that left // the block, and add the row that entered the block. This is done in // column-major order so that the rows being added or subtracted are // contiguous in memory. int ihw = m_options.innerHighlightWindow; int ihw2 = (ihw - 1) / 2; int ohw = m_options.outerHighlightWindow; for (int cx = 0; cx < m_size.width; cx++) { RollingBlockCounter innerCounter(residualMask, cx, ihw); RollingBlockCounter outerCounter(residualMask, cx, ohw); for (int cy = 0; cy < m_size.height; cy++) { int innerCount = innerCounter(cy); int outerCount = outerCounter(cy); if (innerCount != 0 && innerCount == outerCount) { cv::circle(visual, cv::Point(cx, cy), std::min(10, ihw * 2), cv::Scalar(0, 0xff, 0xff), 2); cv::Rect innerRect( cv::Point( std::max(cx - ihw2, 0), std::max(cy - ihw2, 0) ), cv::Point( std::min(cx + ihw2 + 1, m_size.width), std::min(cy + ihw2 + 1, m_size.height) ) ); residualMask(innerRect) = 0; innerCounter.purge(); outerCounter.purge(); } } } intermediateOutput("circled-residual", visual); return visual; } void UprightDiff::annotateMotion() { Mat3b contourVis(m_output.visual.size(), cv::Vec3b()); std::vector palette; palette.push_back(cv::Scalar(0xff, 0x00, 0x00)); palette.push_back(cv::Scalar(0xff, 0x80, 0x00)); palette.push_back(cv::Scalar(0xff, 0x00, 0x80)); int paletteIndex = 0; // Find motion regions by flood filling Mat1i motion(cv::Size(m_motion.cols + 2, m_motion.rows + 2), NOT_FOUND); m_motion.copyTo(motion(cv::Rect(cv::Point(1, 1), m_motion.size()))); int regionIndex = 0; Mat1b mask(cv::Size(motion.cols + 2, motion.rows + 2), uchar(0)); const int minArea = 50; for (int y = 1; y < motion.rows - 1; y++) { for (int x = 1; x < motion.cols - 1; x++) { if (mask(y + 1, x + 1)) { continue; } int currentMotion = motion(y, x); if (currentMotion == 0 || currentMotion == NOT_FOUND) { continue; } int area = floodFill( motion, mask, cv::Point(x, y), // seedPoint cv::Scalar(), // newVal nullptr, // rect cv::Scalar(), // loDiff cv::Scalar(), // upDiff 4 // connectivity | (2 << 8) // mask value | cv::FLOODFILL_MASK_ONLY ); Mat1b currentMask = (mask == 2); if (area < minArea) { // Too small for contour, fill instead contourVis.setTo(palette[paletteIndex], currentMask(cv::Rect(cv::Point(2, 2), mask.size() - cv::Size(4, 4)))); paletteIndex = (paletteIndex + 1) % palette.size(); } else { // Draw arrow cv::Point centrePoint = FindMaskCentre(mask, area); cv::Scalar colour = palette[regionIndex % palette.size()]; ArrowedLine(contourVis, centrePoint + cv::Point(0, currentMotion), centrePoint, colour); // Draw arrow label std::string text = std::to_string(std::abs(currentMotion)); cv::Size textSize = cv::getTextSize(text, cv::FONT_HERSHEY_PLAIN, 1, 1, nullptr); cv::putText(contourVis, text, centrePoint + cv::Point(2, currentMotion / 2 + textSize.height / 2), cv::FONT_HERSHEY_PLAIN, 1, colour); // Find and draw contours std::vector> contours; findContours(currentMask, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE); drawContours(contourVis, contours, -1, colour, 1, 8, cv::noArray(), INT_MAX, cv::Point(-2, -2)); regionIndex++; } // Mark done areas mask.setTo(1, currentMask); } } // Blend with destination Mat3b & visual = m_output.visual; for (int y = 0; y < visual.rows; y++) { for (int x = 0; x < visual.cols; x++) { if (contourVis(y, x) != cv::Vec3b()) { visual(y, x) = visual(y, x) / 2 + contourVis(y, x) / 2; } } } } cv::Point UprightDiff::FindMaskCentre(const Mat1b & mask, int totalArea) { int sumX = 0, sumY = 0; for (int y = 0; y < mask.rows; y++) { for (int x = 0; x < mask.cols; x++) { if (mask(y, x) == 2) { sumX += x; sumY += y; } } } return cv::Point(sumX / totalArea, sumY / totalArea); } /** * Draw an arrowed line, similar to cv::arrowedLine() */ void UprightDiff::ArrowedLine(Mat3b img, cv::Point pt1, cv::Point pt2, const cv::Scalar& color, int thickness, int line_type, int shift, double tipLength) { // Factor to normalize the size of the tip depending on the length of the arrow const double tipSize = std::max(3.0, cv::norm(pt1-pt2)*tipLength); cv::line(img, pt1, pt2, color, thickness, line_type, shift); const double angle = atan2( (double) pt1.y - pt2.y, (double) pt1.x - pt2.x ); cv::Point p(cvRound(pt2.x + tipSize * std::cos(angle + CV_PI / 4)), cvRound(pt2.y + tipSize * std::sin(angle + CV_PI / 4))); cv::line(img, p, pt2, color, thickness, line_type, shift); p.x = cvRound(pt2.x + tipSize * std::cos(angle - CV_PI / 4)); p.y = cvRound(pt2.y + tipSize * std::sin(angle - CV_PI / 4)); cv::line(img, p, pt2, color, thickness, line_type, shift); } void UprightDiff::intermediateOutput(const char* label, const cv::MatExpr & expr) { if (!m_options.intermediateDir.empty()) { intermediateOutput(label, cv::Mat(expr)); } } cv::Mat UprightDiff::convertIntermediate(const cv::Mat & m) { if (m.type() != CV_32S) { return m; } Mat3b out(m.size()); for (int y = 0; y < m.rows; y++) { for (int x = 0; x < m.cols; x++) { int dy = m.at(y, x); cv::Vec3b color; if (dy == NOT_FOUND) { color = cv::Vec3b(255, 255, 255); } else { if (dy < -127) { dy = -127; } else if (dy > 127) { dy = 127; } color = cv::Vec3b(128 + dy, 0, 128 - dy); } out(y, x) = color; } } return out; } void UprightDiff::intermediateOutput(const char* label, const cv::Mat & m) { if (m_options.intermediateDir.empty()) { return; } cv::Mat out = convertIntermediate(m); cv::imwrite(m_options.intermediateDir + "/" + label + ".png", out); } uprightdiff-1.3.0/UprightDiff.h000066400000000000000000000041421317465162200164220ustar00rootroot00000000000000#include #include #include "Logger.h" class UprightDiff { public: typedef unsigned char uchar; typedef cv::Mat_ Mat3b; typedef cv::Mat_ Mat1i; typedef cv::Mat_ Mat1b; struct Options { int blockSize = 16; int windowSize = 200; int brushWidth = 9; int outerHighlightWindow = 21; int innerHighlightWindow = 5; std::string intermediateDir; std::ostream * logStream = nullptr; int logLevel = Logger::FATAL; bool logTimestamp = false; }; struct Output { int totalArea = 0; int maskArea = 0; int movedArea = 0; int residualArea = 0; Mat3b visual; }; enum { NOT_FOUND = std::numeric_limits::max(), INVALID = NOT_FOUND - 1 }; static void Diff(const cv::Mat & alice, const cv::Mat & bob, const Options & options, Output & output); private: UprightDiff(const cv::Mat & alice, const cv::Mat & bob, const Options & options, Output & output); void execute(); void calculateMaskArea(); static Mat3b ConvertInput(const char * label, const cv::Mat & input, const cv::Size & size); static Mat1i ScaleUpMotion(Mat1i & blockMotion, int blockSize, const cv::Size & destSize); void paintSubBlockLine(const cv::Point & start, const cv::Point & step); static uchar BgrToGrey(const cv::Vec3b & bgr); static cv::Vec3b BgrToFadedGreyBgr(const cv::Vec3b & bgr); static int GetStrongConsensus(const cv::Mat1i & block); static int GetWeakConsensus(const cv::Mat1i & block); Mat3b visualizeResidual(); void annotateMotion(); static cv::Point FindMaskCentre(const Mat1b & mask, int totalArea); static void ArrowedLine(Mat3b img, cv::Point pt1, cv::Point pt2, const cv::Scalar& color, int thickness = 1, int line_type = 8, int shift = 0, double tipLength = 0.1); cv::Mat convertIntermediate(const cv::Mat & m); void intermediateOutput(const char* label, const cv::MatExpr & expr); void intermediateOutput(const char* label, const cv::Mat & m); Logger::LogStream & info() { return m_logger.log(Logger::INFO); } const Options & m_options; Output & m_output; Mat3b m_alice; Mat3b m_bob; Mat1i m_motion; cv::Size m_size; Logger m_logger; }; uprightdiff-1.3.0/configure.ac000066400000000000000000000021241317465162200163220ustar00rootroot00000000000000# -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ([2.69]) AC_INIT([uprightdiff], [1.3.0], [https://phabricator.wikimedia.org/tag/uprightdiff/]) AM_INIT_AUTOMAKE EXTRA_CFLAGS=[-g -std=c++11 -Wall -O2] # Checks for programs. AC_PROG_CXX AC_PROG_CC AC_LANG_PUSH([C++]) # Checks for libraries. # FIXME: Replace `main' with a function in `-lboost_program_options': AC_CHECK_LIB([boost_program_options], [main]) # FIXME: Replace `main' with a function in `-lopencv_core': AC_CHECK_LIB([opencv_core], [main]) # FIXME: Replace `main' with a function in `-lopencv_highgui': AC_CHECK_LIB([opencv_highgui], [main]) # FIXME: Replace `main' with a function in `-lopencv_imgproc': AC_CHECK_LIB([opencv_imgproc], [main]) AC_SEARCH_LIBS([_ZN2cv7imwriteERKNS_6StringERKNS_11_InputArrayERKSt6vectorIiSaIiEE], [opencv_imgcodecs]) # Checks for header files. # Checks for typedefs, structures, and compiler characteristics. AC_CHECK_HEADER_STDBOOL AC_C_INLINE # Checks for library functions. AC_CONFIG_FILES([Makefile]) AC_OUTPUT uprightdiff-1.3.0/main.cpp000066400000000000000000000112401317465162200154630ustar00rootroot00000000000000#include #include #include #include #include "UprightDiff.h" namespace po = boost::program_options; struct MainOptions { enum { NONE, TEXT, JSON } format = TEXT; std::string aliceName; std::string bobName; std::string destName; }; bool processCommandLine(int argc, char** argv, MainOptions & mainOptions, UprightDiff::Options & diffOptions); int main(int argc, char** argv) { MainOptions mainOptions; UprightDiff::Options diffOptions; try { if (!processCommandLine(argc, argv, mainOptions, diffOptions)) { return 1; } } catch (po::error & e) { std::cerr << "Error: " << e.what() << "\n"; return 1; } cv::Mat alice = cv::imread(mainOptions.aliceName); cv::Mat bob = cv::imread(mainOptions.bobName); UprightDiff::Output output; try { UprightDiff::Diff(alice, bob, diffOptions, output); } catch (std::runtime_error & e) { std::cerr << "Error: " << e.what() << "\n"; return 1; } cv::imwrite(mainOptions.destName, output.visual); if (mainOptions.format == MainOptions::TEXT) { std::cout << "Total area: " << output.totalArea << " pixels\n"; std::cout << "Modified area: " << output.maskArea << " pixels\n"; std::cout << "Moved area: " << output.movedArea << " pixels\n"; std::cout << "Residual area: " << output.residualArea << " pixels\n"; } else if (mainOptions.format == MainOptions::JSON) { std::cout << "{\"totalArea\":" << output.totalArea << "," << "\"modifiedArea\":" << output.maskArea << "," << "\"movedArea\":" << output.movedArea << "," << "\"residualArea\":" << output.residualArea << "}\n"; } } bool processCommandLine(int argc, char** argv, MainOptions & mainOptions, UprightDiff::Options & diffOptions) { po::options_description visible; std::string format; visible.add_options() ("help", "Show help message and exit") ("block-size", po::value(&diffOptions.blockSize), "Block size for initial search (default 16)") ("window-size", po::value(&diffOptions.windowSize), "Initial range for vertical motion detection (default 200)") ("brush-width", po::value(&diffOptions.brushWidth), "Brush width when heuristically expanding blocks. " "A higher value gives smoother motion regions. " "This should be an odd number. (default 9)") ("outer-hl-window", po::value(&diffOptions.outerHighlightWindow), "The size of the outer square used for detecting isolated small features to highlight. " "This size defines what we mean by \"isolated\". It should be an odd number. (default 21)") ("inner-hl-window", po::value(&diffOptions.innerHighlightWindow), "The size of the inner square used for detecting isolated small features to highlight. " "This size defines what we mean by \"small\". It should be an odd number. (default 5)") ("intermediate-dir", po::value(&diffOptions.intermediateDir), "A directory where intermediate images should be placed. " "This is our equivalent of debug or trace output.") ("verbose,v", "Write progress info to stderr.") ("format", po::value(&format), "The output format for statistics, may be text (the default), json or none.") ("log-timestamp,t", po::bool_switch(&diffOptions.logTimestamp), "Annotate progress info with elapsed time.") ; po::options_description invisible; invisible.add_options() ("alice", po::value(&mainOptions.aliceName)) ("bob", po::value(&mainOptions.bobName)) ("dest", po::value(&mainOptions.destName)) ; po::options_description allDesc; allDesc.add(visible).add(invisible); po::positional_options_description positionalDesc; positionalDesc .add("alice", 1) .add("bob", 1) .add("dest", 1) ; po::variables_map vm; po::store(po::command_line_parser(argc, argv) .options(allDesc) .positional(positionalDesc) .run(), vm); po::notify(vm); if (vm.count("help")) { std::cout << "Usage: " << (argc >= 1 ? argv[0] : "uprightdiff" ) << " [options] \n" << "Accepted options are:\n" << visible; return false; } if (vm.count("verbose")) { diffOptions.logLevel = Logger::INFO; } if (!(vm.count("alice") && vm.count("bob") && vm.count("dest"))) { std::cerr << "Error: two input filenames and an output filename must be specified.\n"; return false; } if (vm.count("format")) { if (format == "text") { mainOptions.format = MainOptions::TEXT; } else if (format == "json") { mainOptions.format = MainOptions::JSON; } else if (format == "none") { mainOptions.format = MainOptions::NONE; } else { std::cerr << "Error: --format must be text, json or none\n"; return false; } } return true; } uprightdiff-1.3.0/tests/000077500000000000000000000000001317465162200151775ustar00rootroot00000000000000uprightdiff-1.3.0/tests/RollingBlockCounterTest.cpp000066400000000000000000000027121317465162200224660ustar00rootroot00000000000000#include #include #include "../RollingBlockCounter.h" typedef cv::Mat_ Mat1i; bool good = true; void rollingCount(const char* message, const Mat1i & mat, int window, bool purge, int * expectedData) { Mat1i expected(mat.rows, mat.cols, expectedData); for (int x = 0; x < mat.cols; x++) { RollingBlockCounter rbc(mat, x, window); for (int y = 0; y < mat.rows; y++) { if (rbc(y) != expected(y, x)) { std::cout << "Error: " << message << ": " << "at (" << y << ", " << x << ") got " << rbc(y) << ", expected " << expected(y, x) << "\n"; good = false; } if (purge) { rbc.purge(); } } } } int main(int argc, char** argv) { int input[] = { 50, 62, 61, 89, 15, 71, 73, 69, 86, 72, 78, 30, 89, 60, 64, 22, 22, 99, 97, 93, 43, 77, 75, 42, 13 }; int sum3[] = { 256, 386, 440, 392, 262, 364, 583, 619, 605, 386, 296, 553, 625, 729, 472, 272, 535, 591, 632, 369, 164, 338, 412, 419, 245 }; int sum5[] = { 583, 818, 969, 770, 605, 726, 1058, 1302, 1081, 894, 921, 1295, 1552, 1288, 1024, 748, 1033, 1275, 1061, 859, 535, 734, 904, 761, 632 }; Mat1i mat(5, 5, input); rollingCount("1-n", mat, 1, false, input); rollingCount("1-p", mat, 1, true, input); rollingCount("3-n", mat, 3, false, sum3); rollingCount("3-p", mat, 3, true, sum3); rollingCount("5-n", mat, 5, false, sum5); rollingCount("5-p", mat, 5, false, sum5); return good ? 0 : 1; } uprightdiff-1.3.0/uprightdiff.1000066400000000000000000000035471317465162200164430ustar00rootroot00000000000000.TH UPRIGHTDIFF "1" "August 2017" "uprightdiff 1.0" "User Commands" .SH NAME uprightdiff \- examines the differences between two images .SH SYNOPSIS .B uprightdiff [\fI\,options\/\fR] \fI\, \/\fR .SH DESCRIPTION uprightdiff examines the differences between two images. It produces a visual annotation and reports statistics. It is optimised for images which come from browser screenshots. It analyses the image for vertical motion, and annotates connected regions that have the same vertical displacement. Then it highlights any remaining ("residual") differences which are not explained by vertical motion on a pixel-by-pixel basis. .TP \fB\-\-help\fR Show help message and exit .TP \fB\-\-block\-size\fR arg Block size for initial search (default 16) .TP \fB\-\-window\-size\fR arg Initial range for vertical motion detection (default 200) .TP \fB\-\-brush\-width\fR arg Brush width when heuristically expanding blocks. A higher value gives smoother motion regions. This should be an odd number. (default 9) .TP \fB\-\-outer\-hl\-window\fR arg The size of the outer square used for detecting isolated small features to highlight. This size defines what we mean by "isolated". It should be an odd number. (default 21) .TP \fB\-\-inner\-hl\-window\fR arg The size of the inner square used for detecting isolated small features to highlight. This size defines what we mean by "small". It should be an odd number. (default 5) .TP \fB\-\-intermediate\-dir\fR arg A directory where intermediate images should be placed. This is our equivalent of debug or trace output. .TP \fB\-v\fR [ \fB\-\-verbose\fR ] Write progress info to stderr. .TP \fB\-\-format\fR arg The output format for statistics, may be text (the default), json or none. .TP \fB\-t\fR [ \fB\-\-log\-timestamp\fR ] Annotate progress info with elapsed time. .SH AUTHOR Tim Starling