1

I have a series of images and was wondering if there is a possibility to write something in python to apply a contrast and brightness curve Like in the images below.

enter image description here. enter image description here

  • Lets say you have picked the 5 points along the desired curve by some interactive means (Python or otherwise). You then need to spline interpolate them to fill out a look up table. The look up table can then be applied via cv2.LUT(). – fmw42 Nov 25 '21 at 00:32
  • 1
    Do you mean you have saved a `.acv` Curves file created by Photoshop? If so, you can apply it from the commandline with `ffmpeg` using `ffmpeg -i input.jpg -vf curves=psfile="curves.acv" output.jpg` – Mark Setchell Nov 25 '21 at 02:31
  • 1
    Also, if you have a Photoshop `.acv` file please share it as I wrote some Python a while back that reads them and generates a spline like Fred suggested. I think I can find it. – Mark Setchell Nov 25 '21 at 02:33

2 Answers2

3

As stated by Fred in the comments, you probably want to create a Catmull-Rom spline from your points and then apply that using a LUT within OpenCV.

#!/usr/bin/env python3

# from https://splines.readthedocs.io/en/latest/euclidean/piecewise-monotone.html#Examples

import matplotlib.pyplot as plt
import numpy as np
import splines
import cv2

# Just helper code for plotting - you don't need this
def grid_lines(x=None, y=None, ax=None):
    if ax is None:
        ax = plt.gca()
    if x is not None:
        ax.set_xticks(x)
        ax.xaxis.grid(True)
    if y is not None:
        ax.set_yticks(y)
        ax.yaxis.grid(True)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['bottom'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.xaxis.set_ticks_position('none')
    ax.yaxis.set_ticks_position('none')

# Just helper code for plotting - you don't need this
def plot_spline_1d(spline, samples=100, **kwargs):
    'Plot a one-dimensional spline.'
    ax = plt.gca()
    times = np.linspace(spline.grid[0], spline.grid[-1], samples)
    ax.plot(times, spline.evaluate(times), **kwargs)
    ax.scatter(spline.grid, spline.evaluate(spline.grid))

The following simulates an S-shaped contrast curve similar to your 5 points:

xvals = 0, 64, 128, 192, 255
yvals = 0, 32, 128, 206, 255

# Just code for plotting the curve - you don't need this
plot_spline_1d(splines.CatmullRom(yvals, xvals), label='Catmull–Rom')
plt.legend()
grid_lines(xvals)
plt.show()

enter image description here

The actual code you need only really starts here:

# Derive Catmull-Rom spline for our X,Y
res = splines.CatmullRom(yvals, xvals)

# Make LUT (Lookup Table) from spline
LUT = np.uint8(res.evaluate(range(0,256)))

# Load ramp image as greyscale
im = cv2.imread('ramp.png', cv2.IMREAD_GRAYSCALE)

# Apply LUT to image
stretched = cv2.LUT(im, LUT)

# Save result
cv2.imwrite('result.png', stretched)

That turns this ramp:

enter image description here

into this:

enter image description here

Note that I artificially added a thin red border so you can see the extent of the image on StackOverflow's annoying background.


As regards reading a Photoshop Curves file (ACV or .acv), I wrote some code a while back that parses them but I haven't integrated that with the code above - it shouldn't be too hard - you basically save the points from the ACV file and use them to generate the spline for the above code. I leave it below for anyone interested to play with:

"""
################################################################################
fauxtoshop - does some things like Adobe Photoshop

Mark Setchell (mark@thesetchells.com)

Reads, interprets and possibly applies Photoshop files:

- Curves files (*.acv)
- Levels files (*.alv)
- Filter Kernel files (*.acf)
- Hue Saturation files (*.ahu)
################################################################################
"""

import sys
import numpy
from struct import unpack

def loadFilter(filename):

    if filename.lower().endswith('.acv'):
        loadCurvesFilter(filename)
        return

    if filename.lower().endswith('.alv'):
        loadLevelsFilter(filename)
        return

    if filename.lower().endswith('.acf'):
        loadKernelFilter(filename)
        return

    if filename.lower().endswith('.ahu'):
        loadHSLFilter(filename)
        return

    sys.exit(f'ERROR: Unknown file extension {filename}')

def loadCurvesFilter(filename):

    with open(filename, 'rb') as f:
       version, ncurves = unpack('>HH', f.read(4))
       print(f'File: {filename}')
       print(f'Version: {version}')
       if version != 4:
          sys.exit('ERROR: Cowardly refusing to read version other than 4')
       print(f'Curve count: {ncurves}')
       curves = []
       for c in range(ncurves):
          npoints, = unpack('>H', f.read(2))
          print(f'Curve: {c}, {npoints} points follow:')
          curve = []
          for p in range(npoints):
             y, x = unpack('>HH', f.read(4))
             print(f'Curve: {c}, point: {p}, x={x}, y={y}')
             curve.append((x,y))
          curves.append(curve)
    return curves

def loadLevelsFilter(filename):
    sys.exit("ERROR: Levels filter not yet implemeted")

def loadKernelFilter(filename):
    sys.exit("ERROR: Kernel filter not yet implemeted")

def loadHSLFilter(filename):

    with open(filename, 'rb') as f:
       version, usage, pad = unpack('>HBB', f.read(4))
       print(f'File: {filename}')
       print(f'Version: {version}')
       if version != 2:
          sys.exit('ERROR: Cowardly refusing to read version other than 2')
       if usage == 0:
           print('Usage: Hue adjustment')
       else:
           print('Usage: Colorization')
           sys.exit(f'ERROR: Cowardly refusing to apply colorization rather than Hue adjustment')
       MasterHue, MasterSaturation, MasterLightness = unpack('>HHH', f.read(6))
       print(f'Master Hue: {MasterHue}')
       print(f'Master Saturation: {MasterSaturation}')
       print(f'Master Lightness: {MasterLightness}')
       # There follow 6 hextants, each with 4 range values and 3 settings values
       for h in range(6):
           ranges = unpack('>HHHH',f.read(8))
           settings = unpack('>HHH', f.read(6))
           print(f'Hextant: {h}, ranges: {ranges}, settings: {settings}')
           
################################################################################
# main
################################################################################
if __name__ == '__main__':

   if len(sys.argv) not in set([2,3]):
      print('Usage: {sys.argv[0]} filter.[acv|ahu|alv|acf] [image]', file=sys.stderr)
      sys.exit(1)

   if len(sys.argv) == 2:
      loadFilter(sys.argv[1])

Note also that you can apply a Photoshop Curves (ACV) file using ffmpeg like this:

ffmpeg -i input.jpg -vf curves=psfile="curves.acv" output.jpg 
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
2

You should refer to, for instance, the OpenCV website (you can learn a lot of interesting methods and have a lot of examples on CV problems), here you have some documentation to check:

I hope it's helpful.

emichester
  • 189
  • 9
  • Okay this useful but i want to know if there's lib for creating curve like photoshop to contrast and brightness – Eslam Said Khashb Nov 25 '21 at 01:05
  • 1
    No direct library method in OpenCV that I know that will do the equivalent of PS "curves". There is a contract and brightness methods for linear transformation that simply adjusts the slope and intercept. See cv2.normalize() and cv2.convertScaleAbs() for example. – fmw42 Nov 25 '21 at 01:21