tomrb commited on
Commit
73fee5f
1 Parent(s): f76d2bb
Files changed (5) hide show
  1. .gitignore +1 -1
  2. README.md +24 -2
  3. YOLOv8_TO.ipynb +0 -0
  4. test.ipynb +0 -0
  5. utils/yolo_utils.py +244 -0
.gitignore CHANGED
@@ -1,7 +1,7 @@
1
  # Created by https://www.toptal.com/developers/gitignore/api/python
2
  # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
 
4
- datasets/
5
 
6
  ### Python ###
7
  # Byte-compiled / optimized / DLL files
 
1
  # Created by https://www.toptal.com/developers/gitignore/api/python
2
  # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
 
4
+ /datasets/
5
 
6
  ### Python ###
7
  # Byte-compiled / optimized / DLL files
README.md CHANGED
@@ -15,8 +15,16 @@ Brief description of what the project does and the problem it solves. Include a
15
  ## Reference
16
  This code aims to reproduce the results presented in the research article:
17
 
18
- > Author(s). (Year). Title. *Journal*, Volume(Issue), Pages. DOI
19
-
 
 
 
 
 
 
 
 
20
  ## Installation
21
 
22
  ### Prerequisites
@@ -32,3 +40,17 @@ pip install -e .
32
  ## Datasets
33
  Links to the dataset on HuggingFace:
34
  - [YOLOv8-TO_Data](https://huggingface.co/datasets/tomrb/yolov8to_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  ## Reference
16
  This code aims to reproduce the results presented in the research article:
17
 
18
+ ```bibtex
19
+ @misc{rochefortbeaudoin2024density,
20
+ title={From Density to Geometry: YOLOv8 Instance Segmentation for Reverse Engineering of Optimized Structures},
21
+ author={Thomas Rochefort-Beaudoin and Aurelian Vadean and Sofiane Achiche and Niels Aage},
22
+ year={2024},
23
+ eprint={2404.18763},
24
+ archivePrefix={arXiv},
25
+ primaryClass={cs.CV}
26
+ }
27
+ ```
28
  ## Installation
29
 
30
  ### Prerequisites
 
40
  ## Datasets
41
  Links to the dataset on HuggingFace:
42
  - [YOLOv8-TO_Data](https://huggingface.co/datasets/tomrb/yolov8to_data)
43
+
44
+ The Huggingface dataset contains the following datasets (see paper for details):
45
+ - MMC
46
+ - MMC-random
47
+ - SIMP
48
+ - SIMP_5%
49
+ - OOD
50
+
51
+
52
+ If you want to use one of the linked datasets, please unzip it inside of the datasets folder. Training labels are provided for the MMC and MMC-random data. To train on the data, please update the data.yaml file with the correct path to the dataset.
53
+ ```yaml
54
+ path: # dataset root dir
55
+ ```
56
+
YOLOv8_TO.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
test.ipynb DELETED
The diff for this file is too large to render. See raw diff
 
utils/yolo_utils.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ import torch.nn.functional as F
4
+ import torch.nn as nn
5
+
6
+
7
+ class CustomTverskyLoss(nn.Module):
8
+ def __init__(self, alpha=0.1, beta=0.9, size_average=True):
9
+ super(CustomTverskyLoss, self).__init__()
10
+ self.alpha = alpha
11
+ self.beta = beta
12
+ self.size_average = size_average
13
+
14
+ def forward(self, inputs, targets, smooth=1):
15
+ # If your model contains a sigmoid or equivalent activation layer, comment this line
16
+ # inputs = F.sigmoid(inputs)
17
+
18
+ # Check if the input tensors are of expected shape
19
+ if inputs.shape != targets.shape:
20
+ raise ValueError("Shape mismatch: inputs and targets must have the same shape")
21
+
22
+ # Compute Tversky loss for each sample in the batch
23
+ tversky_loss_values = []
24
+ for input_sample, target_sample in zip(inputs, targets):
25
+ # Flatten tensors for each sample
26
+ input_sample = input_sample.view(-1)
27
+ target_sample = target_sample.view(-1)
28
+
29
+ # Calculate the true positives, false positives, and false negatives
30
+ true_positives = (input_sample * target_sample).sum()
31
+ false_positives = (input_sample * (1 - target_sample)).sum()
32
+ false_negatives = ((1 - input_sample) * target_sample).sum()
33
+
34
+ # Compute the Tversky index for each sample
35
+ tversky_index = (true_positives + smooth) / (true_positives + self.alpha * false_positives + self.beta * false_negatives + smooth)
36
+
37
+ tversky_loss_values.append(1 - tversky_index)
38
+
39
+ # Convert list of Tversky loss values to a tensor
40
+ tversky_loss_values = torch.stack(tversky_loss_values)
41
+
42
+ # If you want the average loss over the batch to be returned
43
+ if self.size_average:
44
+ return tversky_loss_values.mean()
45
+ else:
46
+ # If you want individual losses for each sample in the batch
47
+ return tversky_loss_values
48
+
49
+ class CustomDiceLoss(nn.Module):
50
+ def __init__(self, weight=None, size_average=True):
51
+ super(CustomDiceLoss, self).__init__()
52
+ self.size_average = size_average
53
+ def forward(self, inputs, targets, smooth=1):
54
+
55
+ # If your model contains a sigmoid or equivalent activation layer, comment this line
56
+ #inputs = F.sigmoid(inputs)
57
+
58
+ # Check if the input tensors are of expected shape
59
+ if inputs.shape != targets.shape:
60
+ raise ValueError("Shape mismatch: inputs and targets must have the same shape")
61
+
62
+ # Compute Dice loss for each sample in the batch
63
+ dice_loss_values = []
64
+ for input_sample, target_sample in zip(inputs, targets):
65
+
66
+ # Flatten tensors for each sample
67
+ input_sample = input_sample.view(-1)
68
+ target_sample = target_sample.view(-1)
69
+
70
+ intersection = (input_sample * target_sample).sum()
71
+ dice = (2. * intersection + smooth) / (input_sample.sum() + target_sample.sum() + smooth)
72
+
73
+ dice_loss_values.append(1 - dice)
74
+
75
+ # Convert list of Dice loss values to a tensor
76
+ dice_loss_values = torch.stack(dice_loss_values)
77
+
78
+ # If you want the average loss over the batch to be returned
79
+ if self.size_average:
80
+ return dice_loss_values.mean()
81
+ else:
82
+ # If you want individual losses for each sample in the batch
83
+ return dice_loss_values
84
+
85
+ def smooth_heaviside(phi, alpha, epsilon):
86
+ # Scale and shift phi for the sigmoid function
87
+ scaled_phi = (phi - alpha) / epsilon
88
+
89
+ # Apply the sigmoid function
90
+ H = torch.sigmoid(scaled_phi)
91
+
92
+ return H
93
+ def calc_Phi(variable, LSgrid):
94
+ device = variable.device # Get the device of the variable
95
+
96
+ x0 = variable[0]
97
+ y0 = variable[1]
98
+ L = variable[2]
99
+ t = variable[3] # Constant thickness
100
+ angle = variable[4]
101
+
102
+ # Rotation
103
+ st = torch.sin(angle)
104
+ ct = torch.cos(angle)
105
+ x1 = ct * (LSgrid[0][:, None].to(device) - x0) + st * (LSgrid[1][:, None].to(device) - y0)
106
+ y1 = -st * (LSgrid[0][:, None].to(device) - x0) + ct * (LSgrid[1][:, None].to(device) - y0)
107
+
108
+ # Regularized hyperellipse equation
109
+ a = L / 2 # Semi-major axis
110
+ b = t / 2 # Constant semi-minor axis
111
+ small_constant = 1e-9 # To avoid division by zero
112
+ temp = ((x1 / (a + small_constant))**6) + ((y1 / (b + small_constant))**6)
113
+
114
+ # # Ensuring the hyperellipse shape
115
+ allPhi = 1 - (temp + small_constant)**(1/6)
116
+
117
+ # # Call Heaviside function with allPhi
118
+ alpha = torch.tensor(0.0, device=device, dtype=torch.float32)
119
+ epsilon = torch.tensor(0.001, device=device, dtype=torch.float32)
120
+ H_phi = smooth_heaviside(allPhi, alpha, epsilon)
121
+ return allPhi, H_phi
122
+
123
+
124
+
125
+ # utils.py
126
+
127
+ import torch
128
+ import numpy as np
129
+ from PIL import Image
130
+ import matplotlib.pyplot as plt
131
+ from matplotlib.colors import TwoSlopeNorm
132
+
133
+ def preprocess_image(image_path, threshold_value=0.9, upscale=False, upscale_factor=2.0):
134
+ image = Image.open(image_path).convert('L')
135
+ image = image.point(lambda x: 255 if x > threshold_value * 255 else 0, '1')
136
+
137
+ if upscale:
138
+ image = image.resize(
139
+ (int(image.width * upscale_factor), int(image.height * upscale_factor)),
140
+ resample=Image.BICUBIC
141
+ )
142
+
143
+ return image
144
+
145
+ def run_model(model, image, conf=0.05, iou=0.5, imgsz=640):
146
+ results = model(image, conf=conf, iou=iou, imgsz=imgsz)
147
+ return results
148
+
149
+ def save_results(results, filename='results.jpg'):
150
+ for r in results:
151
+ im_array = r.plot(boxes=True, labels=False, line_width=1)
152
+ im = Image.fromarray(im_array[..., ::-1])
153
+ im.save(filename)
154
+
155
+ def process_results(results, input_image):
156
+ diceloss = CustomDiceLoss()
157
+ tverskyloss = CustomTverskyLoss()
158
+
159
+ prediction_tensor = results[0].regression_preds.to('cpu').detach()
160
+ input_image_array = np.array(input_image.convert('L'))
161
+ input_image_array_tensor = torch.tensor(input_image_array) / 255.0
162
+ input_image_array_tensor = 1.0 - input_image_array_tensor
163
+ input_image_array_tensor = torch.flip(input_image_array_tensor, [0])
164
+
165
+ for r in results:
166
+ im_array = r.plot(boxes=True, labels=False, line_width=1)
167
+ seg_result = Image.fromarray(im_array[..., ::-1])
168
+
169
+ DH = input_image_array.shape[0] / min(input_image_array.shape[1], input_image_array.shape[0])
170
+ DW = input_image_array.shape[1] / min(input_image_array.shape[1], input_image_array.shape[0])
171
+ nelx = input_image_array.shape[1] - 1
172
+ nely = input_image_array.shape[0] - 1
173
+
174
+ x, y = torch.meshgrid(torch.linspace(0, DW, nelx+1), torch.linspace(0, DH, nely+1))
175
+ LSgrid = torch.stack((x.flatten(), y.flatten()), dim=0)
176
+
177
+ pred_bboxes = results[0].boxes.xyxyn.to('cpu').detach()
178
+ constant_tensor_02 = torch.full((pred_bboxes.shape[0],), 0.2)
179
+ constant_tensor_00 = torch.full((pred_bboxes.shape[0],), 0.001)
180
+
181
+ xmax = torch.stack([pred_bboxes[:,2]*(DW*1.0), pred_bboxes[:,3]*(DH*1.0), pred_bboxes[:,2]*(DW*1.0), pred_bboxes[:,3]*(DH*1.0), constant_tensor_02], dim=1)
182
+ xmin = torch.stack([pred_bboxes[:,0]*(DW*1.0), pred_bboxes[:,1]*(DH*1.0), pred_bboxes[:,0]*(DW*1.0), pred_bboxes[:,1]*(DH*1.0), constant_tensor_00], dim=1)
183
+
184
+ unnormalized_preds = prediction_tensor * (xmax - xmin) + xmin
185
+
186
+ x_center = (unnormalized_preds[:, 0] + unnormalized_preds[:, 2]) / 2
187
+ y_center = (unnormalized_preds[:, 1] + unnormalized_preds[:, 3]) / 2
188
+
189
+ L = torch.sqrt((unnormalized_preds[:, 0] - unnormalized_preds[:, 2])**2 +
190
+ (unnormalized_preds[:, 1] - unnormalized_preds[:, 3])**2)
191
+
192
+ L = L + 1e-4
193
+ t_1 = unnormalized_preds[:, 4]
194
+
195
+ epsilon = 1e-10
196
+ y_diff = unnormalized_preds[:, 3] - unnormalized_preds[:, 1] + epsilon
197
+ x_diff = unnormalized_preds[:, 2] - unnormalized_preds[:, 0] + epsilon
198
+ theta = torch.atan2(y_diff, x_diff)
199
+
200
+ formatted_variables = torch.cat((x_center.unsqueeze(1),
201
+ y_center.unsqueeze(1),
202
+ L.unsqueeze(1),
203
+ t_1.unsqueeze(1),
204
+ theta.unsqueeze(1)), dim=1)
205
+
206
+ pred_Phi, pred_H = calc_Phi(formatted_variables.T, LSgrid)
207
+
208
+ sum_pred_H = torch.sum(pred_H.detach().cpu(), dim=1)
209
+ sum_pred_H[sum_pred_H > 1] = 1
210
+
211
+ final_H = np.flipud(sum_pred_H.detach().numpy().reshape((nely+1, nelx+1), order='F'))
212
+
213
+ dice_loss = diceloss(torch.tensor(final_H.copy()), input_image_array_tensor)
214
+ tversky_loss = tverskyloss(torch.tensor(final_H.copy()), input_image_array_tensor)
215
+
216
+ return input_image_array_tensor, seg_result, pred_Phi, sum_pred_H, final_H, dice_loss, tversky_loss
217
+
218
+ def plot_results(input_image_array_tensor, seg_result, pred_Phi, sum_pred_H, final_H, dice_loss, tversky_loss, filename='combined_plots.png'):
219
+ nelx = input_image_array_tensor.shape[1] - 1
220
+ nely = input_image_array_tensor.shape[0] - 1
221
+ fig, axes = plt.subplots(2, 2, figsize=(8, 8))
222
+
223
+ axes[0, 0].imshow(input_image_array_tensor.squeeze(), origin='lower', cmap='gray_r')
224
+ axes[0, 0].set_title('Input Image')
225
+ axes[0, 0].axis('on')
226
+
227
+ axes[0, 1].imshow(seg_result)
228
+ axes[0, 1].set_title('Segmentation Result')
229
+ axes[0, 1].axis('off')
230
+
231
+ render_colors1 = ['yellow', 'g', 'r', 'c', 'm', 'y', 'black', 'orange', 'pink', 'cyan', 'slategrey', 'wheat', 'purple', 'mediumturquoise', 'darkviolet', 'orangered']
232
+ for i, color in zip(range(0, pred_Phi.shape[1]), render_colors1*100):
233
+ axes[1, 1].contourf(np.flipud(pred_Phi[:, i].numpy().reshape((nely+1, nelx+1), order='F')), [0, 1], colors=color)
234
+ axes[1, 1].set_title('Prediction contours')
235
+ axes[1, 1].set_aspect('equal')
236
+
237
+ axes[1, 0].imshow(np.flipud(sum_pred_H.detach().numpy().reshape((nely+1, nelx+1), order='F')), origin='lower', cmap='gray_r')
238
+ axes[1, 0].set_title('Prediction Projection')
239
+
240
+ plt.subplots_adjust(hspace=0.3, wspace=0.01)
241
+
242
+ plt.figtext(0.5, 0.05, f'Dice Loss: {dice_loss.item():.4f}', ha='center', fontsize=16)
243
+
244
+ fig.savefig(filename, dpi=600)