API reference
Signature generation
- class image_match.goldberg.ImageSignature(n: int = 9, crop_percentiles: tuple[int, int] | None = (5, 95), P: int | None = None, diagonal_neighbors: bool = True, identical_tolerance: float = 0.00784313725490196, n_levels: int = 2, fix_ratio: bool = False)[source]
Image signature generator.
Based on the method of Goldberg, et al. Available at http://www.cs.cmu.edu/~hcwong/Pdfs/icip02.ps
- __init__(n: int = 9, crop_percentiles: tuple[int, int] | None = (5, 95), P: int | None = None, diagonal_neighbors: bool = True, identical_tolerance: float = 0.00784313725490196, n_levels: int = 2, fix_ratio: bool = False) None[source]
Initialize the signature generator.
The default parameters match those given in Goldberg’s paper.
Note
Non-default parameters have not been extensively tested. Use carefully.
- Parameters:
n (Optional[int]) – size of grid imposed on image. Grid is n x n (default 9)
crop_percentiles (Optional[Tuple[int]]) – lower and upper bounds when considering how much variance to keep in the image (default (5, 95))
P (Optional[int]) – size of sample region, P x P. If none, uses a sample region based on the size of the image (default None)
diagonal_neighbors (Optional[boolean]) – whether to include diagonal grid neighbors (default True)
identical_tolerance (Optional[float]) – cutoff difference for declaring two adjacent grid points identical (default 2/255)
n_levels (Optional[int]) – number of positive and negative groups to stratify neighbor differences into. n = 2 -> [-2, -1, 0, 1, 2] (default 2)
fix_ratio (Optional[boolean]) – use the larger crop ratio for both directions. Only use if you can guarantee the incoming image is square (default False)
- static compute_differentials(grey_level_matrix: ndarray, diagonal_neighbors: bool = True) ndarray[source]
Computes differences in greylevels for neighboring grid points.
First part of ‘step 4’ in the paper.
Returns n x n x 8 rank 3 array for an n x n grid (if diagonal_neighbors == True)
The n x nth coordinate corresponds to a grid point. The eight values are the differences between neighboring grid points, in this order:
upper left upper upper right left right lower left lower lower right
- Parameters:
grey_level_matrix (numpy.ndarray) – grid of values sampled from image
diagonal_neighbors (Optional[boolean]) – whether or not to use diagonal neighbors (default True)
- Returns:
a n x n x 8 rank 3 numpy array for an n x n grid (if diagonal_neighbors == True)
Examples
>>> img = gis.preprocess_image('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') >>> window = gis.crop_image(img) >>> grid = gis.compute_grid_points(img, window=window) >>> grey_levels = gis.compute_mean_level(img, grid[0], grid[1]) >>> gis.compute_differentials(grey_levels) array([[[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.82683143e-03, -0.00000000e+00, 2.74085276e-01, 1.24737821e-01], [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, -1.82683143e-03, 2.15563930e-03, 2.72258444e-01, 1.22910990e-01, 3.48522956e-01], [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, -2.15563930e-03, 1.16963917e-01, 1.20755351e-01, 3.46367317e-01, 1.99638513e-01], [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, -1.16963917e-01, 1.32128118e-01, 2.29403399e-01, 8.26745956e-02, 1.16455050e-01], ...
- static compute_grid_points(image: ndarray, n: int = 9, window: list[tuple[int, int]] | None = None) tuple[ndarray, ndarray][source]
Computes grid points for image analysis.
Corresponds to the second part of ‘step 2’ in the paper
- Parameters:
- Returns:
tuple of arrays indicating the vertical and horizontal locations of the grid points
Examples
>>> img = gis.preprocess_image('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') >>> window = gis.crop_image(img) >>> gis.compute_grid_points(img, window=window) (array([100, 165, 230, 295, 360, 424, 489, 554, 619]), array([ 66, 109, 152, 195, 238, 280, 323, 366, 409]))
- static compute_mean_level(image: ndarray, x_coords: ndarray, y_coords: ndarray, P: float | None = None) ndarray[source]
Computes array of greyness means.
Corresponds to ‘step 3’
- Parameters:
image (numpy.ndarray) – n x m array of floats – the greyscale image. Typically, the output of preprocess_image
x_coords (numpy.ndarray) – array of row numbers
y_coords (numpy.ndarray) – array of column numbers
P (Optional[int]) – size of boxes in pixels (default None)
- Returns:
- an N x N array of average greyscale around the gridpoint, where N is the
number of grid points
Examples
>>> img = gis.preprocess_image('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') >>> window = gis.crop_image(img) >>> grid = gis.compute_grid_points(img, window=window) >>> gis.compute_mean_level(img, grid[0], grid[1]) array([[ 0.62746325, 0.62563642, 0.62348078, 0.50651686, 0.37438874, 0.0644063 , 0.55968952, 0.59356148, 0.60473832], [ 0.35337797, 0.50272543, 0.27711346, 0.42384226, 0.39006181, 0.16773968, 0.10471924, 0.33647144, 0.62902124], [ 0.20307514, 0.19021892, 0.12435402, 0.44990121, 0.38527996, 0.08339507, 0.05530059, 0.18469107, 0.21125228], [ 0.25727387, 0.1669419 , 0.08964046, 0.1372754 , 0.48529236, 0.39894004, 0.10387907, 0.11282135, 0.30014612], [ 0.23447867, 0.15702549, 0.25232943, 0.75172715, 0.79488688, 0.4943538 , 0.29645163, 0.10714578, 0.0629376 ], [ 0.22167555, 0.04839472, 0.10125833, 0.1550749 , 0.14346914, 0.04713144, 0.10095568, 0.15349296, 0.04456733], [ 0.09233709, 0.11210942, 0.05361996, 0.07066566, 0.04191625, 0.03548839, 0.03420656, 0.05025029, 0.03519956], [ 0.19226873, 0.20647194, 0.62972106, 0.45514529, 0.05620413, 0.03383168, 0.03413588, 0.04741828, 0.02987698], [ 0.05799523, 0.23310153, 0.43719717, 0.27666873, 0.25106573, 0.11094163, 0.10180622, 0.04633349, 0.02704855]])
- static crop_image(image: ndarray, lower_percentile: int = 5, upper_percentile: int = 95, fix_ratio: bool = False) list[tuple[int, int]][source]
Crops an image, removing featureless border regions.
Corresponds to the first part of ‘step 2’ in Goldberg’s paper
- Parameters:
image (numpy.ndarray) – n x m array of floats – the greyscale image. Typically, the output of preprocess_image
lower_percentile (Optional[int]) – crop image by percentage of difference (default 5)
upper_percentile (Optional[int]) – as lower_percentile (default 95)
fix_ratio (Optional[boolean]) – use the larger ratio for both directions. This is useful for using the fast signature transforms on sparse but very similar images (e.g. renderings from fixed directions). Use with care – only use if you can guarantee the incoming image is square (default False).
- Returns:
[(top, bottom), (left, right)]
- Return type:
A pair of tuples describing the ‘window’ of the image to use in analysis
Examples
>>> img = gis.preprocess_image('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') >>> gis.crop_image(img) [(36, 684), (24, 452)]
- generate_signature(path_or_image: ImageInput, bytestream: bool = False) ndarray[source]
Generates an image signature.
See section 3 of Goldberg, et al.
- Parameters:
path_or_image (string or numpy.ndarray) – image path, or image array
bytestream (Optional[boolean]) – will the image be passed as raw bytes? That is, is the ‘path_or_image’ argument an in-memory image? (default False)
- Returns:
- A rank 1 numpy array of length n x n x 8
(or n x n x 4 if diagonal_neighbors == False)
- Return type:
The image signature
Examples
>>> from image_match.goldberg import ImageSignature >>> gis = ImageSignature() >>> gis.generate_signature('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') array([ 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, -2, 2, 2, 1, 2, 0, 0, 0, -2, 2, -1, -1, 2, 0, 0, 0, -2, -2, -2, -2, -1, 0, 0, 0, 2, -1, 2, 2, 2, 0, 0, 0, 1, -1, 2, 2, -1, 0, 0, 0, 1, 0, 2, -1, 0, 0, -2, -2, 0, -2, 0, 2, 2, -2, -2, -2, 2, 2, 2, 2, 2, -2, -2, -2, -2, -2, 1, 2, -2, -2, -1, 1, 2, 1, 2, -1, 1, -2, 1, 2, -1, 2, -1, 0, 2, -2, 2, -2, -2, 1, -2, 1, 2, 1, -2, -2, -1, -2, 1, 1, -1, -2, -2, -2, 2, -2, 2, 2, 2, 1, 1, 0, 2, 0, 2, 2, 0, 0, -2, -2, 0, 1, 0, -1, 1, -2, -2, -1, -1, 1, -1, 1, 1, -2, -2, -2, -1, -2, -1, 1, -1, 2, 1, 1, 2, 1, 2, 2, -1, -1, 0, 2, -1, 2, 2, -1, -1, -2, -1, -1, -2, 1, -2, -2, -1, -2, -1, -2, -1, -2, -2, -1, -1, 1, -2, -2, 2, -1, 1, 1, -2, -2, -2, 0, 1, 0, 1, -1, 0, 0, 1, 1, 0, 1, 0, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1, -1, -2, -1, -1, -1, -2, -2, 1, -2, -2, 1, -2, -2, -2, -2, 1, 1, 2, 2, 1, -2, -2, -1, 1, 2, 2, -1, 2, -2, -1, 1, 1, 1, -1, -2, -1, -2, -2, 0, 1, -1, -1, 1, -2, -2, 0, 1, 2, 1, 0, 2, 0, 2, 2, 0, 0, -1, 1, 0, 1, 0, 1, 2, -1, -1, 1, -1, -1, -1, 2, 1, 1, 2, 2, 1, -2, 2, 2, 1, 2, 2, 2, 2, -1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 1, 2, -2, 2, 2, 2, 2, -1, 2, 2, -2, 2, 2, 2, 2, 0, 0, -2, -2, 1, 0, -1, 1, -1, -2, 0, -1, 0, -1, 1, 0, 0, -1, 1, 0, 2, 0, 2, 2, -2, -2, -2, -2, -1, -1, -1, 0, -1, -2, -2, 1, -1, -1, 1, 1, -1, -2, -2, 1, 1, 1, 1, 2, -2, -2, -2, -1, 1, 1, 1, 2, -2, -2, -2, -1, -1, 0, 1, 1, -2, -2, 0, 1, -1, 1, 1, 1, -2, 1, 1, 1, 2, 2, 2, 2, -1, -1, 0, -2, 0, 0, 1, 0, 0, -2, 1, 0, -1, 0, -1, -2, -2, 1, 1, 1, 1, -1, -1, -2, 0, -1, -1, -1, -1, -2, -2, -2, -1, -1, -1, 1, 1, -2, -2, 1, -2, -1, 0, -1, 0, -2, -1, 1, -2, -1, -1, 0, 0, -1, 0, 0, -1, -1, -2, 0, -1, 0, 0, -1, -1, -2, 0, 1, 1, 1, 0, 1, -2, -1, 0, -1, 0, -1, 0, 0, 0, 1, 1, 0, -1, 0, 2, -1, 2, 1, 2, 1, -2, 2, -1, -2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -2, 2, 1, 2, 2, -1, 1, 1, -2, 1, -2, -2, -1, -1, 0, 0, -1, 0, -2, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1, 1, 0, 1, 1, 1, -1, 0, 1, -1, 0, 0, -1, 0, -1, 0, 0, 0, -2, -2, 0, -2, 0, 0, 0, 1, 1, -2, 2, -2, 0, 0, 0, 2, -2, -1, 2, 2, 0, 0, 0, -2, -2, 2, -2, 1, 0, 0, 0, -2, 2, 2, -1, 2, 0, 0, 0, 1, 1, 1, -2, 1, 0, 0, 0, 1, 1, 1, -1, 1, 0, 0, 0, 1, 0, 1, -1, 1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0], dtype=int8)
- static normalize_and_threshold(difference_array: ndarray, identical_tolerance: float = 0.00784313725490196, n_levels: int = 2) None[source]
Normalizes difference matrix in place.
‘Step 4’ of the paper. The flattened version of this array is the image signature.
- Parameters:
difference_array (numpy.ndarray) – n x n x l array, where l are the differences between the grid point and its neighbors. Typically the output of compute_differentials
identical_tolerance (Optional[float]) – maximum amount two gray values can differ and still be considered equivalent (default 2/255)
n_levels (Optional[int]) – bin differences into 2 n + 1 bins (e.g. n_levels=2 -> [-2, -1, 0, 1, 2])
Examples
>>> img = gis.preprocess_image('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') >>> window = gis.crop_image(img) >>> grid = gis.compute_grid_points(img, window=window) >>> grey_levels = gis.compute_mean_level(img, grid[0], grid[1]) >>> m = gis.compute_differentials(grey_levels) >>> m array([[[ 0., 0., 0., 0., 0., 0., 2., 2.], [ 0., 0., 0., 0., 0., 2., 2., 2.], [ 0., 0., 0., 0., 2., 2., 2., 2.], [ 0., 0., 0., -2., 2., 2., 1., 2.], [ 0., 0., 0., -2., 2., -1., -1., 2.], [ 0., 0., 0., -2., -2., -2., -2., -1.], [ 0., 0., 0., 2., -1., 2., 2., 2.], [ 0., 0., 0., 1., -1., 2., 2., -1.], [ 0., 0., 0., 1., 0., 2., -1., 0.]],
- [[ 0., -2., -2., 0., -2., 0., 2., 2.],
[-2., -2., -2., 2., 2., 2., 2., 2.], [-2., -2., -2., -2., -2., 1., 2., -2.], [-2., -1., 1., 2., 1., 2., -1., 1.], [-2., 1., 2., -1., 2., -1., 0., 2.], …
- static normalized_distance(_a: ndarray, _b: ndarray) float[source]
Compute normalized distance between two points.
Computes || b - a || / ( ||b|| + ||a||)
- Parameters:
_a (numpy.ndarray) – array of size m
_b (numpy.ndarray) – array of size m
- Returns:
normalized distance between signatures (float)
Examples
>>> a = gis.generate_signature('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> b = gis.generate_signature('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') >>> gis.normalized_distance(a, b) 0.22095170140933634
- static preprocess_image(image_or_path: ImageInput, bytestream: bool = False, handle_mpo: bool = False) ndarray[source]
Loads an image and converts to greyscale.
Corresponds to ‘step 1’ in Goldberg’s paper
- Parameters:
image_or_path (string or numpy.ndarray) – image path, or image array
bytestream (Optional[boolean]) – will the image be passed as raw bytes? That is, is the ‘path_or_image’ argument an in-memory image? (default False)
handle_mpo (Optional[boolean]) – try to compute a signature for steroscopic images by extracting the first image of the set (default False)
- Returns:
Array of floats corresponding to greyscale level at each pixel
Examples
>>> gis.preprocess_image('https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg') array([[ 0.26344431, 0.32423294, 0.30406745, ..., 0.35069725, 0.36499961, 0.36361569], [ 0.29676627, 0.28640118, 0.34523255, ..., 0.3703051 , 0.34931333, 0.31655686], [ 0.35305216, 0.31858431, 0.36202 , ..., 0.40588196, 0.37284275, 0.30871373], ..., [ 0.05932863, 0.05540706, 0.05540706, ..., 0.01954745, 0.01954745, 0.01562588], [ 0.0632502 , 0.05540706, 0.05148549, ..., 0.01954745, 0.02346902, 0.01562588], [ 0.06717176, 0.05540706, 0.05148549, ..., 0.02346902, 0.02739059, 0.01954745]])
Driver base
- class image_match.signature_database_base.SignatureDatabaseBase(k: int = 16, N: int = 63, n_grid: int = 9, crop_percentile: tuple[int, int] | None = (5, 95), distance_cutoff: float = 0.45, *signature_args: Any, **signature_kwargs: Any)[source]
Base class for storing and searching image signatures in a database
Note
You must implement the methods search_single_record and insert_single_record in a derived class
- __init__(k: int = 16, N: int = 63, n_grid: int = 9, crop_percentile: tuple[int, int] | None = (5, 95), distance_cutoff: float = 0.45, *signature_args: Any, **signature_kwargs: Any) None[source]
Set up storage scheme for images
Central to the speed of this approach is the transforming the image signature into something that can be speedily indexed and matched. In our case, that means splitting the image signature into N words of length k, then encoding those words as integers. The idea here is that integer indices are more efficient than array indices.
For example, say your image signature is [0, 1, 2, 0, -1, -2, 0, 1] and k=3 and N=4. That means we want 4 words of length 3. For this signa- ture, that gives us:
[0, 1, 2] [2, 0, -1] [-1, -2, 0] [0, 1]
Note that signature elements can be repeated, and any mismatch in length is chopped off in the last word (which will be padded with zeros). Since these numbers run from -2..2, there 5 possibilites. Adding 2 to each word makes them strictly non-negative, then the quantity, and transforming to base-5 makes unique integers. For the first word:
[0, 1, 2] + 2 = [2, 3, 4] [5**0, 5**1, 5**2] = [1, 5, 25] dot([2, 3, 4], [1, 5, 25]) = 2 + 15 + 100 = 117
So the integer word is 117. Storing all the integer words as different database columns or fields gives us the speedy lookup. In practice, word arrays are ‘squeezed’ to between -1..1 before encoding.
- Parameters:
k (Optional[int]) – the width of a word (default 16)
N (Optional[int]) – the number of words (default 63)
n_grid (Optional[int]) – the n_grid x n_grid size to use in determining the image signature (default 9)
crop_percentile (Optional[Tuple[int]]) – lower and upper bounds when considering how much variance to keep in the image (default (5, 95))
distance_cutoff (Optional [float]) – maximum image signature distance to be considered a match (default 0.45)
*signature_args – Variable length argument list to pass to ImageSignature
**signature_kwargs – Arbitrary keyword arguments to pass to ImageSignature
- add_image(path: str, img: ImageInput | None = None, bytestream: bool = False, metadata: dict[str, Any] | None = None, *args: Any, **kwargs: Any) None[source]
Add a single image to the database
- Parameters:
path (string) – path or identifier for image. If img=None, then path is assumed to be a URL or filesystem path
img (Optional[string]) – usually raw image data. In this case, path will still be stored, but a signature will be generated from data in img. If bytestream is False, but img is not None, then img is assumed to be the URL or filesystem path. Thus, you can store image records with a different ‘path’ than the actual image location (default None)
bytestream (Optional[boolean]) – will the image be passed as raw bytes? That is, is the ‘path_or_image’ argument an in-memory image? If img is None but, this argument will be ignored. If img is not None, and bytestream is False, then the behavior is as described in the explanation for the img argument (default False)
metadata (Optional) – any other information you want to include, can be nested (default None)
*args – Variable length argument list to pass to insert_single_record
**kwargs – Arbitrary keyword arguments to pass to insert_single_record
- add_images(paths: list[ImageInput], metadata: dict[str, Any] | list[dict[str, Any] | None] | None = None, bytestream: bool = False, n_threads: int = 1, chunk_size: int | None = None, **kwargs: Any) int[source]
Generate signatures for many images and bulk-insert the records.
Signature generation is the bottleneck for bulk imports; n_threads > 1 parallelizes it (helps most for URL/bytes inputs since decode and numpy partially release the GIL). For CPU-only workloads, run signature_database_base.make_record in a multiprocessing pool — it is picklable by design — and pass the records to insert_records.
- Parameters:
paths (list) – paths, URLs, or raw image data
metadata (Optional) – a single dict applied to every record, or a list of dicts/None aligned with paths (default None)
bytestream (Optional[boolean]) – inputs are raw image bytes (default False)
n_threads (Optional[int]) – signature-generation threads (default 1)
chunk_size (Optional[int]) – process and insert in batches of this many images instead of buffering all records in memory (default None = one bulk request)
**kwargs – passed to insert_records (e.g. refresh_after — honored on the final chunk only)
- Returns:
the number of successfully indexed records
- insert_records(records: list[dict[str, Any]], **kwargs: Any) int[source]
Bulk-insert records made by make_record/add_images.
Must be implemented by derived class; returns the number of records successfully indexed.
- insert_single_record(rec: dict[str, Any]) None[source]
Insert an image record.
Must be implemented by derived class.
- Parameters:
rec (dict) –
an image record. Will be in the format returned by make_record
For example, rec could have the form:
{'path': 'https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg', 'signature': [0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0 ... ] 'simple_word_0': 42252475, 'simple_word_1': 23885671, 'simple_word_10': 9967839, 'simple_word_11': 4257902, 'simple_word_12': 28651959, 'simple_word_13': 33773597, 'simple_word_14': 39331441, 'simple_word_15': 39327300, 'simple_word_16': 11337345, 'simple_word_17': 9571961, 'simple_word_18': 28697868, 'simple_word_19': 14834907, 'simple_word_2': 7434746, 'simple_word_20': 37985525, 'simple_word_21': 10753207, 'simple_word_22': 9566120, ... 'metadata': {...} }
The number of simple words corresponds to the attribute N
- search_image(path: ImageInput, all_orientations: bool = False, bytestream: bool = False, pre_filter: PreFilter = None, n_threads: int = 1, **kwargs: Any) list[dict[str, Any]][source]
Search for matches
- Parameters:
path (string) – path or image data. If bytestream=False, then path is assumed to be a URL or filesystem path. Otherwise, it’s assumed to be raw image data
all_orientations (Optional[boolean]) – if True, search for all combinations of mirror images, rotations, and color inversions (default False)
bytestream (Optional[boolean]) – will the image be passed as raw bytes? That is, is the ‘path_or_image’ argument an in-memory image? (default False)
pre_filter (Optional[dict]) – filters list before applying the matching algorithm (default None)
n_threads (Optional[int]) – run orientation searches concurrently in this many threads; only relevant when all_orientations=True (default 1 = sequential)
**kwargs – Arbitrary keyword arguments to pass to search_single_record (e.g. n_parallel_words for the MongoDB driver)
- Returns:
a formatted list of dicts representing unique matches, sorted by dist
For example, if three matches are found:
[ {'dist': 0.069116439263706961, 'id': 'AVM37oZq0osmmAxpPvx7', 'path': 'https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg'}, {'dist': 0.22484320805049718, 'id': 'AVM37nMg0osmmAxpPvx6', 'path': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg'}, {'dist': 0.42529792112113302, 'id': 'AVM37p530osmmAxpPvx9', 'path': 'https://c2.staticflickr.com/8/7158/6814444991_08d82de57e_z.jpg'} ]
- search_single_record(rec: dict[str, Any], pre_filter: PreFilter = None) list[dict[str, Any]][source]
Search for a matching image record.
Must be implemented by derived class.
- Parameters:
rec (dict) –
an image record. Will be in the format returned by make_record
For example, rec could have the form:
{'path': 'https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg', 'signature': [0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0 ... ] 'simple_word_0': 42252475, 'simple_word_1': 23885671, 'simple_word_10': 9967839, 'simple_word_11': 4257902, 'simple_word_12': 28651959, 'simple_word_13': 33773597, 'simple_word_14': 39331441, 'simple_word_15': 39327300, 'simple_word_16': 11337345, 'simple_word_17': 9571961, 'simple_word_18': 28697868, 'simple_word_19': 14834907, 'simple_word_2': 7434746, 'simple_word_20': 37985525, 'simple_word_21': 10753207, 'simple_word_22': 9566120, ... 'metadata': {'category': 'art'}, }
The number of simple words corresponds to the attribute N
pre_filter (dict) –
- a filter to be applied by the concrete implementation
before applying the matching strategy
- For example:
{ “term”: { “image.metadata.category”: “art” } }
- Returns:
a formatted list of dicts representing matches.
For example, if three matches are found:
[ {'dist': 0.069116439263706961, 'id': 'AVM37oZq0osmmAxpPvx7', 'path': 'https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg'}, {'dist': 0.22484320805049718, 'id': 'AVM37nMg0osmmAxpPvx6', 'path': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg'}, {'dist': 0.42529792112113302, 'id': 'AVM37p530osmmAxpPvx9', 'metadata': {...}, 'path': 'https://c2.staticflickr.com/8/7158/6814444991_08d82de57e_z.jpg'} ]
You can return any fields you like, but must include at least dist and id. Duplicate entries are ok, and they do not need to be sorted
Module helpers
- image_match.signature_database_base.make_record(path: ImageInput, gis: ImageSignature, k: int, N: int, img: ImageInput | None = None, bytestream: bool = False, metadata: dict[str, Any] | None = None) dict[str, Any][source]
Makes a record suitable for database insertion.
Note
This non-class version of make_record is provided for CPU pooling. Functions passed to worker processes must be picklable.
- Parameters:
path (string) – path or image data. If bytestream=False, then path is assumed to be a URL or filesystem path. Otherwise, it’s assumed to be raw image data
gis (ImageSignature) – an instance of ImageSignature for generating the signature
k (int) – width of words for encoding
N (int) – number of words for encoding
img (Optional[string]) – usually raw image data. In this case, path will still be stored, but a signature will be generated from data in img. If bytestream is False, but img is not None, then img is assumed to be the URL or filesystem path. Thus, you can store image records with a different ‘path’ than the actual image location (default None)
bytestream (Optional[boolean]) – will the image be passed as raw bytes? That is, is the ‘path_or_image’ argument an in-memory image? If img is None but, this argument will be ignored. If img is not None, and bytestream is False, then the behavior is as described in the explanation for the img argument (default False)
metadata (Optional) – any other information you want to include, can be nested (default None)
- Returns:
An image record.
For example:
- {‘path’: ‘https://pixabay.com/static/uploads/photo/2012/11/28/08/56/mona-lisa-67506_960_720.jpg’,
’signature’: [0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0 … ] ‘simple_word_0’: 42252475, ‘simple_word_1’: 23885671, ‘simple_word_10’: 9967839, ‘simple_word_11’: 4257902, ‘simple_word_12’: 28651959, ‘simple_word_13’: 33773597, ‘simple_word_14’: 39331441, ‘simple_word_15’: 39327300, ‘simple_word_16’: 11337345, ‘simple_word_17’: 9571961, ‘simple_word_18’: 28697868, ‘simple_word_19’: 14834907, ‘simple_word_2’: 7434746, ‘simple_word_20’: 37985525, ‘simple_word_21’: 10753207, ‘simple_word_22’: 9566120, … ‘metadata’: {…} }
- image_match.signature_database_base.get_words(array: ndarray, k: int, N: int) ndarray[source]
Gets N words of length k from an array.
Words may overlap.
For example, say your image signature is [0, 1, 2, 0, -1, -2, 0, 1] and k=3 and N=4. That means we want 4 words of length 3. For this signature, that gives us:
[0, 1, 2] [2, 0, -1] [-1, -2, 0] [0, 1]
- image_match.signature_database_base.words_to_int(word_array: ndarray) ndarray[source]
Converts a simplified word to an integer
Encodes a k-byte word to int (as those returned by max_contrast). First digit is least significant.
Returns dot(word + 1, [1, 3, 9, 27 …] ) for each word in word_array
e.g.: [ -1, -1, -1] -> 0 [ 0, 0, 0] -> 13 [ 0, 1, 0] -> 16
- Parameters:
word_array (numpy.ndarray) – N x k array
- Returns:
an array of integers of length N (the integer word encodings)
- image_match.signature_database_base.max_contrast(array: ndarray) None[source]
Sets all positive values to one and all negative values to -1.
Needed for first pass lookup on word table.
- Parameters:
array (numpy.ndarray) – target array
- image_match.signature_database_base.normalized_distance(_target_array: ndarray, _vec: ndarray, nan_value: float = 1.0) ndarray[source]
Compute normalized distance to many points.
Computes || vec - b || / ( ||vec|| + ||b||) for every b in target_array
- Parameters:
_target_array (numpy.ndarray) – N x m array
_vec (numpy.ndarray) – array of size m
nan_value (Optional[float]) – value to replace 0.0/0.0 = nan with (default 1.0, to take those featureless images out of contention)
- Returns:
the normalized distance (float)
Elasticsearch
Requires the elasticsearch extra.
- class image_match.elasticsearch_driver.SignatureES(es: ClientT, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any)[source]
Bases:
SignatureDatabaseBase,GenericElasticsearch driver for image-match
- __init__(es: ClientT, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any) None[source]
Extra setup for Elasticsearch
- Parameters:
es (elasticsearch) – an instance of the elasticsearch python driver
index (Optional[string]) – a name for the Elasticsearch index (default ‘images’)
timeout (Optional[int]) – how long to wait on an Elasticsearch query, in seconds (default 10)
size (Optional[int]) – maximum number of Elasticsearch results (default 100)
delete_duplicates_limit (Optional[int]) – maximum number of duplicate candidates scanned per delete_duplicates call (default 10000)
minimum_should_match (Optional[int | str]) – require this many word clauses to match — e.g. 2 or “3<75%”; prunes low-overlap candidates on large indexes (default None = match any word)
use_filter_context (Optional[bool]) – wrap the word disjunction in a filter context so Elasticsearch skips BM25 scoring entirely; faster on large indexes but the returned ‘score’ is constant (default False)
*args (Optional) – Variable length argument list to pass to base constructor
**kwargs (Optional) – Arbitrary keyword arguments to pass to base constructor
Examples
>>> from elasticsearch import Elasticsearch >>> from image_match.elasticsearch_driver import SignatureES >>> es = Elasticsearch() >>> ses = SignatureES(es) >>> ses.add_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> ses.search_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') [ {'dist': 0.0, 'id': 'AVM37nMg0osmmAxpPvx6', 'path': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg', 'score': 0.28797293} ]
- delete_duplicates(path: str, limit: int | None = None) None[source]
Delete all but one entries in elasticsearch whose path value is equivalent to that of path.
- Parameters:
path (string) – path value to compare to those in the elastic search
limit (Optional[int]) – maximum number of duplicate candidates to scan; defaults to the instance’s delete_duplicates_limit (default 10000)
- delete_image(path: str, limit: int | None = None) int[source]
Delete all records whose stored path exactly equals path.
The path match query is fuzzy, so hits are filtered to exact matches before deletion — documents with merely similar paths are kept.
- Parameters:
path (string) – path value to remove entirely from the index
limit (Optional[int]) – maximum number of candidates to scan; defaults to the instance’s delete_duplicates_limit (default 10000)
- Returns:
the number of documents deleted
- insert_records(records: list[dict[str, Any]], refresh_after: bool = False, **kwargs: Any) int[source]
Bulk-insert pre-made records (see add_images).
- Parameters:
- Returns:
the number of successfully indexed records
- insert_single_record(rec: dict[str, Any], refresh_after: bool = False) None[source]
Insert an image record.
- Parameters:
rec (dict) – an image record in the format returned by make_record
refresh_after (Optional[boolean]) – refresh the index after inserting, making the record searchable immediately (default False)
- image_match.elasticsearch_driver.build_word_query(rec: dict[str, Any], pre_filter: PreFilter = None, minimum_should_match: int | str | None = None, use_filter_context: bool = False) dict[str, Any][source]
Build the bool/should term query over a record’s simple_word_* fields.
Removes ‘path’ and ‘metadata’ from rec; the caller pops ‘signature’. Stray stored fields (e.g. timestamp) must not leak into the query.
- Parameters:
rec – an image record in the format returned by make_record
pre_filter – an ES filter clause (or list of clauses) applied before matching
minimum_should_match – how many word clauses must match (default None = 1)
use_filter_context – evaluate the word disjunction in filter context, skipping scoring entirely (the returned _score is then constant)
- image_match.elasticsearch_driver.format_hits(hits: list[dict[str, Any]], signature: ndarray, distance_cutoff: float) list[dict[str, Any]][source]
Compute distances to a signature and return cutoff-filtered hit dicts.
Async
Requires the elasticsearch-async extra.
- class image_match.elasticsearch_async_driver.AsyncSignatureES(es: ClientT, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any)[source]
Bases:
SignatureDatabaseBase,GenericAsynchronous Elasticsearch driver for image-match.
API-identical to SignatureES, but every database-touching method is a coroutine and image decoding/signature generation is offloaded to a worker thread so the event loop is never blocked. Suited for embedding in async services such as FastAPI.
- Requires the ‘elasticsearch-async’ extra:
pip install image-match[elasticsearch-async]
Examples
>>> from elasticsearch import AsyncElasticsearch >>> from image_match.elasticsearch_async_driver import AsyncSignatureES >>> es = AsyncElasticsearch() >>> ses = AsyncSignatureES(es) >>> await ses.add_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> await ses.search_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') [ {'dist': 0.0, 'id': 'AVM37nMg0osmmAxpPvx6', 'path': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg', 'score': 0.28797293} ]
- __init__(es: ClientT, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any) None[source]
Extra setup for Elasticsearch
- Parameters:
es (elasticsearch.AsyncElasticsearch) – an async elasticsearch client instance
index (Optional[string]) – a name for the Elasticsearch index (default ‘images’)
timeout (Optional[int]) – how long to wait on an Elasticsearch query, in seconds (default 10)
size (Optional[int]) – maximum number of Elasticsearch results (default 100)
delete_duplicates_limit (Optional[int]) – maximum number of duplicate candidates scanned per delete_duplicates call (default 10000)
minimum_should_match (Optional[int | str]) – require this many word clauses to match — e.g. 2 or “3<75%”; prunes low-overlap candidates on large indexes (default None = match any word)
use_filter_context (Optional[bool]) – wrap the word disjunction in a filter context so Elasticsearch skips BM25 scoring entirely; faster on large indexes but the returned ‘score’ is constant (default False)
*args (Optional) – Variable length argument list to pass to base constructor
**kwargs (Optional) – Arbitrary keyword arguments to pass to base constructor
- async add_image(path: str, img: ImageInput | None = None, bytestream: bool = False, metadata: dict[str, Any] | None = None, *args: Any, **kwargs: Any) None[source]
Add a single image to the database.
Signature generation runs in a worker thread; only the index request is awaited.
- Parameters:
path (string) – path or identifier for image. If img=None, then path is assumed to be a URL or filesystem path
img (Optional[string]) – usually raw image data. In this case, path will still be stored, but a signature will be generated from data in img (default None)
bytestream (Optional[boolean]) – will the image be passed as raw bytes? (default False)
metadata (Optional) – any other information you want to include, can be nested (default None)
*args – Variable length argument list to pass to insert_single_record
**kwargs – Arbitrary keyword arguments to pass to insert_single_record
- async add_images(paths: list[ImageInput], metadata: dict[str, Any] | list[dict[str, Any] | None] | None = None, bytestream: bool = False, n_threads: int = 1, chunk_size: int | None = None, **kwargs: Any) int[source]
Generate signatures for many images and bulk-insert the records.
Signature generation runs in a worker thread (with its own thread pool when n_threads > 1); only the bulk request is awaited.
- Parameters:
paths (list) – paths, URLs, or raw image data
metadata (Optional) – a single dict applied to every record, or a list of dicts/None aligned with paths (default None)
bytestream (Optional[boolean]) – inputs are raw image bytes (default False)
n_threads (Optional[int]) – signature-generation threads (default 1)
chunk_size (Optional[int]) – process and insert in batches of this many images instead of buffering all records in memory (default None = one bulk request)
**kwargs – passed to insert_records (e.g. refresh_after — honored on the final chunk only)
- Returns:
the number of successfully indexed records
- async delete_duplicates(path: str, limit: int | None = None) None[source]
Delete all but one entries in elasticsearch whose path value is equivalent to that of path.
- Parameters:
path (string) – path value to compare to those in the elastic search
limit (Optional[int]) – maximum number of duplicate candidates to scan; defaults to the instance’s delete_duplicates_limit (default 10000)
- async delete_image(path: str, limit: int | None = None) int[source]
Delete all records whose stored path exactly equals path.
The path match query is fuzzy, so hits are filtered to exact matches before deletion — documents with merely similar paths are kept.
- Parameters:
path (string) – path value to remove entirely from the index
limit (Optional[int]) – maximum number of candidates to scan; defaults to the instance’s delete_duplicates_limit (default 10000)
- Returns:
the number of documents deleted
- async insert_records(records: list[dict[str, Any]], refresh_after: bool = False, **kwargs: Any) int[source]
Bulk-insert pre-made records (see add_images).
- Parameters:
- Returns:
the number of successfully indexed records
- async insert_single_record(rec: dict[str, Any], refresh_after: bool = False) None[source]
Insert an image record.
- Parameters:
rec (dict) – an image record in the format returned by make_record
refresh_after (Optional[boolean]) – refresh the index after inserting, making the record searchable immediately (default False)
- async search_image(path: ImageInput, all_orientations: bool = False, bytestream: bool = False, pre_filter: PreFilter = None, **kwargs: Any) list[dict[str, Any]][source]
Search for matches.
Image preprocessing and signature generation run in a worker thread; the search requests are awaited.
- Parameters:
path (string) – path or image data. If bytestream=False, then path is assumed to be a URL or filesystem path. Otherwise, it’s assumed to be raw image data
all_orientations (Optional[boolean]) – if True, search for all combinations of mirror images, rotations, and color inversions (default False)
bytestream (Optional[boolean]) – will the image be passed as raw bytes? (default False)
pre_filter (Optional[dict]) – filters list before applying the matching algorithm (default None)
**kwargs – Arbitrary keyword arguments to pass to search_single_record
- Returns:
a formatted list of dicts representing unique matches, sorted by dist
OpenSearch
Requires the opensearch extra.
- class image_match.opensearch_driver.SignatureOpenSearch(es: OpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any)[source]
Bases:
SignatureES[OpenSearch]OpenSearch driver for image-match.
Shares the record format and query DSL with SignatureES – OpenSearch is API-compatible with Elasticsearch 7.x for the operations used here. The only differences are in the opensearch-py client API: index() still takes a ‘body’ parameter, refresh is a params string, and ‘timeout’/’size’ are passed through params because opensearch-py reserves those kwargs.
- Install the client with the ‘opensearch’ extra:
pip install image-match[opensearch]
Examples
>>> from opensearchpy import OpenSearch >>> from image_match.opensearch_driver import SignatureOpenSearch >>> os_client = OpenSearch() >>> ses = SignatureOpenSearch(os_client) >>> ses.add_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> ses.search_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') [ {'dist': 0.0, 'id': 'AVM37nMg0osmmAxpPvx6', 'path': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg', 'score': 0.28797293} ]
- __init__(es: OpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any) None[source]
Extra setup for OpenSearch
- Parameters:
es (opensearchpy.OpenSearch) – an instance of the opensearch-py client
index (Optional[string]) – a name for the OpenSearch index (default ‘images’)
timeout (Optional[int]) – how long to wait on an OpenSearch query, in seconds (default 10)
size (Optional[int]) – maximum number of OpenSearch results (default 100)
delete_duplicates_limit (Optional[int]) – maximum number of duplicate candidates scanned per delete_duplicates call (default 10000)
minimum_should_match (Optional[int | str]) – require this many word clauses to match — e.g. 2 or “3<75%”; prunes low-overlap candidates on large indexes (default None = match any word)
use_filter_context (Optional[bool]) – wrap the word disjunction in a filter context so OpenSearch skips BM25 scoring entirely; faster on large indexes but the returned ‘score’ is constant (default False)
*args (Optional) – Variable length argument list to pass to base constructor
**kwargs (Optional) – Arbitrary keyword arguments to pass to base constructor
- insert_single_record(rec: dict[str, Any], refresh_after: bool = False) None[source]
Insert an image record.
- Parameters:
rec (dict) – an image record in the format returned by make_record
refresh_after (Optional[boolean]) – refresh the index after inserting, making the record searchable immediately (default False)
Async
Requires the opensearch-async extra.
- class image_match.opensearch_async_driver.AsyncSignatureOpenSearch(es: AsyncOpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any)[source]
Bases:
AsyncSignatureES[AsyncOpenSearch]Asynchronous OpenSearch driver for image-match.
Shares the record format and query DSL with AsyncSignatureES. Differs only in the opensearch-py client API: index() takes a ‘body’ parameter, refresh is a params string, and ‘timeout’/’size’ go through params because opensearch-py reserves those kwargs.
- Requires the ‘opensearch-async’ extra:
pip install image-match[opensearch-async]
Examples
>>> from opensearchpy import AsyncOpenSearch >>> from image_match.opensearch_async_driver import AsyncSignatureOpenSearch >>> os_client = AsyncOpenSearch() >>> ses = AsyncSignatureOpenSearch(os_client) >>> await ses.add_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> await ses.search_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg')
- __init__(es: AsyncOpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, minimum_should_match: int | str | None = None, use_filter_context: bool = False, *args: Any, **kwargs: Any) None[source]
Extra setup for OpenSearch
- Parameters:
es (opensearchpy.AsyncOpenSearch) – an async opensearch-py client instance
index (Optional[string]) – a name for the OpenSearch index (default ‘images’)
timeout (Optional[int]) – how long to wait on an OpenSearch query, in seconds (default 10)
size (Optional[int]) – maximum number of OpenSearch results (default 100)
delete_duplicates_limit (Optional[int]) – maximum number of duplicate candidates scanned per delete_duplicates call (default 10000)
minimum_should_match (Optional[int | str]) – require this many word clauses to match — e.g. 2 or “3<75%”; prunes low-overlap candidates on large indexes (default None = match any word)
use_filter_context (Optional[bool]) – wrap the word disjunction in a filter context so OpenSearch skips BM25 scoring entirely; faster on large indexes but the returned ‘score’ is constant (default False)
*args (Optional) – Variable length argument list to pass to base constructor
**kwargs (Optional) – Arbitrary keyword arguments to pass to base constructor
- async insert_single_record(rec: dict[str, Any], refresh_after: bool = False) None[source]
Insert an image record.
- Parameters:
rec (dict) – an image record in the format returned by make_record
refresh_after (Optional[boolean]) – refresh the index after inserting, making the record searchable immediately (default False)
k-NN drivers
Search the signature vector directly with approximate nearest
neighbours instead of word pre-filtering. See Migrating to the OpenSearch k-NN index.
- class image_match.opensearch_knn_driver.SignatureOpenSearchKNN(es: OpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, *args: Any, **kwargs: Any)[source]
Bases:
SignatureOpenSearchOpenSearch k-NN driver for image-match.
Stores the same record format as the word drivers, but searches with an approximate nearest-neighbour query (HNSW) directly on the ‘signature’ vector instead of the simple_word_* term disjunction. The k-NN hit list is rescored client-side with the usual normalized distance, so output format and distance_cutoff semantics are identical to SignatureES.
Trade-off vs. the word drivers: HNSW is approximate — it may miss a candidate the word query would have found, and vice versa. ‘size’ acts as the candidate count (k); raise it for better recall.
The index must be created with knn enabled — see knn_index_body() or tools/migrate_to_knn.py for converting an existing index.
- Requires the ‘opensearch’ extra:
pip install image-match[opensearch]
Examples
>>> from opensearchpy import OpenSearch >>> from image_match.opensearch_knn_driver import SignatureOpenSearchKNN, knn_index_body >>> os_client = OpenSearch() >>> os_client.indices.create(index="images_knn", body=knn_index_body(648)) >>> ses = SignatureOpenSearchKNN(os_client, index="images_knn") >>> ses.add_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> ses.search_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') [ {'dist': 0.0, 'id': 'AVM37nMg0osmmAxpPvx6', 'path': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg', 'score': 0.28797293} ]
- __init__(es: OpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, *args: Any, **kwargs: Any) None[source]
Extra setup for OpenSearch k-NN
- Parameters:
es (opensearchpy.OpenSearch) – an instance of the opensearch-py client
index (Optional[string]) – a name for the OpenSearch index (default ‘images’)
timeout (Optional[int]) – how long to wait on an OpenSearch query, in seconds (default 10)
size (Optional[int]) – number of ANN candidates (k) fetched per search before distance rescoring (default 100)
delete_duplicates_limit (Optional[int]) – maximum number of duplicate candidates scanned per delete_duplicates call (default 10000)
*args (Optional) – Variable length argument list to pass to base constructor
**kwargs (Optional) – Arbitrary keyword arguments to pass to base constructor
- class image_match.opensearch_knn_async_driver.AsyncSignatureOpenSearchKNN(es: AsyncOpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, *args: Any, **kwargs: Any)[source]
Bases:
AsyncSignatureOpenSearchAsynchronous OpenSearch k-NN driver for image-match.
Async counterpart of SignatureOpenSearchKNN: every database-touching method is a coroutine, image decoding/signature generation is offloaded to a worker thread, and all_orientations searches run concurrently. Suited for embedding in async services such as FastAPI.
The index must be created with knn enabled — see opensearch_knn_driver.knn_index_body() or tools/migrate_to_knn.py.
- Requires the ‘opensearch-async’ extra:
pip install image-match[opensearch-async]
Examples
>>> from opensearchpy import AsyncOpenSearch >>> from image_match.opensearch_knn_async_driver import AsyncSignatureOpenSearchKNN >>> os_client = AsyncOpenSearch() >>> ses = AsyncSignatureOpenSearchKNN(os_client, index="images_knn") >>> await ses.add_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> await ses.search_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg')
- __init__(es: AsyncOpenSearch, index: str = 'images', timeout: str = '10s', size: int = 100, delete_duplicates_limit: int = 10000, *args: Any, **kwargs: Any) None[source]
Extra setup for OpenSearch k-NN
- Parameters:
es (opensearchpy.AsyncOpenSearch) – an async opensearch-py client instance
index (Optional[string]) – a name for the OpenSearch index (default ‘images’)
timeout (Optional[int]) – how long to wait on an OpenSearch query, in seconds (default 10)
size (Optional[int]) – number of ANN candidates (k) fetched per search before distance rescoring (default 100)
delete_duplicates_limit (Optional[int]) – maximum number of duplicate candidates scanned per delete_duplicates call (default 10000)
*args (Optional) – Variable length argument list to pass to base constructor
**kwargs (Optional) – Arbitrary keyword arguments to pass to base constructor
- image_match.opensearch_knn_driver.knn_index_body(dimension: int, engine: str = 'lucene', space_type: str = 'l2', data_type: str = 'float') dict[str, Any][source]
Build the settings/mappings body for a hybrid k-NN index.
Only ‘signature’ is mapped explicitly (as knn_vector); all other fields rely on dynamic mapping, so ‘simple_word_*’ fields still index as long and the classic word-overlap drivers keep working on the same index.
- Parameters:
dimension (int) – signature vector length (648 for default n_grid=9)
engine (Optional[str]) – knn engine — ‘lucene’ (default, no native deps) or ‘faiss’; ‘nmslib’ is OS2-only and rejected for new indexes on OpenSearch 3
space_type (Optional[str]) – vector space — ‘l2’ (default) or ‘cosinesimil’/’innerproduct’/’hamming’ depending on engine
data_type (Optional[str]) – knn_vector data type — ‘float’ (default) or ‘byte’ (4x smaller; signatures are int8 so ‘byte’ fits losslessly; requires OpenSearch >= 2.9)
- Returns:
a dict suitable for client.indices.create(index=…, body=…)
- image_match.opensearch_knn_driver.build_knn_query(signature: Any, k: int, pre_filter: PreFilter = None) dict[str, Any][source]
Build the knn query over the record’s signature vector.
- Parameters:
signature – the image signature (list or ndarray)
k – number of approximate nearest neighbours to fetch
pre_filter – an OpenSearch filter clause (dict) or list of clauses applied inside the knn query
- image_match.opensearch_knn_driver.format_knn_hits(hits: list[dict[str, Any]], signature: ndarray, distance_cutoff: float) list[dict[str, Any]][source]
Rescore knn candidates with normalized_distance and filter by cutoff.
k-NN hits arrive in the same {_id, _score, _source} shape as word-query hits, so rescoring is shared with elasticsearch_driver.format_hits.
MongoDB
Requires the mongo extra.
- class image_match.mongodb_driver.SignatureMongo(collection: Collection[dict[str, Any]], *args: Any, **kwargs: Any)[source]
Bases:
SignatureDatabaseBaseMongoDB driver for image-match
- __init__(collection: Collection[dict[str, Any]], *args: Any, **kwargs: Any) None[source]
Additional MongoDB setup
- Parameters:
collection (collection) – a MongoDB collection instance
args (Optional) – Variable length argument list to pass to base constructor
kwargs (Optional) – Arbitrary keyword arguments to pass to base constructor
Examples
>>> from image_match.mongodb_driver import SignatureMongo >>> from pymongo import MongoClient >>> client = MongoClient(connect=False) >>> c = client.images.images >>> ses = SignatureMongo(c) >>> ses.add_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') >>> ses.search_image('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg') [ {'dist': 0.0, 'id': 'AVM37nMg0osmmAxpPvx7', 'path': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg/687px-Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg', 'score': 0.28797293} ]
- delete_image(path: str) int[source]
Delete all records whose path equals path.
- Parameters:
path (string) – path value to remove entirely from the collection
- Returns:
the number of documents deleted
- insert_records(records: list[dict[str, Any]], **kwargs: Any) int[source]
Bulk-insert pre-made records via insert_many (see add_images).
Creates the word indexes on first insert, like insert_single_record.
- insert_single_record(rec: dict[str, Any]) None[source]
Insert an image record, creating the word indexes if needed.
- Parameters:
rec (dict) – an image record in the format returned by make_record
- search_single_record(rec: dict[str, Any], pre_filter: PreFilter = None, *, n_parallel_words: int | None = 4, word_limit: int | None = None, process_timeout: float | None = None, maximum_matches: int = 1000) list[dict[str, Any]][source]
Search for a matching image record.
- Parameters:
rec (dict) – an image record in the format returned by make_record
pre_filter (Optional[dict]) – additional query conditions merged into each find query (default None)
n_parallel_words (Optional[int]) – number of words to scan in parallel; if None, uses cpu_count() (default 4)
word_limit (Optional[int]) – only scan this many words; if None, all N words are used (default None)
process_timeout (Optional[float]) – seconds to wait for a worker result before giving up; None waits indefinitely (default None)
maximum_matches (Optional[int]) – if a word matches more records than this, the word is considered non-discriminatory and skipped (default 1000)
- Returns:
a list of dicts representing unique matches
- image_match.mongodb_driver.get_next_match(result_q: Queue[dict[str, Any] | str], word: dict[str, Any], collection: Collection[dict[str, Any]], signature: np.ndarray, cutoff: float = 0.5, max_in_cursor: int = 100, pre_filter: dict[str, Any] | None = None) None[source]
Given a cursor, iterate through matches
Scans a cursor for word matches below a distance threshold. Exhausts a cursor, possibly enqueuing many matches Note that placing this function outside the SignatureCollection class breaks encapsulation. This is done for compatibility with multiprocessing.
- Parameters:
result_q (queue.Queue) – a queue in which to queue results
word (dict) – {word_name: word_value} dict to scan against
collection (collection) – a pymongo collection
signature (numpy.ndarray) – signature array to match against
cutoff (Optional[float]) – normalized distance limit (default 0.5)
max_in_cursor (Optional[int]) – if more than max_in_cursor matches are in the cursor, ignore this cursor; this column is not discriminatory (default 100)
pre_filter (Optional[dict]) – additional query conditions merged into the find query (default None)