Skip to content

Commit 3976c8b

Browse files
committed
feat(IBA): Add FLIP perceptual image difference metric
Implements the FLIP algorithm from Andersson et al. 2020, rewritten with OIIO idioms (no source code from the BSD-licensed reference FLIP.h is directly incorporated). Included are C++ and Python API for ImageBufAlgo::FLIP_diff(), and `oiiotool --flipdiff` command. The basic operation is to compare two images and produce a per-pixel error map that conveys perceptual difference to human observers. I won't explain it all here; see the extensive comments in imagebufalgo.h, imagebufalgo_flip.cpp, oiiotool.cpp, imagebufalgo.rst, oiiotool.rst. There are some important changes (especially for how we expose this via oiiotool) versus the way the original NVIDIA reference implementation's command line tool worked. Please read the extensive comments at the top of imagebufalgo_flip.cpp for details. This is a preliminary, experimental implementation. It's hidden behind an `experimental` namespace (and the oiiotool command requires use of the `--experimental` argument) to emphasize that it may change and is not yet considered part of OIIO's public API, and thus is exempt from our usual strict rules about breaking backward compatibility. Try it out and give feedback, but do not rely on this yet! Assisted-by: Claude Code / sonnet-4.6 + opus-4.6 I used Claude Code for the inital stab at transforming the NVIDIA reference implementation into OIIO idiomatic equivalents. But to be honest, that got me over the hump of the blank page, but I rewrote most of it bit by bit as I continued to refine my design ideas for how it should work and be exposed to users. Signed-off-by: Larry Gritz <lg@larrygritz.com>
1 parent c6c80bc commit 3976c8b

29 files changed

Lines changed: 2021 additions & 686 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ jobs:
225225
container: aswf/ci-oiio:2025.5
226226
cxx_std: 17
227227
build_type: Debug
228-
ctest_test_timeout: "240"
228+
ctest_test_timeout: "300"
229229
python_ver: "3.11"
230230
simd: "avx2,f16c"
231231
fmt_ver: 11.2.0

CHANGES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,21 @@ Release 3.2 (target: Sept 2026?) -- compared to 3.1
88
### ⛰️ New features and public API changes:
99
* *New image file format support:*
1010
* *oiiotool new features and major improvements*:
11+
- `oiiotool --flipdiff` computes the FLIP perceptual difference between two
12+
images, prints statistics, and leaves the error map on the image stack for
13+
further processing or saving. Options: `hdr=1` for HDR-FLIP, `colormap=NAME`
14+
to apply a false-color map (e.g. "magma"), `ppd=N` to override pixels-per-
15+
degree, `tonemapper=NAME` for HDR tonemapper ("aces", "reinhard", "hable").
1116
* *Command line utilities*:
1217
- *iv*: Flip, rotate and save image [#5003](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5003) (by Valery Angelique) (3.2.0.0, 3.1.11.0)
1318
* *ImageBuf/ImageBufAlgo*:
19+
- `ImageBufAlgo::FLIP()` computes the FLIP (eLearning perceptual Image
20+
difference Predictor) metric between two LDR or HDR images. The result is
21+
a single-channel float image with per-pixel FLIP error in [0,1]. A
22+
`FLIPResults` struct returns mean error, max error, and location. The
23+
optional `colormap` kwarg applies a false-color map to the result.
24+
`FLIP_ppd()` helper computes pixels-per-degree for a given display setup.
25+
Python bindings and `oiiotool --flipdiff` are also provided.
1426
- *ImageBuf*: `IB::localpixels_as_[writable_]byte_image_span` [#5011](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5011) (3.2.0.0, 3.1.10.0)
1527
* *ImageCache/TextureSystem*:
1628
- *api/TS*: `IBA::make_texture()` now honors "maketx:threads" hint [#5014](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5014) (3.2.0.0, 3.1.10.0)

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ MY_CMAKE_FLAGS += -DTEX_BATCH_SIZE:STRING="${TEX_BATCH_SIZE}"
159159
endif
160160

161161
ifneq (${TEST},)
162-
TEST_FLAGS += -R ${TEST}
162+
TEST_FLAGS += -R '${TEST}'
163163
endif
164164

165165
ifneq (${USE_CCACHE},)

src/cmake/testing.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ macro (oiio_add_all_tests)
168168
oiiotool-text
169169
oiiotool-xform
170170
diff
171+
flip
171172
dither dup-channels
172173
jpeg jpeg-corrupt jpeg-metadata
173174
maketx oiiotool-maketx

src/doc/imagebufalgo.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2190,6 +2190,66 @@ Image comparison and statistics
21902190
..
21912191
21922192

2193+
|
2194+
2195+
.. _sec-iba-flip:
2196+
2197+
FLIP perceptual difference
2198+
^^^^^^^^^^^^^^^^^^^^^^^^^^
2199+
2200+
.. doxygengroup:: FLIP_diff
2201+
:content-only:
2202+
..
2203+
2204+
Examples:
2205+
2206+
.. tabs::
2207+
2208+
.. code-tab:: c++
2209+
2210+
ImageBuf ref ("ref.exr");
2211+
ImageBuf test ("test.exr");
2212+
2213+
// Basic use: returns 1-channel float error map in [0,1].
2214+
ImageBuf flipmap = ImageBufAlgo::FLIP_diff(ref, test);
2215+
OIIO::print("Mean FLIP error: {}\n",
2216+
flipmap.spec().get_float_attribute("FLIP:meanerror"));
2217+
OIIO::print("Max FLIP error: {} at ({}, {})\n",
2218+
flipmap.spec().get_float_attribute("FLIP:maxerror"),
2219+
flipmap.spec().get_int_attribute("FLIP:maxx"),
2220+
flipmap.spec().get_int_attribute("FLIP:maxy"));
2221+
2222+
// LDR mode (display-referred images only):
2223+
ImageBufAlgo::FLIP_diff(flipmap, ref, test, { {"hdr", 0} });
2224+
2225+
// For a false-color visualization, pass the result to color_map():
2226+
ImageBuf colored = ImageBufAlgo::color_map(flipmap, 0, "magma");
2227+
2228+
.. code-tab:: py
2229+
2230+
ref = ImageBuf("ref.exr")
2231+
test = ImageBuf("test.exr")
2232+
2233+
# Basic use: returns 1-channel float error map in [0,1].
2234+
flipmap = ImageBufAlgo.FLIP_diff(ref, test)
2235+
print("Mean FLIP error:", flipmap.spec().get_float_attribute("FLIP:meanerror"))
2236+
print("Max FLIP error:", flipmap.spec().get_float_attribute("FLIP:maxerror"),
2237+
"at", flipmap.spec().get_int_attribute("FLIP:maxx"),
2238+
flipmap.spec().get_int_attribute("FLIP:maxy"))
2239+
2240+
# For a false-color visualization, pass the result to color_map():
2241+
colored = ImageBufAlgo.color_map(flipmap, 0, "magma")
2242+
2243+
# LDR mode:
2244+
flipmap = ImageBufAlgo.FLIP_diff(ref, test, hdr=0)
2245+
2246+
.. code-tab:: bash oiiotool
2247+
2248+
# Default HDR mode, outputting per-pixel error map
2249+
oiiotool ref.exr test.exr --flipdiff -o errormap.exr
2250+
# Output the false color visualization)
2251+
oiiotool ref.exr test.exr --flipdiff:colormap=magma -o errorvis.exr
2252+
21932253
|
21942254
21952255
.. doxygenfunction:: isConstantColor

src/doc/oiiotool.rst

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,10 @@ Writing images
19011901
221 > 1,1,1
19021902
65315 within range
19031903

1904+
1905+
:program:`oiiotool` commands that compare images
1906+
================================================
1907+
19041908
.. option:: --diff
19051909
--fail <A> --failpercent <B> --hardfail <C>
19061910
--warn <A> --warnpercent <B> --hardwarn <C>
@@ -1925,10 +1929,90 @@ Writing images
19251929
.. option:: --pdiff
19261930

19271931
This command computes the difference of the current image and the next
1928-
image on the stack using a perceptual metric, and prints whether or not
1929-
they match according to that metric. This command does not alter the
1932+
image on the stack using the Yee perceptual metric, and prints whether or
1933+
not they match according to that metric. This command does not alter the
19301934
image stack.
19311935

1936+
.. option:: --flipdiff
1937+
1938+
Compute the FLIP perceptual difference of the top two images on the stack
1939+
(removing them), optionally print error metrics, and leave the per-pixel
1940+
error map on the stack.
1941+
1942+
Limitations: Currently, this only operates on the first subimage,
1943+
and the first three (RGB) channels, and does not work on volumetric
1944+
or "deep" images.
1945+
1946+
Optional appended modifiers include:
1947+
1948+
`:hdr=` *int* (default: 1)
1949+
If nonzero, computes the HDR FLIP comparison. If zero, computes
1950+
the LDR FLIP comparision. The default is 1, for HDR mode.
1951+
`:maxluminance=` *float* (default: 2.0)
1952+
The top of the expected luminance range, used to compute exposure
1953+
settings. If set to 0.0, the "startExposure" and "stopExposure"
1954+
will be used instead. The default is 2.0, which should be adequate
1955+
for most production scenarios.
1956+
`:medianluminance=` *float* (default: 0.18)
1957+
The assumed median luminance (used if "maxluminance" is not 0, so
1958+
we are using these estimates instead of measuring from the image).
1959+
The default 0.18 assumes that "middle grey" is a good guess for
1960+
a typical median luminance of the image.
1961+
`:ppd=` *float* (default:67.02)
1962+
Specifies the horizontal pixels per degree of viewing. The default
1963+
value of 67.02 is computed as the value for a 3840 pixel (2xHD) image
1964+
filling a display that is 0.7m wide, 0.7m in front of the viewer.
1965+
`:tonemapper=` *name* (default: "aces")
1966+
Specifies the HDR tonemapper: one of "aces" (default), "reinhard", or
1967+
"hable".
1968+
`:startExposure=` *float* `:stopExposure=` *float*
1969+
If supplied, and if "maxluminance" is set to 0, specify start and stop
1970+
exposures for the HDR FLIP method. If not supplied, they will be
1971+
automatically computed from the contents of the image.
1972+
`:numExposures=` *int* (default: 0)
1973+
The number of exposures for HDR FLIP computation (default: 0, which
1974+
means to automatically compute it).
1975+
`:colormap=` *name*
1976+
If absent or empty, the output error image will be a single channel
1977+
grey value. If present and the name of a color map (the same set
1978+
accepted by the oiiotool `--colormap` action), the output error
1979+
image will be a 3-channel RGB false-color visualization of the
1980+
perceptual error for each pixel.
1981+
`:print=` *int* (default: 1)
1982+
If nonzero, will print information such as the average and maximum
1983+
error of the pixels. The default is 1, meaning that the report will
1984+
print to stdout. Set to 0 to suppress the printing.
1985+
`:fail=` *float*
1986+
If set, a PASS/FAIL message will be printed to the console, with
1987+
"failing" meaning that any pixel's error metric exceeded the threshold
1988+
specified as the value for this parameter. In the case of a failure,
1989+
the oiiotool run itself will have a shell error code.
1990+
1991+
The command also sets metadata on the result image based on the FLIP
1992+
computation:
1993+
1994+
`"FLIP:meanerror"`
1995+
Mean of error map in [0,1].
1996+
`"FLIP:maxerror"`
1997+
Maximum perceptual pixel error.
1998+
`"FLIP:maxx"`
1999+
x coordinate of the pixel with the highest error.
2000+
`"FLIP:maxy"`
2001+
y coordinate of the pixel with the highest error.
2002+
`"FLIP:startExposure"`, `"FLIP:stopExposure"`
2003+
(HDR mode only) The start and stop exposure stops used.
2004+
`"FLIP:numExposures"`
2005+
(HDR mode only) Number of exposure steps used.
2006+
2007+
Examples::
2008+
2009+
# Default HDR mode, with false-color visualization
2010+
oiiotool reference.exr test.exr --flipdiff:colormap=magma -o error.exr
2011+
2012+
# LDR mode (for display-referred images), 1-channel error map
2013+
oiiotool reference.jpg test.jpg --flipdiff:hdr=0 -o error.exr
2014+
2015+
This command was added in OIIO 3.2.
19322016

19332017

19342018
:program:`oiiotool` commands that change the current image metadata

src/doc/pythonbindings.rst

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3287,6 +3287,98 @@ Image comparison and statistics
32873287
32883288
32893289
3290+
.. py:method:: ImageBuf ImageBufAlgo.FLIP_diff (ref, test, hdr=1, maxluminance=2.0, ppd=0.0, tonemapper="aces", roi=ROI.All, nthreads=0)
3291+
bool ImageBufAlgo.FLIP_diff (dst, ref, test, hdr=1, maxluminance=2.0, ppd=0.0, tonemapper="aces", roi=ROI.All, nthreads=0)
3292+
3293+
WARNING: This is EXPERIMENTAL and may change at any time. Do not rely
3294+
on it prior to the release of OIIO 3.2.
3295+
3296+
Compute the `FLIP <https://research.nvidia.com/publication/2020-07_flip-difference-evaluator-alternating-images>`_
3297+
perceptual difference between images `ref` and `test`, returning a
3298+
single-channel float error map whose pixel values lie in [0,1]. Higher
3299+
values indicate larger perceived differences.
3300+
3301+
If the image's ``oiio:ColorSpace`` metadata does not clearly define a
3302+
color space, it will be assumed to be ``lin_rec709_scene`` before
3303+
processing. Three channels starting at ``roi.chbegin`` are used.
3304+
3305+
Summary statistics are stored as metadata attributes on the result
3306+
``ImageBuf``:
3307+
3308+
.. code-block:: python
3309+
3310+
errmap = ImageBufAlgo.FLIP_diff (ref_image, test_image)
3311+
errmap.spec().get_float_attribute("FLIP:meanerror") # mean perceptual error
3312+
errmap.spec().get_float_attribute("FLIP:maxerror") # maximum per-pixel error
3313+
errmap.spec().get_int_attribute("FLIP:maxx") # x coordinate of max-error pixel
3314+
errmap.spec().get_int_attribute("FLIP:maxy") # y coordinate of max-error pixel
3315+
errmap.spec().get_float_attribute("FLIP:startExposure") # HDR: first exposure stop used
3316+
errmap.spec().get_float_attribute("FLIP:stopExposure") # HDR: last exposure stop used
3317+
errmap.spec().get_int_attribute("FLIP:numExposures") # HDR: number of exposure steps used
3318+
3319+
Options:
3320+
3321+
* `hdr` (int, default 1) — set to 0 to force use of LDR-FLIP mode (should
3322+
only be used for images in a LDR display-referred color space).
3323+
* `maxluminance` (float, default 2.0) — estimated maximum luminance
3324+
* `medianluminance` (float, default 0.18) — estimated median luminance
3325+
* `ppd` (float, default 67.02) — pixels per degree of visual angle
3326+
* `tonemapper` (str, default ``"aces"``) — HDR tonemapper:
3327+
``"aces"``, ``"reinhard"``, or ``"hable"``
3328+
* `startExposure`, `stopExposure` (float) - The start and end exposures
3329+
for HDR FLIP. If not supplied, they will be computed automatically based
3330+
on the `maxluminance`, which if it is 0, will be computed based on the
3331+
value range in the reference image.
3332+
* `numExposures` (int) - The number of exposures used for HDR FLIP. If
3333+
not supplied, it will be computed automatically.
3334+
3335+
Tip: For a false-color visualization pass the result to
3336+
:py:meth:`ImageBufAlgo.color_map`.
3337+
3338+
See also :py:meth:`ImageBufAlgo.FLIP_ppd` for computing `ppd` from
3339+
display geometry.
3340+
3341+
This was added in OpenImageIO 3.2.
3342+
3343+
Example:
3344+
3345+
.. code-block:: python
3346+
3347+
ref = ImageBuf("ref.exr")
3348+
test = ImageBuf("test.exr")
3349+
3350+
# Basic use: 1-channel float error map in [0,1].
3351+
errmap = ImageBufAlgo.FLIP_diff(ref, test)
3352+
print("Mean FLIP error:", errmap.spec().get_float_attribute("FLIP:meanerror"))
3353+
print("Max FLIP error:", errmap.spec().get_float_attribute("FLIP:maxerror"),
3354+
"at", errmap.spec().get_int_attribute("FLIP:maxx"),
3355+
errmap.spec().get_int_attribute("FLIP:maxy"))
3356+
3357+
# LDR mode (display-referred images only):
3358+
errmap = ImageBufAlgo.FLIP_diff(ref, test, hdr=0)
3359+
3360+
# False-color visualization:
3361+
colored = ImageBufAlgo.color_map(errmap, 0, "magma")
3362+
3363+
3364+
3365+
.. py:method:: float ImageBufAlgo.FLIP_ppd (monitor_distance_m=0.7, screen_width_px=3840, screen_width_m=0.7)
3366+
3367+
Compute the pixels-per-degree value for use as the `ppd` argument to
3368+
:py:meth:`ImageBufAlgo.FLIP_diff`, given the viewing distance in meters,
3369+
the screen width in pixels, and the screen width in meters. The default
3370+
values correspond to ~67 ppd (a common desktop monitor at typical viewing
3371+
distance).
3372+
3373+
Example:
3374+
3375+
.. code-block:: python
3376+
3377+
ppd = ImageBufAlgo.FLIP_ppd(0.5, 2560, 0.6)
3378+
errmap = ImageBufAlgo.FLIP_diff(ref, test, ppd=ppd)
3379+
3380+
3381+
32903382
.. py:method:: tuple ImageBufAlgo.isConstantColor (src, threshold=0.0, roi=ROI.All, nthreads=0)
32913383
32923384
If all pixels of `src` within the ROI have the same values (for the

0 commit comments

Comments
 (0)