How to apply a transformation to a certain tonal range in VIPS/Python
<p>I have to apply various transformations to different tonal ranges of 16-bit tiff files in VIPS (and Python). I have managed to do so, but I am new to VIPS and I am not convinced I am doing this in an efficient manner. These images are several hundred megabytes each, and cutting each excess step can save me a few seconds per image. </p>

<p>I wonder if there is a more efficient way to achieve the same results I obtain from the code below, for instance using lookup tables (I couldn't really figure out how they work in VIPS). The code separates the shadows in the red channel and passes them through a transformation. </p>

<pre><code>im = Vips.Image.new_from_file("test.tiff")

# Separate the red channel
band = im[0]

# Find the tone limit for the bottom 5%
lim = band.percent(5) 

# Create a mask using the tone limit
mask = (band &lt;= lim) 

# Convert the mask to 16 bits
mask = mask.cast(band.BandFmt, shift = True) 

# Run the transformation on the image and keep only the shadow areas
new_shadows = (65535 * (shadows / lim * 0.1)) &amp; mask 
</code></pre>

<p>After running more or less similar codes for each tonal range (highlight, shadows, midtones, I add all the resulting images together to reconstruct the original band:</p>

<pre><code>new_band = (new_shadows.add(new_highlights).add(new_midtones)).cast(band.BandFmt)
</code></pre>

<p>I made you a demo program showing how to do something like this with the vips histogram functions:</p>

<pre><code>#!/usr/bin/python

import sys

import gi
gi.require_version('Vips', '8.0')
from gi.repository import Vips

im = Vips.Image.new_from_file(sys.argv[1])

# find the image histogram 
# 
# we'll get a uint image, one pixel high and 256 or
# 65536 pixels across, it'll have three bands for an RGB image source
hist = im.hist_find()

# find the normalised cumulative histogram 
#
# for a 16-bit source, we'll have 65535 as the right-most element in each band
norm = hist.hist_cum().hist_norm()

# search from the left for the first pixel &gt; 5%: the position of this pixel 
# will give us the pixel value that 5% of pixels fall below
# 
# .profile() gives back a pair of [column-profile, row-profile], we want index 1
# one. .getpoint() reads out a pixel as a Python array, so for an RGB Image 
# we'll have something like [19.0, 16.0, 15.0] in shadows
shadows = (norm &gt; 5.0 / 100.0 * norm.width).profile()[1].getpoint(0, 0)

# Now make an identity LUT that matches our original image
lut = Vips.Image.identity(bands = im.bands, 
                          ushort = im.format == Vips.BandFormat.USHORT)

# do something to the shadows ... here we just brighten them a lot
lut = (lut &lt; shadows).ifthenelse(lut * 100, lut)

# make sure our lut is back in the original format, then map the image through
# it
im = im.maplut(lut.cast(im.format))

im.write_to_file(sys.argv[2])
</code></pre>

<p>It does a single find-histogram operation on the source image, then a single map-histogram operation, so it should be fast.</p>

<p>This is just adjusting the shadows, you'll need to extend it slightly to do midtones and highlights as well, but you can do all three modifications from the single initial histogram, so it shouldn't be any slower. </p>

<p>Please open an issue on the libvips tracker if you have any more questions:</p>

<p><a href="https://github.com/jcupitt/libvips/issues" rel="nofollow">https://github.com/jcupitt/libvips/issues</a></p>

python image-processing vips