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:

Gamma correction is not directly implemented in opencv.
What you can do is to implement a lookup table that maps each value from 0 to 255 to the corresponding value after the gamma correction: ((value/255) ** (1/gamma)) * 255. After creating the LUT you can use the cv2.LUT to map the values of the image to the values after correction - cv2.LUT(image, table).
Link to python implementation:
https://www.pyimagesearch.com/2015/10/05/opencv-gamma-correction/#:~:text=There%20are%20two%20(easy)%20ways,range%20%5B0%2C%20255%5D.
Eventually, I found the way to do it (according to this tutorial https://docs.opencv.org/3.4/d3/dc1/tutorial_basic_linear_transform.html):
//>>>Get Frame
Decoder decoder;
decoder.HandleRequest_GetFrame(nullptr, filename, 1, image);
//<<<
//>>> Collor conversion from BGR to RGB
cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
//<<<
#if 1
//>>> Gamma correction
cv::Mat new_image = cv::Mat::zeros(image.size(), image.type());
double alpha = 2.2; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
for (int y = 0; y < image.rows; y++) {
for (int x = 0; x < image.cols; x++) {
for (int c = 0; c < image.channels(); c++) {
new_image.at<cv::Vec3b>(y, x)[c] =
cv::saturate_cast<uchar>(alpha*image.at<cv::Vec3b>(y, x)[c] + beta);
}
}
}
image = new_image;
//<<<
#endif // 0
bool isOk = cv::imwrite(save_location, image);