Python plug-in: Sigmoid curve

Hi,
I made another new Python plug-in for GIMP3. This plug-in apply Sigmoid curve to the selected layer parametrically. You can download it from below link.

If you install it, you can start it from [color] menu.

It duplicates original target layer and applies edits on the duplicated layer.
You can adjust the applying curve with Contrast, Symmetric Point, Darkest Point (Black point), Brightest Point (White point), and you can easily confirm the shape of the curve on the dialog.

Also you can switch the applying channel.

The default calculation mode is legacy mode (the curve calculation is performed in perceptual RGB space), however you can switch it to linear mode (the curve calculation is performed in linear RGB space).

If you’re interested in it, please give it a try.

4 Likes

Greetings and many thanks.
Like the previous ones, I immediately published
on the pages of Poland

Thank you very much!

I have updated this script. In this version, you can select curve graph display mode from “Perceptual” and “Linear”. And also I added mid point and symmetry point marker on X axis.

I have updated this script. In this version, you can adjust the Gamma of calculation color space to create more flexible curve.

If you select “legacy” as “Calculation space”, it calculates the curve in perceptual TRC encoded space, and with “linear”, it calculates the curve in linear color space. If you select “specify Gamma”, you can adjust the Gamma of the calculation color space with slider.

Download link is here.

I have internationalized this plug-in (ver. 2.0). In this version, I provide following language translation files.

CatalĂ 
Deutsch
Español
français
Italiano
Japanese
Korean
Magyar
Nederlands
Polski

Translation files are “locale” directory under this plug-in’s directory.

Even if your language is not included this list, you can easily make your language locale file. Throw “defaut.txt” in locale directory to online AI translation service, and save the result as [your language code].txt in locale directory. For example, you would like to add Vietnamese translation, save the result as vi.txt in locale directory. You can find the list of language code in [Edit] > [Preferences] > [Interface]>[Language].

And if you use Mac OS, please set your language explicitly in preferences and do not use system language setting. Otherwise my plug-in will be displayed in English.

1 Like

Hi,

I have updated and internationalized this plug-in (ver. 2.1). In this version, I added following features.

Display of the mean, standard deviation, median of targeted channel image.

Some tooltips.

1 Like

How to restore reddish faded reversal film image with Sigmoid curve plug-in

Hi,
I would like to show one of the usage of this plug-in.

Original image.

  1. Run the Sigmoid curve plug-in and check each channel image using target channel selector. You must check what channel has the highest or lowest mean value or standard deviation.


[Red Channel]


[Green Channel]


[Blue Channel]

  1. In this image, R channel has highest mean value, you must correct the R channel first. And it seems that B channel mean value is relatively low, so, in next, correct the B channel.

  2. In this plug-in, basically using Sigmoid correction to change the image brightness level. So to lower the R channel level, you must move IP to 100.0 and change contrast value as far the R level becomes similar level with other channels.

  3. After once complete the editing process and correct the R channel, you run this plug-in again to correct the B channel level. In this turn, you set the IP under 50, but not 0, because it seems that in highlight it is needed to raise the blue lever but in shadow it is not needed to raise it.

  1. Complete the edition process with Sigmoid curve plug-in. And after that you run color enhance to saturate more.

  1. In the result, it seems that magenta is too high, so you use another tool, the relative RGB color mask maker plug-in and make magenta mask.

  1. With magenta mask added layer, you correct the layer as below to diminish magenta.

  1. In the result image it seems that concrete parts is too bluish, so to diminish the blue cast in those parts, you make blue mask with he relative RGB color mask maker plug-in and edit the mask to cover the concrete parts.

  1. De-saturate the concrete parts.

Hello

My method is less sophisticated more mundane, using existing filters:


Oryginal image

  1. I set the Master Point to the point of the image that should be white
  2. Now Colors => Eqalize or
  3. Colors => Levels, I select Channel Red, because that is the Color Cast, now White Point Selection for all channels, I click near the Master Point. I set the sliders Channel Red, observing the displayed values in the Reference Point and visually

    And that’s it, possibly still Color-enhance

Descriptions on the screenshots are in Polish PL

Hi,
I have update this program for GIMP 3.2 (ver. 3.0). UI is not changed.

Hi,
I have updated this plug-in.

Updated points are below.
1) Adjustment ranges for the darkest and brightest points have been changed

For the darkest point (black point), the minimum adjustable value has been lowered to -10, and for the brightest point (white points), the maximum adjustable value has been raised to 120.

2) In GIMP 3.2, a [Remove Edit in Current Channel] button has been added

Although this feature is limited to GIMP 3.2, I have added a button to clear and reset the edits made to the currently selected channel.

3) In GIMP 3.2, enabled editing of all channels using non-destructive filters

Related to the new feature above, in previous versions, only one channel could be edited per layer. However, in this version, I’ve improved the system so that edits can coexist across up to four channels by allowing users to apply one non-destructive filter to each of the Luminance, R, G, and B channels. This has made the software much more practical.

In previous versions, to achieve the same effect, you had to first edit one channel, then launch the plugin again to edit another channel on the resulting layer… and so on, stacking result layers one on top of another. In this version, however, you can achieve the same results by adjusting the filters for each channel within a single layer, which also improves file size and performance.

Note that GIMP 3.0 will continue to operate as before.

4) General Performance Optimization
I have revised the algorithms across the board and optimized them to operate more efficiently, thereby improving performance.

1 Like

Sigmoid Curve — @yasuo, post:1, topic:51168

Sigmoid Curve plugin — explanation for photographers + bug fixes

Thanks so much for this plugin. It’s really sophisticated, giving parametric
control over a mathematically precise curve, calculation space options icluding custom gamma, per-channel editing, and a live preview that updates as you drag. That’s a lot of thoughtful engineering packed into a clean interface.

I wanted to add two things: some context for photographers who might scroll past because “sigmoid curve” sounds like a math thing, and some bug fixes I worked out while testing.


What is a sigmoid curve, and why should I care?

A sigmoid curve is a mathematically precise s-curve. Instead of dragging curve points around by hand, the math delivers a smooth, symmetrical curve that’s easily reproducible.

How is it different from drawing an s-curve manually?

Drawing an s-curve by hand is finicky. You have fine control, and can generate lovely creative effects, depending on where you place your points. A sigmoid curve is more mathematically correct because it’s defined by a formula. It can be a good starting point for more creative effects, and it’s ideal for batch processing when you want consistency across a series of photos.

The sliders make this plugin super usable

  • Contrast (gain) controls how aggressive the s-shape is: a low value gives a gentle lift while a high value is more aggressive.
  • Inflection Point shifts where the contrast boost is centred in the tonal
    range. Below 50 biases contrast toward shadows; above 50 biases toward
    highlights. You can target exactly where you want the adjustment to land
    without touching separate shadow/highlight sliders.
  • Black Point / White Point clamp the extremes, protecting shadow detail and preventing highlights from blowing out while the contrast adjustment does its work.
  • Calculation space controls whether the sigmoid is applied in perceptual (sRGB) space, linear light, or a custom gamma. Legacy/perceptual is the most intuitive because the curve behaves the way your eye sees it. Linear is more physically accurate and tends to preserve shadow detail better on high bit-depth images. Specify Gamma lets you specify a specific gamma value to the linear option.

When is this useful?

  • Flat images that need punch without clipping shadows or highlights
  • A general s-curve that you then fine tune for creative reasons
  • Consistent results across a series of shots taken in the same conditions
  • Contrast adjustments targeted at a specific tonal range (just shadows, just
    midtones, just highlights)
  • High bit-depth images where working in linear space gives cleaner results

When won’t it help?

  • Recovering badly blown highlights or crushed shadows — those need targeted recovery adjustments, not a contrast curve
  • Heavy creative grading with asymmetric tonal shifts (warmer shadows, cooler highlights) — a freeform curve gives more flexibility there

For everyday contrast work, this plugin will often be faster, more precise, and more repeatable than drawing curves by hand.

Bug fixes

While testing I found several bugs, all in update_curve() and the graph
display code. I’m posting diffs against the version 3.1 source. All fixes have
been tested on GIMP 3.2.4, 16-bit non-linear image.

Bug 1 — dRange() remapping logic was inverted

The original code applied white point and black point scaling in the wrong order, distorting the lower end of the input range before it reached the sigmoid. The correct formula is a simple linear remap from [darkest, brightest] to [0, 1].

-def dRange(x,darkest,brightest):
-    # BP, WP adjustment
-    maxFactor = 1.0 / brightest
-    x2 = min(x * maxFactor, 1.0)
-    minFactor = 1.0 / (1.0 - darkest)
-    x2 = 1.0 - (1.0 - x2) * minFactor
-    x2 = max(x2, 0.0)
-    return x2
+def dRange(x, darkest, brightest):
+    """Remap x from [darkest, brightest] to [0.0, 1.0].
+    Values outside the range are clamped.
+    """
+    if brightest <= darkest:
+        return 0.0
+    x2 = (x - darkest) / (brightest - darkest)
+    return max(0.0, min(1.0, x2))

Bug 2 — Inflection Point slider direction was inverted

The tooltip said “below 50 = more shadow contrast” but the behaviour was the opposite. Fixed by inverting sp after all space conversions, immediately before it is passed to sigmoidAdj. The inversion must happen last — doing it before srgbToLinear corrupts the value in linear mode.

-        sp = self.sp / 100.0
+        sp = self.sp / 100.0
         darkest = self.darkest / 100.0
         brightest = self.brightest / 100.0
+        calcGamma = 1.0  # safe default; overwritten below if linear mode is active
         if self.linear == True:
-            sp = srgbToLinear(sp)
             darkest = srgbToLinear(darkest)
             brightest = srgbToLinear(brightest)
             calcGamma = self.savedValue[i][4]
             if calcGamma != 1.0:
-                sp = sp ** (1.0 / calcGamma)
                 darkest = darkest ** (1.0 / calcGamma)
                 brightest = brightest ** (1.0 / calcGamma)
+        # Invert AFTER space conversion so shift is in the same space as
+        # pointsX2. Low IP = shadow contrast, high IP = highlight contrast.
+        sp = 1.0 - sp

Bug 3 — X/Y misalignment: curve X axis did not match Y values

pointsX2 (the dRange-adjusted values) was fed to sigmoidAdj to produce pointsY, but the original pointsX ramp was used as the curve’s X axis. The
Y values corresponded to adjusted input positions, not the original ones, so the curve was applied at the wrong X positions.

-        curve=[p for xy in zip(pointsX,pointsY) for p in xy]
+        # Use pointsX2 (dRange-adjusted) as X axis so curve X positions
+        # match the Y values computed from them.
+        curve=[p for xy in zip(pointsX2,pointsY) for p in xy]

Bug 4 — TRCType hardcoded to LINEAR regardless of calculation space

In GIMP 3.2, TRCType tells GIMP which colour space to apply the curve in.
Legacy mode calculates the sigmoid in sRGB (encoded) space and needs NON_LINEAR. Linear and specify Gamma modes calculate in linear light and need LINEAR. With LINEAR hardcoded, switching calculation space changed the graph display but had no effect on the actual image.

Also corrected: pointsX (raw ramp) was passed instead of pointsX2 (dRange-adjusted) to setup_curves_filter.

+            # TRCType must match the space the sigmoid was calculated in.
+            # Legacy: calculated in sRGB (encoded) space → NON_LINEAR.
+            # Linear / specify Gamma: calculated in linear light → LINEAR.
+            trc = Gimp.TRCType.LINEAR if self.linear else Gimp.TRCType.NON_LINEAR
             curves_filter = setup_curves_filter(
                 self.layer, self.channel,
-                pointsX, pointsY, Gimp.TRCType.LINEAR, filterName)
+                pointsX2, pointsY, trc, filterName)

Bug 5 — Graph X axis did not reflect dRange adjustment

The display graph was passing pointsX (unadjusted) instead of pointsX2 to
update_data, so the graph didn’t show the flat regions at the black and white point limits. A photographer needs to see exactly what the curve is doing to their image.

         if self.linear == True and self.dispmode == mTrans["LBL_DISPMODE1"]:
-            pointsX_disp = [linearToSrgb(p) for p in pointsX]
+            pointsX_disp = [linearToSrgb(p) for p in pointsX2]
             pointsY_disp = [linearToSrgb(p) for p in pointsY]
         elif self.linear == False and self.dispmode == mTrans["LBL_DISPMODE2"]:
-            pointsX_disp = [srgbToLinear(p) for p in pointsX]
+            pointsX_disp = [srgbToLinear(p) for p in pointsX2]
             pointsY_disp = [srgbToLinear(p) for p in pointsY]
         else:
             pointsY_disp = pointsY
-            pointsX_disp = pointsX
+            pointsX_disp = pointsX2

Bug 6 — Inflection Point marker in graph was not inverted

After fixing the slider direction, the tick mark on the graph X axis still drew at the old (uninverted) position.

         if self.dispmode == mTrans["LBL_DISPMODE1"]:
-            g_sp = 40 + (width - 10 - 40) * self.sp / 100
+            g_sp = 40 + (width - 10 - 40) * (1.0 - self.sp / 100)
         else:
-            g_sp = 40 + (width - 10 - 40) * srgbToLinear(self.sp / 100)
+            g_sp = 40 + (width - 10 - 40) * srgbToLinear(1.0 - self.sp / 100)

Bug 7 — TP_IP tooltip silently truncated (syntax error)

A missing backslash on the second line of the TP_IP string caused Python to silently discard everything after the first two lines. Also took the opportunity to rewrite the tooltip in photographer-friendly language.

-    TP_IP = "50.0: Apply a symmetric S shape curve.\n"\
-        + "0.0: Apply an upward curve.\n"
-        + "100.0: Apply an downward curve.\n"
-        + "under 50.0: Apply an s-curve with a high upward proportion.\n"
-        + "over 50.0: Apply an s-curve with a high downward proportion.\n"
-        + "The symmetry here is based on perceptual color space.",
+    TP_IP = "Controls where the contrast boost is centred.\n"\
+        + "50: symmetric s-curve (equal shadow and highlight contrast).\n"\
+        + "Below 50: more contrast in shadows.\n"\
+        + "Above 50: more contrast in highlights.",

Bug 8 — dead initialisation of self.contrast

self.contrast = 0.1 was set and immediately overwritten by self.contrast = 0.0 a few lines later. The 0.1 line is dead code and was removed.

-        self.contrast = 0.1  # Default value for scale1
         self.sp = 50.0
         self.darkest = 0.0
         self.brightest = 100.0

After all those fixes the plugin behaves correctly across all three calculation
spaces, the inflection point slider direction matches its tooltip, the graph
accurately reflects what is applied to the image, and legacy vs linear modes
produce visibly different results as intended. Tested on a 16-bit non-linear
sRGB image in GIMP 3.2.4 on Ubuntu 26.04.

2 Likes

(post deleted by author)

Thank you for your information!
I’ll take a look.

1 Like

Hi,

Thank you very much for your sincere advices. I made the bug fixes based on them. However I didn’t changed the moving direction of IP slider, and instead of it I changed the description of tool tip of it as I think it is intuitive the IP slider moving direction is same as IP mark moving direction on the tone curve graph.

Here is the download link.

The tool tip is below.

    TP_IP = "Controls where the contrast boost is centred.\n"\
        + "50: symmetric s-curve in perceptual graph \n(equal shadow and highlight contrast).\n"\
        + "Below 50: Assign a wider tonal range to the shadow.\n"\
        + "Above 50: Assign a wider tonal range to the highlights.",

1 Like

Hi,
I have updated this plugin (version 3.3).

The main changes are as follows:

Fixed a bug that occurred when a negative value was set for the “darkest point.”

In GIMP 3.0, it is now possible to specify the target layer for editing.

The user-specified gamma value is now preserved even when the calculation color space is changed. This makes it easier to compare effects across different calculation color spaces. In previous versions, changing the calculation color space from “User-specified gamma” to “Linear” would cause the specified gamma value to revert to 1.0 instead of being retained.

Improved the curve graph display. Changes to the “darkest point” or “brightest point” values ​​are now clearly reflected on the graph.

1 Like