InEase commited on
Commit
b78dbf0
·
1 Parent(s): 1bb7c81

Upload Files

Browse files
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)