-
Notifications
You must be signed in to change notification settings - Fork 1
Library
fmetrics exposes a C ABI shared by two frontends:
- C API declared in
fmetrics.h(linked vialibfmetrics.a) - Zig API exported by the
fmetricsZig module.
Both wrap the same underlying metric implementations.
This section covers functionality common to both C & Zig.
The Zig build system is also a build system for C, so you may use either API while still using the Zig build system.
Fetch the Zig package:
zig fetch --save git+https://github.com/halidecx/fmetrics.gitThis should show something like this in build.zig.zon:
.dependencies = .{
.fmetrics = .{
.url = "git+https://github.com/halidecx/fmetrics.git#<commit>",
.hash = "fmetrics-<version>-<hash>",
},
},Then you can link it from your build.zig:
There are two supported integration styles:
- Link the C ABI artifact and include the header, for projects looking to access the C API:
const fmetrics_dep = b.dependency("fmetrics", .{
.target = target,
.optimize = optimize,
});
const fmetrics = fmetrics_dep.artifact("fmetrics");
exe.root_module.linkLibrary(fmetrics);
exe.root_module.addIncludePath(fmetrics.getEmittedIncludeTree());- Import the native Zig module, for Zig projects to access the native Zig API:
const fmetrics_dep = b.dependency("fmetrics", .{
.target = target,
.optimize = optimize,
});
const fmetrics_mod = fmetrics_dep.module("fmetrics");
exe.root_module.addImport("fmetrics", fmetrics_mod);
exe.root_module.linkLibrary(fmetrics_dep.artifact("libfmetrics"));Images are 8-bit packed RGB data in the sRGB colorspace. There is currently one supported pixel format / colorspace pair:
| Field | C constant | Zig enum value |
|---|---|---|
| Pixel format | FMETRICS_PIX_FMT_RGB_UINT8 |
.rgb_uint8 |
| Colorspace | FMETRICS_COLORSPACE_SRGB |
.srgb |
-
widthandheightare in pixels. -
strideis the number of bytes per row. For tightly packed RGB it iswidth * 3, but it may be larger to allow row padding. It must be>= width * 3. - The
databuffer must contain at leaststride * heightbytes.
Every metric call requires a Workspace; this is an opaque handle that owns scratch memory reused across calls. Create one once, reuse it for many comparisons, and destroy it when done. The workspace grows its internal buffer on demand, so reusing it across images of similar size avoids reallocations.
fmetrics supports:
- Butteraugli: reference implementation in libjxl's
butteraugli_main - IW-SSIM: reference implementation is Python IW-SSIM
- MS-SSIM: reference implementation in libvmaf
- SSIMULACRA2: reference implementation is Cloudinary's
ssimulacra2
| Metric | Higher is better? | Supports --err-map? |
Notes |
|---|---|---|---|
iwssim |
yes | no | Five-scale; needs >=161x161 images |
msssim |
yes | no | Multi-scale SSIM |
ssimu2 |
yes | yes | SSIMULACRA2 |
butteraugli |
no | yes | Takes intensity_target & pnorm
|
Butteraugli options:
| Field | Type | Default | Meaning |
|---|---|---|---|
intensity_target |
f32 | 203.0 | Peak luminance in nits (SDR ~ 203, HDR 1000) |
pnorm |
i32 | 3 | p-norm for the aggregate score |
For ssimu2 and butteraugli, you can request a per-pixel error map by passing a uint32_t* (C) or []u32 (Zig) buffer of length width * height. The metric fills it in addition to returning the scalar score.
| C constant | Zig error | Meaning |
|---|---|---|
FMETRICS_OK |
(success) | No error |
FMETRICS_ERR_INVALID_ARGUMENT |
InvalidArgument |
Null pointer, zero size, bad stride |
FMETRICS_ERR_UNSUPPORTED_FORMAT |
UnsupportedFormat |
Format/colorspace not RGB_UINT8/sRGB
|
FMETRICS_ERR_DIMENSION_MISMATCH |
DimensionMismatch |
Reference and distorted differ in W/H |
FMETRICS_ERR_OUT_OF_MEMORY |
OutOfMemory |
Allocation failed |
FMETRICS_ERR_IWSSIM_IMG_TOO_SMALL |
IwssimImageTooSmall |
Image < 161x161 for five-scale IW-SSIM |
FMETRICS_ERR_INTERNAL |
Internal |
Unexpected internal error |
Compilation emits the static library to be used with C. libfmetrics.a statically depends on libspng and libcvvdp (pulled in by the fmetrics build). When linking a C program, include zig-out/include on the include path and link the archive plus its transitive static dependencies and libm. Example:
cc -O3 -Izig-out/include my_program.c \
zig-out/lib/libfmetrics.a \
-lm -lpthread#include <stdio.h>
#include <stdlib.h>
#include "fmetrics.h"
int main(void) {
/* 2x2 tightly-packed sRGB RGB images (3 bytes/pixel). */
const uint8_t ref_rgb[] = {
255, 0, 0, 0, 255, 0,
0, 0, 255, 255, 255, 0,
};
const uint8_t dst_rgb[] = {
240, 10, 10, 10, 240, 10,
10, 10, 240, 240, 240, 10,
};
const uint32_t width = 2, height = 2;
const uint32_t stride = width * 3;
FmetricsImg ref = {
.data = ref_rgb,
.width = width,
.height = height,
.stride = stride,
.format = FMETRICS_PIX_FMT_RGB_UINT8,
.colorspace = FMETRICS_COLORSPACE_SRGB,
};
FmetricsImg dst = {
.data = dst_rgb,
.width = width,
.height = height,
.stride = stride,
.format = FMETRICS_PIX_FMT_RGB_UINT8,
.colorspace = FMETRICS_COLORSPACE_SRGB,
};
FmetricsWorkspace *ws = fmetrics_workspace_create();
if (ws == NULL) { fprintf(stderr, "OOM\n"); return 1; }
double score = 0.0;
FmetricsErr err = fmetrics_msssim_cmp(ws, &ref, &dst, &score);
if (err != FMETRICS_OK) {
fprintf(stderr, "msssim: %s\n", fmetrics_error_str(err));
fmetrics_workspace_destroy(ws);
return 1;
}
printf("MS-SSIM: %f\n", score);
fmetrics_workspace_destroy(ws);
return 0;
}FmetricsButteraugliOptions opts = {
.intensity_target = 203.0f, /* nits; use 1000.0f for HDR */
.pnorm = 3,
};
double score = 0.0;
FmetricsErr err = fmetrics_butteraugli_cmp(ws, &ref, &dst, &opts, &score);
if (err != FMETRICS_OK) {
fprintf(stderr, "butteraugli: %s\n", fmetrics_error_str(err));
}const size_t pixels = (size_t)width * (size_t)height;
uint32_t *map = malloc(pixels * sizeof(uint32_t));
if (!map) { /* handle OOM */ }
double score = 0.0;
FmetricsErr err = fmetrics_ssimu2_cmp_map(ws, &ref, &dst, &score, map);
if (err == FMETRICS_OK) {
printf("SSIMULACRA2: %f\n", score);
/* map[y * width + x] holds the per-pixel error. */
}
free(map);The workspace is the only allocation you need to keep alive between calls. Reuse it to avoid repeated reallocations:
FmetricsWorkspace *ws = fmetrics_workspace_create();
for (size_t i = 0; i < n_pairs; i++) {
double score = 0.0;
FmetricsErr e = fmetrics_ssimu2_cmp(ws, &refs[i], &dsts[i], &score);
if (e != FMETRICS_OK) { /* ... */ }
}
fmetrics_workspace_destroy(ws);printf("fmetrics %s\n", fmetrics_version_str());Integration with Zig projects is covered in the Common section of this document.
const std = @import("std");
const fmetrics = @import("fmetrics");
pub fn main() !void {
const ref_rgb = [_]u8{
255, 0, 0, 0, 255, 0,
0, 0, 255, 255, 255, 0,
};
const dst_rgb = [_]u8{
240, 10, 10, 10, 240, 10,
10, 10, 240, 240, 240, 10,
};
const width: u32 = 2;
const height: u32 = 2;
const ref = try fmetrics.Image.init(&ref_rgb, width, height);
const dst = try fmetrics.Image.init(&dst_rgb, width, height);
var workspace = try fmetrics.Workspace.init();
defer workspace.deinit();
const score = try fmetrics.msssim(&workspace, ref, dst);
std.debug.print("MS-SSIM: {d}\n", .{score});
}Image.init assumes tightly packed RGB (stride = width * 3). Use initWithStride for row padding:
const ref = try fmetrics.Image.init(rgb_slice, width, height);
const padded = try fmetrics.Image.initWithStride(
rgb_with_padding, width, height, stride_bytes,
);The Zig API validates inputs and returns a typed fmetrics.Error:
fmetrics.Error |
When it occurs |
|---|---|
InvalidArgument |
Zero dims, bad stride, buffer too small |
UnsupportedFormat |
Format/colorspace not rgb_uint8/srgb
|
DimensionMismatch |
Reference and distorted differ in W/H |
OutOfMemory |
Workspace or internal allocation failed |
IwssimImageTooSmall |
Image < 161x161 for five-scale IW-SSIM |
Internal |
Unexpected internal error |
const opts = fmetrics.ButteraugliOptions{
.intensity_target = 203.0, // nits; 1000.0 for HDR
.pnorm = 3,
};
const score = try fmetrics.butteraugli(&workspace, ref, dst, opts);
std.debug.print("Butteraugli: {d}\n", .{score});var allocator = std.heap.page_allocator;
const pixels = @as(usize, width) * @as(usize, height);
const map = try allocator.alloc(u32, pixels);
defer allocator.free(map);
const score = try fmetrics.ssimu2Map(&workspace, ref, dst, map);
std.debug.print("SSIMULACRA2: {d}\n", .{score});
// map[y * width + x] holds the per-pixel error.const score = fmetrics.iwssim(&workspace, ref, dst) catch |err| {
std.debug.print("iwssim failed: {s}\n", .{fmetrics.errorString(err)});
return err;
};const ws = try fmetrics.Workspace.init();
defer ws.deinit();
const iw = try fmetrics.iwssim(ws, ref, dst);
const ms = try fmetrics.msssim(ws, ref, dst);
const u2 = try fmetrics.ssimu2(ws, ref, dst);
const opts = fmetrics.ButteraugliOptions{};
const ba = try fmetrics.butteraugli(ws, ref, dst, opts);
const map = try allocator.alloc(u32, pixels);
defer allocator.free(map);
const u2_map = try fmetrics.ssimu2Map(ws, ref, dst, map);
const ba_map = try fmetrics.butteraugliMap(ws, ref, dst, opts, map);std.debug.print("fmetrics {s}\n", .{fmetrics.version}); // "0.0.2"Notes apply to both C and Zig.
- A single
Workspacemust not be used concurrently from multiple threads. Create oneWorkspaceper worker thread (as ourmain.zig'sVideoWorkerdoes). -
Imagevalues are immutable views over caller-owned buffers and may be shared across threads freely, as long as the underlyingdatais not mutated during the call.
-
Workspace: caller owns the handle returned by
fmetrics_workspace_create(C) /Workspace.init(Zig). Free it withfmetrics_workspace_destroy(C) orworkspace.deinit()(Zig). -
Image data: the caller owns the pixel buffer.
Image/FmetricsImgonly borrows it for the duration of the metric call. - Error maps: caller allocates and owns the buffer; the metric writes into it.
Numbers last updated: 5f1ef3c