Skip to content

Configuring a context

A context carries its settings next to the image: thirteen ph_context_set_*() functions, each with a default that a freshly created context already has. This page lists all of them, says when each one is read, which ones change the value of a hash, and how to give every context in a program, and every worker of a batch, the same configuration. What a setting does to a particular hash, measured, is on that algorithm's page.

Every setting

Eight settings are read by the hash, and only the algorithms they name read them.

Setter Sets Accepted Default
ph_context_set_gray_weights() the weights of R, G and B in the grayscale of aHash, dHash, pHash, wHash, mHash, BMH and Radial each ≥ 0, sum 1 … 8 421 504 38, 75, 15
ph_context_set_gamma() Radial's gamma, and nothing else's finite, (0.001, 1000] 1, the step skipped
ph_context_set_phash_params() pHash's DCT grid and block of coefficients dct_size from reduction_size to 32; reduction_size 4 … 8 32, 8
ph_context_set_whash_mode() wHash's mode PH_WHASH_FAST, PH_WHASH_FULL PH_WHASH_FAST
ph_context_set_whash_remove_max_haar_ll() wHash's removal of the coarsest LL band any int; non-zero is on off
ph_context_set_mhash_params() mHash's kernel scale and normalized size alpha > 1, level ≥ 0, a kernel of at most 65 on a side; size 62 … 4096 2, 1, 512
ph_context_set_block_params() BMH's grid, and with it the digest's size 2 … 32 16
ph_context_set_radial_params() Radial's projections, samples and blur 40 … 4096, 2 … 4096, σ in (0, 64/3] 180, 128, 3.5

Five are read by the load, and reach every algorithm through the image they load.

Setter Sets Accepted Default
ph_context_set_load_grayscale() one channel from the decoder instead of color; ColorHash and ColorMoments refuse such an image any int; non-zero is on off
ph_context_set_alpha_mode() the background of an image with alpha the four ph_alpha_mode_t values PH_ALPHA_BLEND_GREY
ph_context_set_auto_orient() applying the EXIF orientation any int; non-zero is on on
ph_context_set_decode_scale() decoding a JPEG at ½, ¼ or ⅛ of its size the four ph_decode_scale_t values PH_DECODE_SCALE_FULL
ph_context_set_max_pixels() the largest image a load accepts any uint64_t; 0 is no limit of its own 256 × 1024 × 1024

ColorHash and ColorMoments have no parameters, so only the load settings reach them. Loading images says when to change each of the five.

When a setting is read

flowchart LR
    S1[Load settings] --> L["ph_load_from_…()"]
    L --> I[Loaded image]
    I --> H["ph_compute_…()"]
    S2[Hash settings] --> H
    H --> R[Hash]

A hash setting is read by the next hash. The image already loaded is hashed again with the new value, without a reload. The grayscale weights are one of these: setting them drops the grayscale cached from the loaded image, and the next hash converts it again.

A load setting is read by the next load. The image in the context keeps what it was loaded with: grayscale loading switched on after a color load leaves the color image, and ColorHash still works on it until the next load replaces it.

A refused value changes nothing

Every setter returns PH_SUCCESS for a value in range and PH_ERR_INVALID_ARGUMENT for anything else, a NULL context included. A refused call leaves the whole configuration as it was: a value is never clamped into range, a pair is never half applied, and a refusal never installs the defaults. Three weights that sum to zero are an error, not a request for the standard ones.

Each upper bound is a limit of the implementation: the 64 bits of a hash, the 1024 bits of a digest, the largest kernel the buffer holds. A lower bound keeps the hash meaningful: below a 4×4 block most unrelated images share a pHash, and a Radial line of one sample has no variance to measure. An unknown enum value is refused too, as a C caller or a binding can pass any integer through an enum parameter. A setter called with a constant needs no check; one called with a value read from a file, a command line or another program does, since the hashes computed after an ignored refusal use the setting that was there before.

Settings that change the hash

A hash or digest does not record the settings it was computed with, and neither does its text form. Two hashes are comparable only when computed with the same settings, so hashes are stored next to the configuration that made them, and recomputed when it changes. Every setting except the size limit can change a value:

  • Always, for the algorithms it names: the pHash, wHash, mHash, BMH and Radial parameters, and the grayscale weights for the seven grayscale hashes. The BMH grid changes the digest's length too, except between sizes that round to the same number of bytes; the other parameters keep it, so a digest from another configuration is compared without complaint, and the number returned measures nothing.
  • Only on some images: the gamma, when it is not 1; the background, on an image with an alpha channel; the orientation, on an image whose tag turns or mirrors it; the decode scale, on a JPEG read by the native decoder. A program tested on opaque, untagged PNGs sees no change from the last three, until the first image that has what they act on.
  • By a level here and there: grayscale loading, on a JPEG, whose decoder rounds the luma where the library's conversion truncates (measured). Grayscale weights do nothing to an image loaded as grayscale, which has no color to weigh.
  • Never: ph_context_set_max_pixels() decides whether a load succeeds, not what it loads.

Reading the configuration back

ph_context_get_gray_weights() is the only getter. The grayscale weights are the one setting stored differently from what was passed: they are scaled to a sum of 128, the red and green shares rounded down and blue taking the rest, so (1, 1, 1) is stored as (42, 42, 44). Every other setter stores its value as given, a flag as on or off, so the value the program passed is the value in use.

A digest's size follows from the configuration too, and ph_digest_info() reports it for a context without an image: BMH's is block_size² bits rounded up to whole bytes, and every other algorithm's is fixed.

The same settings everywhere

There is no function that copies one context's settings into another. A program that hashes on several threads, where each thread has its own context, configures every context with one function, so that a new setting cannot reach some contexts and miss others (the configure() function in the example below).

The batch functions take the configuration from a template instead: ph_hash_files_ex() and ph_hash_buffers_ex() copy everything set on options.config into every worker's context, on the calling thread, before any worker starts; an image loaded on the template is ignored. ph_hash_files() and ph_hash_buffers() take no context and always use the defaults, the default size limit included. A batch computes the four 64-bit hashes, so of the hash settings it reads only the grayscale weights and the pHash and wHash ones (Hashing many files).

In code

The example configures a context in one function, reads back what it stored, has a value refused, and shows a hash setting taking effect at the next hash and a load setting at the next load.

examples/configure.c
/* A context configured in one function, a refused value that changes nothing, the one
 * setting that reads back differently, and when a setting takes effect: at the next hash
 * or at the next load. Build/run:
 *   cc configure.c -o configure $(pkg-config --cflags --libs libphash)
 *   ./configure image.jpg
 */
#include <libphash.h>
#include <stdio.h>

/* Every context that computes hashes to be compared with each other is configured by
 * this one function, so that they all compute them alike. Each setter returns
 * PH_ERR_INVALID_ARGUMENT for a value out of range and leaves the context as it was. */
static ph_error_t configure(ph_context_t *ctx) {
    ph_error_t err;
    if ((err = ph_context_set_max_pixels(ctx, 50u * 1000u * 1000u)) != PH_SUCCESS ||
        (err = ph_context_set_alpha_mode(ctx, PH_ALPHA_BLEND_WHITE)) != PH_SUCCESS ||
        (err = ph_context_set_gray_weights(ctx, 1, 1, 1)) != PH_SUCCESS ||
        (err = ph_context_set_block_params(ctx, 8)) != PH_SUCCESS) {
        return err;
    }
    return PH_SUCCESS;
}

static int fail(ph_context_t *ctx, const char *what, ph_error_t err) {
    fprintf(stderr, "%s: %s (%s)\n", what, ph_get_error_string(err),
            ph_get_last_error_message(ctx));
    ph_free(ctx);
    return 1;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s <image>\n", argv[0]);
        return 1;
    }
    ph_context_t *ctx = NULL;
    if (ph_create(&ctx) != PH_SUCCESS) {
        fprintf(stderr, "ph_create failed\n");
        return 1;
    }
    ph_error_t err = configure(ctx);
    if (err != PH_SUCCESS) {
        return fail(ctx, "configure", err);
    }

    /* The grayscale weights are stored normalized to a sum of 128, so they are the one
     * setting with a getter. A digest's size follows from the settings too. */
    int r, g, b;
    size_t bmh_size;
    if (ph_context_get_gray_weights(ctx, &r, &g, &b) != PH_SUCCESS ||
        ph_digest_info(ctx, PH_ALGO_BMH, &bmh_size, NULL) != PH_SUCCESS) {
        return fail(ctx, "read back", PH_ERR_INVALID_ARGUMENT);
    }
    printf("gray weights (1, 1, 1) are stored as (%d, %d, %d)\n", r, g, b);
    printf("BMH digest at block_size 8: %zu bytes\n", bmh_size);

    if ((err = ph_load_from_file(ctx, argv[1])) != PH_SUCCESS) {
        return fail(ctx, argv[1], err);
    }
    uint64_t before, after;
    if ((err = ph_compute_phash(ctx, &before)) != PH_SUCCESS) {
        return fail(ctx, "pHash", err);
    }

    /* A 9x9 block does not fit 64 bits: refused, and pHash is computed as before. */
    err = ph_context_set_phash_params(ctx, 32, 9);
    printf("set_phash_params(32, 9): %s\n", ph_get_error_string(err));
    if ((err = ph_compute_phash(ctx, &after)) != PH_SUCCESS) {
        return fail(ctx, "pHash", err);
    }
    int failed = 0;
    if (after != before) {
        printf("a refused value changed the hash\n");
        failed = 1;
    }

    /* A hash setting is read by the next hash: no reload. */
    ph_context_set_phash_params(ctx, 32, 6);
    if ((err = ph_compute_phash(ctx, &after)) != PH_SUCCESS) {
        return fail(ctx, "pHash", err);
    }
    printf("pHash, 8x8 block: %016llx\npHash, 6x6 block: %016llx\n", (unsigned long long)before,
           (unsigned long long)after);

    /* A load setting is read by the next load: the image in the context stays in color
     * until it is loaded again. */
    ph_context_set_load_grayscale(ctx, 1);
    ph_digest_t color;
    ph_error_t before_reload = ph_compute_color_hash(ctx, &color);
    if ((err = ph_load_from_file(ctx, argv[1])) != PH_SUCCESS) {
        return fail(ctx, argv[1], err);
    }
    ph_error_t after_reload = ph_compute_color_hash(ctx, &color);
    printf("ColorHash before the reload: %s\n", ph_get_error_string(before_reload));
    printf("ColorHash after the reload:  %s\n", ph_get_error_string(after_reload));

    ph_free(ctx);
    if (r + g + b != 128 || bmh_size != 8 || after == before || before_reload != PH_SUCCESS ||
        after_reload != PH_ERR_REQUIRES_COLOR) {
        printf("unexpected result\n");
        failed = 1;
    }
    return failed;
}