How Do I Develop a negative film image using python

Question:

I have tried inverting a negative film images color with the bitwise_not() function in python but it has this blue tint. I would like to know how I could develop a negative film image that looks somewhat good. Here’s the outcome of what I did. (I just cropped the negative image for a new test I was doing so don’t mind that)

enter image description here

Asked By: RYAN RAMU

||

Answers:

Here is one simple way to do that in Python/OpenCV. Basically one stretches each channel of the image to full dynamic range separately. Then recombines. Then inverts.

Input:

enter image description here

import cv2
import numpy as np
import skimage.exposure

# read image
img = cv2.imread('boys_negative.png')

# separate channels
r,g,b = cv2.split(img)

# stretch each channel
r_stretch = skimage.exposure.rescale_intensity(r, in_range='image', out_range=(0,255)).astype(np.uint8)
g_stretch = skimage.exposure.rescale_intensity(g, in_range='image', out_range=(0,255)).astype(np.uint8)
b_stretch = skimage.exposure.rescale_intensity(b, in_range='image', out_range=(0,255)).astype(np.uint8)

# combine channels
img_stretch = cv2.merge([r_stretch, g_stretch, b_stretch])

# invert
result = 255 - img_stretch

cv2.imshow('input', img)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()

# save results
cv2.imwrite('boys_negative_inverted.jpg', result)

Result:

enter image description here

Caveat: This works for this image, but may not be a universal solution for all images.

ADDITION

In the above, I did not clip when stretching as I wanted to preserver all information. But if one wants to clip and use skimage.exposure.rescale_intensity for stretching, then it is easy enough by the following:

import cv2
import numpy as np
import skimage.exposure

# read image
img = cv2.imread('boys_negative.png')

# separate channels
r,g,b = cv2.split(img)

# compute clip points -- clip 1% only on high side
clip_rmax = np.percentile(r, 99)
clip_gmax = np.percentile(g, 99)
clip_bmax = np.percentile(b, 99)
clip_rmin = np.percentile(r, 0)
clip_gmin = np.percentile(g, 0)
clip_bmin = np.percentile(b, 0)

# stretch each channel
r_stretch = skimage.exposure.rescale_intensity(r, in_range=(clip_rmin,clip_rmax), out_range=(0,255)).astype(np.uint8)
g_stretch = skimage.exposure.rescale_intensity(g, in_range=(clip_gmin,clip_gmax), out_range=(0,255)).astype(np.uint8)
b_stretch = skimage.exposure.rescale_intensity(b, in_range=(clip_bmin,clip_bmax), out_range=(0,255)).astype(np.uint8)

# combine channels
img_stretch = cv2.merge([r_stretch, g_stretch, b_stretch])

# invert
result = 255 - img_stretch

cv2.imshow('input', img)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()

# save results
cv2.imwrite('boys_negative_inverted2.jpg', result)

Result:

enter image description here

Answered By: fmw42

If you don’t use exact maximum and minimum, but 1st and 99th percentile, or something nearby (0.1%?), you’ll get some nicer contrast. It’ll cut away outliers due to noise, compression, etc.

Additionally, you should want to mess with gamma, or scale the values linearly, to achieve white balance.

I’ll apply a "gray world assumption" and scale each plane so the mean is gray. I’ll also mess with gamma, but that’s just messing around.

And… all of that completely ignores gamma mapping, both of the "negative" and of the outputs.

import numpy as np
import cv2 as cv
import skimage

im = cv.imread("negative.png")
(bneg,gneg,rneg) = cv.split(im)

def stretch(plane):
    # take 1st and 99th percentile
    imin = np.percentile(plane, 1)
    imax = np.percentile(plane, 99)

    # stretch the image
    plane = (plane - imin) / (imax - imin)

    return plane
b = 1 - stretch(bneg)
g = 1 - stretch(gneg)
r = 1 - stretch(rneg)

bgr = cv.merge([b,g,r])
cv.imwrite("positive.png", bgr * 255)

plain

b = 1 - stretch(bneg)
g = 1 - stretch(gneg)
r = 1 - stretch(rneg)

# gray world
b *= 0.5 / b.mean()
g *= 0.5 / g.mean()
r *= 0.5 / r.mean()

bgr = cv.merge([b,g,r])
cv.imwrite("positive_grayworld.png", bgr * 255)

gray world

b = 1 - np.clip(stretch(bneg), 0, 1)
g = 1 - np.clip(stretch(gneg), 0, 1)
r = 1 - np.clip(stretch(rneg), 0, 1)

# goes in the right direction
b = skimage.exposure.adjust_gamma(b, gamma=b.mean()/0.5)
g = skimage.exposure.adjust_gamma(g, gamma=g.mean()/0.5)
r = skimage.exposure.adjust_gamma(r, gamma=r.mean()/0.5)

bgr = cv.merge([b,g,r])
cv.imwrite("positive_gamma.png", bgr * 255)

gamma

Here’s what happens when gamma is applied to the inverted picture… a reasonably tolerable transfer function results from applying the same factor twice, instead of applying its inverse.

screenshot

screenshot

Trying to "undo" the gamma while ignoring that the values were inverted… causes serious distortions:

screenshot

screenshot

And the min/max values for contrast stretching also affect the whole thing.

A simple photo of a negative simply won’t do. It’ll include stray light that offsets the black point, at the very least. You need a proper scan of the negative.

Answered By: Christoph Rackwitz
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.