Here are two ways to do that in Python/OpenCV. Both are based upon the ratio of the log(mid-gray)/log(mean). Results are often reasonable, especially for dark image, but do not work in all cases. For bright image, invert the gray or value image, process as for dark images, then invert again and recombine if using the value image.
- Read the input
- Convert to gray or HSV value
- Compute the ratio log(mid-gray)/log(mean) on the gray or value channel
- Raise the input or value to the power of the ratio
- If using the value channel, combine the new value channel with the hue and saturation channels and convert back to RGB
Input:

import cv2
import numpy as np
import math
# read image
img = cv2.imread('lioncuddle1.jpg')
# METHOD 1: RGB
# convert img to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# compute gamma = log(mid*255)/log(mean)
mid = 0.5
mean = np.mean(gray)
gamma = math.log(mid*255)/math.log(mean)
print(gamma)
# do gamma correction
img_gamma1 = np.power(img, gamma).clip(0,255).astype(np.uint8)
# METHOD 2: HSV (or other color spaces)
# convert img to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
hue, sat, val = cv2.split(hsv)
# compute gamma = log(mid*255)/log(mean)
mid = 0.5
mean = np.mean(val)
gamma = math.log(mid*255)/math.log(mean)
print(gamma)
# do gamma correction on value channel
val_gamma = np.power(val, gamma).clip(0,255).astype(np.uint8)
# combine new value channel with original hue and sat channels
hsv_gamma = cv2.merge([hue, sat, val_gamma])
img_gamma2 = cv2.cvtColor(hsv_gamma, cv2.COLOR_HSV2BGR)
# show results
cv2.imshow('input', img)
cv2.imshow('result1', img_gamma1)
cv2.imshow('result2', img_gamma2)
cv2.waitKey(0)
cv2.destroyAllWindows()
# save results
cv2.imwrite('lioncuddle1_gamma1.jpg', img_gamma1)
cv2.imwrite('lioncuddle1_gamma2.jpg', img_gamma2)
Result from method 1:

Result from method 2:

I know I am late, but I would suggest using gamma correction.
Now what is gamma correction?
I will make it clear in layman's terms:
- To display image on a screen, input voltage is needed.
- This voltage is output as light intensity.
- In perfect world, input voltage would be linear to output intensity.
- But the real screen output is close to an exponential curve, the exponent being gamma.
Since the computer screen applies a gamma value to the image on screen, the process of applying inverse gamma to counter this effect is called gamma correction.

Here is the code for the same using OpenCV 3.0.0 and python:
import cv2
import numpy as np
def adjust_gamma(image, gamma=1.0):
invGamma = 1.0 / gamma
table = np.array([((i / 255.0) ** invGamma) * 255
for i in np.arange(0, 256)]).astype("uint8")
return cv2.LUT(image, table)
x = 'C:/Users/524316/Desktop/stack/test.jpg' #location of the image
original = cv2.imread(x, 1)
cv2.imshow('original',original)
gamma = 0.5 # change the value here to get different result
adjusted = adjust_gamma(original, gamma=gamma)
cv2.putText(adjusted, "g={}".format(gamma), (10, 30),cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 3)
cv2.imshow("gammam image 1", adjusted)
cv2.waitKey(0)
cv2.destroyAllWindows()
Here is the original image:

Applying gamma of value 0.5 will yield:

Applying gamma of value 1.5 will yield:

Applying gamma of value 2.5 will yield:

Applying gamma of value 1.0 will yield the same image.
Code was borrowed from this link
I think you can done this with opencv. Here is my suggestion
import cv2
import numpy as np
img1 = cv2.imread('abc.jpg')
a = np.double(img1)
b = a + 15
img2 = np.uint8(b)
cv2.imshow("frame",img1)
cv2.imshow("frame2",img2)
cv2.waitKey(0)
cv2.destroyAllWindows()
Here i increased the brightness of image. If you use subtraction that will makes darker.
On a high level, I believe that your problem is simply one of method compatibility: LUT works on the standard 8-bit format. You have to convert your image to that format before feeding it to the method. Alternately, you can write your own 12-bit LUT method, replacing all of the upper limits in the original code with your 4095/4096.
You can do Gamma Correction without cv2 for data whose bandwidth is not 8bit or not formulated with integer type.
import numpy as np
def gamma_correction(img: np.ndarray, gamma: float=1.0):
igamma = 1.0 / gamma
imin, imax = img.min(), img.max()
img_c = img.copy()
img_c = ((img_c - imin) / (imax - imin)) ** igamma
img_c = img_c * (imax - imin) + imin
return img_c
Example of Usage
import imageio
import matplotlib.pyplot as plt
import numpy as np
test = imageio.imread('test.png')
test_g = gamma_correction(test, gamma=2.2).astype(np.uint8)
plt.subplot(1, 2, 1)
plt.imshow(test)
plt.subplot(1, 2, 2)
plt.imshow(test_g)
plt.show()
Example Result











