#!/usr/bin/env python3 """ ====== Sigmoidal Adjustment of Brightness and Contrast ====== Debian packages required: python3-matplotlib python3-numpy View the graph of a tonal transformation performed by applying the ImageMagick -/+sigmoidal-contrast option twice. Two sigmoidal-contrast operations are combined together: the first one to adjust the contrast and a second one to adjust the brightness. This approach is used by the Fred's sigmoidal scritp found here: http://www.fmwconcepts.com/imagemagick/sigmoidal/index.php The sigmoidal function will keep black and white points of the original image without saturating highlights or shadows. Sigmoidal and inverse sigmoidal formulas were taken from the ImageMagick source code: https://github.com/ImageMagick/ImageMagick/blob/main/MagickCore/enhance.c """ import os import subprocess import sys import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import Slider, Button __author__ = "Niccolo Rigacci" __copyright__ = "Copyright 2022 Niccolo Rigacci " __license__ = "GPLv3-or-later" __email__ = "niccolo@rigacci.org" __version__ = "0.1.0" EPSILON = 0.0000000001 QUANTUM = 256 # Define initial parameters INIT_CONTRAST = 0.0 INIT_BRIGHTNESS = 0.0 MIN_CONTRAST = -10.0 MAX_CONTRAST = 10.0 MIN_BRIGHTNESS = -10.0 MAX_BRIGHTNESS = 10.0 STEP_CONTRAST = 0.1 STEP_BRIGHTNESS = 0.1 def sigmoidal(a, b, x): return 1.0 / (1.0 + np.exp(a * (b - x))) def scaled_sigmoidal(a, b, x): return (sigmoidal(a, b, x) - sigmoidal(a, b, 0.0)) / (sigmoidal(a, b, 1.0) - sigmoidal(a, b, 0.0)) def inverse_scaled_sigmoidal(a, b, x): sig0 = sigmoidal(a, b, 0.0) sig1 = sigmoidal(a, b, 1.0) argument = (sig1 - sig0) * x + sig0 clamped = np.clip(argument, EPSILON, (1 - EPSILON)) return b - np.log(1.0 / clamped - 1.0) / a # The parametrized function to be plotted. def f(t, brightness, contrast): x = t / float(QUANTUM) if abs(contrast) < EPSILON: y1 = x elif contrast > 0: y1 = scaled_sigmoidal(contrast, 0.5, x) elif contrast < 0: y1 = inverse_scaled_sigmoidal(contrast, 0.5, x) if abs(brightness) < EPSILON: y2 = y1 elif brightness > 0: y2 = scaled_sigmoidal(brightness, 0.0, y1) elif brightness < 0: y2 = inverse_scaled_sigmoidal(brightness, 0.0, y1) return y2 * float(QUANTUM) # Initialize the data serie. t = np.linspace(0, QUANTUM-1, QUANTUM) # Create the figure and the line that we will manipulate fig, ax = plt.subplots() line, = ax.plot(t, f(t, INIT_BRIGHTNESS, INIT_CONTRAST), lw=2) plt.axis('equal') ax.set_xlabel('Input') ax.set_ylabel('Output') ax.set_xlim(0, QUANTUM) ax.set_ylim(0, QUANTUM) ax.set_title('Sigmoidal Brightness and Contrast') #plt.gcf().canvas.set_window_title('ImageMagick Double sigmoidal-contrast Graph') plt.gcf().canvas.set_window_title('ImageMagick: sigmoidal-contrast twice') # adjust the main plot to make room for the sliders fig.subplots_adjust(left=0.25, bottom=0.25) # Make a horizontal slider. ax_param1 = fig.add_axes([0.25, 0.1, 0.65, 0.03]) param1_slider = Slider( ax=ax_param1, label='Contrast', valmin=MIN_CONTRAST, valmax=MAX_CONTRAST, valstep=STEP_CONTRAST, valinit=INIT_CONTRAST, ) # Make a vertically oriented slider ax_param2 = fig.add_axes([0.1, 0.25, 0.0225, 0.63]) param2_slider = Slider( ax=ax_param2, label="Brightness", valmin=MIN_BRIGHTNESS, valmax=MAX_BRIGHTNESS, valstep=STEP_BRIGHTNESS, valinit=INIT_BRIGHTNESS, orientation="vertical" ) # The function to be called anytime a slider's value changes def update(val): line.set_ydata(f(t, param2_slider.val, param1_slider.val)) fig.canvas.draw_idle() # register the update function with each slider param1_slider.on_changed(update) param2_slider.on_changed(update) # Create a "matplotlib.widgets.Button" to reset the sliders to initial values. resetax = fig.add_axes([0.4, 0.025, 0.1, 0.04]) btn_reset = Button(resetax, 'Reset', hovercolor='0.975') preview_ax = fig.add_axes([0.6, 0.025, 0.1, 0.04]) btn_preview = Button(preview_ax, 'Preview', hovercolor='0.975') # Create some "matplotlib.widgets.Button" to move the sliders. slider2_up = fig.add_axes([0.03, 0.84, 0.05, 0.04]) btn_slider2_up = Button(slider2_up, '+', hovercolor='0.975') slider2_down = fig.add_axes([0.03, 0.25, 0.05, 0.04]) btn_slider2_down = Button(slider2_down, '-', hovercolor='0.975') slider1_up = fig.add_axes([0.85, 0.03, 0.05, 0.04]) btn_slider1_up = Button(slider1_up, '+', hovercolor='0.975') slider1_down = fig.add_axes([0.25, 0.03, 0.05, 0.04]) btn_slider1_down = Button(slider1_down, '-', hovercolor='0.975') def reset(event): param1_slider.reset() param2_slider.reset() def slider_move(event, slider, delta): val = slider.val + delta if val > slider.valmax: val = slider.valmax if val < slider.valmin: val = slider.valmin slider.set_val(val) def preview(event): """ Prepare che ImageMagick convert command line """ input_filename = None if len(sys.argv) > 1: input_filename = sys.argv[1] if not os.path.exists(input_filename): input_filename = None if not input_filename: print('ERROR: Missing input image. Execute: %s [INPUT_IMAGE]' % (os.path.basename(sys.argv[0]),)) return contrast = param1_slider.val brightness = param2_slider.val output_filename = '%s_c%+.2f_b%+.2f.jpg' % (input_filename, contrast, brightness) cmd = ['convert'] cmd.append(input_filename) if abs(contrast) < EPSILON: pass elif contrast < 0: cmd.append('+sigmoidal-contrast') cmd.append('%.2f,50%%' % (-contrast,)) elif contrast > 0: cmd.append('-sigmoidal-contrast') cmd.append('%.2f,50%%' % (contrast,)) if abs(brightness) < EPSILON: pass elif brightness < 0: cmd.append('+sigmoidal-contrast') cmd.append('%.2f,0%%' % (-brightness,)) elif brightness > 0: cmd.append('-sigmoidal-contrast') cmd.append('%.2f,0%%' % (brightness,)) #cmd.extend(['-adaptive-sharpen', '0x1.8', '-quality', '97']) cmd.append(output_filename) # Print the command line to be executed. print('%s' % (' '.join(cmd),)) subprocess.call(cmd) # Call the user's preferred application to view the file. cmd = ['xdg-open', output_filename] subprocess.call(cmd) btn_reset.on_clicked(reset) btn_preview.on_clicked(preview) btn_slider1_up.on_clicked(lambda e: slider_move(e, param1_slider, STEP_CONTRAST)) btn_slider1_down.on_clicked(lambda e: slider_move(e, param1_slider, -STEP_CONTRAST)) btn_slider2_up.on_clicked(lambda e: slider_move(e, param2_slider, STEP_BRIGHTNESS)) btn_slider2_down.on_clicked(lambda e: slider_move(e, param2_slider, -STEP_BRIGHTNESS)) plt.show()