Spaces:
Runtime error
Runtime error
Upload Files
Browse files- app.py +82 -0
- data/1a_dog.bmp +0 -0
- data/1b_cat.bmp +0 -0
- data/2a_motorcycle.bmp +0 -0
- data/2b_bicycle.bmp +0 -0
- data/3a_plane.bmp +0 -0
- data/3b_bird.bmp +0 -0
- data/4a_einstein.bmp +0 -0
- data/4b_marilyn.bmp +0 -0
- data/5a_submarine.bmp +0 -0
- data/5b_fish.bmp +0 -0
- data/Message+1.png +0 -0
- data/Message+2.png +0 -0
- requirements.txt +5 -0
- src/cutoff_frequencies.txt +5 -0
- src/cutoff_frequencies_temp.txt +5 -0
- src/datasets.py +132 -0
- src/models.py +117 -0
- src/part1.py +127 -0
- src/proj1.ipynb +0 -0
- src/proj1_test_filtering.ipynb +0 -0
- src/results/part1/high_frequencies.jpg +0 -0
- src/results/part1/hybrid_image.jpg +0 -0
- src/results/part1/hybrid_image_scales.jpg +0 -0
- src/results/part1/low_frequencies.jpg +0 -0
- src/results/part2/0_high_frequencies.jpg +0 -0
- src/results/part2/0_hybrid_image.jpg +0 -0
- src/results/part2/0_low_frequencies.jpg +0 -0
- src/results/part2/1_high_frequencies.jpg +0 -0
- src/results/part2/1_hybrid_image.jpg +0 -0
- src/results/part2/1_low_frequencies.jpg +0 -0
- src/results/part2/2_high_frequencies.jpg +0 -0
- src/results/part2/2_hybrid_image.jpg +0 -0
- src/results/part2/2_low_frequencies.jpg +0 -0
- src/results/part2/3_high_frequencies.jpg +0 -0
- src/results/part2/3_hybrid_image.jpg +0 -0
- src/results/part2/3_low_frequencies.jpg +0 -0
- src/results/part2/4_high_frequencies.jpg +0 -0
- src/results/part2/4_hybrid_image.jpg +0 -0
- src/results/part2/4_low_frequencies.jpg +0 -0
- src/unit_test.py +584 -0
- src/utils.py +174 -0
- webpage_martix.py +90 -0
app.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
from src.part1 import create_Gaussian_kernel, create_hybrid_image
|
| 5 |
+
|
| 6 |
+
input_pictures_examples = [
|
| 7 |
+
["./data/1a_dog.bmp", "./data/1b_cat.bmp", 5],
|
| 8 |
+
["./data/2a_motorcycle.bmp", "./data/2b_bicycle.bmp", 5],
|
| 9 |
+
["./data/3a_plane.bmp", "./data/3b_bird.bmp", 5],
|
| 10 |
+
["./data/4a_einstein.bmp", "./data/4b_marilyn.bmp", 5],
|
| 11 |
+
["./data/5a_submarine.bmp", "./data/5b_fish.bmp", 5],
|
| 12 |
+
["./data/Message+1.png", "./data/Message+2.png", 5],
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def action_hybrid_image(a, b, c):
|
| 17 |
+
# check input is None
|
| 18 |
+
if a is None or b is None or c is None:
|
| 19 |
+
return None, None, None
|
| 20 |
+
|
| 21 |
+
l, h, lh = create_hybrid_image(a / 255, b / 255, c / 255)
|
| 22 |
+
|
| 23 |
+
l, h, lh = (l * 255).astype(np.uint8), ((h + 0.5) * 255).astype(np.uint8), (lh * 255).astype(np.uint8),
|
| 24 |
+
|
| 25 |
+
return l, h, lh, lh, lh, lh, lh, lh
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
| 29 |
+
gr.Markdown("## Input")
|
| 30 |
+
with gr.Row() as uploader:
|
| 31 |
+
# allow user to upload 2 images TODO: Limit picture size, button: exchange pictures
|
| 32 |
+
image1 = gr.Image(label="Image 1 (Low Frequency)", interactive=True)
|
| 33 |
+
|
| 34 |
+
# Gaussian Kernel
|
| 35 |
+
with gr.Column() as k:
|
| 36 |
+
# allow user to select a cutoff frequency
|
| 37 |
+
cutoff = gr.Slider(1, 7, 5, step=1, label="Cutoff Frequency")
|
| 38 |
+
# display the kernel
|
| 39 |
+
kernel = gr.Image(label="Kernel", image_mode="L")
|
| 40 |
+
|
| 41 |
+
image2 = gr.Image(label="Image 2 (High Frequency)", interactive=True)
|
| 42 |
+
|
| 43 |
+
cutoff.release(create_Gaussian_kernel,
|
| 44 |
+
inputs=cutoff, outputs=kernel, api_name="draw_gaussian_kernel")
|
| 45 |
+
|
| 46 |
+
submit = gr.Button("Submit", variant="primary")
|
| 47 |
+
|
| 48 |
+
gr.Markdown("## Output")
|
| 49 |
+
# output
|
| 50 |
+
with gr.Row() as output:
|
| 51 |
+
low_frequencies = gr.Image(label="Low Frequencies")
|
| 52 |
+
hybrid_image = gr.Image(label="Hybrid Image")
|
| 53 |
+
high_frequencies = gr.Image(label="High Frequencies")
|
| 54 |
+
|
| 55 |
+
gr.Markdown("## Outputs In Different Sizes")
|
| 56 |
+
with gr.Row() as output:
|
| 57 |
+
hybrid_large = gr.Image(label="Large", show_label=False).style(height=400, width=400)
|
| 58 |
+
hybrid_medium = gr.Image(label="Medium", show_label=False).style(height=300, width=300)
|
| 59 |
+
hybrid_small = gr.Image(label="Small", show_label=False).style(height=200, width=200)
|
| 60 |
+
hybrid_tiny = gr.Image(label="Tiny", show_label=False).style(height=100, width=100)
|
| 61 |
+
hybrid_super_tiny = gr.Image(label="Super Tiny", show_label=False).style(height=50, width=50)
|
| 62 |
+
|
| 63 |
+
submit.click(
|
| 64 |
+
action_hybrid_image,
|
| 65 |
+
inputs=[image1, image2, kernel],
|
| 66 |
+
outputs=[
|
| 67 |
+
low_frequencies, high_frequencies, hybrid_image,
|
| 68 |
+
hybrid_large, hybrid_medium, hybrid_small, hybrid_tiny, hybrid_super_tiny
|
| 69 |
+
]
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
gr.Markdown("## Use Examples")
|
| 73 |
+
|
| 74 |
+
gr.Examples(
|
| 75 |
+
examples=input_pictures_examples,
|
| 76 |
+
inputs=[image1, image2, cutoff],
|
| 77 |
+
# outputs=txt_3,
|
| 78 |
+
# fn=combine,
|
| 79 |
+
# cache_examples=True, # cache examples to local storage
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
app.launch(server_port=3030)
|
data/1a_dog.bmp
ADDED
|
data/1b_cat.bmp
ADDED
|
data/2a_motorcycle.bmp
ADDED
|
data/2b_bicycle.bmp
ADDED
|
data/3a_plane.bmp
ADDED
|
data/3b_bird.bmp
ADDED
|
data/4a_einstein.bmp
ADDED
|
data/4b_marilyn.bmp
ADDED
|
data/5a_submarine.bmp
ADDED
|
data/5b_fish.bmp
ADDED
|
data/Message+1.png
ADDED
|
data/Message+2.png
ADDED
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
torchvision
|
| 3 |
+
numpy
|
| 4 |
+
matplotlib
|
| 5 |
+
Pillow
|
src/cutoff_frequencies.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
7
|
| 2 |
+
5
|
| 3 |
+
5
|
| 4 |
+
3
|
| 5 |
+
4
|
src/cutoff_frequencies_temp.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
7
|
| 2 |
+
7
|
| 3 |
+
7
|
| 4 |
+
7
|
| 5 |
+
7
|
src/datasets.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import os
|
| 3 |
+
import torch
|
| 4 |
+
import torch.utils.data as data
|
| 5 |
+
import torchvision.transforms as transforms
|
| 6 |
+
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from typing import List, Tuple
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def make_dataset(path: str) -> Tuple[List[str], List[str]]:
|
| 12 |
+
"""
|
| 13 |
+
Creates a dataset of paired images from a directory.
|
| 14 |
+
|
| 15 |
+
The dataset should be partitioned into two sets: one contains images that
|
| 16 |
+
will have the low pass filter applied, and the other contains images that
|
| 17 |
+
will have the high pass filter applied.
|
| 18 |
+
|
| 19 |
+
Args
|
| 20 |
+
- path: string specifying the directory containing images
|
| 21 |
+
Returns
|
| 22 |
+
- images_a: list of strings specifying the paths to the images in set A,
|
| 23 |
+
in lexicographically-sorted order
|
| 24 |
+
- images_b: list of strings specifying the paths to the images in set B,
|
| 25 |
+
in lexicographically-sorted order
|
| 26 |
+
"""
|
| 27 |
+
images_a = []
|
| 28 |
+
images_b = []
|
| 29 |
+
|
| 30 |
+
all_images = os.listdir(path)
|
| 31 |
+
for image in all_images:
|
| 32 |
+
if image[1] == 'a':
|
| 33 |
+
images_a.append(os.path.join(path, image))
|
| 34 |
+
else:
|
| 35 |
+
images_b.append(os.path.join(path, image))
|
| 36 |
+
images_a, images_b = np.sort(images_a), np.sort(images_b)
|
| 37 |
+
return images_a, images_b
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_cutoff_frequencies(path: str) -> List[int]:
|
| 41 |
+
"""
|
| 42 |
+
Gets the cutoff frequencies corresponding to each pair of images.
|
| 43 |
+
|
| 44 |
+
The cutoff frequencies are the values you discovered from experimenting in
|
| 45 |
+
part 1.
|
| 46 |
+
|
| 47 |
+
Args
|
| 48 |
+
- path: string specifying the path to the .txt file with cutoff frequency
|
| 49 |
+
values
|
| 50 |
+
Returns
|
| 51 |
+
- cutoff_frequencies: numpy array of ints. The array should have the same
|
| 52 |
+
length as the number of image pairs in the dataset
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
cutoff_frequencies = []
|
| 56 |
+
freq_file = open(path, 'r')
|
| 57 |
+
freqs = freq_file.readlines()
|
| 58 |
+
for freq in freqs:
|
| 59 |
+
freq = int(freq.strip())
|
| 60 |
+
cutoff_frequencies.append(freq)
|
| 61 |
+
cutoff_frequencies = np.array(cutoff_frequencies)
|
| 62 |
+
return cutoff_frequencies
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class HybridImageDataset(data.Dataset):
|
| 66 |
+
"""Hybrid images dataset."""
|
| 67 |
+
|
| 68 |
+
def __init__(self, image_dir: str, cf_file: str) -> None:
|
| 69 |
+
"""
|
| 70 |
+
HybridImageDataset class constructor.
|
| 71 |
+
|
| 72 |
+
You must replace self.transform with the appropriate transform from
|
| 73 |
+
torchvision.transforms that converts a PIL image to a torch Tensor. You can
|
| 74 |
+
specify additional transforms (e.g. image resizing) if you want to, but
|
| 75 |
+
it's not necessary for the images we provide you since each pair has the
|
| 76 |
+
same dimensions.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
- image_dir: string specifying the directory containing images
|
| 80 |
+
- cf_file: string specifying the path to the .txt file with cutoff
|
| 81 |
+
frequency values
|
| 82 |
+
"""
|
| 83 |
+
images_a, images_b = make_dataset(image_dir)
|
| 84 |
+
cutoff_frequencies = get_cutoff_frequencies(cf_file)
|
| 85 |
+
|
| 86 |
+
self.transform = transforms.Compose([transforms.ToTensor()])
|
| 87 |
+
|
| 88 |
+
self.images_a = images_a
|
| 89 |
+
self.images_b = images_b
|
| 90 |
+
self.cutoff_frequencies = cutoff_frequencies
|
| 91 |
+
|
| 92 |
+
def __len__(self) -> int:
|
| 93 |
+
"""Returns number of pairs of images in dataset."""
|
| 94 |
+
return len(self.images_a)
|
| 95 |
+
|
| 96 |
+
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor, int]:
|
| 97 |
+
"""
|
| 98 |
+
Returns the pair of images and corresponding cutoff frequency value at
|
| 99 |
+
index `idx`.
|
| 100 |
+
|
| 101 |
+
Since self.images_a and self.images_b contain paths to the images, you
|
| 102 |
+
should read the images here and normalize the pixels to be between 0 and 1.
|
| 103 |
+
Make sure you transpose the dimensions so that image_a and image_b are of
|
| 104 |
+
shape (c, m, n) instead of the typical (m, n, c), and convert them to
|
| 105 |
+
torch Tensors.
|
| 106 |
+
|
| 107 |
+
Args
|
| 108 |
+
- idx: int specifying the index at which data should be retrieved
|
| 109 |
+
Returns
|
| 110 |
+
- image_a: Tensor of shape (c, m, n)
|
| 111 |
+
- image_b: Tensor of shape (c, m, n)
|
| 112 |
+
- cutoff_frequency: int specifying the cutoff frequency corresponding to
|
| 113 |
+
(image_a, image_b) pair
|
| 114 |
+
|
| 115 |
+
HINTS:
|
| 116 |
+
- You should use the PIL library to read images
|
| 117 |
+
- You will use self.transform to convert the PIL image to a torch Tensor
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
image_a_dir = self.images_a[idx]
|
| 121 |
+
image_b_dir = self.images_b[idx]
|
| 122 |
+
cutoff_frequency = self.cutoff_frequencies[idx]
|
| 123 |
+
|
| 124 |
+
image_a = Image.open(image_a_dir)
|
| 125 |
+
image_b = Image.open(image_b_dir)
|
| 126 |
+
pixels_a = np.array(image_a) / 255.0
|
| 127 |
+
pixels_b = np.array(image_b) / 255.0
|
| 128 |
+
|
| 129 |
+
image_a = self.transform(pixels_a).float()
|
| 130 |
+
image_b = self.transform(pixels_b).float()
|
| 131 |
+
|
| 132 |
+
return image_a, image_b, cutoff_frequency
|
src/models.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
from src.part1 import create_Gaussian_kernel
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class HybridImageModel(nn.Module):
|
| 10 |
+
def __init__(self):
|
| 11 |
+
"""
|
| 12 |
+
Initializes an instance of the HybridImageModel class.
|
| 13 |
+
"""
|
| 14 |
+
super(HybridImageModel, self).__init__()
|
| 15 |
+
|
| 16 |
+
def get_kernel(self, cutoff_frequency: int) -> torch.Tensor:
|
| 17 |
+
"""
|
| 18 |
+
Returns a Gaussian kernel using the specified cutoff frequency.
|
| 19 |
+
|
| 20 |
+
PyTorch requires the kernel to be of a particular shape in order to apply
|
| 21 |
+
it to an image. Specifically, the kernel needs to be of shape (c, 1, k, k)
|
| 22 |
+
where c is the # channels in the image. Start by getting a 2D Gaussian
|
| 23 |
+
kernel using your implementation from Part 1, which will be of shape
|
| 24 |
+
(k, k). Then, let's say you have an RGB image, you will need to turn this
|
| 25 |
+
into a Tensor of shape (3, 1, k, k) by stacking the Gaussian kernel 3
|
| 26 |
+
times.
|
| 27 |
+
|
| 28 |
+
Args
|
| 29 |
+
- cutoff_frequency: int specifying cutoff_frequency
|
| 30 |
+
Returns
|
| 31 |
+
- kernel: Tensor of shape (c, 1, k, k) where c is # channels
|
| 32 |
+
|
| 33 |
+
HINTS:
|
| 34 |
+
- You will use the create_Gaussian_kernel() function from part1.py in this
|
| 35 |
+
function.
|
| 36 |
+
- Since the # channels may differ across each image in the dataset, make
|
| 37 |
+
sure you don't hardcode the dimensions you reshape the kernel to. There
|
| 38 |
+
is a variable defined in this class to give you channel information.
|
| 39 |
+
- You can use np.reshape() to change the dimensions of a numpy array.
|
| 40 |
+
- You can use np.tile() to repeat a numpy array along specified axes.
|
| 41 |
+
- You can use torch.Tensor() to convert numpy arrays to torch Tensors.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
kernel = create_Gaussian_kernel(cutoff_frequency)
|
| 45 |
+
c = self.n_channels
|
| 46 |
+
k = kernel.shape[0]
|
| 47 |
+
|
| 48 |
+
kernel = np.reshape(kernel, (1, k ** 2))
|
| 49 |
+
kernel = np.tile(kernel, c)
|
| 50 |
+
kernel = np.reshape(kernel, (c, 1, k, k))
|
| 51 |
+
kernel = torch.Tensor(kernel)
|
| 52 |
+
return kernel
|
| 53 |
+
|
| 54 |
+
def low_pass(self, x, kernel):
|
| 55 |
+
"""
|
| 56 |
+
Applies low pass filter to the input image.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
- x: Tensor of shape (b, c, m, n) where b is batch size
|
| 60 |
+
- kernel: low pass filter to be applied to the image
|
| 61 |
+
Returns:
|
| 62 |
+
- filtered_image: Tensor of shape (b, c, m, n)
|
| 63 |
+
|
| 64 |
+
HINT:
|
| 65 |
+
- You should use the 2d convolution operator from torch.nn.functional.
|
| 66 |
+
- Make sure to pad the image appropriately (it's a parameter to the
|
| 67 |
+
convolution function you should use here!).
|
| 68 |
+
- Pass self.n_channels as the value to the "groups" parameter of the
|
| 69 |
+
convolution function. This represents the # of channels that the filter
|
| 70 |
+
will be applied to.
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
k = kernel.shape[2]
|
| 74 |
+
filtered_image = F.conv2d(input=x.float(),
|
| 75 |
+
weight=kernel,
|
| 76 |
+
padding=k // 2,
|
| 77 |
+
groups=self.n_channels)
|
| 78 |
+
return filtered_image
|
| 79 |
+
|
| 80 |
+
def forward(self, image1, image2, cutoff_frequency):
|
| 81 |
+
"""
|
| 82 |
+
Takes two images and creates a hybrid image. Returns the low frequency
|
| 83 |
+
content of image1, the high frequency content of image 2, and the hybrid
|
| 84 |
+
image.
|
| 85 |
+
|
| 86 |
+
Args
|
| 87 |
+
- image1: Tensor of shape (b, c, m, n)
|
| 88 |
+
- image2: Tensor of shape (b, c, m, n)
|
| 89 |
+
- cutoff_frequency: Tensor of shape (b)
|
| 90 |
+
Returns:
|
| 91 |
+
- low_frequencies: Tensor of shape (b, c, m, n)
|
| 92 |
+
- high_frequencies: Tensor of shape (b, c, m, n)
|
| 93 |
+
- hybrid_image: Tensor of shape (b, c, m, n)
|
| 94 |
+
|
| 95 |
+
HINTS:
|
| 96 |
+
- You will use the get_kernel() function and your low_pass() function in
|
| 97 |
+
this function.
|
| 98 |
+
- Similar to Part 1, you can get just the high frequency content of an
|
| 99 |
+
image by removing its low frequency content.
|
| 100 |
+
- Don't forget to make sure to clip the pixel values >=0 and <=1. You can
|
| 101 |
+
use torch.clamp().
|
| 102 |
+
- If you want to use images with different dimensions, you should resize
|
| 103 |
+
them in the HybridImageDataset class using torchvision.transforms.
|
| 104 |
+
"""
|
| 105 |
+
self.n_channels = image1.shape[1]
|
| 106 |
+
|
| 107 |
+
kernel = self.get_kernel(int(cutoff_frequency.item()))
|
| 108 |
+
|
| 109 |
+
low_freq_1 = self.low_pass(image1, kernel)
|
| 110 |
+
low_freq_2 = self.low_pass(image2, kernel)
|
| 111 |
+
|
| 112 |
+
high_freq_2 = image2.float() - low_freq_2
|
| 113 |
+
|
| 114 |
+
hybrid_image = torch.clamp(low_freq_1 + high_freq_2, 0, 1)
|
| 115 |
+
low_frequencies = low_freq_1
|
| 116 |
+
high_frequencies = high_freq_2
|
| 117 |
+
return low_frequencies, high_frequencies, hybrid_image
|
src/part1.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def create_Gaussian_kernel(cutoff_frequency):
|
| 5 |
+
"""
|
| 6 |
+
Returns a 2D Gaussian kernel using the specified filter size standard
|
| 7 |
+
deviation and cutoff frequency.
|
| 8 |
+
|
| 9 |
+
The kernel should have:
|
| 10 |
+
- shape (k, k) where k = cutoff_frequency * 4 + 1
|
| 11 |
+
- mean = floor(k / 2)
|
| 12 |
+
- standard deviation = cutoff_frequency
|
| 13 |
+
- values that sum to 1
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
- cutoff_frequency: an int controlling how much low frequency to leave in
|
| 17 |
+
the image.
|
| 18 |
+
Returns:
|
| 19 |
+
- kernel: numpy nd-array of shape (k, k)
|
| 20 |
+
|
| 21 |
+
HINT:
|
| 22 |
+
- The 2D Gaussian kernel here can be calculated as the outer product of two
|
| 23 |
+
vectors with values populated from evaluating the 1D Gaussian PDF at each
|
| 24 |
+
corrdinate.
|
| 25 |
+
"""
|
| 26 |
+
k = cutoff_frequency * 4 + 1
|
| 27 |
+
mean = np.floor(k / 2)
|
| 28 |
+
std = cutoff_frequency
|
| 29 |
+
gauss_1d = np.zeros((k, 1))
|
| 30 |
+
|
| 31 |
+
total = 0
|
| 32 |
+
index = 0
|
| 33 |
+
for x in range(-int(mean), int(mean) + 1):
|
| 34 |
+
x1 = 1 / np.sqrt(2 * np.pi * std ** 2)
|
| 35 |
+
x2 = np.exp(-(x ** 2) / (2 * std ** 2))
|
| 36 |
+
g = x1 * x2
|
| 37 |
+
gauss_1d[index] = g
|
| 38 |
+
index += 1
|
| 39 |
+
total += g
|
| 40 |
+
kernel = np.outer(gauss_1d, gauss_1d) / total ** 2
|
| 41 |
+
return kernel
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def my_imfilter(image, filter):
|
| 45 |
+
"""
|
| 46 |
+
Apply a filter to an image. Return the filtered image.
|
| 47 |
+
|
| 48 |
+
Args
|
| 49 |
+
- image: numpy nd-array of shape (m, n, c)
|
| 50 |
+
- filter: numpy nd-array of shape (k, j)
|
| 51 |
+
Returns
|
| 52 |
+
- filtered_image: numpy nd-array of shape (m, n, c)
|
| 53 |
+
|
| 54 |
+
HINTS:
|
| 55 |
+
- You may not use any libraries that do the work for you. Using numpy to work
|
| 56 |
+
with matrices is fine and encouraged. Using OpenCV or similar to do the
|
| 57 |
+
filtering for you is not allowed.
|
| 58 |
+
- I encourage you to try implementing this naively first, just be aware that
|
| 59 |
+
it may take an absurdly long time to run. You will need to get a function
|
| 60 |
+
that takes a reasonable amount of time to run so that the TAs can verify
|
| 61 |
+
your code works.
|
| 62 |
+
"""
|
| 63 |
+
m = image.shape[0]
|
| 64 |
+
n = image.shape[1]
|
| 65 |
+
c = image.shape[2]
|
| 66 |
+
|
| 67 |
+
padding_height = filter.shape[0] // 2
|
| 68 |
+
padding_width = filter.shape[1] // 2
|
| 69 |
+
|
| 70 |
+
# padding manually
|
| 71 |
+
padded_image = np.zeros((m + padding_height * 2, n + padding_width * 2, c))
|
| 72 |
+
padded_image[padding_height:padding_height + m, padding_width:padding_width + n, :] = image
|
| 73 |
+
|
| 74 |
+
# convolution
|
| 75 |
+
filtered_image = np.zeros((m, n, c))
|
| 76 |
+
|
| 77 |
+
for a in range(0, c):
|
| 78 |
+
for i in range(0, m):
|
| 79 |
+
for j in range(0, n):
|
| 80 |
+
x = np.multiply(padded_image[i: i + filter.shape[0], j:j + filter.shape[1], a], filter)
|
| 81 |
+
filtered_image[i, j, a] = x.sum()
|
| 82 |
+
|
| 83 |
+
return filtered_image
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def create_hybrid_image(image1, image2, filter):
|
| 87 |
+
"""
|
| 88 |
+
Takes two images and a low-pass filter and creates a hybrid image. Returns
|
| 89 |
+
the low frequency content of image1, the high frequency content of image 2,
|
| 90 |
+
and the hybrid image.
|
| 91 |
+
|
| 92 |
+
Args
|
| 93 |
+
- image1: numpy nd-array of dim (m, n, c)
|
| 94 |
+
- image2: numpy nd-array of dim (m, n, c)
|
| 95 |
+
- filter: numpy nd-array of dim (x, y)
|
| 96 |
+
Returns
|
| 97 |
+
- low_frequencies: numpy nd-array of shape (m, n, c)
|
| 98 |
+
- high_frequencies: numpy nd-array of shape (m, n, c)
|
| 99 |
+
- hybrid_image: numpy nd-array of shape (m, n, c)
|
| 100 |
+
|
| 101 |
+
HINTS:
|
| 102 |
+
- You will use your my_imfilter function in this function.
|
| 103 |
+
- You can get just the high frequency content of an image by removing its low
|
| 104 |
+
frequency content. Think about how to do this in mathematical terms.
|
| 105 |
+
- Don't forget to make sure the pixel values of the hybrid image are between
|
| 106 |
+
0 and 1. This is known as 'clipping'.
|
| 107 |
+
- If you want to use images with different dimensions, you should resize them
|
| 108 |
+
in the notebook code.
|
| 109 |
+
"""
|
| 110 |
+
assert image1.shape[0] == image2.shape[0]
|
| 111 |
+
assert image1.shape[1] == image2.shape[1]
|
| 112 |
+
assert image1.shape[2] == image2.shape[2]
|
| 113 |
+
assert filter.shape[0] <= image1.shape[0]
|
| 114 |
+
assert filter.shape[1] <= image1.shape[1]
|
| 115 |
+
assert filter.shape[0] % 2 == 1
|
| 116 |
+
assert filter.shape[1] % 2 == 1
|
| 117 |
+
|
| 118 |
+
image1_low = my_imfilter(image1, filter)
|
| 119 |
+
image2_low = my_imfilter(image2, filter)
|
| 120 |
+
image2_high = image2 - image2_low
|
| 121 |
+
|
| 122 |
+
hybrid_image = np.clip(image1_low + image2_high, 0, 1)
|
| 123 |
+
|
| 124 |
+
low_frequencies = image1_low
|
| 125 |
+
high_frequencies = image2_high
|
| 126 |
+
|
| 127 |
+
return low_frequencies, high_frequencies, hybrid_image
|
src/proj1.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/proj1_test_filtering.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/results/part1/high_frequencies.jpg
ADDED
|
src/results/part1/hybrid_image.jpg
ADDED
|
src/results/part1/hybrid_image_scales.jpg
ADDED
|
src/results/part1/low_frequencies.jpg
ADDED
|
src/results/part2/0_high_frequencies.jpg
ADDED
|
src/results/part2/0_hybrid_image.jpg
ADDED
|
src/results/part2/0_low_frequencies.jpg
ADDED
|
src/results/part2/1_high_frequencies.jpg
ADDED
|
src/results/part2/1_hybrid_image.jpg
ADDED
|
src/results/part2/1_low_frequencies.jpg
ADDED
|
src/results/part2/2_high_frequencies.jpg
ADDED
|
src/results/part2/2_hybrid_image.jpg
ADDED
|
src/results/part2/2_low_frequencies.jpg
ADDED
|
src/results/part2/3_high_frequencies.jpg
ADDED
|
src/results/part2/3_hybrid_image.jpg
ADDED
|
src/results/part2/3_low_frequencies.jpg
ADDED
|
src/results/part2/4_high_frequencies.jpg
ADDED
|
src/results/part2/4_hybrid_image.jpg
ADDED
|
src/results/part2/4_low_frequencies.jpg
ADDED
|
src/unit_test.py
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import torch
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
sys.path.append("../")
|
| 9 |
+
from src.part1 import my_imfilter
|
| 10 |
+
from src.datasets import HybridImageDataset
|
| 11 |
+
from src.models import HybridImageModel, create_Gaussian_kernel
|
| 12 |
+
from src.utils import (
|
| 13 |
+
vis_image_scales_numpy,
|
| 14 |
+
im2single,
|
| 15 |
+
single2im,
|
| 16 |
+
load_image,
|
| 17 |
+
save_image,
|
| 18 |
+
write_objects_to_file
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
ROOT = Path(__file__).resolve().parent.parent # ../..
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
Even size kernels are not required for this project, so we exclude this test case.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_dog_img():
|
| 29 |
+
"""
|
| 30 |
+
"""
|
| 31 |
+
dog_img_fpath = f'{ROOT}/data/1a_dog.bmp'
|
| 32 |
+
dog_img = load_image(dog_img_fpath)
|
| 33 |
+
return dog_img
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_dataloader_len():
|
| 37 |
+
"""
|
| 38 |
+
Check dataloader __len__ for correct size (should be 5 pairs of images).
|
| 39 |
+
"""
|
| 40 |
+
img_dir = f'{ROOT}/data'
|
| 41 |
+
cut_off_file = f'{ROOT}/cutoff_frequencies.txt'
|
| 42 |
+
hid = HybridImageDataset(img_dir, cut_off_file)
|
| 43 |
+
assert len(hid) == 5
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_dataloader_get_item():
|
| 47 |
+
"""
|
| 48 |
+
Verify that __getitem__ is implemented correctly, for the first dog/cat entry.
|
| 49 |
+
"""
|
| 50 |
+
img_dir = f'{ROOT}/data'
|
| 51 |
+
cut_off_file = f'{ROOT}/cutoff_frequencies.txt'
|
| 52 |
+
hid = HybridImageDataset(img_dir, cut_off_file)
|
| 53 |
+
|
| 54 |
+
first_item = hid[0]
|
| 55 |
+
dog_img, cat_img, cutoff = first_item
|
| 56 |
+
|
| 57 |
+
gt_size = [3, 361, 410]
|
| 58 |
+
# low frequency should be 1a_dog.bmp, high freq should be cat
|
| 59 |
+
assert [dog_img.shape[i] for i in range(3)] == gt_size
|
| 60 |
+
assert [cat_img.shape[i] for i in range(3)] == gt_size
|
| 61 |
+
|
| 62 |
+
# ground truth values
|
| 63 |
+
dog_img_crop = torch.tensor(
|
| 64 |
+
[
|
| 65 |
+
[[0.4784, 0.4745],
|
| 66 |
+
[0.5255, 0.5176]],
|
| 67 |
+
|
| 68 |
+
[[0.4627, 0.4667],
|
| 69 |
+
[0.5098, 0.5137]],
|
| 70 |
+
|
| 71 |
+
[[0.4588, 0.4706],
|
| 72 |
+
[0.5059, 0.5059]]
|
| 73 |
+
]
|
| 74 |
+
)
|
| 75 |
+
assert torch.allclose(dog_img[:, 100:102, 100:102], dog_img_crop, atol=1e-3)
|
| 76 |
+
assert 0. < cutoff < 1000.
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_low_pass_filter_square_kernel():
|
| 80 |
+
"""
|
| 81 |
+
Allow students to use arbitrary padding types without penalty.
|
| 82 |
+
"""
|
| 83 |
+
dog_img = get_dog_img()
|
| 84 |
+
img_h, img_w, _ = dog_img.shape
|
| 85 |
+
low_pass_filter = create_Gaussian_kernel(cutoff_frequency=7)
|
| 86 |
+
k_h, k_w = low_pass_filter.shape
|
| 87 |
+
student_filtered_img = my_imfilter(dog_img, low_pass_filter)
|
| 88 |
+
|
| 89 |
+
# Exclude the border pixels.
|
| 90 |
+
student_filtered_img_interior = student_filtered_img[k_h:img_h - k_h, k_w:img_w - k_w]
|
| 91 |
+
assert np.allclose(158332.02, student_filtered_img_interior.sum())
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_random_filter_nonsquare_kernel():
|
| 95 |
+
"""
|
| 96 |
+
Test a non-square filter (that is not a low-pass filter).
|
| 97 |
+
"""
|
| 98 |
+
image = np.array(range(10 * 15 * 3), dtype=np.uint8)
|
| 99 |
+
image = image.reshape(10, 15, 3)
|
| 100 |
+
image = image.astype(np.float32)
|
| 101 |
+
kernel = np.array(range(3 * 5), dtype=np.float32).reshape(3, 5) / 15
|
| 102 |
+
img_h, img_w, _ = image.shape
|
| 103 |
+
|
| 104 |
+
student_output = my_imfilter(image, kernel)
|
| 105 |
+
|
| 106 |
+
h_center = img_h // 2
|
| 107 |
+
w_center = img_w // 2
|
| 108 |
+
|
| 109 |
+
gt_center_crop = np.array(
|
| 110 |
+
[
|
| 111 |
+
[[1542.0001, 1549., 1556.0001],
|
| 112 |
+
[1563., 1569.9999, 1577.0001]],
|
| 113 |
+
|
| 114 |
+
[[832.99994, 840.00006, 847.],
|
| 115 |
+
[854., 861., 868.0001]]
|
| 116 |
+
], dtype=np.float32
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
student_center_crop = student_output[h_center - 1:h_center + 1, w_center - 1:w_center + 1]
|
| 120 |
+
assert np.allclose(student_center_crop, gt_center_crop, atol=1e-3)
|
| 121 |
+
|
| 122 |
+
student_filtered_interior = student_output[1:img_h - 1, 3:img_w - 3, :]
|
| 123 |
+
assert np.allclose(student_filtered_interior.sum(), 194196.0, atol=1e-1)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_random_filter_square_kernel():
|
| 127 |
+
"""
|
| 128 |
+
Test a square filter (that is not a low-pass filter).
|
| 129 |
+
"""
|
| 130 |
+
image = np.array(range(4 * 5 * 3), dtype=np.uint8)
|
| 131 |
+
image = image.reshape(4, 5, 3)
|
| 132 |
+
image = image.astype(np.float32)
|
| 133 |
+
kernel = np.array(range(3 * 3), dtype=np.float32).reshape(3, 3) / 9
|
| 134 |
+
img_h, img_w, _ = image.shape
|
| 135 |
+
|
| 136 |
+
student_output = my_imfilter(image, kernel)
|
| 137 |
+
|
| 138 |
+
student_filtered_interior = student_output[1:img_h - 1, 1:img_w - 1, :]
|
| 139 |
+
gt_interior_values = np.array(
|
| 140 |
+
[
|
| 141 |
+
[[104., 108., 112.],
|
| 142 |
+
[116., 120.00001, 124.],
|
| 143 |
+
[128., 132., 136.]],
|
| 144 |
+
|
| 145 |
+
[[164., 168.00002, 172.],
|
| 146 |
+
[176., 180., 184.],
|
| 147 |
+
[188.00002, 192., 196.]]
|
| 148 |
+
], dtype=np.float32
|
| 149 |
+
)
|
| 150 |
+
assert np.allclose(student_filtered_interior, gt_interior_values)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def verify_low_freq_sq_kernel_np(image1, kernel, low_frequencies) -> bool:
|
| 154 |
+
"""
|
| 155 |
+
Interactive test to be used in IPython notebook, that will print out
|
| 156 |
+
test result, and return value can also be queried for success (true).
|
| 157 |
+
|
| 158 |
+
Args:
|
| 159 |
+
- image1
|
| 160 |
+
- kernel
|
| 161 |
+
- low_frequencies
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
- Boolean indicating success.
|
| 165 |
+
"""
|
| 166 |
+
gt_image1 = load_image(f'{ROOT}/data/1a_dog.bmp')
|
| 167 |
+
if not np.allclose(image1, gt_image1):
|
| 168 |
+
print('Please pass in the dog image `1a_dog.bmp` as the `image1` argument.')
|
| 169 |
+
return False
|
| 170 |
+
|
| 171 |
+
img_h, img_w, _ = image1.shape
|
| 172 |
+
k_h, k_w = kernel.shape
|
| 173 |
+
# Exclude the border pixels.
|
| 174 |
+
low_freq_interior = low_frequencies[k_h:img_h - k_h, k_w:img_w - k_w]
|
| 175 |
+
correct_sum = np.allclose(158332.02, low_freq_interior.sum())
|
| 176 |
+
|
| 177 |
+
# ground truth values
|
| 178 |
+
gt_low_freq_crop = np.array(
|
| 179 |
+
[
|
| 180 |
+
[[0.53500533, 0.523871, 0.5142517],
|
| 181 |
+
[0.5367106, 0.526209, 0.51830757]],
|
| 182 |
+
|
| 183 |
+
[[0.53472066, 0.5236291, 0.5149963],
|
| 184 |
+
[0.5368732, 0.5264317, 0.5193449]]
|
| 185 |
+
], dtype=np.float32
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# H,W,C order in Numpy
|
| 189 |
+
correct_crop = np.allclose(low_frequencies[100:102, 100:102, :], gt_low_freq_crop, atol=1e-3)
|
| 190 |
+
if correct_sum and correct_crop:
|
| 191 |
+
print('Success! Low frequencies values are correct.')
|
| 192 |
+
return True
|
| 193 |
+
else:
|
| 194 |
+
print('Low frequencies values are not correct, please double check your implementation.')
|
| 195 |
+
return False
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
## Purely for visualization/debugging ########
|
| 199 |
+
# plt.subplot(1,2,1)
|
| 200 |
+
# plt.imshow(image1)
|
| 201 |
+
|
| 202 |
+
# plt.subplot(1,2,2)
|
| 203 |
+
# plt.imshow(low_frequencies)
|
| 204 |
+
# plt.show()
|
| 205 |
+
##############################################
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def verify_high_freq_sq_kernel_np(image2, kernel, high_frequencies) -> bool:
|
| 209 |
+
"""
|
| 210 |
+
Interactive test to be used in IPython notebook, that will print out
|
| 211 |
+
test result, and return value can also be queried for success (true).
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
- image2: Array representing the cat image (1b_cat.bmp)
|
| 215 |
+
- kernel: Low pass kernel (2d Gaussian)
|
| 216 |
+
- high_frequencies: High frequencies of image2 (output of high-pass filter)
|
| 217 |
+
|
| 218 |
+
Returns:
|
| 219 |
+
- retval: Boolean indicating success.
|
| 220 |
+
"""
|
| 221 |
+
gt_image2 = load_image(f'{ROOT}/data/1b_cat.bmp')
|
| 222 |
+
if not np.allclose(image2, gt_image2):
|
| 223 |
+
print('Please pass in the cat image `1b_cat.bmp` as the `image2` argument.')
|
| 224 |
+
return False
|
| 225 |
+
|
| 226 |
+
img_h, img_w, _ = image2.shape
|
| 227 |
+
k_h, k_w = kernel.shape
|
| 228 |
+
# Exclude the border pixels.
|
| 229 |
+
high_freq_interior = high_frequencies[k_h:img_h - k_h, k_w:img_w - k_w]
|
| 230 |
+
correct_sum = np.allclose(12.029784, high_freq_interior.sum(), atol=1e-2)
|
| 231 |
+
|
| 232 |
+
# ground truth values
|
| 233 |
+
gt_high_freq_crop = np.array(
|
| 234 |
+
[
|
| 235 |
+
[[7.9535842e-03, 2.9861331e-02, 3.0958146e-02],
|
| 236 |
+
[-7.6553226e-03, 2.2351682e-02, 2.7430430e-02]],
|
| 237 |
+
|
| 238 |
+
[[1.5485287e-02, 3.3503681e-02, 3.0706093e-02],
|
| 239 |
+
[-6.8724155e-05, 3.3921897e-02, 3.1234175e-02]]
|
| 240 |
+
], dtype=np.float32
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# H,W,C order in Numpy
|
| 244 |
+
correct_crop = np.allclose(high_frequencies[100:102, 100:102, :], gt_high_freq_crop, atol=1e-3)
|
| 245 |
+
if correct_sum and correct_crop:
|
| 246 |
+
print('Success! High frequencies values are correct.')
|
| 247 |
+
return True
|
| 248 |
+
else:
|
| 249 |
+
print('High frequencies values are not correct, please double check your implementation.')
|
| 250 |
+
return False
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
## Purely for visualization/debugging ########
|
| 254 |
+
# plt.subplot(1,2,1)
|
| 255 |
+
# plt.imshow(image2)
|
| 256 |
+
|
| 257 |
+
# plt.subplot(1,2,2)
|
| 258 |
+
# high_frequencies += 0.5 # np.clip(high_frequencies, 0., 1.0)
|
| 259 |
+
# plt.imshow(high_frequencies)
|
| 260 |
+
# plt.show()
|
| 261 |
+
##############################################
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def verify_hybrid_image_np(image1, image2, kernel, hybrid_image) -> bool:
|
| 265 |
+
"""
|
| 266 |
+
Interactive test to be used in IPython notebook, that will print out
|
| 267 |
+
test result, and return value can also be queried for success (true).
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
- image1
|
| 271 |
+
- image2
|
| 272 |
+
- kernel
|
| 273 |
+
- hybrid_image
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
- Boolean indicating success.
|
| 277 |
+
"""
|
| 278 |
+
gt_image1 = load_image(f'{ROOT}/data/1a_dog.bmp')
|
| 279 |
+
if not np.allclose(image1, gt_image1):
|
| 280 |
+
print('Please pass in the dog image `1a_dog.bmp` as the `image1` argument.')
|
| 281 |
+
return False
|
| 282 |
+
|
| 283 |
+
gt_image2 = load_image(f'{ROOT}/data/1b_cat.bmp')
|
| 284 |
+
if not np.allclose(image2, gt_image2):
|
| 285 |
+
print('Please pass in the cat image `1b_cat.bmp` as the `image2` argument.')
|
| 286 |
+
return False
|
| 287 |
+
|
| 288 |
+
img_h, img_w, _ = image2.shape
|
| 289 |
+
k_h, k_w = kernel.shape
|
| 290 |
+
# Exclude the border pixels.
|
| 291 |
+
hybrid_interior = hybrid_image[k_h:img_h - k_h, k_w:img_w - k_w]
|
| 292 |
+
correct_sum = np.allclose(158339.52, hybrid_interior.sum())
|
| 293 |
+
|
| 294 |
+
# ground truth values
|
| 295 |
+
gt_hybrid_crop = np.array(
|
| 296 |
+
[
|
| 297 |
+
[[0.5429589, 0.55373234, 0.5452099],
|
| 298 |
+
[0.5290553, 0.5485607, 0.545738]],
|
| 299 |
+
|
| 300 |
+
[[0.55020595, 0.55713284, 0.5457024],
|
| 301 |
+
[0.5368045, 0.5603536, 0.5505791]]
|
| 302 |
+
], dtype=np.float32
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
# H,W,C order in Numpy
|
| 306 |
+
correct_crop = np.allclose(hybrid_image[100:102, 100:102, :], gt_hybrid_crop, atol=1e-3)
|
| 307 |
+
if correct_sum and correct_crop:
|
| 308 |
+
print('Success! Hybrid image values are correct.')
|
| 309 |
+
return True
|
| 310 |
+
else:
|
| 311 |
+
print('Hybrid image values are not correct, please double check your implementation.')
|
| 312 |
+
return False
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
## Purely for debugging/visualization ##
|
| 316 |
+
# plt.imshow(hybrid_image)
|
| 317 |
+
# plt.show()
|
| 318 |
+
########################################
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def verify_gaussian_kernel(kernel, cutoff_frequency) -> bool:
|
| 322 |
+
"""
|
| 323 |
+
Interactive test to be used in IPython notebook, that will print out
|
| 324 |
+
test result, and return value can also be queried for success (true).
|
| 325 |
+
|
| 326 |
+
Args:
|
| 327 |
+
- kernel
|
| 328 |
+
- cutoff_frequency
|
| 329 |
+
|
| 330 |
+
Returns:
|
| 331 |
+
- Boolean indicating success.
|
| 332 |
+
"""
|
| 333 |
+
if cutoff_frequency != 7:
|
| 334 |
+
print('Please change the cutoff_frequency back to 7 and rerun this test')
|
| 335 |
+
return False
|
| 336 |
+
if kernel.shape != (29, 29):
|
| 337 |
+
print('The kernel is not the correct size')
|
| 338 |
+
return False
|
| 339 |
+
|
| 340 |
+
kernel_h, kernel_w = kernel.shape
|
| 341 |
+
gt_kernel_crop = np.array(
|
| 342 |
+
[
|
| 343 |
+
[0.00323564, 0.00333623, 0.00337044, 0.00333623],
|
| 344 |
+
[0.00333623, 0.00343993, 0.00347522, 0.00343993],
|
| 345 |
+
[0.00337044, 0.00347522, 0.00351086, 0.00347522],
|
| 346 |
+
[0.00333623, 0.00343993, 0.00347522, 0.00343993]
|
| 347 |
+
]
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
h_center = kernel_h // 2
|
| 351 |
+
w_center = kernel_w // 2
|
| 352 |
+
student_kernel_crop = kernel[h_center - 2:h_center + 2, w_center - 2:w_center + 2]
|
| 353 |
+
|
| 354 |
+
correct_crop = np.allclose(gt_kernel_crop, student_kernel_crop, atol=1e-7)
|
| 355 |
+
correct_sum = np.allclose(kernel.sum(), 1.0, atol=1e-3)
|
| 356 |
+
correct_vals = correct_crop and correct_sum
|
| 357 |
+
|
| 358 |
+
if correct_vals:
|
| 359 |
+
print('Success -- kernel values are correct.')
|
| 360 |
+
return True
|
| 361 |
+
else:
|
| 362 |
+
print('Kernel values are not correct.')
|
| 363 |
+
return False
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def test_pytorch_low_pass_filter_square_kernel():
|
| 367 |
+
"""
|
| 368 |
+
Test the low pass filter, but not the output of the forward() pass.
|
| 369 |
+
"""
|
| 370 |
+
hi_model = HybridImageModel()
|
| 371 |
+
img_dir = f'{ROOT}/data'
|
| 372 |
+
cut_off_file = f'{ROOT}/cutoff_frequencies_temp.txt'
|
| 373 |
+
|
| 374 |
+
# Dump to a file
|
| 375 |
+
cutoff_freqs = [7, 7, 7, 7, 7]
|
| 376 |
+
write_objects_to_file(fpath=cut_off_file, obj_list=cutoff_freqs)
|
| 377 |
+
hi_dataset = HybridImageDataset(img_dir, cut_off_file)
|
| 378 |
+
|
| 379 |
+
# should be the dog image
|
| 380 |
+
img_a, img_b, cutoff_freq = hi_dataset[0]
|
| 381 |
+
# turn CHW into NCHW
|
| 382 |
+
img_a = img_a.unsqueeze(0)
|
| 383 |
+
|
| 384 |
+
hi_model.n_channels = 3
|
| 385 |
+
kernel = hi_model.get_kernel(cutoff_freq)
|
| 386 |
+
pytorch_low_freq = hi_model.low_pass(img_a, kernel)
|
| 387 |
+
|
| 388 |
+
assert list(pytorch_low_freq.shape) == [1, 3, 361, 410]
|
| 389 |
+
assert isinstance(pytorch_low_freq, torch.Tensor)
|
| 390 |
+
|
| 391 |
+
# crop from pytorch_output[:,:,20:22,20:22]
|
| 392 |
+
gt_crop = torch.tensor(
|
| 393 |
+
[
|
| 394 |
+
[
|
| 395 |
+
[[0.7941, 0.7989],
|
| 396 |
+
[0.7906, 0.7953]],
|
| 397 |
+
|
| 398 |
+
[[0.9031, 0.9064],
|
| 399 |
+
[0.9021, 0.9052]],
|
| 400 |
+
|
| 401 |
+
[[0.9152, 0.9173],
|
| 402 |
+
[0.9168, 0.9187]]
|
| 403 |
+
]
|
| 404 |
+
], dtype=torch.float32
|
| 405 |
+
)
|
| 406 |
+
assert torch.allclose(pytorch_low_freq[:, :, 20:22, 20:22], gt_crop, atol=1e-3)
|
| 407 |
+
|
| 408 |
+
# ground truth element sum
|
| 409 |
+
assert np.allclose(pytorch_low_freq.numpy().sum(), 209926.3481)
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def verify_low_freq_sq_kernel_pytorch(image_a, model, cutoff_freq, low_frequencies) -> bool:
|
| 413 |
+
"""
|
| 414 |
+
Test the output of the forward pass.
|
| 415 |
+
|
| 416 |
+
Args:
|
| 417 |
+
- image_a
|
| 418 |
+
- model
|
| 419 |
+
- cutoff_freq
|
| 420 |
+
- low_frequencies
|
| 421 |
+
|
| 422 |
+
Returns:
|
| 423 |
+
- None
|
| 424 |
+
"""
|
| 425 |
+
if not isinstance(cutoff_freq, torch.Tensor) or not torch.allclose(cutoff_freq, torch.Tensor([7])):
|
| 426 |
+
print('Please pass a Pytorch tensor containing `7` as the cutoff frequency.')
|
| 427 |
+
return False
|
| 428 |
+
|
| 429 |
+
img_a_val_sum = float(image_a.sum())
|
| 430 |
+
if not np.allclose(img_a_val_sum, 215154.9531):
|
| 431 |
+
print('Please pass in the dog image `1a_dog.bmp` as the `image_a` argument.')
|
| 432 |
+
return False
|
| 433 |
+
|
| 434 |
+
gt_low_freq_crop = torch.tensor(
|
| 435 |
+
[
|
| 436 |
+
[[0.5350, 0.5367],
|
| 437 |
+
[0.5347, 0.5369]],
|
| 438 |
+
|
| 439 |
+
[[0.5239, 0.5262],
|
| 440 |
+
[0.5236, 0.5264]],
|
| 441 |
+
|
| 442 |
+
[[0.5143, 0.5183],
|
| 443 |
+
[0.5150, 0.5193]]
|
| 444 |
+
]
|
| 445 |
+
)
|
| 446 |
+
correct_crop = torch.allclose(gt_low_freq_crop, low_frequencies[0, :, 100:102, 100:102], atol=1e-3)
|
| 447 |
+
|
| 448 |
+
img_h = image_a.shape[2]
|
| 449 |
+
img_w = image_a.shape[3]
|
| 450 |
+
kernel = model.get_kernel(int(cutoff_freq))
|
| 451 |
+
if not isinstance(kernel, torch.Tensor):
|
| 452 |
+
print('Kernel is not a torch tensor')
|
| 453 |
+
return False
|
| 454 |
+
|
| 455 |
+
gt_kernel_sz_list = [3, 1, 29, 29]
|
| 456 |
+
kernel_sz_list = [int(val) for val in kernel.shape]
|
| 457 |
+
|
| 458 |
+
if gt_kernel_sz_list != kernel_sz_list:
|
| 459 |
+
print('Kernel is not the correct size')
|
| 460 |
+
return False
|
| 461 |
+
|
| 462 |
+
k_h = kernel.shape[2]
|
| 463 |
+
k_w = kernel.shape[3]
|
| 464 |
+
|
| 465 |
+
# Exclude the border pixels.
|
| 466 |
+
low_freq_interior = low_frequencies[0, :, k_h:img_h - k_h, k_w:img_w - k_w]
|
| 467 |
+
correct_sum = np.allclose(158332.06, float(low_freq_interior.sum()), atol=1)
|
| 468 |
+
|
| 469 |
+
if correct_sum and correct_crop:
|
| 470 |
+
print('Success! Pytorch low frequencies values are correct.')
|
| 471 |
+
return True
|
| 472 |
+
else:
|
| 473 |
+
print('Pytorch low frequencies values are not correct, please double check your implementation.')
|
| 474 |
+
return False
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def verify_high_freq_sq_kernel_pytorch(image_b, model, cutoff_freq, high_frequencies) -> bool:
|
| 478 |
+
"""
|
| 479 |
+
Test the output of the forward pass.
|
| 480 |
+
|
| 481 |
+
Args:
|
| 482 |
+
- image_b
|
| 483 |
+
- model
|
| 484 |
+
- cutoff_freq
|
| 485 |
+
- high_frequencies
|
| 486 |
+
|
| 487 |
+
Returns:
|
| 488 |
+
- None
|
| 489 |
+
"""
|
| 490 |
+
if not isinstance(cutoff_freq, torch.Tensor) or not torch.allclose(cutoff_freq, torch.Tensor([7])):
|
| 491 |
+
print('Please pass a Pytorch tensor containing `7` as the cutoff frequency.')
|
| 492 |
+
return False
|
| 493 |
+
|
| 494 |
+
img_b_val_sum = float(image_b.sum())
|
| 495 |
+
if not np.allclose(img_b_val_sum, 230960.1875, atol=5.0):
|
| 496 |
+
print('Please pass in the cat image `1b_cat.bmp` as the `image_b` argument.')
|
| 497 |
+
return False
|
| 498 |
+
|
| 499 |
+
gt_high_freq_crop = torch.tensor(
|
| 500 |
+
[
|
| 501 |
+
[[7.9527e-03, -7.6560e-03],
|
| 502 |
+
[1.5484e-02, -6.9082e-05]],
|
| 503 |
+
|
| 504 |
+
[[2.9861e-02, 2.2352e-02],
|
| 505 |
+
[3.3504e-02, 3.3922e-02]],
|
| 506 |
+
|
| 507 |
+
[[3.0958e-02, 2.7430e-02],
|
| 508 |
+
[3.0706e-02, 3.1234e-02]]
|
| 509 |
+
]
|
| 510 |
+
)
|
| 511 |
+
correct_crop = torch.allclose(gt_high_freq_crop, high_frequencies[0, :, 100:102, 100:102], atol=1e-3)
|
| 512 |
+
|
| 513 |
+
img_h = image_b.shape[2]
|
| 514 |
+
img_w = image_b.shape[3]
|
| 515 |
+
kernel = model.get_kernel(int(cutoff_freq))
|
| 516 |
+
if not isinstance(kernel, torch.Tensor):
|
| 517 |
+
print('Kernel is not a torch tensor')
|
| 518 |
+
return False
|
| 519 |
+
|
| 520 |
+
gt_kernel_sz_list = [3, 1, 29, 29]
|
| 521 |
+
kernel_sz_list = [int(val) for val in kernel.shape]
|
| 522 |
+
|
| 523 |
+
if gt_kernel_sz_list != kernel_sz_list:
|
| 524 |
+
print('Kernel is not the correct size')
|
| 525 |
+
return False
|
| 526 |
+
|
| 527 |
+
k_h = kernel.shape[2]
|
| 528 |
+
k_w = kernel.shape[3]
|
| 529 |
+
|
| 530 |
+
# Exclude the border pixels.
|
| 531 |
+
high_freq_interior = high_frequencies[0, :, k_h:img_h - k_h, k_w:img_w - k_w]
|
| 532 |
+
correct_sum = np.allclose(12.012651, float(high_freq_interior.sum()), atol=1e-1)
|
| 533 |
+
|
| 534 |
+
if correct_sum and correct_crop:
|
| 535 |
+
print('Success! Pytorch high frequencies values are correct.')
|
| 536 |
+
return True
|
| 537 |
+
else:
|
| 538 |
+
print('Pytorch high frequencies values are not correct, please double check your implementation.')
|
| 539 |
+
return False
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def verify_hybrid_image_pytorch(image_a, image_b, model, cutoff_freq, hybrid_image) -> bool:
|
| 543 |
+
"""
|
| 544 |
+
Test the output of the forward pass.
|
| 545 |
+
|
| 546 |
+
Args:
|
| 547 |
+
- image_a
|
| 548 |
+
- image_b
|
| 549 |
+
- model
|
| 550 |
+
- cutoff_freq
|
| 551 |
+
- hybrid_image
|
| 552 |
+
|
| 553 |
+
Returns:
|
| 554 |
+
- None
|
| 555 |
+
"""
|
| 556 |
+
_, _, img_h, img_w = image_b.shape
|
| 557 |
+
kernel = model.get_kernel(int(cutoff_freq))
|
| 558 |
+
_, _, k_h, k_w = kernel.shape
|
| 559 |
+
|
| 560 |
+
# Exclude the border pixels.
|
| 561 |
+
hybrid_interior = hybrid_image[0, :, k_h:img_h - k_h, k_w:img_w - k_w]
|
| 562 |
+
correct_sum = np.allclose(158339.5469, hybrid_interior.sum(), atol=1e-2)
|
| 563 |
+
|
| 564 |
+
# ground truth values
|
| 565 |
+
gt_hybrid_crop = torch.tensor(
|
| 566 |
+
[
|
| 567 |
+
[[0.5430, 0.5291],
|
| 568 |
+
[0.5502, 0.5368]],
|
| 569 |
+
|
| 570 |
+
[[0.5537, 0.5486],
|
| 571 |
+
[0.5571, 0.5604]],
|
| 572 |
+
|
| 573 |
+
[[0.5452, 0.5457],
|
| 574 |
+
[0.5457, 0.5506]]
|
| 575 |
+
]
|
| 576 |
+
)
|
| 577 |
+
# H,W,C order in Numpy
|
| 578 |
+
correct_crop = torch.allclose(hybrid_image[0, :, 100:102, 100:102], gt_hybrid_crop, atol=1e-3)
|
| 579 |
+
if correct_sum and correct_crop:
|
| 580 |
+
print('Success! Pytorch hybrid image values are correct.')
|
| 581 |
+
return True
|
| 582 |
+
else:
|
| 583 |
+
print('Pytorch hybrid image values are not correct, please double check your implementation.')
|
| 584 |
+
return False
|
src/utils.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import numpy as np
|
| 3 |
+
import PIL
|
| 4 |
+
|
| 5 |
+
from typing import Any, List, Tuple
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def PIL_resize(img: np.ndarray, size: Tuple[int, int]) -> np.ndarray:
|
| 9 |
+
"""
|
| 10 |
+
Args:
|
| 11 |
+
- img: Array representing an image
|
| 12 |
+
- size: Tuple representing new desired (width, height)
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
- img
|
| 16 |
+
"""
|
| 17 |
+
img = numpy_arr_to_PIL_image(img, scale_to_255=True)
|
| 18 |
+
img = img.resize(size)
|
| 19 |
+
img = PIL_image_to_numpy_arr(img)
|
| 20 |
+
return img
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def PIL_image_to_numpy_arr(img, downscale_by_255=True):
|
| 24 |
+
"""
|
| 25 |
+
Args:
|
| 26 |
+
- img
|
| 27 |
+
- downscale_by_255
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
- img
|
| 31 |
+
"""
|
| 32 |
+
img = np.asarray(img)
|
| 33 |
+
img = img.astype(np.float32)
|
| 34 |
+
if downscale_by_255:
|
| 35 |
+
img /= 255
|
| 36 |
+
return img
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def vis_image_scales_numpy(image: np.ndarray) -> np.ndarray:
|
| 40 |
+
"""
|
| 41 |
+
This function will display an image at different scales (zoom factors). The
|
| 42 |
+
original image will appear at the far left, and then the image will
|
| 43 |
+
iteratively be shrunk by 2x in each image to the right.
|
| 44 |
+
|
| 45 |
+
This is a particular effective way to simulate the perspective effect, as
|
| 46 |
+
if viewing an image at different distances. We thus use it to visualize
|
| 47 |
+
hybrid images, which represent a combination of two images, as described
|
| 48 |
+
in the SIGGRAPH 2006 paper "Hybrid Images" by Oliva, Torralba, Schyns.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
- image: Array of shape (H, W, C)
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
- img_scales: Array of shape (M, K, C) representing horizontally stacked
|
| 55 |
+
images, growing smaller from left to right.
|
| 56 |
+
K = W + int(1/2 W + 1/4 W + 1/8 W + 1/16 W) + (5 * 4)
|
| 57 |
+
"""
|
| 58 |
+
original_height = image.shape[0]
|
| 59 |
+
original_width = image.shape[1]
|
| 60 |
+
num_colors = 1 if image.ndim == 2 else 3
|
| 61 |
+
img_scales = np.copy(image)
|
| 62 |
+
cur_image = np.copy(image)
|
| 63 |
+
|
| 64 |
+
scales = 5
|
| 65 |
+
scale_factor = 0.5
|
| 66 |
+
padding = 5
|
| 67 |
+
|
| 68 |
+
new_h = original_height
|
| 69 |
+
new_w = original_width
|
| 70 |
+
|
| 71 |
+
for scale in range(2, scales + 1):
|
| 72 |
+
# add padding
|
| 73 |
+
img_scales = np.hstack((img_scales,
|
| 74 |
+
np.ones((original_height, padding, num_colors), dtype=np.float32))
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
new_h = int(scale_factor * new_h)
|
| 78 |
+
new_w = int(scale_factor * new_w)
|
| 79 |
+
# downsample image iteratively
|
| 80 |
+
cur_image = PIL_resize(cur_image, size=(new_w, new_h))
|
| 81 |
+
|
| 82 |
+
# pad the top to append to the output
|
| 83 |
+
h_pad = original_height - cur_image.shape[0]
|
| 84 |
+
pad = np.ones((h_pad, cur_image.shape[1], num_colors), dtype=np.float32)
|
| 85 |
+
tmp = np.vstack((pad, cur_image))
|
| 86 |
+
img_scales = np.hstack((img_scales, tmp))
|
| 87 |
+
|
| 88 |
+
return img_scales
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def im2single(im: np.ndarray) -> np.ndarray:
|
| 92 |
+
"""
|
| 93 |
+
Args:
|
| 94 |
+
- img: uint8 array of shape (m,n,c) or (m,n) and in range [0,255]
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
- im: float or double array of identical shape and in range [0,1]
|
| 98 |
+
"""
|
| 99 |
+
im = im.astype(np.float32) / 255
|
| 100 |
+
return im
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def single2im(im: np.ndarray) -> np.ndarray:
|
| 104 |
+
"""
|
| 105 |
+
Args:
|
| 106 |
+
- im: float or double array of shape (m,n,c) or (m,n) and in range [0,1]
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
- im: uint8 array of identical shape and in range [0,255]
|
| 110 |
+
"""
|
| 111 |
+
im *= 255
|
| 112 |
+
im = im.astype(np.uint8)
|
| 113 |
+
return im
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def numpy_arr_to_PIL_image(img: np.ndarray, scale_to_255: False) -> PIL.Image:
|
| 117 |
+
"""
|
| 118 |
+
Args:
|
| 119 |
+
- img: in [0,1]
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
- img in [0,255]
|
| 123 |
+
|
| 124 |
+
"""
|
| 125 |
+
if scale_to_255:
|
| 126 |
+
img *= 255
|
| 127 |
+
return PIL.Image.fromarray(np.uint8(img))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def load_image(path: str) -> np.ndarray:
|
| 131 |
+
"""
|
| 132 |
+
Args:
|
| 133 |
+
- path: string representing a file path to an image
|
| 134 |
+
|
| 135 |
+
Returns:
|
| 136 |
+
- float or double array of shape (m,n,c) or (m,n) and in range [0,1],
|
| 137 |
+
representing an RGB image
|
| 138 |
+
"""
|
| 139 |
+
pil_img = PIL.Image.open(path)
|
| 140 |
+
img = PIL_image_to_numpy_arr(pil_img, False)
|
| 141 |
+
img = im2single(img)
|
| 142 |
+
return img
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def save_image(path: str, im: np.ndarray) -> bool:
|
| 146 |
+
"""
|
| 147 |
+
Args:
|
| 148 |
+
- path: string representing a file path to an image
|
| 149 |
+
- img: numpy array
|
| 150 |
+
|
| 151 |
+
Returns:
|
| 152 |
+
- retval indicating write success
|
| 153 |
+
"""
|
| 154 |
+
img = copy.deepcopy(im)
|
| 155 |
+
img = single2im(img)
|
| 156 |
+
pil_img = numpy_arr_to_PIL_image(img, scale_to_255=False)
|
| 157 |
+
return pil_img.save(path)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def write_objects_to_file(fpath: str, obj_list: List[Any]):
|
| 161 |
+
"""
|
| 162 |
+
If the list contents are float or int, convert them to strings.
|
| 163 |
+
Separate with carriage return.
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
- fpath: string representing path to a file
|
| 167 |
+
- obj_list: List of strings, floats, or integers to be written out to a file, one per line.
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
- None
|
| 171 |
+
"""
|
| 172 |
+
obj_list = [str(obj) + '\n' for obj in obj_list]
|
| 173 |
+
with open(fpath, 'w') as f:
|
| 174 |
+
f.writelines(obj_list)
|
webpage_martix.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import numpy as np
|
| 3 |
+
from tqdm import trange
|
| 4 |
+
|
| 5 |
+
from src.part1 import create_Gaussian_kernel, create_hybrid_image
|
| 6 |
+
|
| 7 |
+
input_pictures_examples = [
|
| 8 |
+
["./data/1a_dog.bmp", "./data/1b_cat.bmp"],
|
| 9 |
+
["./data/2a_motorcycle.bmp", "./data/2b_bicycle.bmp"],
|
| 10 |
+
["./data/3a_plane.bmp", "./data/3b_bird.bmp"],
|
| 11 |
+
["./data/4a_einstein.bmp", "./data/4b_marilyn.bmp"],
|
| 12 |
+
["./data/5a_submarine.bmp", "./data/5b_fish.bmp"],
|
| 13 |
+
["./data/Message+1.png", "./data/Message+2.png", 5],
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def action_hybrid_image(a, b, c):
|
| 18 |
+
# check input is None
|
| 19 |
+
if a is None or b is None or c is None:
|
| 20 |
+
return None, None, None
|
| 21 |
+
|
| 22 |
+
l, h, lh = create_hybrid_image(a / 255, b / 255, c / 255)
|
| 23 |
+
|
| 24 |
+
return (l * 255).astype(np.uint8), ((h + 0.5) * 255).astype(np.uint8), (lh * 255).astype(np.uint8)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def action_hybrid_matrix(a, b):
|
| 28 |
+
result_matrix = []
|
| 29 |
+
|
| 30 |
+
for i in trange(1, 8):
|
| 31 |
+
kernel = create_Gaussian_kernel(i)
|
| 32 |
+
l, h, lh = action_hybrid_image(a, b, kernel)
|
| 33 |
+
result_matrix.append(lh)
|
| 34 |
+
|
| 35 |
+
for i in trange(1, 8):
|
| 36 |
+
kernel = create_Gaussian_kernel(i)
|
| 37 |
+
l, h, lh = action_hybrid_image(b, a, kernel)
|
| 38 |
+
result_matrix.append(lh)
|
| 39 |
+
|
| 40 |
+
return result_matrix
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
with gr.Blocks() as app:
|
| 44 |
+
gr.Markdown("## Input")
|
| 45 |
+
with gr.Row() as uploader:
|
| 46 |
+
# allow user to upload 2 images TODO: Limit picture size, button: exchange pictures
|
| 47 |
+
image1 = gr.Image(label="Image 1", interactive=True)
|
| 48 |
+
|
| 49 |
+
image2 = gr.Image(label="Image 2", interactive=True)
|
| 50 |
+
|
| 51 |
+
submit = gr.Button("Submit")
|
| 52 |
+
|
| 53 |
+
gr.Markdown("## Output")
|
| 54 |
+
# output
|
| 55 |
+
with gr.Row() as output:
|
| 56 |
+
h_1_2_1 = gr.Image(label="Hybrid Image_121")
|
| 57 |
+
h_1_2_2 = gr.Image(label="Hybrid Image_122")
|
| 58 |
+
h_1_2_3 = gr.Image(label="Hybrid Image_123")
|
| 59 |
+
h_1_2_4 = gr.Image(label="Hybrid Image_124")
|
| 60 |
+
h_1_2_5 = gr.Image(label="Hybrid Image_125")
|
| 61 |
+
h_1_2_6 = gr.Image(label="Hybrid Image_126")
|
| 62 |
+
h_1_2_7 = gr.Image(label="Hybrid Image_127")
|
| 63 |
+
|
| 64 |
+
with gr.Row() as exchanged:
|
| 65 |
+
h_2_1_1 = gr.Image(label="Hybrid Image_211")
|
| 66 |
+
h_2_1_2 = gr.Image(label="Hybrid Image_212")
|
| 67 |
+
h_2_1_3 = gr.Image(label="Hybrid Image_213")
|
| 68 |
+
h_2_1_4 = gr.Image(label="Hybrid Image_214")
|
| 69 |
+
h_2_1_5 = gr.Image(label="Hybrid Image_215")
|
| 70 |
+
h_2_1_6 = gr.Image(label="Hybrid Image_216")
|
| 71 |
+
h_2_1_7 = gr.Image(label="Hybrid Image_217")
|
| 72 |
+
|
| 73 |
+
submit.click(
|
| 74 |
+
action_hybrid_matrix,
|
| 75 |
+
inputs=[image1, image2],
|
| 76 |
+
outputs=[h_1_2_1, h_1_2_2, h_1_2_3, h_1_2_4, h_1_2_5, h_1_2_6, h_1_2_7,
|
| 77 |
+
h_2_1_1, h_2_1_2, h_2_1_3, h_2_1_4, h_2_1_5, h_2_1_6, h_2_1_7]
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
gr.Markdown("## Use Examples")
|
| 81 |
+
|
| 82 |
+
gr.Examples(
|
| 83 |
+
examples=input_pictures_examples,
|
| 84 |
+
inputs=[image1, image2],
|
| 85 |
+
# outputs=txt_3,
|
| 86 |
+
# fn=combine,
|
| 87 |
+
# cache_examples=True, # cache examples to local storage
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
app.launch(server_port=3030)
|