mirror of
https://github.com/go-vgo/robotgo.git
synced 2025-06-01 14:43:55 +00:00
271 lines
9.3 KiB
C
271 lines
9.3 KiB
C
#include "bitmap_find.h"
|
|
#include "../base/UTHashTable_c.h"
|
|
#include <assert.h>
|
|
|
|
/* Node to be used in hash table. */
|
|
struct shiftNode {
|
|
UTHashNode_HEAD /* Make structure hashable */
|
|
MMRGBHex color; /* Key */
|
|
MMPoint offset; /* Value */
|
|
};
|
|
|
|
/* --- Hash table helper functions --- */
|
|
|
|
/* Adds hex-color/offset pair to jump table. */
|
|
static void addNodeToTable(UTHashTable *table, MMRGBHex color, MMPoint offset);
|
|
|
|
/* Returns node associated with color in jump table, or NULL if it
|
|
* doesn't exist. */
|
|
static struct shiftNode *nodeForColor(UTHashTable *table, MMRGBHex color);
|
|
|
|
/* Returns nonzero (true) if table has key, or zero (false) if not. */
|
|
#define tableHasKey(table, color) (nodeForColor(table, color) != NULL)
|
|
|
|
/* --- Boyer-Moore helper functions --- */
|
|
|
|
/* Calculates the first table for use in a Boyer-Moore search algorithm.
|
|
* Table is in the form [colors: shift_values], where colors are those in
|
|
* |needle|, and the shift values are each color's distance from the rightmost
|
|
* offset. All other colors are assumed to have a shift value equal to the
|
|
* length of needle.
|
|
*/
|
|
static void initBadShiftTable(UTHashTable *jumpTable, MMBitmapRef needle);
|
|
|
|
/* Frees memory occupied by calling initBadShiftTable().
|
|
* Currently this is just an alias for destroyHashTable(). */
|
|
#define destroyBadShiftTable(jumpTable) destroyHashTable(jumpTable)
|
|
|
|
/* Returns true if |needle| is found in |haystack| at |offset|. */
|
|
static int needleAtOffset(MMBitmapRef needle, MMBitmapRef haystack,
|
|
MMPoint offset, float tolerance);
|
|
/* --- --- */
|
|
|
|
/* An modification of the Boyer-Moore-Horspool Algorithm, only applied to
|
|
* bitmaps and colors instead of strings and characters.
|
|
*
|
|
* TODO: The Boyer-Moore algorithm (with the second jump table) would probably
|
|
* be more efficient, but this was simpler (for now).
|
|
*
|
|
* The jump table (|badShiftTable|) is passed as a parameter to avoid being
|
|
* recalculated each time. It should be a pointer to a UTHashTable init'd with
|
|
* initBadShiftTable().
|
|
*
|
|
* Returns 0 and sets |point| to the starting point of |needle| in |haystack|
|
|
* if |needle| was found in |haystack|, or returns -1 if not. */
|
|
static int findBitmapInRectAt(MMBitmapRef needle,
|
|
MMBitmapRef haystack,
|
|
MMPoint *point,
|
|
MMRect rect,
|
|
float tolerance,
|
|
MMPoint startPoint,
|
|
UTHashTable *badShiftTable)
|
|
{
|
|
const size_t scanHeight = rect.size.height - needle->height;
|
|
const size_t scanWidth = rect.size.width - needle->width;
|
|
MMPoint pointOffset = startPoint;
|
|
/* const MMPoint lastPoint = MMPointMake(needle->width - 1, needle->height - 1); */
|
|
|
|
/* Sanity check */
|
|
if (needle->height > haystack->height || needle->width > haystack->width ||
|
|
!MMBitmapRectInBounds(haystack, rect)) {
|
|
return -1;
|
|
}
|
|
|
|
assert(point != NULL);
|
|
assert(needle != NULL);
|
|
assert(needle->height > 0 && needle->width > 0);
|
|
assert(haystack != NULL);
|
|
assert(haystack->height > 0 && haystack->width > 0);
|
|
assert(badShiftTable != NULL);
|
|
|
|
/* Search |haystack|, while |needle| can still be within it. */
|
|
while (pointOffset.y <= scanHeight) {
|
|
/* struct shiftNode *node = NULL;
|
|
MMRGBHex lastColor; */
|
|
|
|
while (pointOffset.x <= scanWidth) {
|
|
/* Check offset in |haystack| for |needle|. */
|
|
if (needleAtOffset(needle, haystack, pointOffset, tolerance)) {
|
|
// ++pointOffset.x;
|
|
// ++pointOffset.y;
|
|
*point = pointOffset;
|
|
return 0;
|
|
}
|
|
|
|
/* Otherwise, calculate next x offset to check. */
|
|
/*
|
|
* Note that here we are getting the skip value based on the last
|
|
* color of |needle|, no matter where we didn't match. The
|
|
* alternative of pretending that the mismatched color was the previous
|
|
* color is slower in the normal case.
|
|
*/
|
|
/* lastColor = MMRGBHexAtPoint(haystack, pointOffset.x + lastPoint.x,
|
|
pointOffset.y + lastPoint.y); */
|
|
|
|
/* TODO: This fails on certain edge cases (issue#7). */
|
|
/* When a color is encountered that does not occur in |needle|, we can
|
|
* safely skip ahead for the whole length of |needle|.
|
|
* Otherwise, use the value stored in the jump table. */
|
|
/* node = nodeForColor(badShiftTable, lastColor);
|
|
pointOffset.x += (node == NULL) ? needle->width : (node->offset).x; */
|
|
|
|
/* For now, be naive. */
|
|
++pointOffset.x;
|
|
}
|
|
|
|
pointOffset.x = rect.origin.x;
|
|
|
|
/* lastColor = MMRGBHexAtPoint(haystack, pointOffset.x + lastPoint.x,
|
|
pointOffset.y + lastPoint.y);
|
|
node = nodeForColor(badShiftTable, lastColor);
|
|
pointOffset.y += node == NULL ? lastPoint.y : (node->offset).y; */
|
|
|
|
/* TODO: The above commented out code fails at certain edge cases, e.g.:
|
|
* Needle: [B, b
|
|
* b, b,
|
|
* B, b]
|
|
* Haystack: [w, w, w, w, w
|
|
* w, w, w, w, b
|
|
* w, w, w, b, b
|
|
* w, w, w, w, b]
|
|
* The previous algorithm noticed that the first 3 x 3 block had nothing
|
|
* in common with the image, and thus, after scanning the first row,
|
|
* skipped three blocks downward to scan the next (which didn't exist,
|
|
* so the loop ended). However, the needle was hidden IN-BETWEEN this
|
|
* jump -- skipping was appropriate for scanning the column but not
|
|
* the row.
|
|
*
|
|
* I need to figure out a more optimal solution; temporarily I am just
|
|
* scanning every single y coordinate, only skipping on x's. This
|
|
* always works, but is probably not optimal.
|
|
*/
|
|
++pointOffset.y;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
int findBitmapInRect(MMBitmapRef needle,
|
|
MMBitmapRef haystack,
|
|
MMPoint *point,
|
|
MMRect rect,
|
|
float tolerance)
|
|
{
|
|
UTHashTable badShiftTable;
|
|
int ret;
|
|
|
|
initBadShiftTable(&badShiftTable, needle);
|
|
ret = findBitmapInRectAt(needle, haystack, point, rect,
|
|
tolerance, MMPointZero, &badShiftTable);
|
|
destroyBadShiftTable(&badShiftTable);
|
|
return ret;
|
|
}
|
|
|
|
MMPointArrayRef findAllBitmapInRect(MMBitmapRef needle, MMBitmapRef haystack,
|
|
MMRect rect, float tolerance)
|
|
{
|
|
MMPointArrayRef pointArray = createMMPointArray(0);
|
|
MMPoint point = MMPointZero;
|
|
UTHashTable badShiftTable;
|
|
|
|
initBadShiftTable(&badShiftTable, needle);
|
|
while (findBitmapInRectAt(needle, haystack, &point, rect,
|
|
tolerance, point, &badShiftTable) == 0) {
|
|
const size_t scanWidth = (haystack->width - needle->width) + 1;
|
|
MMPointArrayAppendPoint(pointArray, point);
|
|
ITER_NEXT_POINT(point, scanWidth, 0);
|
|
}
|
|
destroyBadShiftTable(&badShiftTable);
|
|
|
|
return pointArray;
|
|
}
|
|
|
|
size_t countOfBitmapInRect(MMBitmapRef needle, MMBitmapRef haystack,
|
|
MMRect rect, float tolerance)
|
|
{
|
|
size_t count = 0;
|
|
MMPoint point = MMPointZero;
|
|
UTHashTable badShiftTable;
|
|
|
|
initBadShiftTable(&badShiftTable, needle);
|
|
while (findBitmapInRectAt(needle, haystack, &point, rect,
|
|
tolerance, point, &badShiftTable) == 0) {
|
|
const size_t scanWidth = (haystack->width - needle->width) + 1;
|
|
++count;
|
|
ITER_NEXT_POINT(point, scanWidth, 0);
|
|
}
|
|
destroyBadShiftTable(&badShiftTable);
|
|
|
|
return count;
|
|
}
|
|
|
|
/* --- Boyer-Moore helper functions --- */
|
|
|
|
static void initBadShiftTable(UTHashTable *jumpTable, MMBitmapRef needle)
|
|
{
|
|
const MMPoint lastPoint = MMPointMake(needle->width - 1, needle->height - 1);
|
|
const size_t maxColors = needle->width * needle->height;
|
|
MMPoint scan;
|
|
|
|
/* Allocate max size initially to avoid a million calls to malloc(). */
|
|
initHashTable(jumpTable, maxColors, sizeof(struct shiftNode));
|
|
|
|
/* Populate jumpTable with analysis of |needle|. */
|
|
for (scan.y = lastPoint.y; ; --scan.y) {
|
|
for (scan.x = lastPoint.x; ; --scan.x) {
|
|
MMRGBHex color = MMRGBHexAtPoint(needle, scan.x, scan.y);
|
|
if (!tableHasKey(jumpTable, color)) {
|
|
addNodeToTable(jumpTable, color,
|
|
MMPointMake(needle->width - scan.x,
|
|
needle->height - scan.y));
|
|
}
|
|
|
|
if (scan.x == 0) break; /* Avoid infinite loop from unsigned type. */
|
|
}
|
|
if (scan.y == 0) break;
|
|
}
|
|
}
|
|
|
|
static int needleAtOffset(MMBitmapRef needle, MMBitmapRef haystack,
|
|
MMPoint offset, float tolerance)
|
|
{
|
|
const MMPoint lastPoint = MMPointMake(needle->width - 1, needle->height - 1);
|
|
MMPoint scan;
|
|
|
|
/* Note that |needle| is searched backwards, in accordance with the
|
|
* Boyer-Moore search algorithm. */
|
|
for (scan.y = lastPoint.y; ; --scan.y) {
|
|
for (scan.x = lastPoint.x; ; --scan.x) {
|
|
MMRGBHex ncolor = MMRGBHexAtPoint(needle, scan.x, scan.y);
|
|
MMRGBHex hcolor = MMRGBHexAtPoint(haystack, offset.x + scan.x,
|
|
offset.y + scan.y);
|
|
if (!MMRGBHexSimilarToColor(ncolor, hcolor, tolerance)) return 0;
|
|
if (scan.x == 0) break; /* Avoid infinite loop from unsigned type. */
|
|
}
|
|
if (scan.y == 0) break;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* --- Hash table helper functions --- */
|
|
|
|
static void addNodeToTable(UTHashTable *table,
|
|
MMRGBHex hexColor,
|
|
MMPoint offset)
|
|
{
|
|
struct shiftNode *node = getNewNode(table);
|
|
node->color = hexColor;
|
|
node->offset = offset;
|
|
UTHASHTABLE_ADD_INT(table, color, node, struct shiftNode);
|
|
}
|
|
|
|
static struct shiftNode *nodeForColor(UTHashTable *table,
|
|
MMRGBHex color)
|
|
{
|
|
struct shiftNode *uttable = table->uttable;
|
|
struct shiftNode *node;
|
|
HASH_FIND_INT(uttable, &color, node);
|
|
return node;
|
|
}
|