Skip to content

Commit

Permalink
pylint: make core functions pylint compliant
Browse files Browse the repository at this point in the history
  • Loading branch information
fel-thomas committed Jun 5, 2024
1 parent 0deab32 commit db41586
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 52 deletions.
12 changes: 4 additions & 8 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
"""
Horama being design to be a compact hackable library, I choose to voluntarily
lower the exgicence in term of coding pattern.
"""

[MASTER]
disable=
R0903, # allows to expose only one public method
Expand All @@ -12,8 +7,6 @@ disable=
E1120, # see pylint#3613
C3001, # lambda function as variable
C0116, C0114, # docstring
E1101, # torch members are not properly detected

[FORMAT]
max-line-length=100
max-args=12
Expand All @@ -22,4 +15,7 @@ max-args=12
min-similarity-lines=6
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=no
ignore-imports=no

[TYPECHECK]
ignored-modules=torch
2 changes: 1 addition & 1 deletion horama/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
from .maco_fv import maco
from .fourier_fv import fourier
from .plots import plot_maco
from .losses import dot_cossim
from .losses import dot_cossim
16 changes: 12 additions & 4 deletions horama/common.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import torch
from torchvision.ops import roi_align


def standardize(tensor):
# standardizes the tensor to have 0 mean and unit variance
tensor = tensor - torch.mean(tensor)
tensor = tensor / (torch.std(tensor) + 1e-4)
return tensor


def recorrelate_colors(image, device):
# recorrelates the colors of the images
assert len(image.shape) == 3
Expand All @@ -27,8 +29,11 @@ def recorrelate_colors(image, device):

return recorrelated_image

def optimization_step(objective_function, image, box_size, noise_level, number_of_crops_per_iteration, model_input_size):

def optimization_step(objective_function, image, box_size, noise_level,
number_of_crops_per_iteration, model_input_size):
# performs an optimization step on the generated image
# pylint: disable=C0103
assert box_size[1] >= box_size[0]
assert len(image.shape) == 3

Expand All @@ -38,7 +43,8 @@ def optimization_step(objective_function, image, box_size, noise_level, number_o
# generate random boxes
x0 = 0.5 + torch.randn((number_of_crops_per_iteration,), device=device) * 0.15
y0 = 0.5 + torch.randn((number_of_crops_per_iteration,), device=device) * 0.15
delta_x = torch.rand((number_of_crops_per_iteration,), device=device) * (box_size[1] - box_size[0]) + box_size[1]
delta_x = torch.rand((number_of_crops_per_iteration,),
device=device) * (box_size[1] - box_size[0]) + box_size[1]
delta_y = delta_x

boxes = torch.stack([torch.zeros((number_of_crops_per_iteration,), device=device),
Expand All @@ -47,11 +53,13 @@ def optimization_step(objective_function, image, box_size, noise_level, number_o
x0 + delta_x * 0.5,
y0 + delta_y * 0.5], dim=1) * image.shape[1]

cropped_and_resized_images = roi_align(image.unsqueeze(0), boxes, output_size=(model_input_size, model_input_size)).squeeze(0)
cropped_and_resized_images = roi_align(image.unsqueeze(
0), boxes, output_size=(model_input_size, model_input_size)).squeeze(0)

# add normal and uniform noise for better robustness
cropped_and_resized_images.add_(torch.randn_like(cropped_and_resized_images) * noise_level)
cropped_and_resized_images.add_((torch.rand_like(cropped_and_resized_images) - 0.5) * noise_level)
cropped_and_resized_images.add_(
(torch.rand_like(cropped_and_resized_images) - 0.5) * noise_level)

# compute the score and loss
score = objective_function(cropped_and_resized_images)
Expand Down
29 changes: 21 additions & 8 deletions horama/fourier_fv.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import torch
from .common import standardize, recorrelate_colors, optimization_step
from tqdm import tqdm

from .common import standardize, recorrelate_colors, optimization_step


def fft_2d_freq(width, height):
# calculate the 2D frequency grid for FFT
freq_y = torch.fft.fftfreq(height).unsqueeze(1)
Expand All @@ -11,21 +13,26 @@ def fft_2d_freq(width, height):

return torch.sqrt(freq_x**2 + freq_y**2)


def get_fft_scale(width, height, decay_power=1.0):
# generate the scaler that account for power decay in FFT space
frequencies = fft_2d_freq(width, height)

fft_scale = 1.0 / torch.maximum(frequencies, torch.tensor(1.0 / max(width, height))) ** decay_power
fft_scale = 1.0 / torch.maximum(frequencies,
torch.tensor(1.0 / max(width, height))) ** decay_power
fft_scale = fft_scale * torch.sqrt(torch.tensor(width * height).float())

return fft_scale.to(torch.complex64)


def init_lucid_buffer(width, height, std=1.0):
# initialize the buffer with a random spectrum a la Lucid
spectrum_shape = (3, width, height // 2 + 1)
random_spectrum = torch.complex(torch.randn(spectrum_shape) * std, torch.randn(spectrum_shape) * std)
random_spectrum = torch.complex(torch.randn(spectrum_shape) * std,
torch.randn(spectrum_shape) * std)
return random_spectrum


def fourier_preconditionner(spectrum, spectrum_scaler, values_range, device):
# precondition the Fourier spectrum and convert it to spatial domain
assert spectrum.shape[0] == 3
Expand All @@ -37,11 +44,16 @@ def fourier_preconditionner(spectrum, spectrum_scaler, values_range, device):
spatial_image = standardize(spatial_image)
color_recorrelated_image = recorrelate_colors(spatial_image, device)

image = torch.sigmoid(color_recorrelated_image) * (values_range[1] - values_range[0]) + values_range[0]
image = torch.sigmoid(
color_recorrelated_image) * (values_range[1] - values_range[0]) + values_range[0]
return image

def fourier(objective_function, decay_power=1.5, total_steps=1000, learning_rate=1.0, image_size=1280, model_input_size=224,
noise=0.05, values_range=(-2.5, 2.5), crops_per_iteration=6, box_size=(0.20, 0.25), device='cuda'):

def fourier(
objective_function, decay_power=1.5, total_steps=1000, learning_rate=1.0, image_size=1280,
model_input_size=224, noise=0.05, values_range=(-2.5, 2.5),
crops_per_iteration=6, box_size=(0.20, 0.25),
device='cuda'):
# perform the Lucid (Olah & al.) optimization process
assert values_range[1] >= values_range[0]
assert box_size[1] >= box_size[0]
Expand All @@ -56,11 +68,12 @@ def fourier(objective_function, decay_power=1.5, total_steps=1000, learning_rate
optimizer = torch.optim.NAdam([spectrum], lr=learning_rate)
transparency_accumulator = torch.zeros((3, image_size, image_size)).to(device)

for step in tqdm(range(total_steps)):
for _ in tqdm(range(total_steps)):
optimizer.zero_grad()

image = fourier_preconditionner(spectrum, spectrum_scaler, values_range, device)
loss, img = optimization_step(objective_function, image, box_size, noise, crops_per_iteration, model_input_size)
loss, img = optimization_step(objective_function, image, box_size,
noise, crops_per_iteration, model_input_size)
loss.backward()
transparency_accumulator += torch.abs(img.grad)
optimizer.step()
Expand Down
2 changes: 2 additions & 0 deletions horama/losses.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import torch


def cosine_similarity(tensor_a, tensor_b):
norm_dims = list(range(1, len(tensor_a.shape)))
tensor_a = torch.nn.functional.normalize(tensor_a.float(), dim=norm_dims)
tensor_b = torch.nn.functional.normalize(tensor_b.float(), dim=norm_dims)
return torch.sum(tensor_a * tensor_b, dim=norm_dims)


def dot_cossim(tensor_a, tensor_b, cossim_pow=2.0):
# see https://github.com/tensorflow/lucid/issues/116
cosim = torch.clamp(cosine_similarity(tensor_a, tensor_b), min=1e-1) ** cossim_pow
Expand Down
38 changes: 26 additions & 12 deletions horama/maco_fv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,32 @@

from .common import optimization_step, standardize, recorrelate_colors

MACO_SPECTRUM_URL = "https://storage.googleapis.com/serrelab/loupe/spectrums/imagenet_decorrelated.npy"
MACO_SPECTRUM_URL = ("https://storage.googleapis.com/serrelab/loupe/"
"spectrums/imagenet_decorrelated.npy")
MACO_SPECTRUM_FILENAME = 'spectrum_decorrelated.npy'


def init_maco_buffer(image_shape, std_deviation=1.0):
# initialize the maco buffer with a random phase and a magnitude template
spectrum_shape = (image_shape[0], image_shape[1] // 2 + 1)
# generate random phase
random_phase = torch.randn(3, *spectrum_shape, dtype=torch.float32) * std_deviation
random_phase = torch.randn(
3, *spectrum_shape, dtype=torch.float32) * std_deviation

# download magnitude template if not exists
if not os.path.isfile(MACO_SPECTRUM_FILENAME):
download_url(MACO_SPECTRUM_URL, root=".", filename=MACO_SPECTRUM_FILENAME)
download_url(MACO_SPECTRUM_URL, root=".",
filename=MACO_SPECTRUM_FILENAME)

# load and resize magnitude template
magnitude = torch.tensor(np.load(MACO_SPECTRUM_FILENAME), dtype=torch.float32)
magnitude = F.interpolate(magnitude.unsqueeze(0), size=spectrum_shape, mode='bilinear', align_corners=False, antialias=True)[0]
magnitude = torch.tensor(
np.load(MACO_SPECTRUM_FILENAME), dtype=torch.float32)
magnitude = F.interpolate(magnitude.unsqueeze(
0), size=spectrum_shape, mode='bilinear', align_corners=False, antialias=True)[0]

return magnitude, random_phase


def maco_preconditioner(magnitude_template, phase, values_range, device):
# apply the maco preconditioner to generate spatial images from magnitude and phase
# tfel: check why r exp^(j theta) give slighly diff results
Expand All @@ -40,29 +47,36 @@ def maco_preconditioner(magnitude_template, phase, values_range, device):

# recorrelate colors and adjust value range
color_recorrelated_image = recorrelate_colors(spatial_image, device)
final_image = torch.sigmoid(color_recorrelated_image) * (values_range[1] - values_range[0]) + values_range[0]
final_image = torch.sigmoid(
color_recorrelated_image) * (values_range[1] - values_range[0]) + values_range[0]
return final_image

def maco(objective_function, total_steps=1000, learning_rate=1.0, image_size=1280, model_input_size=224,
noise=0.05, values_range=(-2.5, 2.5), crops_per_iteration=6, box_size=(0.20, 0.25), device='cuda'):

def maco(objective_function, total_steps=1000, learning_rate=1.0, image_size=1280,
model_input_size=224, noise=0.05, values_range=(-2.5, 2.5),
crops_per_iteration=6, box_size=(0.20, 0.25),
device='cuda'):
# perform the maco optimization process
assert values_range[1] >= values_range[0]
assert box_size[1] >= box_size[0]

magnitude, phase = init_maco_buffer((image_size, image_size), std_deviation=1.0)
magnitude, phase = init_maco_buffer(
(image_size, image_size), std_deviation=1.0)
magnitude = magnitude.to(device)
phase = phase.to(device)
phase.requires_grad = True

optimizer = torch.optim.NAdam([phase], lr=learning_rate)
transparency_accumulator = torch.zeros((3, image_size, image_size)).to(device)
transparency_accumulator = torch.zeros(
(3, image_size, image_size)).to(device)

for step in tqdm(range(total_steps)):
for _ in tqdm(range(total_steps)):
optimizer.zero_grad()

# preprocess and compute loss
img = maco_preconditioner(magnitude, phase, values_range, device)
loss, img = optimization_step(objective_function, img, box_size, noise, crops_per_iteration, model_input_size)
loss, img = optimization_step(
objective_function, img, box_size, noise, crops_per_iteration, model_input_size)

loss.backward()
# get dL/dx to update transparency mask
Expand Down
10 changes: 8 additions & 2 deletions horama/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
import matplotlib.pyplot as plt
import torch


def to_numpy(tensor):
# Ensure tensor is on CPU and convert to NumPy
return tensor.detach().cpu().numpy()


def check_format(arr):
# ensure numpy array and move channels to the last dimension
# if they are in the first dimension
Expand All @@ -15,16 +17,19 @@ def check_format(arr):
return np.moveaxis(arr, 0, -1)
return arr


def normalize(image):
# normalize image to 0-1 range
image = np.array(image, dtype=np.float32)
image -= image.min()
image /= image.max()
return image

def clip_percentile(img, p=0.1):

def clip_percentile(img, percentile=0.1):
# clip pixel values to specified percentile range
return np.clip(img, np.percentile(img, p), np.percentile(img, 100-p))
return np.clip(img, np.percentile(img, percentile), np.percentile(img, 100-percentile))


def show(img, **kwargs):
# display image with normalization and channels in the last dimension
Expand All @@ -34,6 +39,7 @@ def show(img, **kwargs):
plt.imshow(img, **kwargs)
plt.axis('off')


def plot_maco(image, alpha, percentile_image=1.0, percentile_alpha=80):
# visualize image with alpha mask overlay after normalization and clipping
image, alpha = check_format(image), check_format(alpha)
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
author="Thomas FEL, Thibaut BOISSIN, Victor BOUTIN, Agustin PICARD, Paul NOVELLO",
author_email="[email protected]",
license="MIT",
install_requires=['numpy','matplotlib', 'torch', 'torchvision'],
install_requires=['numpy', 'matplotlib', 'torch', 'torchvision'],
packages=find_packages(),
python_requires=">=3.6",
classifiers=[
Expand All @@ -22,4 +22,4 @@
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
],
)
)
3 changes: 2 additions & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import torch
import torch.nn as nn


class SimpleDummyModel(nn.Module):
def __init__(self):
super(SimpleDummyModel, self).__init__()

def forward(self, x):
x = torch.mean(x, (1,2,3))
x = torch.mean(x, (1, 2, 3))
x = torch.relu(x)
return x
8 changes: 5 additions & 3 deletions tests/test_fourier.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

from .common import SimpleDummyModel


def test_fourier():
objective = lambda images: torch.mean(model(images))
def objective(images): return torch.mean(model(images))
model = SimpleDummyModel()

img_size = 200
image, alpha = fourier(objective, total_steps=10, image_size=img_size, model_input_size=100, device='cpu')
image, alpha = fourier(objective, total_steps=10, image_size=img_size,
model_input_size=100, device='cpu')

assert image.size() == (3, img_size, img_size)
assert alpha.size() == (3, img_size, img_size)
assert alpha.size() == (3, img_size, img_size)
8 changes: 5 additions & 3 deletions tests/test_maco.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

from .common import SimpleDummyModel


def test_maco():
objective = lambda images: torch.mean(model(images))
def objective(images): return torch.mean(model(images))
model = SimpleDummyModel()

img_size = 200
image, alpha = maco(objective, total_steps=10, image_size=img_size, model_input_size=100, device='cpu')
image, alpha = maco(objective, total_steps=10, image_size=img_size,
model_input_size=100, device='cpu')

assert image.size() == (3, img_size, img_size)
assert alpha.size() == (3, img_size, img_size)
assert alpha.size() == (3, img_size, img_size)
Loading

0 comments on commit db41586

Please sign in to comment.