Loading images¶
Every hash reads the image last loaded into a context. This page covers the three ways to load one, which formats each build reads, how to load images you do not trust, the settings that shape what is loaded, and how one context serves many images. Image preparation explains what each step does to a hash, with measurements; this page says when to reach for it.
flowchart LR
F[File] -->|opened once,<br/>mapped or read| B[Encoded bytes]
M[Buffer in memory] --> B
B --> D[Decoder]
D --> T[Transparency]
T --> O[Orientation]
P[Pixels in memory] --> T2[Transparency]
O --> I[Loaded image]
T2 --> I
Three sources¶
| Source | Function | Use it for |
|---|---|---|
| A file | ph_load_from_file() |
images on disk |
| An encoded file in memory | ph_load_from_memory() |
bytes from a network, a database, an archive |
| Decoded pixels | ph_load_from_pixels() |
a frame another library has already decoded |
The program below makes one picture and loads it all three ways: as pixels, as a PPM file encoded in a buffer, and as the same file on disk. It prints the same pHash three times.
/* Pixels: width, height, channels, and the bytes from one row to the next. */
err = ph_load_from_pixels(ctx, frame, WIDTH, HEIGHT, 3, STRIDE);
if (err != PH_SUCCESS) {
failed = report(ctx, "pixels", err);
} else {
failed = print_phash(ctx, "pixels", &from_pixels);
}
/* Memory: an encoded file -- from a network, a database, an archive -- as bytes. */
err = ph_load_from_memory(ctx, encoded, length);
if (err != PH_SUCCESS) {
failed |= report(ctx, "memory", err);
} else {
failed |= print_phash(ctx, "memory", &from_memory);
}
/* File: the same bytes on disk. */
err = ph_load_from_file(ctx, argv[1]);
if (err != PH_SUCCESS) {
failed |= report(ctx, "file", err);
} else {
failed |= print_phash(ctx, "file", &from_file);
}
The whole program is
examples/load_sources.c.
From a file or a buffer¶
ph_load_from_file() opens the file once and
hands its whole content to the decoder, memory-mapped where the platform has mmap and
read into a buffer otherwise. From there it is
ph_load_from_memory(): the same decoders, the
same limits, the same orientation, and the same hash for the same bytes, as if you had
read the file yourself.
The decoder is chosen by the first bytes, never by the file name, so an image with the wrong extension loads as what it is. A buffer may run on past the image's end; the bytes after it are ignored. A buffer that ends before the image does is refused, as a truncated file is (Decoding).
From pixels¶
ph_load_from_pixels() copies pixels you already
have. It takes:
- width and height, both above 0;
- channels: 1 (gray), 3 (RGB) or 4 (RGBA with straight alpha, not premultiplied), interleaved;
- stride, the number of bytes from the start of one row to the start of the next, or 0 for rows packed with no gap. The example pads every row to 512 bytes and passes 512.
The channels are read as red, green, blue in that order. A buffer in BGR order, as OpenCV keeps its images, gives other grayscale values and other color statistics: swap the channels before loading.
A 4-channel image is composited onto the alpha mode's background, like a
decoded one. Nothing in a pixel buffer says which way is up, so there is no orientation to
apply: pass the pixels upright. The pixel count is held to
max_pixels; the per-side limit, which protects decoders,
does not apply.
The same pixels hash the same whichever way they arrive: the example's three hashes are equal, and it exits with an error if they are not.
Formats and builds¶
The formats a build reads depend on which decoders it was built with (Installing):
| Format | Full build | Minimal build |
|---|---|---|
| JPEG | libjpeg-turbo | stb_image |
| PNG | libpng | stb_image |
| WebP | libwebp | refused: PH_ERR_DECODER_UNAVAILABLE |
| BMP, GIF, TGA, PSD, HDR, PIC, PNM | stb_image | stb_image |
| TIFF and anything else | refused: PH_ERR_UNSUPPORTED_FORMAT |
the same |
Two kinds of file load other than a reader might expect:
- An animated GIF loads as its first frame. There is no way to choose another.
- An animated WebP is refused as
PH_ERR_CORRUPT_DATA. libwebp's simple decoder, which the library uses, cannot read a frame out of an animation. A still WebP loads as usual.
A program that depends on a format asks the build at run time:
ph_can_use_jpeg(),
ph_can_use_png() and
ph_can_use_webp() say whether the native decoder is
there. ph_get_build_info() names every decoder in
one line, for a log or a bug report:
/* The smallest program against an installed libphash: prints the version and the build
* it links. See CMakeLists.txt next to this file for the build. */
#include <libphash.h>
#include <stdio.h>
int main(void) {
ph_context_t *ctx = NULL;
if (ph_create(&ctx) != PH_SUCCESS) {
fprintf(stderr, "ph_create failed\n");
return 1;
}
printf("libphash %s (header %s)\n", ph_version(), PH_VERSION_STRING);
printf("%s\n", ph_get_build_info());
ph_free(ctx);
return 0;
}
The two JPEG decoders round differently, so a JPEG can hash a few bits apart between a Full and a Minimal build. Hashes stored from one build are compared with another's by distance, not equality (Same hash on every machine).
Untrusted and large images¶
A small file can declare an enormous image. The library checks the dimensions a file
declares before it allocates anything for its pixels, and refuses an image over the limit
with PH_ERR_IMAGE_TOO_LARGE:
ph_context_set_max_pixels()sets the most pixels, width × height, a load accepts; 256 × 1024 × 1024 by default. A service that hashes uploads sets it to the largest image it expects. 0 removes this limit but not the fixed ones.- Fixed limits always apply: 2 147 483 647 pixels, the largest count the library indexes; 1 000 000 pixels on either side, so that one enormous row cannot pass the area limit; and 2 GiB − 1 byte of encoded file (Size limits).
What a load holds in memory, at its peak, is the encoded file (mapped or read), the decoded image at 3 bytes a pixel (1 when loaded as grayscale), and a second image of the same size while an orientation is applied. The first grayscale hash adds 1 byte a pixel, kept until the next load.
A large JPEG can also be decoded at ½, ¼ or ⅛ of its size with
ph_context_set_decode_scale(). That
saves time after the decode more than in it, and changes some hashes more than others:
Decoding at a reduced scale has
both measured. The limit is still judged by the full dimensions.
Settings that shape the image¶
These settings are read when an image is loaded, so they apply from the next load on; an image already loaded keeps what it was loaded with. Hashes are comparable only when computed with the same settings.
| Setting | Default | Change it when |
|---|---|---|
ph_context_set_auto_orient() |
on: the EXIF orientation is applied | the stored orientation is what you want to compare (Orientation) |
ph_context_set_alpha_mode() |
composited on gray | your images are cut-outs whose visible part is close to mid-gray, or you match hashes from ImageHash (Transparency) |
ph_context_set_load_grayscale() |
off: color | you compute no color hash (below) |
ph_context_set_decode_scale() |
full size | large JPEGs, and a hash that tolerates it (above) |
ph_context_set_max_pixels() |
256 × 1024 × 1024 | above |
Transparency¶
An image with an alpha channel is composited onto a background as it loads, mid-gray by
default, so that the color stored under transparent pixels, which no viewer shows, does
not reach the hash. No background is best for every algorithm: on white, wHash gives
every mostly transparent image the same hash; on black, BMH does. Which background suits
which algorithm is measured in Which background.
PH_ALPHA_IGNORE keeps the stored color, which is what ImageHash hashes.
Loading as grayscale¶
ph_context_set_load_grayscale()
asks the decoder for one channel instead of three. A JPEG's decoder then skips its color
conversion, stb_image converts as it decodes, the PNG decoder converts each row as it is
decoded, the WebP decoder converts once the image is decoded, and the library skips its
own conversion: the loaded image is a third of the size, and the grayscale hashes read it
as it is.
Two JPEG files; the minimum of the runs, on the machine named at the foot of the page:
| Load | 400×400: load | 400×400: load + pHash | 5472×3648: load | 5472×3648: load + pHash |
|---|---|---|---|---|
| In color, the default | 0.53 ms | 0.78 ms | 105 ms | 128 ms |
As grayscale, ph_context_set_load_grayscale(ctx, 1) |
0.36 ms | 0.44 ms | 83.5 ms | 88.6 ms |
| In color, into a context created and freed for it | 0.53 ms | 0.76 ms | 105 ms | 128 ms |
The saving is a part of the load, not a multiple of it: the decoding of the compressed data stays.1 It is worth taking for a JPEG when every hash you compute is a grayscale one; on a PNG or a WebP it saves memory, not time (Decoding by format). Two things come with it:
- ColorHash and ColorMoments refuse the image with
PH_ERR_REQUIRES_COLOR: one channel has no color to count. - A JPEG's grayscale comes from the file, rounded where the library's own conversion truncates, so a hash can move by a bit or two against a color load of the same file (The decoder's grayscale). Hashes stored with the setting on are compared with hashes computed with it on.
How this was measured
Each case of site_stages time loads the file, and computes pHash where the table
says so, in a loop: one warm-up, then at least 5 runs and 1 second; the table gives
the fastest run. Without "created and freed", each kind of load has a context of its
own, kept from run to run.
# Each row: how the image is loaded, the case that only loads it, the case that loads it
# and computes pHash. "decode" and "scale_full_phash" load into a context kept across runs.
LOADS = (
("In color, the default", "decode", "scale_full_phash"),
("As grayscale, `ph_context_set_load_grayscale(ctx, 1)`", "gray_decode", "gray_phash"),
("In color, into a context created and freed for it", "fresh_decode", "fresh_phash"),
)
/* Seconds on a monotonic clock, as tests/src/bench_hash.c reads it. The calendar clock
* (timespec_get() with TIME_UTC) would step back when the system corrects its time, and a
* run spanning the step would come out negative and become the minimum. On Darwin,
* CLOCK_MONOTONIC counts whole microseconds, 2 % of the cheapest hash; mach_absolute_time()
* counts nanoseconds. */
static double seconds(void) {
#ifdef __APPLE__
static mach_timebase_info_data_t tb;
if (tb.denom == 0) {
mach_timebase_info(&tb);
}
return (double)mach_absolute_time() * tb.numer / tb.denom * 1e-9;
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
#endif
}
/* One timed case: `run` does the work once on a loaded image and returns nonzero on
* failure; `arg` selects a variant of it. A case named after an algorithm, or starting
* with its name and "_", is shown on that algorithm's page. */
typedef struct {
const char *name;
int (*run)(ph_context_t *ctx, const char *path, int arg);
int arg;
} time_case_t;
static int time_decode(ph_context_t *ctx, const char *path, int arg) {
(void)arg;
return ph_load_from_file(ctx, path) != PH_SUCCESS;
}
/* The first hash computed on a loaded image, which is what a caller who loads an image
* and hashes it pays: the grayscale conversion and the shared area grid are dropped
* before every run, so each run computes them (the dropping is untimed). */
static int time_hash(ph_context_t *ctx, const char *path, int algo) {
(void)path;
ph_digest_t d;
return ph_compute_digest(ctx, (ph_algorithm_t)algo, &d) != PH_SUCCESS;
}
/* The four 64-bit hashes in one ph_compute_multi() call, which computes the grayscale
* image and the area grid once for all of them. */
static int time_multi(ph_context_t *ctx, const char *path, int arg) {
(void)path;
(void)arg;
uint64_t h[PH_HASH_FLAGS_COUNT];
return ph_compute_multi(ctx, PH_HASH_AHASH | PH_HASH_DHASH | PH_HASH_PHASH | PH_HASH_WHASH,
h) != PH_SUCCESS;
}
static int time_whash_full(ph_context_t *ctx, const char *path, int arg) {
(void)path;
(void)arg;
uint64_t h;
int bad = ph_context_set_whash_mode(ctx, PH_WHASH_FULL) != PH_SUCCESS ||
ph_compute_whash(ctx, &h) != PH_SUCCESS;
return ph_context_set_whash_mode(ctx, PH_WHASH_FAST) != PH_SUCCESS || bad;
}
static int time_mhash_size(ph_context_t *ctx, const char *path, int size) {
(void)path;
ph_digest_t d;
int bad = ph_context_set_mhash_params(ctx, PH_MH_ALPHA, PH_MH_LEVEL, size) != PH_SUCCESS ||
ph_compute_mhash(ctx, &d) != PH_SUCCESS;
return ph_context_set_mhash_params(ctx, PH_MH_ALPHA, PH_MH_LEVEL, PH_MH_IMAGE_SIZE) !=
PH_SUCCESS ||
bad;
}
/* pHash with a dct_size other than the default, and the default block. */
static int time_phash_dct(ph_context_t *ctx, const char *path, int size) {
(void)path;
uint64_t h;
int bad = ph_context_set_phash_params(ctx, size, PH_DCT_REDUCTION_SIZE) != PH_SUCCESS ||
ph_compute_phash(ctx, &h) != PH_SUCCESS;
return ph_context_set_phash_params(ctx, PH_DCT_SIZE, PH_DCT_REDUCTION_SIZE) != PH_SUCCESS ||
bad;
}
/* Radial with the settings of radial_variants[variant]. */
static int time_radial(ph_context_t *ctx, const char *path, int variant) {
(void)path;
ph_digest_t d;
int bad = !radial_settings(ctx, &radial_variants[variant]) ||
ph_compute_radial_hash(ctx, &d) != PH_SUCCESS;
return !radial_settings(ctx, &radial_variants[RADIAL_DEFAULT]) || bad;
}
/* Loading the image, then, unless `algo` is LOAD_ONLY, computing that algorithm: what a
* caller pays for a load and a hash. `arg` packs the load -- a ph_decode_scale_t, plus
* LOAD_GRAY for ph_context_set_load_grayscale() and LOAD_FRESH for a context created and
* freed around the load, as a caller who creates one per image does -- and the algorithm
* above it. Without LOAD_FRESH, a context of its own per kind of load, kept across runs,
* so the cases after it hash the image as it was first loaded. */
enum {
LOAD_GRAY = 4,
LOAD_FRESH = 8,
LOAD_ALGO = 16,
LOAD_ONLY = -1,
};
static int time_load(ph_context_t *ctx, const char *path, int arg) {
(void)ctx;
static ph_context_t *kept[LOAD_FRESH];
int kind = arg % LOAD_FRESH, fresh = arg & LOAD_FRESH, algo = arg / LOAD_ALGO - 1;
ph_context_t *c = fresh ? NULL : kept[kind];
if (!c && ph_create(&c) != PH_SUCCESS) {
return 1;
}
ph_digest_t d;
int bad = ph_context_set_decode_scale(c, (ph_decode_scale_t)(kind & 3)) != PH_SUCCESS ||
ph_context_set_load_grayscale(c, (kind & LOAD_GRAY) != 0) != PH_SUCCESS ||
ph_load_from_file(c, path) != PH_SUCCESS ||
(algo != LOAD_ONLY && ph_compute_digest(c, (ph_algorithm_t)algo, &d) != PH_SUCCESS);
if (fresh) {
ph_free(c);
} else {
kept[kind] = c;
}
return bad;
}
#define LOADED(name, flags, algo) {name, time_load, (flags) + ((algo) + 1) * LOAD_ALGO}
static const time_case_t time_cases[] = {
{"decode", time_decode, 0},
{"ahash", time_hash, PH_ALGO_AHASH},
{"dhash", time_hash, PH_ALGO_DHASH},
{"phash", time_hash, PH_ALGO_PHASH},
{"whash", time_hash, PH_ALGO_WHASH},
{"whash_full", time_whash_full, 0},
{"bmh", time_hash, PH_ALGO_BMH},
{"mhash", time_hash, PH_ALGO_MHASH},
{"radial", time_hash, PH_ALGO_RADIAL},
{"color_hash", time_hash, PH_ALGO_COLOR_HASH},
{"color_moments", time_hash, PH_ALGO_COLOR_MOMENTS},
{"multi", time_multi, 0},
{"mhash_size_62", time_mhash_size, 62},
{"mhash_size_128", time_mhash_size, 128},
{"mhash_size_256", time_mhash_size, 256},
{"mhash_size_512", time_mhash_size, 512},
{"mhash_size_1024", time_mhash_size, 1024},
{"mhash_size_2048", time_mhash_size, 2048},
{"mhash_size_4096", time_mhash_size, 4096},
{"phash_dct_8", time_phash_dct, 8},
{"phash_dct_16", time_phash_dct, 16},
{"phash_dct_24", time_phash_dct, 24},
{"radial_sigma_1", time_radial, RADIAL_SIGMA_1},
{"radial_sigma_8", time_radial, RADIAL_SIGMA_8},
{"radial_gamma_2", time_radial, RADIAL_GAMMA_2},
{"radial_grid_40x32", time_radial, RADIAL_GRID_40X32},
{"radial_grid_90x64", time_radial, RADIAL_GRID_90X64},
{"radial_grid_360x256", time_radial, RADIAL_GRID_360X256},
{"radial_grid_1440x1024", time_radial, RADIAL_GRID_1440X1024},
{"radial_grid_4096x4096", time_radial, RADIAL_GRID_4096X4096},
LOADED("scale_half_decode", PH_DECODE_SCALE_HALF, LOAD_ONLY),
LOADED("scale_quarter_decode", PH_DECODE_SCALE_QUARTER, LOAD_ONLY),
LOADED("scale_eighth_decode", PH_DECODE_SCALE_EIGHTH, LOAD_ONLY),
LOADED("scale_full_phash", PH_DECODE_SCALE_FULL, PH_ALGO_PHASH),
LOADED("scale_half_phash", PH_DECODE_SCALE_HALF, PH_ALGO_PHASH),
LOADED("scale_quarter_phash", PH_DECODE_SCALE_QUARTER, PH_ALGO_PHASH),
LOADED("scale_eighth_phash", PH_DECODE_SCALE_EIGHTH, PH_ALGO_PHASH),
LOADED("scale_full_mhash", PH_DECODE_SCALE_FULL, PH_ALGO_MHASH),
LOADED("scale_half_mhash", PH_DECODE_SCALE_HALF, PH_ALGO_MHASH),
LOADED("scale_quarter_mhash", PH_DECODE_SCALE_QUARTER, PH_ALGO_MHASH),
LOADED("scale_eighth_mhash", PH_DECODE_SCALE_EIGHTH, PH_ALGO_MHASH),
LOADED("scale_full_radial", PH_DECODE_SCALE_FULL, PH_ALGO_RADIAL),
LOADED("scale_half_radial", PH_DECODE_SCALE_HALF, PH_ALGO_RADIAL),
LOADED("scale_quarter_radial", PH_DECODE_SCALE_QUARTER, PH_ALGO_RADIAL),
LOADED("scale_eighth_radial", PH_DECODE_SCALE_EIGHTH, PH_ALGO_RADIAL),
LOADED("gray_decode", LOAD_GRAY, LOAD_ONLY),
LOADED("gray_phash", LOAD_GRAY, PH_ALGO_PHASH),
LOADED("gray_mhash", LOAD_GRAY, PH_ALGO_MHASH),
LOADED("gray_radial", LOAD_GRAY, PH_ALGO_RADIAL),
LOADED("fresh_decode", LOAD_FRESH, LOAD_ONLY),
LOADED("fresh_phash", LOAD_FRESH, PH_ALGO_PHASH),
};
static int cmp_double(const void *a, const void *b) {
double x = *(const double *)a, y = *(const double *)b;
return (x > y) - (x < y);
}
/* One warm-up run, then at least TIME_MIN_RUNS runs and TIME_MIN_SECONDS, at most
* TIME_MAX_RUNS; the minimum is what the pages quote. */
enum {
TIME_MIN_RUNS = 5,
TIME_MAX_RUNS = 300,
};
#define TIME_MIN_SECONDS 1.0
/* Whether case `name` is among the `n` names of `only`; every case is when `n` is 0. */
static int time_chosen(const char *name, int n, char **only) {
for (int i = 0; i < n; i++) {
if (strcmp(name, only[i]) == 0) {
return 1;
}
}
return n == 0;
}
/* site_stages time <image> [case...]: every case of time_cases[] on the image, or the cases
* named; the minimum and the median of its runs, in milliseconds, as one JSON object, with
* the build that ran them. */
int mode_time(int argc, char **argv) {
for (int i = 1; i < argc; i++) {
int known = 0;
for (size_t c = 0; c < COUNT(time_cases); c++) {
known |= strcmp(argv[i], time_cases[c].name) == 0;
}
if (!known) {
return fail("no such timed case", argv[i]);
}
}
ph_context_t *ctx = NULL;
if (load_image(&ctx, argv[0])) {
return 1;
}
json_t j = json_begin(stdout);
json_int(&j, "width", ctx->image.width);
json_int(&j, "height", ctx->image.height);
json_string(&j, "build_info", ph_get_build_info());
#ifdef __VERSION__
json_string(&j, "compiler", __VERSION__);
#endif
json_t cases = json_object(&j, "cases");
static double ms[TIME_MAX_RUNS];
for (size_t c = 0; c < COUNT(time_cases); c++) {
const time_case_t *tc = &time_cases[c];
if (!time_chosen(tc->name, argc - 1, argv + 1)) {
continue;
}
int runs = 0;
double total = 0.0;
for (int k = -1; k < TIME_MAX_RUNS; k++) { /* k = -1: the warm-up */
ph_drop_gray_cache(ctx);
double t0 = seconds();
if (tc->run(ctx, argv[0], tc->arg)) {
ph_free(ctx);
return fail("timed case failed", tc->name);
}
double t = seconds() - t0;
if (k >= 0) {
ms[runs++] = t * 1e3;
total += t;
if (runs >= TIME_MIN_RUNS && total >= TIME_MIN_SECONDS) {
break;
}
}
}
qsort(ms, (size_t)runs, sizeof(ms[0]), cmp_double);
json_t o = json_object(&cases, tc->name);
json_double(&o, "min_ms", ms[0]);
json_double(&o, "median_ms", ms[runs / 2]);
json_int(&o, "runs", runs);
json_close_object(&o);
}
json_close_object(&cases);
json_end(&j);
ph_free(ctx);
return 0;
}
One context, many images¶
A context holds one image at a time, and its settings. Load the next image into the same context:
- A load replaces the image. It frees the previous image and the grayscale cached from it before it decodes the next one. The settings stay as they were set.
- A failed load from a file or a buffer leaves the context empty, whatever it failed
on, its arguments included: the previous image is dropped as soon as the call starts,
so the next hash fails with
PH_ERR_EMPTY_IMAGErather than describing the image before it.ph_load_from_pixels()is checked before it replaces anything, and a call it refuses leaves the previous image loaded.ph_is_loaded()andph_context_get_dimensions()say what the context holds. - Hashes of one image share their work. The grayscale image and the area sums are
computed by the first hash that needs them and kept until the next load, so several
hashes of one load cost less than as many loads.
ph_compute_multi()computes several 64-bit hashes in one call.
A context created and freed for each image takes about as long as one kept, the last row of the table above: keeping one saves little time, and keeps the settings in one place.
A context is used by one thread at a time. A program that loads on several threads gives
each its own context, or hands the files to
ph_hash_files(), whose workers do the same
(Hashing many files).
When a load fails¶
A failed load returns one of seven codes, and a failed file or buffer load leaves no image in the context. Handling errors lists what each code means and what to do about it, which step of a load returns it, and the detail message that names the file and the reason.
-
Measured when this site was built, on Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 4 cores, Linux x86_64; compiled by 13.3.0, optimized, with libjpeg-turbo decoding the JPEGs: the minimum of at least five runs after a warm-up. What each time covers, and the code, are under “How this was measured” beside it. ↩