8

I am trying to implement shadow removal in python OpenCV using the method of entropy minimization by Finlayson, et. al.:

"Intrinsic Images by Entropy Minimization", Finlayson, et. al.

I can't seem to match the results from the paper. My entropy plot does not match up with those from the paper and I am getting the wrong minimum entropy.

Any thoughts? (I have much more source code and papers upon request)

#############
# LIBRARIES
#############
import numpy as np
import cv2
import os
import sys
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
from PIL import Image
import scipy
from scipy.optimize import leastsq
from scipy.stats.mstats import gmean
from scipy.signal import argrelextrema
from scipy.stats import entropy
from scipy.signal import savgol_filter

root = r'\path\to\my_folder'
fl = r'my_file.jpg'

#############
# PROGRAM
#############
if __name__ == '__main__':

    #-----------------------------------
    ## 1. Create Chromaticity Vectors ##
    #-----------------------------------

    # Get Image
    img = cv2.imread(os.path.join(root, fl))
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    h, w = img.shape[:2]

    plt.imshow(img)
    plt.title('Original')
    plt.show()

    img = cv2.GaussianBlur(img, (5,5), 0)

    # Separate Channels
    r, g, b = cv2.split(img) 

    im_sum = np.sum(img, axis=2)
    im_mean = gmean(img, axis=2)

    # Create "normalized", mean, and rg chromaticity vectors
    #  We use mean (works better than norm). rg Chromaticity is
    #  for visualization
    n_r = np.ma.divide( 1.*r, g )
    n_b = np.ma.divide( 1.*b, g )

    mean_r = np.ma.divide(1.*r, im_mean)
    mean_g = np.ma.divide(1.*g, im_mean)
    mean_b = np.ma.divide(1.*b, im_mean)

    rg_chrom_r = np.ma.divide(1.*r, im_sum)
    rg_chrom_g = np.ma.divide(1.*g, im_sum)
    rg_chrom_b = np.ma.divide(1.*b, im_sum)

    # Visualize rg Chromaticity --> DEBUGGING
    rg_chrom = np.zeros_like(img)

    rg_chrom[:,:,0] = np.clip(np.uint8(rg_chrom_r*255), 0, 255)
    rg_chrom[:,:,1] = np.clip(np.uint8(rg_chrom_g*255), 0, 255)
    rg_chrom[:,:,2] = np.clip(np.uint8(rg_chrom_b*255), 0, 255)

    plt.imshow(rg_chrom)
    plt.title('rg Chromaticity')
    plt.show()

    #-----------------------
    ## 2. Take Logarithms ##
    #-----------------------

    l_rg = np.ma.log(n_r)
    l_bg = np.ma.log(n_b)

    log_r = np.ma.log(mean_r)
    log_g = np.ma.log(mean_g)
    log_b = np.ma.log(mean_b)

    ##  rho = np.zeros_like(img, dtype=np.float64)
    ##
    ##  rho[:,:,0] = log_r
    ##  rho[:,:,1] = log_g
    ##  rho[:,:,2] = log_b

    rho = cv2.merge((log_r, log_g, log_b))

    # Visualize Logarithms --> DEBUGGING
    plt.scatter(l_rg, l_bg, s = 2)
    plt.xlabel('Log(R/G)')
    plt.ylabel('Log(B/G)')
    plt.title('Log Chromaticities')
    plt.show()

    plt.scatter(log_r, log_b, s = 2)
    plt.xlabel('Log( R / 3root(R*G*B) )')
    plt.ylabel('Log( B / 3root(R*G*B) )')
    plt.title('Geometric Mean Log Chromaticities')
    plt.show()

    #----------------------------
    ## 3. Rotate through Theta ##
    #----------------------------
    u = 1./np.sqrt(3)*np.array([[1,1,1]]).T
    I = np.eye(3)

    tol = 1e-15

    P_u_norm = I - u.dot(u.T)
    U_, s, V_ = np.linalg.svd(P_u_norm, full_matrices = False)

    s[ np.where( s <= tol ) ] = 0.

    U = np.dot(np.eye(3)*np.sqrt(s), V_)
    U = U[ ~np.all( U == 0, axis = 1) ].T

    # Columns are upside down and column 2 is negated...?
    U = U[::-1,:]
    U[:,1] *= -1.

    ##  TRUE ARRAY:
    ##
    ##  U = np.array([[ 0.70710678,  0.40824829],
    ##                [-0.70710678,  0.40824829],
    ##                [ 0.        , -0.81649658]])

    chi = rho.dot(U) 

    # Visualize chi --> DEBUGGING
    plt.scatter(chi[:,:,0], chi[:,:,1], s = 2)
    plt.xlabel('chi1')
    plt.ylabel('chi2')
    plt.title('2D Log Chromaticities')
    plt.show()

    e = np.array([[np.cos(np.radians(np.linspace(1, 180, 180))), \
                   np.sin(np.radians(np.linspace(1, 180, 180)))]])

    gs = chi.dot(e)

    prob = np.array([np.histogram(gs[...,i], bins='scott', density=True)[0] 
                      for i in range(np.size(gs, axis=3))])

    eta = np.array([entropy(p, base=2) for p in prob])

    plt.plot(eta)
    plt.xlabel('Angle (deg)')
    plt.ylabel('Entropy, eta')
    plt.title('Entropy Minimization')
    plt.show()

    theta_min = np.radians(np.argmin(eta))

    print('Min Angle: ', np.degrees(theta_min))

    e = np.array([[-1.*np.sin(theta_min)],
                  [np.cos(theta_min)]])

    gs_approx = chi.dot(e)

    # Visualize Grayscale Approximation --> DEBUGGING
    plt.imshow(gs_approx.squeeze(), cmap='gray')
    plt.title('Grayscale Approximation')
    plt.show()

    P_theta = np.ma.divide( np.dot(e, e.T), np.linalg.norm(e) )

    chi_theta = chi.dot(P_theta)
    rho_estim = chi_theta.dot(U.T)
    mean_estim = np.ma.exp(rho_estim)

    estim = np.zeros_like(mean_estim, dtype=np.float64)

    estim[:,:,0] = np.divide(mean_estim[:,:,0], np.sum(mean_estim, axis=2))
    estim[:,:,1] = np.divide(mean_estim[:,:,1], np.sum(mean_estim, axis=2))
    estim[:,:,2] = np.divide(mean_estim[:,:,2], np.sum(mean_estim, axis=2))

    plt.imshow(estim)
    plt.title('Invariant rg Chromaticity')
    plt.show()

Output:

Original

rg Chromaticity

Log Chromaticities

Geometric Mean Log Chromaticities

2D Geometric Log Chromaticities

Entropy Minimization

Grayscale Approximation

Invariant rg Chromaticity

adam.hendry
  • 4,458
  • 5
  • 24
  • 51
  • I get something like that, different with the paper too. https://i.stack.imgur.com/ZK0Zn.png – Kinght 金 Dec 11 '17 at 09:52
  • Frustrating, right! It's like it's almost there, but it's still missing something. I feel like it's obvious, but I don't know... – adam.hendry Dec 11 '17 at 15:47
  • Does anyone have a solution? – adam.hendry Dec 11 '17 at 21:11
  • Hello A. Hendry, did you make it work? I'm trying to remove shadow from some leaves to segment it. – Tarcisiofl Jun 25 '18 at 05:06
  • I notice someone attempting same in python [here](https://github.com/srijan-mishra/Shadow-Removal) But they appeared to stop at similar chromaticity step as you reached above. Two interesting papers from 2012: [Shadow Detection: A Survey and Comparative Evaluation of Recent Methods](https://arxiv.org/pdf/1304.1233.pdf) Covering following methods - **chromacity-based, physical, geometry-based, texture-based.** [Shadow Removal for Aerial Imagery by Information Theoretic Intrinsic Image Analysis](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37743.pdf) From 3 goo – Rossoe Jun 03 '19 at 02:02
  • Have you been able to convert the chromaticity image back to RGB at the end? – Daniel C Jacobs Feb 25 '21 at 21:23

1 Answers1

0

Shadow Removal Using Illumination Invariant Image Formation (Ranaweera, Drew) notes under Results and Discussion that the results from JPEG images and PNG images differ due to the JPEG compression. So expecting results exactly like what "Intrinsic Images by Entropy Minimization" (Finlayson, et. al.) shows may not be reasonable.

I also notice that you are not adding back the 'extra light' that the author recommends in other papers.

Also, while defining rg_chrom, the order of the channels needs to be BGR instead of RGB like you have used.

I'm working on implementing the paper, so your code was extremely useful to me. Thanks for that

Shawn Mathew
  • 2,198
  • 1
  • 14
  • 31
  • I have a working implementation now. I've been in correspondence with the authors of the paper. It is there express wish not to have this released to GitHub. Please do not if you were intending to. – adam.hendry Feb 21 '18 at 03:40
  • Sorry, that came out arrogant, harsh, and all wrong. Please let me explain. Thank you so much for looking at my post and my code! That is absolutely the point: sharing with the world to learn and grow. I appreciate your feedback too! You are absolutely right. JPEG compression via FFT (or any other lossless compression) removes vital data from the image, and you will not get the right result.The extra light is also important and you do have to be careful against BGR vs RGB. – adam.hendry Feb 21 '18 at 14:05
  • The reason I asked if you would not post to Github is because Dr.'s Finlayson, et. al., have students who must implement the code as part of their coursework. I think I'm going to ask again if I can submit a pull request to OpenCV to add their algorithm to the library, making sure to give them credit. I don't believe there is anything legally preventing us from posting to GitHub as Python is open source, but I'm simply doing it out of honor. Let me see if I can get in touch with them again. Again, @Shawn Mathew thank you so much and good luck! – adam.hendry Feb 21 '18 at 14:12
  • By chance, are you doing this for Udacity, another course, or for personal edification? – adam.hendry Feb 21 '18 at 14:19
  • @A.Hendry thanks for letting me know. I'm implementing this for a project I'm working on. I've been going over his papers over the last two days to figure out how to implement it. Wish the papers had more implementation detail. Hopefully you get permission to add the code to the opencv repo. That would be incredibly valuable to a lot of people as the results shown in the paper are fantastic – Shawn Mathew Feb 21 '18 at 19:46
  • Couldn't agree more. I emailed last night and am still awaiting a response. – adam.hendry Feb 22 '18 at 20:06
  • Perhaps the students could just use the honor system and not directly use the OpenCV functions. We did that when I was in college. – Shawn Mathew Feb 23 '18 at 21:25
  • Perhaps. Me too. I never learned anything if I copied the code anyway...I still haven't heard back from them, so I'm assuming for now it's still a no go – adam.hendry Feb 24 '18 at 22:10
  • @A.Hendry if you're able to share your solution, would you be able to contact me via my GH - https://github.com/GeorgeR/ – George R Mar 18 '20 at 02:23
  • hmm, any word on whether we could publish this to github? I was looking to try this out on a weekend exercise and was wondering how does this fare against adaptivethresholding. my github handle is github.com/lackdaz – lackdaz Nov 08 '21 at 17:10