Search code examples
pythonoptimizationdesign-patternsfunctional-programming

How to avoid recomputing several variables without completely refactoring code


I have a category of functions 'detectors', that, given a video, a space scale parameter and a time scale parameter, detect some interest points. I wrote a 'MultiscaleDetector' function, that basically takes a list of space scale parameters and a 'detector' function as parameters and executes the given detector factor for every scale.

This is how they look:

def GaborDetector(v, sigma, tau, threshold, num_points):
    """
    Gabor Detector
    
    Keyword arguments:
    video -- input video (y_len, x_len, frames)
    sigma -- Gaussian kernel space standard deviation
    tau -- Gaussian kernel time standard deviation
    kappa -- Gabor response threshold
    """
    # setup video
    video = v.copy()
    video = video.astype(float)/video.max()
    video = video_smoothen_space(video, sigma)
    # first define a linspace of width -2tau to 2tau
    time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
    omega = 4/tau
    # define the gabor filters
    h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
    h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
    # normalize the L1 norm
    h_ev /= np.linalg.norm(h_ev, ord=1)
    h_od /= np.linalg.norm(h_od, ord=1)
    # compute the response
    response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
    points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
    return points
def MultiscaleDetector(detector, video, sigmas, tau, num_points):
    """
    Multiscale Detector

    Executes a detector at multiple scales. Detector has to be a function that
    takes a video as input, along with other parameters, and returns a list of interest points.

    
    Keyword arguments:
    detector -- function that returns interest points
    video -- input video (y_len, x_len, frames)
    sigmas -- list of scales
    """
    
    # for every scale, compute the response
    points = []
    for sigm in sigmas:
        found = detector(video, sigm, tau)
        points.append(found)

    # filter the points, currently irrelevant

As you can maybe already notice, there is some heavy computation done inside the "GaborDetector" function, that depends only on the time parameter. Therefore, the "MultiscaleDetector" function unnecessarily recomputes these variables in each call.

In an attempt to avoid refactoring the code, I came up with what I wished was a nice trick, but believed it would be vain:

def MultiscaleDetector(detector, video, sigmas, tau, num_points):
    """
    Multiscale Detector

    Executes a detector at multiple scales. Detector has to be a function that
    takes a video as input, along with other parameters, and returns a list of interest points.

    
    Keyword arguments:
    detector -- function that returns interest points
    video -- input video (y_len, x_len, frames)
    sigmas -- list of scales
    """
    
    optimization_trick = lambda s_param: detector(video, s_param, tau)
    # for every scale, compute the response
    points = []
    for sigm in sigmas:
        found = optimization_trick(sigm)
        points.append(found)

    # filter the points, currently irrelevant

I hoped that the variables dependent only on tau would remain "stored" somehow in the "optimization_trick" and avoid being recomputed. However, when I timed the different implementations, the difference was around 0.02 seconds, with the "optimized" function being faster.

EDIT:

The actual call that takes place is the following:

# read the video
video = read_video(video_name, num_frames, 0)
# get the interest points
detector = lambda v, s, t: GaborDetector(v, s, t, 0.3, 500)
scales = [3*(1.1)**i for i in range(6)]
start = time.time()
points = MultiscaleDetector(detector, video, scales, 1.5, 500)
end = time.time()
print("Time: {}".format(end-start))

I tried putting all the variables that i wanted to avoid in a different function like this:

def GaborDetectorTrial(v, sigma, tau, threshold, num_points):
    """
    Gabor Detector
    
    Keyword arguments:
    video -- input video (y_len, x_len, frames)
    sigma -- Gaussian kernel space standard deviation
    tau -- Gaussian kernel time standard deviation
    kappa -- Gabor response threshold
    """
    @lru_cache(maxsize=None)
    def time_function(tau):
        time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
        omega = 4/tau
        # define the gabor filters
        h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
        h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
        # normalize the L1 norm
        h_ev /= np.linalg.norm(h_ev, ord=1)
        h_od /= np.linalg.norm(h_od, ord=1)
        return h_ev, h_od
    # setup video
    video = v.copy()
    video = video.astype(float)/video.max()
    video = video_smoothen_space(video, sigma)
    # compute the response
    h_ev, h_od = time_function(tau)
    response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
    points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
    return points

The variables that i want to avoid recomputing are h_ev and h_od. The execution time was basically the same (actually, some ms slower).


Solution

  • Can't you just split in multiple function?

    Example :

    def time_function(tau): #To compute hev and hod
            time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
            omega = 4/tau
            # define the gabor filters
            h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
            h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
            # normalize the L1 norm
            h_ev /= np.linalg.norm(h_ev, ord=1)
            h_od /= np.linalg.norm(h_od, ord=1)
            return h_ev, h_od
    

    and then

    # read the video
    video = read_video(video_name, num_frames, 0)
    # get the interest points
    custom_tau = {}
    
    scales = [3*(1.1)**i for i in range(6)]
    start = time.time()
    for i in range(0,10): # for loop cause otherwise you call GaborDetector only once so the question is not revelant
        #Insert logic here because for your question to be revelant Tau must not always be the same
        if tau not in custom_tau.keys(): # Logic to "cache" hev and hod for the same tau value
            hev, hod = time_function(tau)
            custom_tau[tau] = (hev, hod)
        else:
            hev, hod = custom_tau[tau]
        gabor = GaborDetector(v, s, t, hev, hod, 0.3, 500)
        points = MultiscaleDetector(gabor, video, scales, 1.5, 500)
    end = time.time()
    print("Time: {}".format(end-start))
    

    and finally you change GarborCollector to not recompute time

    def GaborDetectorTrial(v, sigma, tau,hev, hod, threshold, num_points):
    
        video = v.copy()
        video = video.astype(float)/video.max()
        video = video_smoothen_space(video, sigma)
        response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
        points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
        return points
    

    What you do in this code is "caching" (through a dictionnary) hev and hod, so that if you already computed it for a given "tau", you instead lookup in the dictionnary for the result, otherwise you compute it and put it in the dictionnary

    I had to extrapolate and guess how your code would work, because in the case you had given, he_v and ho_d are never re-computed since you call garbor only once