Soheib Takhtardeshir commited on
Commit
702e8d8
·
1 Parent(s): 45c78a3
.gitattributes CHANGED
@@ -1 +1,2 @@
1
  *.png filter=lfs diff=lfs merge=lfs -text
 
 
1
  *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.pth filter=lfs diff=lfs merge=lfs -text
README_demo.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Light Field Image Compression using VAE
2
+
3
+ The more details about our training and model will be made available after acceptance of our paper.
4
+
5
+ [Project Page (Will be active after paper acceptance)](https://takhtardeshirsoheib.github.io/DUALF_D/index.html)
app.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # 1. IMPORTS
3
+ import gradio as gr
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from torchvision import transforms
8
+ from PIL import Image
9
+ import numpy as np
10
+ import os
11
+ import math
12
+ import warnings
13
+ from compressai.layers import GDN, conv3x3, subpel_conv3x3
14
+ from compressai.entropy_models import EntropyBottleneck, GaussianConditional
15
+ from skimage.metrics import structural_similarity as ssim
16
+ from skimage.metrics import peak_signal_noise_ratio as psnr
17
+
18
+ '''
19
+ 01 - Best for Low Bit Rates ModelvLowBit
20
+ 005 - Mid Level for Low Bit Rates ModelvMidBit
21
+ 001 - Mid Level for High Bit Rates ModelvHighBit
22
+ 0001 - Best for High Bit Rates ModelvBestHighBit
23
+ '''
24
+
25
+ '''
26
+ QP - Smaller value is worst quality but best for storage
27
+ '''
28
+
29
+ warnings.filterwarnings("ignore", "Inputs have mismatched dtype", UserWarning)
30
+ filt_n = 128
31
+ latent_channels = 128
32
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
33
+ save_path = "./checkpoint/"
34
+
35
+ # 3. MODEL DEFINITIONS (from model.py)
36
+ def get_scale_table(min_val=0.11, max_val=256, levels=64):
37
+ """Get the scale table as a list of floats."""
38
+ return [float(f) for f in torch.exp(torch.linspace(math.log(min_val), math.log(max_val), levels))]
39
+
40
+ class SpatialEncoder(nn.Module):
41
+ def __init__(self):
42
+ super(SpatialEncoder, self).__init__()
43
+ self.conv_layers_S1 = nn.Sequential(nn.Conv2d(3, filt_n, kernel_size=5, stride=1, padding=1, dilation=3), GDN(filt_n))
44
+ self.conv_layers_S2 = nn.Sequential(nn.Conv2d(filt_n, filt_n, kernel_size=5, stride=2, padding=1), GDN(filt_n))
45
+ self.conv_layers_S3 = nn.Sequential(nn.Conv2d(filt_n, filt_n, kernel_size=5, stride=2, padding=1), GDN(filt_n))
46
+ self.conv_layers_S4 = nn.Sequential(nn.Conv2d(filt_n, filt_n, kernel_size=5, stride=2, padding=1), GDN(filt_n))
47
+ self.conv_layers_S5 = nn.Sequential(nn.Conv2d(filt_n, 64, kernel_size=5, stride=3, padding=1), GDN(64))
48
+ def forward(self, x):
49
+ x = self.conv_layers_S1(x)
50
+ x = self.conv_layers_S2(x)
51
+ x = self.conv_layers_S3(x)
52
+ x = self.conv_layers_S4(x)
53
+ x = self.conv_layers_S5(x)
54
+ return x
55
+
56
+ class AngularEncoder(nn.Module):
57
+ def __init__(self):
58
+ super(AngularEncoder, self).__init__()
59
+ self.conv_layers_A1 = nn.Sequential(nn.Conv2d(3, filt_n, kernel_size=3, stride=3, padding=1), GDN(filt_n))
60
+ self.conv_layers_A2 = nn.Sequential(nn.Conv2d(filt_n, filt_n, kernel_size=5, stride=2, padding=1), GDN(filt_n))
61
+ self.conv_layers_A3 = nn.Sequential(nn.Conv2d(filt_n, filt_n, kernel_size=5, stride=2, padding=1), GDN(filt_n))
62
+ self.conv_layers_A4 = nn.Sequential(nn.Conv2d(filt_n, 64, kernel_size=5, stride=2, padding=1), GDN(64))
63
+ def forward(self, x):
64
+ x = self.conv_layers_A1(x)
65
+ x = self.conv_layers_A2(x)
66
+ x = self.conv_layers_A3(x)
67
+ x = self.conv_layers_A4(x)
68
+ return x
69
+
70
+ class HyperpriorNetwork(nn.Module):
71
+ def __init__(self, channels):
72
+ super().__init__()
73
+ self.entropy_bottleneck = EntropyBottleneck(channels)
74
+ self.h_a = nn.Sequential(
75
+ conv3x3(channels, channels), nn.LeakyReLU(inplace=True),
76
+ conv3x3(channels, channels, stride=2), nn.LeakyReLU(inplace=True),
77
+ conv3x3(channels, channels, stride=2),
78
+ )
79
+ self.h_s = nn.Sequential(
80
+ conv3x3(channels, channels), nn.LeakyReLU(inplace=True),
81
+ subpel_conv3x3(channels, channels, 2), nn.LeakyReLU(inplace=True),
82
+ subpel_conv3x3(channels, channels, 2), nn.LeakyReLU(inplace=True),
83
+ conv3x3(channels, channels),
84
+ )
85
+ def forward(self, x):
86
+ z = self.h_a(x)
87
+ z_hat, z_likelihoods = self.entropy_bottleneck(z)
88
+ scales = torch.exp(self.h_s(z_hat))
89
+ return scales, z_likelihoods
90
+
91
+ class Encoder(nn.Module):
92
+ def __init__(self, latent_channels):
93
+ super().__init__()
94
+ self.spatial_encoder = SpatialEncoder()
95
+ self.angular_encoder = AngularEncoder()
96
+ self.spatial_hyperprior = HyperpriorNetwork(64)
97
+ self.angular_hyperprior = HyperpriorNetwork(64)
98
+ self.entropy_model_s = GaussianConditional(get_scale_table())
99
+ self.entropy_model_a = GaussianConditional(get_scale_table())
100
+ def forward(self, x):
101
+ y_s = self.spatial_encoder(x)
102
+ y_a = self.angular_encoder(x)
103
+ scales_s, z_likelihood_s = self.spatial_hyperprior(y_s)
104
+ scales_a, z_likelihood_a = self.angular_hyperprior(y_a)
105
+ z_s, likelihood_s = self.entropy_model_s(y_s, scales_s)
106
+ z_a, likelihood_a = self.entropy_model_a(y_a, scales_a)
107
+ concatenated = torch.cat((z_s, z_a), dim=1)
108
+ return {
109
+ "y_hat": concatenated,
110
+ "latents": {"y_s": y_s, "y_a": y_a},
111
+ "likelihoods": {"y_s": likelihood_s, "y_a": likelihood_a, "z_s": z_likelihood_s, "z_a": z_likelihood_a}
112
+ }
113
+
114
+ class Decoder(nn.Module):
115
+ def __init__(self, latent_channels):
116
+ super().__init__()
117
+ self.initial_layer = nn.Sequential(nn.ConvTranspose2d(latent_channels, filt_n, kernel_size=5, stride=3, padding=0), GDN(filt_n, inverse=True))
118
+ self.conv_layers_D1 = nn.Sequential(nn.ConvTranspose2d(filt_n, filt_n, kernel_size=4, stride=2, padding=0), GDN(filt_n, inverse=True))
119
+ self.conv_layers_D2 = nn.Sequential(nn.ConvTranspose2d(filt_n, filt_n, kernel_size=4, stride=2, padding=1), GDN(filt_n, inverse=True))
120
+ self.conv_layers_D3 = nn.Sequential(nn.ConvTranspose2d(filt_n, 3, kernel_size=4, stride=2, padding=1), nn.Sigmoid())
121
+ def forward(self, z):
122
+ x = self.initial_layer(z)
123
+ x = self.conv_layers_D1(x)
124
+ x = self.conv_layers_D2(x)
125
+ x = self.conv_layers_D3(x)
126
+ return x
127
+
128
+ class VAE(nn.Module):
129
+ def __init__(self, latent_channels):
130
+ super().__init__()
131
+ self.encoder = Encoder(latent_channels)
132
+ self.decoder = Decoder(latent_channels)
133
+ def forward(self, x):
134
+ enc_out = self.encoder(x)
135
+ dec_out = self.decoder(enc_out["y_hat"])
136
+ return {"x_hat": dec_out, "likelihoods": enc_out["likelihoods"], "latents": enc_out["latents"]}
137
+
138
+ def extract_patches(image, patch_size=(216, 312), step_size=(180, 260)):
139
+ patches = []
140
+ img_width, img_height = image.size
141
+ for y in range(0, img_height - patch_size[0] + 1, step_size[0]):
142
+ for x in range(0, img_width - patch_size[1] + 1, step_size[1]):
143
+ box = (x, y, x + patch_size[1], y + patch_size[0])
144
+ patch = image.crop(box)
145
+ patches.append(patch)
146
+ if len(patches) == 49:
147
+ return patches
148
+ return patches
149
+
150
+ def reassemble_image(patches, original_size, patch_size, step_size):
151
+ original_width, original_height = original_size
152
+ reconstructed = torch.zeros((3, original_height, original_width), device='cpu')
153
+ counts = torch.zeros_like(reconstructed)
154
+ patch_idx = 0
155
+ for y in range(0, original_height - patch_size[0] + 1, step_size[0]):
156
+ for x in range(0, original_width - patch_size[1] + 1, step_size[1]):
157
+ if patch_idx >= len(patches):
158
+ break
159
+ patch = patches[patch_idx]
160
+ reconstructed[:, y:y + patch_size[0], x:x + patch_size[1]] += patch
161
+ counts[:, y:y + patch_size[0], x:x + patch_size[1]] += 1
162
+ patch_idx += 1
163
+ reconstructed /= counts.clamp(min=1)
164
+ return reconstructed
165
+
166
+ def rgb_to_ycbcr(rgb_image):
167
+ if isinstance(rgb_image, torch.Tensor):
168
+ rgb_image = rgb_image.cpu().numpy()
169
+ if rgb_image.shape[0] == 3:
170
+ rgb_image = np.transpose(rgb_image, (1, 2, 0))
171
+ R, G, B = rgb_image[:, :, 0], rgb_image[:, :, 1], rgb_image[:, :, 2]
172
+ Y = 0.299 * R + 0.587 * G + 0.114 * B
173
+ return Y
174
+
175
+ def calculate_metrics(original, reconstructed):
176
+ original_np = original.cpu().numpy()
177
+ reconstructed_np = reconstructed.cpu().numpy()
178
+ if original_np.shape[0] == 3:
179
+ original_np_hwc = np.transpose(original_np, (1, 2, 0))
180
+ reconstructed_np_hwc = np.transpose(reconstructed_np, (1, 2, 0))
181
+ else:
182
+ original_np_hwc = original_np
183
+ reconstructed_np_hwc = reconstructed_np
184
+
185
+ psnr_rgb = psnr(original_np_hwc, reconstructed_np_hwc, data_range=1.0)
186
+ ssim_rgb = ssim(original_np_hwc, reconstructed_np_hwc, channel_axis=2, data_range=1.0, win_size=11)
187
+
188
+ y_original = rgb_to_ycbcr(original_np)
189
+ y_reconstructed = rgb_to_ycbcr(reconstructed_np)
190
+
191
+ psnr_y = psnr(y_original, y_reconstructed, data_range=1.0)
192
+ ssim_y = ssim(y_original, y_reconstructed, data_range=1.0, win_size=11)
193
+
194
+ return {'PSNR_RGB': psnr_rgb, 'SSIM_RGB': ssim_rgb, 'PSNR_Y': psnr_y, 'SSIM_Y': ssim_y}
195
+
196
+ def calculate_entropy(tensor):
197
+ symbols = tensor.flatten()
198
+ _, counts = torch.unique(symbols, return_counts=True)
199
+ probs = counts.float() / symbols.numel()
200
+ entropy = -torch.sum(probs * torch.log2(probs + 1e-10))
201
+ return entropy * symbols.numel()
202
+
203
+ MODEL_LIST = ['DUALF_D_v_Best_High_Bit_Rate.pth', 'DUALF_D_v_Low_Bit_Rate.pth', 'DUALF_D_v_High_Bit_Rate.pth', 'DUALF_D_v_Mid_Bit_Rate.pth']
204
+ QP_LIST = [0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3.0]
205
+ model_cache = {}
206
+
207
+ def load_model_for_gradio(model_filename):
208
+ if model_filename in model_cache:
209
+ return model_cache[model_filename]
210
+
211
+ model = VAE(latent_channels).to(device)
212
+ model_path = os.path.join(save_path, model_filename)
213
+ if not os.path.exists(model_path):
214
+ raise FileNotFoundError(f"Model file not found at {model_path}. Please place models in the '{save_path}' directory.")
215
+
216
+ state_dict = torch.load(model_path, map_location=device)
217
+
218
+ try:
219
+ spatial_cdf_size = state_dict['encoder.spatial_hyperprior.entropy_bottleneck._quantized_cdf'].size(1)
220
+ angular_cdf_size = state_dict['encoder.angular_hyperprior.entropy_bottleneck._quantized_cdf'].size(1)
221
+
222
+ model.encoder.spatial_hyperprior.entropy_bottleneck._offset = torch.zeros(64, device=device)
223
+ model.encoder.spatial_hyperprior.entropy_bottleneck._quantized_cdf = torch.zeros(64, spatial_cdf_size, device=device)
224
+ model.encoder.spatial_hyperprior.entropy_bottleneck._cdf_length = torch.zeros(64, dtype=torch.int32, device=device)
225
+
226
+ model.encoder.angular_hyperprior.entropy_bottleneck._offset = torch.zeros(64, device=device)
227
+ model.encoder.angular_hyperprior.entropy_bottleneck._quantized_cdf = torch.zeros(64, angular_cdf_size, device=device)
228
+ model.encoder.angular_hyperprior.entropy_bottleneck._cdf_length = torch.zeros(64, dtype=torch.int32, device=device)
229
+ except KeyError as e:
230
+ print(f"Warning: Could not find key {e} in state_dict. This may happen with older models. Trying to load without it.")
231
+
232
+ model.load_state_dict(state_dict, strict=False)
233
+ model.eval()
234
+ model_cache[model_filename] = model
235
+ return model
236
+
237
+ def compress_and_display(image_pil, model_filename, qp_value):
238
+ print(f"Processing with model: {model_filename} and QP: {qp_value}")
239
+
240
+ model = load_model_for_gradio(model_filename)
241
+
242
+ original_tensor = transforms.ToTensor()(image_pil)
243
+ patch_size_config = (216, 312)
244
+ step_size_config = (180, 260)
245
+ patches = extract_patches(image_pil, patch_size=patch_size_config, step_size=step_size_config)
246
+ patches_tensor = [transforms.ToTensor()(p) for p in patches]
247
+
248
+ total_bits = 0
249
+ reconstructed_patches = []
250
+ with torch.no_grad():
251
+ for patch in patches_tensor:
252
+ patch = patch.unsqueeze(0).to(device)
253
+ enc_out = model.encoder(patch)
254
+ y_s = enc_out["latents"]["y_s"]
255
+ y_a = enc_out["latents"]["y_a"]
256
+
257
+ step_size = 1.0 / qp_value
258
+ y_s_quantized = torch.round(y_s / step_size)
259
+ y_a_quantized = torch.round(y_a / step_size)
260
+
261
+ y_s_dequantized = y_s_quantized * step_size
262
+ y_a_dequantized = y_a_quantized * step_size
263
+ latents_dequantized = torch.cat((y_s_dequantized, y_a_dequantized), dim=1)
264
+
265
+ reconstructed = model.decoder(latents_dequantized)
266
+ reconstructed_patches.append(reconstructed.squeeze(0).cpu())
267
+
268
+ bits_spatial = calculate_entropy(y_s_quantized)
269
+ bits_angular = calculate_entropy(y_a_quantized)
270
+ total_bits += bits_spatial.item() + bits_angular.item()
271
+
272
+ reconstructed_tensor = reassemble_image(reconstructed_patches, image_pil.size, patch_size_config, step_size_config)
273
+ reconstructed_tensor = reconstructed_tensor.clamp(0, 1)
274
+
275
+ total_pixels = image_pil.width * image_pil.height
276
+ bpp = total_bits / total_pixels
277
+ metrics_dict = calculate_metrics(original_tensor, reconstructed_tensor)
278
+ metrics_dict['BPP'] = bpp
279
+
280
+ original_np = (original_tensor.cpu().numpy().transpose(1, 2, 0) * 255).astype(np.uint8)
281
+ reconstructed_np = (reconstructed_tensor.cpu().numpy().transpose(1, 2, 0) * 255).astype(np.uint8)
282
+
283
+ comparison_image = np.hstack((original_np, reconstructed_np))
284
+ metrics_str = (
285
+ f"Bits Per Pixel (BPP): {metrics_dict['BPP']:.4f}\n\n"
286
+ f"--- RGB Metrics ---\n"
287
+ f"PSNR (RGB): {metrics_dict['PSNR_RGB']:.2f} dB\n"
288
+ f"SSIM (RGB): {metrics_dict['SSIM_RGB']:.4f}\n\n"
289
+ f"--- Luma (Y) Metrics ---\n"
290
+ f"PSNR (Y): {metrics_dict['PSNR_Y']:.2f} dB\n"
291
+ f"SSIM (Y): {metrics_dict['SSIM_Y']:.4f}"
292
+ )
293
+ return comparison_image, metrics_str
294
+
295
+ title = "Light Field Image Compression with DUALF_D VAE"
296
+ description = """
297
+ Upload a macropixel image (e.g., a 3x3 view light field image taken with Lytro Illum 2.0) to compress and decompress it using a VAE-based neural network.
298
+
299
+ * You can select different pre-trained model checkpoints and adjust the Quantization Parameter (QP) to see its effect on quality and bitrate.
300
+ * A lower QP generally results in lower quality and a lower storage requirement, while a higher QP means better quality but requires more storage for image.
301
+ * Please refer to our [GitHub Page](https://takhtardeshirsoheib.github.io/DUALF_D/index.html) for more details (it will be public after acceptance of our paper)
302
+ """
303
+ with gr.Blocks() as demo:
304
+ gr.Markdown(f"<h1 style='text-align: center;'>{title}</h1>")
305
+ gr.Markdown(description)
306
+
307
+ with gr.Row():
308
+ with gr.Column(scale=1):
309
+ input_image = gr.Image(type="pil", label="Upload Macropixel Image")
310
+ model_selector = gr.Dropdown(choices=MODEL_LIST, value=MODEL_LIST[3], label="Selected Model")
311
+ qp_selector = gr.Dropdown(choices=QP_LIST, value=1.0, label="Selected Quantization Parameter (QP)")
312
+ submit_button = gr.Button("Compress and Analyze")
313
+
314
+ with gr.Column(scale=2):
315
+ output_comparison = gr.Image(label="Original vs. Compressed")
316
+ output_metrics = gr.Textbox(label="Performance Metrics")
317
+
318
+ submit_button.click(
319
+ fn=compress_and_display,
320
+ inputs=[input_image, model_selector, qp_selector],
321
+ outputs=[output_comparison, output_metrics]
322
+ )
323
+ with gr.Row():
324
+ gr.Examples(
325
+ examples=[
326
+ ["./samples/macropixel_059.png", MODEL_LIST[3], 0.5],
327
+ ["./samples/macropixel_033.png", MODEL_LIST[2], 0.5],
328
+ ["./samples/macropixel_028.png", MODEL_LIST[3], 2.0],
329
+ ["./samples/macropixel_026.png", MODEL_LIST[3], 2.5],
330
+ ["./samples/macropixel_019.png", MODEL_LIST[3], 2.6],
331
+ ["./samples/macropixel_203.png", MODEL_LIST[3], 2.8],
332
+ ["./samples/macropixel_923.png", MODEL_LIST[3], 3.0]
333
+ ],
334
+ inputs=[input_image, model_selector, qp_selector]
335
+ )
336
+
337
+ if __name__ == "__main__":
338
+ demo.launch()
checkpoint/DUALF_D_v_Best_High_Bit_Rate.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:663df825eaf0e0359cf18ff02b65cf1ee827873113862f2cd430d28ec655575b
3
+ size 18339498
checkpoint/DUALF_D_v_High_Bit_Rate.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a44171f24a1aaaf69b8437c46ee0558e8a04da0bb1cabb6600bd034ce464c79
3
+ size 18344362
checkpoint/DUALF_D_v_Low_Bit_Rate.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8f3cc3aa976755fe9cde13316dfadb9cfd0e946999cc68c6065eba26178754a
3
+ size 18332842
checkpoint/DUALF_D_v_Mid_Bit_Rate.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:945df8ca6caf60f9ef83d4dc9dd104c456d0cc550d2178f9dee62ffe0cdab2cc
3
+ size 18339242
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch>=1.9.0
2
+ torchvision>=0.10.0
3
+ gradio>=3.50.0
4
+ Pillow>=8.0.0
5
+ numpy>=1.21.0
6
+ scikit-image>=0.18.0
7
+ compressai>=1.7.0
samples/macropixel_002.png ADDED

Git LFS Details

  • SHA256: 9e49f48839cfaa17792a71a0fd28813e736b87e1b44a7a3ad7f7370792c89b7a
  • Pointer size: 132 Bytes
  • Size of remote file: 3.46 MB
samples/macropixel_019.png ADDED

Git LFS Details

  • SHA256: fdc8743a83c08d75c86bc61822f326dd166d81393a44d41b53a73a239a08be24
  • Pointer size: 132 Bytes
  • Size of remote file: 3.29 MB
samples/macropixel_024.png ADDED

Git LFS Details

  • SHA256: 3b472537ed4b747fc2e7191c376c398b47f36ee9c3ec3e9f9cc2e0909193445c
  • Pointer size: 132 Bytes
  • Size of remote file: 2.87 MB
samples/macropixel_026.png ADDED

Git LFS Details

  • SHA256: 541b2b7cdd3dba4408cffc86835fd12be20037f5858bd6af8707e8f758c9f573
  • Pointer size: 132 Bytes
  • Size of remote file: 3.83 MB
samples/macropixel_028.png ADDED

Git LFS Details

  • SHA256: 011bd87c7b0e4bffa433c2b0830aaac0a80fcb0a0df05e3bd589c9dc2ad3ca51
  • Pointer size: 132 Bytes
  • Size of remote file: 4.29 MB
samples/macropixel_033.png ADDED

Git LFS Details

  • SHA256: ff01d0f32afdb0ed5e31737337db6c062eeedfda70bef9876a484ca45e1bd4b2
  • Pointer size: 132 Bytes
  • Size of remote file: 3.48 MB
samples/macropixel_059.png ADDED

Git LFS Details

  • SHA256: 61b3427ad5d71d9bd3cc6cc6ce3d4a099bc06dc7101c92dd41733e33fa3dad22
  • Pointer size: 132 Bytes
  • Size of remote file: 4.06 MB
samples/macropixel_203.png ADDED

Git LFS Details

  • SHA256: a611e7895f91d612521442c9ac927ca29a900c30e1debe5d420a11edac4ed239
  • Pointer size: 132 Bytes
  • Size of remote file: 3.93 MB
samples/macropixel_257.png ADDED

Git LFS Details

  • SHA256: 2b1a88ed8050a40e58b4df575828cf8aef41723389afe329b35c1b1bb7cf1dc9
  • Pointer size: 132 Bytes
  • Size of remote file: 4.04 MB
samples/macropixel_923.png ADDED

Git LFS Details

  • SHA256: 9c103ebf3bf88e49e2545bcc62727534733ace1d8c91692ee35c7bdd7d767b04
  • Pointer size: 132 Bytes
  • Size of remote file: 4.04 MB