Loading images¶
ph.hash() and ImageContext.load() take any of these sources and tell them apart by
type:
| Source | Example | Goes through |
|---|---|---|
| a path | "photo.jpg", pathlib.Path(...) |
libphash opens and decodes the file |
| encoded bytes | bytes, bytearray, a 1-D memoryview |
decoded from memory, without a copy |
| a binary file object | open(..., "rb"), io.BytesIO, a response body |
read to its end, then as bytes |
| an array of pixels | a numpy uint8 array, a 2-D or 3-D memoryview |
hashed from the pixels |
| a Pillow image | PIL.Image.open(...) |
its pixels |
Bytes are always the encoded file, never a path. Each source has its own method on a
context too: load_file(), load_bytes(), load_fileobj(), load_pixels(),
load_pil().
import io
from pathlib import Path
import libphash as ph
data = Path("photo.jpg").read_bytes()
assert ph.hash("photo.jpg") == ph.hash(data) == ph.hash(io.BytesIO(data))
Files and encoded data¶
libphash decodes JPEG with libjpeg-turbo, PNG with libpng and WebP with libwebp; BMP,
GIF (the first frame), TGA, PSD, HDR, PIC and PNM with stb_image. A file is decoded as
it is displayed: the EXIF orientation of a JPEG or PNG is applied (Config.auto_orient),
and transparency is composited onto gray (Config.alpha_mode).
A load fails with an exception that says why (Handling errors): a missing
file is a FileNotFoundError, a file that is not an image an UnsupportedFormatError, a
truncated or damaged one a CorruptDataError, and an image larger than
Config.max_pixels an ImageTooLargeError, raised before any pixel is decoded.
Paths may hold any character on every platform, Windows included.
Pixels: numpy, OpenCV, Pillow¶
An image another library has decoded is hashed from its pixels:
import numpy as np
from PIL import Image
with Image.open("photo.png") as img:
rgb = np.asarray(img.convert("RGB")) # shape (height, width, 3), uint8
ph.hash(rgb) == ph.hash("photo.png") # True: the same pixels
An array is 8-bit, of shape (h, w) (gray), (h, w, 1), (h, w, 3) (RGB) or
(h, w, 4) (RGBA). OpenCV's arrays are BGR: say so, or the colors are swapped.
with ph.ImageContext() as ctx:
ctx.load_pixels(rgb[..., ::-1], channel_order="bgr") # as cv2.imread gives it
assert ctx.phash() == ph.hash(rgb)
An array whose rows are the only gaps, such as a crop arr[10:-10, 5:-5], is passed to
C without a copy; any other layout (reversed or interleaved channels, BGR) is copied once.
Pillow images are converted as needed: 1 to gray, palette and gray-with-alpha to RGBA,
CMYK, YCbCr, HSV and LAB to RGB; 16-bit and float modes are refused, since there is no
single right way to bring them to 8 bits.
Pixels carry no orientation tag: rotate them yourself (Pillow's
ImageOps.exif_transpose) if the source had one. A decoder other than libphash's may
also round differently: PNG pixels are the same everywhere, JPEG pixels differ between
decoders by a level here and there, so a JPEG decoded by Pillow or OpenCV hashes a few
bits away from the same file loaded by path.
Grayscale¶
Config(grayscale=True) decodes to one gray channel, which is faster for the seven
algorithms that work in gray. ColorHash and ColorMoments then raise
RequiresColorError.
Reusing a context¶
A context holds one image at a time and keeps its buffers when it loads the next, so a loop over many images reuses one:
with ph.ImageContext() as ctx:
for path in ["photo.jpg", "photo_copy.jpg"]:
ctx.load_file(path)
print(path, ctx.width, ctx.height, ph.hash_to_hex(ctx.phash()))
A failed load from a file or bytes leaves the context empty; a failed load_pixels()
keeps the image loaded before.