openmlcore 1.1.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openmlcore-1.1.0 → openmlcore-2.0.0}/PKG-INFO +6 -8
- {openmlcore-1.1.0 → openmlcore-2.0.0}/README.md +5 -7
- {openmlcore-1.1.0 → openmlcore-2.0.0}/pyproject.toml +1 -1
- openmlcore-2.0.0/src/openmlcore/base/data3d.py +174 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/loss/loss.py +67 -75
- openmlcore-2.0.0/src/openmlcore/loss/loss3d.py +92 -0
- openmlcore-2.0.0/src/openmlcore/metrics/metrics3d.py +119 -0
- openmlcore-2.0.0/src/openmlcore/models/MobileUnetR.py +329 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/base_model.py +54 -5
- openmlcore-2.0.0/src/openmlcore/models/lib/tnet.py +43 -0
- openmlcore-2.0.0/src/openmlcore/models/pointNetPP.py +209 -0
- openmlcore-2.0.0/src/openmlcore/models/pointnet.py +62 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/unetpp.py +1 -1
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/PKG-INFO +6 -8
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/SOURCES.txt +7 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/tests/test_loss.py +6 -12
- {openmlcore-1.1.0 → openmlcore-2.0.0}/setup.cfg +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/__init__.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/base/data.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/metrics/metrics.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/UNets.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/attUnets.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/lib/attention.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/lib/convolutions.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/segformer.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/dependency_links.txt +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/requires.txt +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/top_level.txt +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/tests/test_base_model.py +0 -0
- {openmlcore-1.1.0 → openmlcore-2.0.0}/tests/test_main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openmlcore
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: OpenMLCore is an open source framework (wrapper which simplify the use of PyTorch)
|
|
5
5
|
Author-email: Doyez Sébastien <doyez.sebastien34090@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://sebastien-doyez2812.github.io/openMLCore.github.io/
|
|
@@ -25,23 +25,21 @@ Today's computer vision ecosystem is heavily fragmented. If you want YOLO, Ultra
|
|
|
25
25
|
OpenML bridges this gap by bringing a clean, Hugging Face-inspired Developer Experience (DX) to advanced computer vision tasks—from classical models to SOTA transformers and parameter-efficient fine-tuning.
|
|
26
26
|
## Supported Architectures:
|
|
27
27
|
|
|
28
|
-
- Classic & Deep Segmentation: U-Net, Attention U-Net, UNet
|
|
28
|
+
- Classic & Deep Segmentation: U-Net, Attention U-Net, UNet++
|
|
29
29
|
|
|
30
|
-
- Advanced Transformers:
|
|
30
|
+
- Advanced Transformers: MobileUnetR, SegFormer
|
|
31
31
|
|
|
32
|
-
- SOTA & Edge: Vision Transformers, SAM 2 adaptations via efficient Low-Rank Adaptation (LoRA)
|
|
32
|
+
- SOTA & Edge: Vision Transformers, SAM 2 adaptations via efficient Low-Rank Adaptation (LoRA) (Coming soon)
|
|
33
33
|
## Key features:
|
|
34
34
|
|
|
35
35
|
Unified Object-Oriented API: Every model inherits from a clean ModelBase class supporting standard .train(), .eval(), and .help() workflows.
|
|
36
36
|
|
|
37
|
-
Native LoRA Wrappers: Fine-tune massive vision models and transformers on modest hardware (like a single RTX 3070) without blowing up your VRAM.
|
|
37
|
+
Native LoRA Wrappers: Fine-tune massive vision models and transformers on modest hardware (like a single RTX 3070) without blowing up your VRAM. (Coming soonn)
|
|
38
38
|
|
|
39
|
-
Hugging Face
|
|
39
|
+
Hugging Face pretrained weights availablle
|
|
40
40
|
|
|
41
41
|
MLflow Tracking Out-of-the-Box: Keep tabs on your losses, metrics, and hyperparameter logs without writing boilerplate monitoring code.
|
|
42
42
|
|
|
43
|
-
Interactive Help System: Running model.help() directly opens up the exact architectural documentation page on the web.
|
|
44
|
-
|
|
45
43
|
## Contributions:
|
|
46
44
|
|
|
47
45
|
Contributions, feature requests, and bug reports are welcome! Feel free to open an Issue or submit a Pull Request on GitHub.
|
|
@@ -7,23 +7,21 @@ Today's computer vision ecosystem is heavily fragmented. If you want YOLO, Ultra
|
|
|
7
7
|
OpenML bridges this gap by bringing a clean, Hugging Face-inspired Developer Experience (DX) to advanced computer vision tasks—from classical models to SOTA transformers and parameter-efficient fine-tuning.
|
|
8
8
|
## Supported Architectures:
|
|
9
9
|
|
|
10
|
-
- Classic & Deep Segmentation: U-Net, Attention U-Net, UNet
|
|
10
|
+
- Classic & Deep Segmentation: U-Net, Attention U-Net, UNet++
|
|
11
11
|
|
|
12
|
-
- Advanced Transformers:
|
|
12
|
+
- Advanced Transformers: MobileUnetR, SegFormer
|
|
13
13
|
|
|
14
|
-
- SOTA & Edge: Vision Transformers, SAM 2 adaptations via efficient Low-Rank Adaptation (LoRA)
|
|
14
|
+
- SOTA & Edge: Vision Transformers, SAM 2 adaptations via efficient Low-Rank Adaptation (LoRA) (Coming soon)
|
|
15
15
|
## Key features:
|
|
16
16
|
|
|
17
17
|
Unified Object-Oriented API: Every model inherits from a clean ModelBase class supporting standard .train(), .eval(), and .help() workflows.
|
|
18
18
|
|
|
19
|
-
Native LoRA Wrappers: Fine-tune massive vision models and transformers on modest hardware (like a single RTX 3070) without blowing up your VRAM.
|
|
19
|
+
Native LoRA Wrappers: Fine-tune massive vision models and transformers on modest hardware (like a single RTX 3070) without blowing up your VRAM. (Coming soonn)
|
|
20
20
|
|
|
21
|
-
Hugging Face
|
|
21
|
+
Hugging Face pretrained weights availablle
|
|
22
22
|
|
|
23
23
|
MLflow Tracking Out-of-the-Box: Keep tabs on your losses, metrics, and hyperparameter logs without writing boilerplate monitoring code.
|
|
24
24
|
|
|
25
|
-
Interactive Help System: Running model.help() directly opens up the exact architectural documentation page on the web.
|
|
26
|
-
|
|
27
25
|
## Contributions:
|
|
28
26
|
|
|
29
27
|
Contributions, feature requests, and bug reports are welcome! Feel free to open an Issue or submit a Pull Request on GitHub.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: Seb Doyez
|
|
3
|
+
|
|
4
|
+
3D dataset should be a folder, where inside we can find subfolder corresponding to a scene
|
|
5
|
+
1 scene = 1 subfolder
|
|
6
|
+
Inside a subfolder, there qre .pcd and ..._label.txt
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
from torch.utils.data import Dataset
|
|
13
|
+
import numpy as np
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
class Segmentation3dDataset(Dataset):
|
|
17
|
+
def __init__(self, dataset_path, num_points = 2048, split="train"):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.num_points = num_points
|
|
20
|
+
self.dataset_path = dataset_path
|
|
21
|
+
|
|
22
|
+
self.scene_dirs = self._get_object_paths(self.dataset_path) # Only treat the airplane!
|
|
23
|
+
total_data = len(self.scene_dirs)
|
|
24
|
+
split_idx = int(total_data * 0.8)
|
|
25
|
+
if split.lower() == "train":
|
|
26
|
+
self.scene_dirs = self.scene_dirs[:split_idx]
|
|
27
|
+
else:
|
|
28
|
+
self.scene_dirs = self.scene_dirs[split_idx:]
|
|
29
|
+
|
|
30
|
+
def _get_object_paths(self, dataset_path):
|
|
31
|
+
# Dataset should be like:
|
|
32
|
+
# subfolders, and inside, you will find a .pcd and the _label.txt
|
|
33
|
+
# .pcd is the point cloud and _label.txt is the label
|
|
34
|
+
# Label goes from 1 to n ( number of classes)
|
|
35
|
+
|
|
36
|
+
object_paths = []
|
|
37
|
+
for dirpath, dirnames, filenames in os.walk(dataset_path):
|
|
38
|
+
for file in filenames:
|
|
39
|
+
if file.endswith(".pcd"):
|
|
40
|
+
base_path = os.path.join(dirpath, file.replace(".pcd", ""))
|
|
41
|
+
if os.path.exists(base_path +"_label.txt"):
|
|
42
|
+
object_paths.append(base_path)
|
|
43
|
+
return object_paths
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_scene_data(self, scene_base_path):
|
|
47
|
+
label_path = scene_base_path + "_label.txt"
|
|
48
|
+
pcd_path = scene_base_path + ".pcd"
|
|
49
|
+
|
|
50
|
+
if not os.path.exists(label_path):
|
|
51
|
+
raise FileNotFoundError(f"No labels found for {label_path}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if not os.path.exists(pcd_path):
|
|
55
|
+
raise FileNotFoundError(f"No PCD found for {pcd_path}")
|
|
56
|
+
|
|
57
|
+
with open(pcd_path, 'rb') as f:
|
|
58
|
+
header = []
|
|
59
|
+
data_type = 'ascii'
|
|
60
|
+
num_pts = 0
|
|
61
|
+
fields = []
|
|
62
|
+
sizes = []
|
|
63
|
+
types = []
|
|
64
|
+
|
|
65
|
+
# Skip the header
|
|
66
|
+
while True:
|
|
67
|
+
line_bytes = f.readline()
|
|
68
|
+
if not line_bytes:
|
|
69
|
+
break
|
|
70
|
+
line = line_bytes.decode('latin-1')
|
|
71
|
+
header.append(line)
|
|
72
|
+
if line.startswith("DATA"):
|
|
73
|
+
parts = line.strip().split()
|
|
74
|
+
if len(parts) > 1:
|
|
75
|
+
data_type = parts[1]
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
for line in header:
|
|
79
|
+
if line.startswith("POINTS"):
|
|
80
|
+
num_pts = int(line.strip().split()[1])
|
|
81
|
+
elif line.startswith("WIDTH") and num_pts == 0:
|
|
82
|
+
num_pts = int(line.strip().split()[1])
|
|
83
|
+
elif line.startswith("FIELDS"):
|
|
84
|
+
fields = line.strip().split()[1:]
|
|
85
|
+
elif line.startswith("SIZE"):
|
|
86
|
+
sizes = [int(s) for s in line.strip().split()[1:]]
|
|
87
|
+
elif line.startswith("TYPE"):
|
|
88
|
+
types = line.strip().split()[1:]
|
|
89
|
+
|
|
90
|
+
# Loading the data:
|
|
91
|
+
if data_type.lower() == 'ascii':
|
|
92
|
+
points_data = np.loadtxt(f, encoding="latin-1")
|
|
93
|
+
points = points_data[:, :3]
|
|
94
|
+
else:
|
|
95
|
+
binary_data = f.read()
|
|
96
|
+
try:
|
|
97
|
+
dt_list = []
|
|
98
|
+
for field, size, t in zip(fields, sizes, types):
|
|
99
|
+
if t == 'F':
|
|
100
|
+
np_t = np.float32 if size == 4 else np.float64
|
|
101
|
+
elif t in ['I', 'U']:
|
|
102
|
+
np_t = np.int32 if size == 4 else (np.int16 if size == 2 else np.int8)
|
|
103
|
+
else:
|
|
104
|
+
np_t = np.float32
|
|
105
|
+
dt_list.append((field, np_t))
|
|
106
|
+
|
|
107
|
+
dt = np.dtype(dt_list)
|
|
108
|
+
data = np.frombuffer(binary_data, dtype=dt, count=num_pts)
|
|
109
|
+
points = np.stack([data['x'], data['y'], data['z']], axis=-1)
|
|
110
|
+
except Exception:
|
|
111
|
+
raw_data = np.frombuffer(binary_data, dtype=np.float32)
|
|
112
|
+
stride = len(raw_data) // num_pts
|
|
113
|
+
points = raw_data.reshape(num_pts, stride)[:, :3]
|
|
114
|
+
|
|
115
|
+
# Loading the labels
|
|
116
|
+
labels = np.loadtxt(label_path, dtype=np.int64, encoding="latin-1")
|
|
117
|
+
|
|
118
|
+
assert len(points) == len(labels), f"[-] Error, number of points mismatch: {len(points)} pts vs {len(labels)} labels."
|
|
119
|
+
|
|
120
|
+
return points, labels
|
|
121
|
+
|
|
122
|
+
def _normalize_point_cloud(self, points):
|
|
123
|
+
centroid = np.mean(points, axis=0)
|
|
124
|
+
points_centered = points - centroid
|
|
125
|
+
|
|
126
|
+
max_dist = np.max(np.sqrt(np.sum(points_centered ** 2, axis=1)))
|
|
127
|
+
|
|
128
|
+
if max_dist > 0:
|
|
129
|
+
points_normalized = points_centered / max_dist
|
|
130
|
+
else:
|
|
131
|
+
points_normalized = points_centered
|
|
132
|
+
|
|
133
|
+
return points_normalized
|
|
134
|
+
|
|
135
|
+
def __len__(self):
|
|
136
|
+
return len(self.scene_dirs)
|
|
137
|
+
|
|
138
|
+
def __getitem__(self, idx):
|
|
139
|
+
scene_path = self.scene_dirs[idx]
|
|
140
|
+
points, labels = self._load_scene_data(scene_path)
|
|
141
|
+
|
|
142
|
+
totals_pts = points.shape[0]
|
|
143
|
+
if totals_pts >= self.num_points:
|
|
144
|
+
# Block_sampling:
|
|
145
|
+
center_idx = np.random.choice(points.shape[0])
|
|
146
|
+
center = points[center_idx]
|
|
147
|
+
|
|
148
|
+
radius = 1.5
|
|
149
|
+
distances = np.sqrt(np.sum((points - center) ** 2, axis = 1))
|
|
150
|
+
|
|
151
|
+
mask = distances < radius
|
|
152
|
+
block_points = points[mask]
|
|
153
|
+
block_labels = labels[mask]
|
|
154
|
+
|
|
155
|
+
total_pts = block_points.shape[0]
|
|
156
|
+
if total_pts >= self.num_points:
|
|
157
|
+
choice = np.random.choice(total_pts, self.num_points, replace=False)
|
|
158
|
+
else:
|
|
159
|
+
choice = np.random.choice(total_pts, self.num_points, replace=True)
|
|
160
|
+
|
|
161
|
+
points_sampled = block_points[choice]
|
|
162
|
+
labels_sampled = block_labels[choice]
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
choice = np.random.choice(totals_pts, self.num_points, replace=True)
|
|
166
|
+
points_sampled = points[choice]
|
|
167
|
+
labels_sampled = labels[choice]
|
|
168
|
+
|
|
169
|
+
points_normalized = self._normalize_point_cloud(points_sampled)
|
|
170
|
+
points_tensor = torch.tensor(points_normalized, dtype=torch.float32).transpose(1, 0)
|
|
171
|
+
labels_tensor = torch.tensor(labels_sampled - 1, dtype=torch.long)
|
|
172
|
+
|
|
173
|
+
return points_tensor, labels_tensor
|
|
174
|
+
|
|
@@ -7,12 +7,16 @@ class BaseLoss(nn.Module):
|
|
|
7
7
|
def __init__(self, name: str = "BaseLoss", doc_url: str = ""):
|
|
8
8
|
super().__init__()
|
|
9
9
|
self.name = name
|
|
10
|
+
self.type = "segmetation2d"
|
|
10
11
|
self.smooth = 1e-6
|
|
11
12
|
self.doc_url = doc_url
|
|
12
13
|
|
|
13
14
|
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
14
15
|
raise NotImplementedError("Subclasses should implement this method.")
|
|
15
16
|
|
|
17
|
+
def get_type(self):
|
|
18
|
+
return self.type
|
|
19
|
+
|
|
16
20
|
def documentation(self) -> str:
|
|
17
21
|
print(f"Documentation for {self.name} is available here:\n{self.doc_url}")
|
|
18
22
|
try:
|
|
@@ -22,13 +26,16 @@ class BaseLoss(nn.Module):
|
|
|
22
26
|
print(f"[-] {e}")
|
|
23
27
|
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
# Class Loss Functions
|
|
27
|
-
|
|
29
|
+
################################################
|
|
30
|
+
# Class Loss Functions for 2d Segmentation #
|
|
31
|
+
################################################
|
|
28
32
|
|
|
29
33
|
class BCEWithLogitsLoss(BaseLoss):
|
|
30
34
|
def __init__(self, weights=None):
|
|
31
35
|
super().__init__(name="BCEWithLogitsLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html")
|
|
36
|
+
if weights is None:
|
|
37
|
+
raise ValueError("weights need to be set!")
|
|
38
|
+
|
|
32
39
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
33
40
|
|
|
34
41
|
pos_weights = None
|
|
@@ -48,6 +55,9 @@ class BCEWithLogitsLoss(BaseLoss):
|
|
|
48
55
|
class DiceLoss(BaseLoss):
|
|
49
56
|
def __init__(self, weights = None):
|
|
50
57
|
super().__init__(name="DiceLoss", doc_url="https://arxiv.org/abs/1606.04797")
|
|
58
|
+
if weights is None:
|
|
59
|
+
raise ValueError("weights need to be set!")
|
|
60
|
+
|
|
51
61
|
self.weights = weights
|
|
52
62
|
|
|
53
63
|
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
@@ -71,76 +81,67 @@ class DiceLoss(BaseLoss):
|
|
|
71
81
|
|
|
72
82
|
return dice_loss.sum(dim=1).mean()
|
|
73
83
|
return dice_loss.mean()
|
|
74
|
-
|
|
75
|
-
# TODO:
|
|
76
|
-
# 1 Hot encoding loss
|
|
77
|
-
#
|
|
78
|
-
|
|
79
84
|
|
|
80
85
|
class MeanSquaredErrorLoss(BaseLoss):
|
|
81
|
-
def __init__(self,
|
|
86
|
+
def __init__(self, weights = None):
|
|
82
87
|
super().__init__(name="MeanSquaredErrorLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html")
|
|
83
|
-
|
|
88
|
+
if weights is None:
|
|
89
|
+
raise ValueError("weights need to be set!")
|
|
90
|
+
|
|
91
|
+
self.weights = weights
|
|
84
92
|
|
|
85
93
|
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
86
|
-
|
|
87
|
-
probs
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
mse = (probs - targets_one_hot) ** 2
|
|
91
|
-
mse = mse * valid_mask
|
|
92
|
-
|
|
93
|
-
num_valids = valid_mask.sum() * num_classes
|
|
94
|
-
return mse.sum() / torch.clamp(num_valids, min=1.0)
|
|
94
|
+
probs = torch.sigmoid(outputs)
|
|
95
|
+
probs = probs.view (probs.size(0) , probs.size(1), -1)
|
|
96
|
+
targets = targets.view(probs.size(0), probs.size(1), -1)
|
|
95
97
|
|
|
98
|
+
mse = (probs - targets) ** 2
|
|
99
|
+
weighted_mse = 0
|
|
100
|
+
for idx, weight in enumerate(self.weights):
|
|
101
|
+
weighted_mse+= weight * mse[:, idx, :]
|
|
96
102
|
|
|
103
|
+
return weighted_mse.mean()
|
|
97
104
|
|
|
98
105
|
class FocalLoss(BaseLoss):
|
|
99
|
-
def __init__(self, alpha=0.25, gamma=2.0,
|
|
106
|
+
def __init__(self, alpha=0.25, gamma=2.0, weights = None):
|
|
100
107
|
super().__init__(name="FocalLoss", doc_url="https://arxiv.org/abs/1708.02002")
|
|
108
|
+
if weights is None:
|
|
109
|
+
raise ValueError("weights need to be set!")
|
|
110
|
+
|
|
101
111
|
self.alpha = alpha
|
|
102
112
|
self.gamma = gamma
|
|
103
|
-
self.
|
|
113
|
+
self.weights = weights
|
|
104
114
|
|
|
105
115
|
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
bce_loss = F.binary_cross_entropy_with_logits(
|
|
116
|
+
probs = outputs.view(outputs.size(0) , outputs.size(1), -1)
|
|
117
|
+
targets = targets.view(outputs.size(0), outputs.size(1), -1)
|
|
118
|
+
|
|
119
|
+
bce_loss = F.binary_cross_entropy_with_logits(probs, targets, reduction='none') # Apply a sigmoid inside...
|
|
110
120
|
pt = torch.exp(-bce_loss)
|
|
111
121
|
focal_loss = self.alpha * ((1 - pt) ** self.gamma) * bce_loss
|
|
112
|
-
focal_loss = focal_loss * valid_mask
|
|
113
|
-
num_valids = valid_mask.sum() * num_classes
|
|
114
|
-
return focal_loss.sum() / torch.clamp(num_valids, min=1.0)
|
|
115
|
-
|
|
116
|
-
class MeanSquaredErrorLoss(BaseLoss):
|
|
117
|
-
def __init__(self, index_ignore=255):
|
|
118
|
-
super().__init__(name="MeanSquaredErrorLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html")
|
|
119
|
-
self.index_ignore = index_ignore
|
|
120
|
-
|
|
121
|
-
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
122
|
-
num_classes = outputs.shape[1]
|
|
123
|
-
probs = F.softmax(outputs, dim=1)
|
|
124
|
-
targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
|
|
125
|
-
|
|
126
|
-
mse = (probs - targets_one_hot) ** 2
|
|
127
|
-
mse = mse * valid_mask
|
|
128
|
-
|
|
129
|
-
num_valids = valid_mask.sum() * num_classes
|
|
130
|
-
return mse.sum() / torch.clamp(num_valids, min=1.0)
|
|
131
122
|
|
|
123
|
+
final_loss = 0
|
|
124
|
+
for idx, weight in enumerate(self.weights):
|
|
125
|
+
final_loss += weight * focal_loss[:, idx, :]
|
|
126
|
+
return final_loss.mean()
|
|
127
|
+
|
|
132
128
|
class HuberLoss(BaseLoss):
|
|
133
|
-
def __init__(self, delta=1.0,
|
|
129
|
+
def __init__(self, delta=1.0, weights = None):
|
|
134
130
|
super().__init__(name="HuberLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.HuberLoss.html")
|
|
131
|
+
if weights is None:
|
|
132
|
+
raise ValueError("weights need to be set!")
|
|
133
|
+
self.weights = weights
|
|
135
134
|
self.delta = delta
|
|
136
|
-
self.index_ignore = index_ignore
|
|
137
135
|
|
|
138
136
|
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
139
|
-
num_classes = outputs.shape[1]
|
|
140
|
-
probs = F.softmax(outputs, dim=1)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
# num_classes = outputs.shape[1]
|
|
138
|
+
# probs = F.softmax(outputs, dim=1)
|
|
139
|
+
|
|
140
|
+
probs = torch.sigmoid(outputs)
|
|
141
|
+
probs = probs.view (probs.size(0) , probs.size(1), -1)
|
|
142
|
+
targets = targets.view(probs.size(0), probs.size(1), -1)
|
|
143
|
+
|
|
144
|
+
error = probs - targets
|
|
144
145
|
abs_error = torch.abs(error)
|
|
145
146
|
|
|
146
147
|
huber = torch.where(
|
|
@@ -148,30 +149,15 @@ class HuberLoss(BaseLoss):
|
|
|
148
149
|
0.5 * (error ** 2),
|
|
149
150
|
self.delta * (abs_error - 0.5 * self.delta)
|
|
150
151
|
)
|
|
151
|
-
huber = huber * valid_mask
|
|
152
|
-
|
|
153
|
-
num_valids = valid_mask.sum() * num_classes
|
|
154
|
-
return huber.sum() / torch.clamp(num_valids, min=1.0)
|
|
155
|
-
|
|
156
|
-
class KLDivergenceLoss(BaseLoss):
|
|
157
|
-
def __init__(self, index_ignore=255):
|
|
158
|
-
super().__init__(name="KLDivergenceLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.KLDivLoss.html")
|
|
159
|
-
self.eps = 1e-8
|
|
160
|
-
self.index_ignore = index_ignore
|
|
161
|
-
|
|
162
|
-
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
163
|
-
num_classes = outputs.shape[1]
|
|
164
|
-
log_probs = F.log_softmax(outputs, dim=1)
|
|
165
|
-
targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
|
|
166
|
-
|
|
167
|
-
# KL-Div = p(x) * (log(p(x)) - log(q(x)))
|
|
168
|
-
# Pour une distribution One-Hot, cela se réduit à : - log(q(x)) sur la classe vraie
|
|
169
|
-
kl = F.kl_div(log_probs, targets_one_hot, reduction='none')
|
|
170
|
-
kl = kl * valid_mask
|
|
171
|
-
|
|
172
|
-
num_valids = valid_mask.sum() * num_classes
|
|
173
|
-
return kl.sum() / torch.clamp(num_valids, min=1.0)
|
|
174
152
|
|
|
153
|
+
if not isinstance(self.weights, torch.Tensor):
|
|
154
|
+
weights_tensor = torch.tensor(self.weights, device=outputs.device, dtype=outputs.dtype)
|
|
155
|
+
else:
|
|
156
|
+
weights_tensor = self.weights.to(outputs.device)
|
|
157
|
+
huber = huber * weights_tensor.view(1, -1, 1)
|
|
158
|
+
|
|
159
|
+
return huber.mean()
|
|
160
|
+
|
|
175
161
|
####################################
|
|
176
162
|
# Custom Loss implementation #
|
|
177
163
|
####################################
|
|
@@ -207,4 +193,10 @@ class CustomLoss(BaseLoss):
|
|
|
207
193
|
for loss_fn, coef in zip(self.loss_fcns, self.coefficients):
|
|
208
194
|
total_loss += coef * loss_fn(outputs, targets)
|
|
209
195
|
return total_loss
|
|
210
|
-
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
################################################
|
|
201
|
+
# Class Loss Functions for 3d Segmentation #
|
|
202
|
+
################################################
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
|
|
5
|
+
class Base3dLoss(nn.Module):
|
|
6
|
+
def __init__(self, name: str= "BaseLoss3d", doc_url:str= ""):
|
|
7
|
+
super().__init__()
|
|
8
|
+
self.name = name
|
|
9
|
+
self.type = "segmentation3d"
|
|
10
|
+
self.smooth = 1e-6
|
|
11
|
+
self.doc_url = doc_url
|
|
12
|
+
|
|
13
|
+
def get_type(self):
|
|
14
|
+
return self.type
|
|
15
|
+
|
|
16
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor):
|
|
17
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
18
|
+
|
|
19
|
+
def documentation(self) -> str:
|
|
20
|
+
print(f"Documentation available here:{self.doc_url}")
|
|
21
|
+
try:
|
|
22
|
+
import webbrowser
|
|
23
|
+
webbrowser.open(self.doc_url)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"[-] {e}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CustomLoss3d(Base3dLoss):
|
|
29
|
+
def __init__(self, loss_fcns: list[Base3dLoss], name: str, coefficients: list[float]):
|
|
30
|
+
assert len(loss_fcns) == len(coefficients), "The number of loss functions must match the number of coefficients."
|
|
31
|
+
super().__init__(name=name, doc_url=None)
|
|
32
|
+
self.loss_fcns = nn.ModuleList(loss_fcns)
|
|
33
|
+
self.coefficients = coefficients
|
|
34
|
+
|
|
35
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
36
|
+
total_loss = 0.0
|
|
37
|
+
for loss_fn, coef in zip(self.loss_fcns, self.coefficients):
|
|
38
|
+
total_loss += coef * loss_fn(outputs, targets)
|
|
39
|
+
return total_loss
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CrossEntropyLoss(Base3dLoss):
|
|
45
|
+
def __init__(self, name = "CrossEntropy", weights = None, doc_url = ""):
|
|
46
|
+
super().__init__(name, doc_url)
|
|
47
|
+
self.weights = weights
|
|
48
|
+
|
|
49
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
50
|
+
|
|
51
|
+
pos_weights = None
|
|
52
|
+
if weights is not None:
|
|
53
|
+
if not isinstance(weights, torch.Tensor):
|
|
54
|
+
pos_weights = torch.tensor(weights, device=device, dtype=torch.float32)
|
|
55
|
+
else:
|
|
56
|
+
pos_weights = weights.to(device)
|
|
57
|
+
|
|
58
|
+
self.loss_fn = nn.CrossEntropyLoss(weight=pos_weights)
|
|
59
|
+
|
|
60
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor):
|
|
61
|
+
return self.loss_fn(outputs, targets.long())
|
|
62
|
+
|
|
63
|
+
class DiceLoss3d(Base3dLoss):
|
|
64
|
+
def __init__(self, weights = None):
|
|
65
|
+
super().__init__(name="DiceLoss3d")
|
|
66
|
+
if weights is None:
|
|
67
|
+
raise ValueError("weights need to be set!")
|
|
68
|
+
|
|
69
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
70
|
+
|
|
71
|
+
if not isinstance(weights, torch.Tensor):
|
|
72
|
+
self.weights = torch.tensor(weights, device=device, dtype=torch.float32)
|
|
73
|
+
else:
|
|
74
|
+
self.weights = weights.to(device)
|
|
75
|
+
|
|
76
|
+
def forward(self, outputs, targets):
|
|
77
|
+
probs = F.softmax(outputs, dim=1)
|
|
78
|
+
B, C, N = probs.shape
|
|
79
|
+
|
|
80
|
+
targets_one_hot = F.one_hot(targets.long(), num_classes=C)
|
|
81
|
+
targets_one_hot = targets_one_hot.permute(0, 2, 1).float()
|
|
82
|
+
|
|
83
|
+
intersection = torch.sum(probs * targets_one_hot, dim=(0, 2))
|
|
84
|
+
union = torch.sum(probs + targets_one_hot, dim=(0, 2))
|
|
85
|
+
|
|
86
|
+
dice_score = (2.0 * intersection) / (self.smooth + union)
|
|
87
|
+
|
|
88
|
+
dice_loss = 0
|
|
89
|
+
for idx_class, weight in enumerate(self.weights):
|
|
90
|
+
dice_loss += weight * (1 - dice_score[idx_class])
|
|
91
|
+
|
|
92
|
+
return dice_loss
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
def iou3d(preds, targets):
|
|
5
|
+
num_batches = preds.shape[0]
|
|
6
|
+
num_classes = preds.shape[1]
|
|
7
|
+
|
|
8
|
+
metric_to_return = [0.0] * num_classes
|
|
9
|
+
valid_batches_per_class = [0] * num_classes
|
|
10
|
+
|
|
11
|
+
for idx_current_batch in range(num_batches):
|
|
12
|
+
for idx_class in range(num_classes):
|
|
13
|
+
pred = preds[idx_current_batch, idx_class]
|
|
14
|
+
gt = targets[idx_current_batch, idx_class]
|
|
15
|
+
|
|
16
|
+
pred_mask = (pred > 0.5)
|
|
17
|
+
gt_mask = (gt > 0.5)
|
|
18
|
+
|
|
19
|
+
intersection = torch.logical_and(pred_mask, gt_mask).sum().item()
|
|
20
|
+
union = torch.logical_or(pred_mask, gt_mask).sum().item()
|
|
21
|
+
|
|
22
|
+
if union > 0:
|
|
23
|
+
iou = intersection / union
|
|
24
|
+
metric_to_return[idx_class] += iou
|
|
25
|
+
valid_batches_per_class[idx_class] += 1
|
|
26
|
+
|
|
27
|
+
final_metric = []
|
|
28
|
+
for i in range(num_classes):
|
|
29
|
+
if valid_batches_per_class[i] > 0:
|
|
30
|
+
final_metric.append(metric_to_return[i] / valid_batches_per_class[i])
|
|
31
|
+
else:
|
|
32
|
+
final_metric.append(float('nan'))
|
|
33
|
+
|
|
34
|
+
return final_metric
|
|
35
|
+
|
|
36
|
+
def accuracy3d(preds, targets):
|
|
37
|
+
num_batches = preds.shape[0]
|
|
38
|
+
num_classes = preds.shape[1]
|
|
39
|
+
|
|
40
|
+
metrics_to_return = [0.0] * num_classes
|
|
41
|
+
valid_batches_per_class = [0] * num_classes
|
|
42
|
+
|
|
43
|
+
for idx_current_batch in range(num_batches):
|
|
44
|
+
pred_batch = preds[idx_current_batch]
|
|
45
|
+
|
|
46
|
+
if targets.ndim == preds.ndim:
|
|
47
|
+
target_batch = targets[idx_current_batch] # Shape: (C, ...)
|
|
48
|
+
else:
|
|
49
|
+
target_indices = targets[idx_current_batch] # Shape: (...)
|
|
50
|
+
|
|
51
|
+
for idx_class in range(num_classes):
|
|
52
|
+
pred = pred_batch[idx_class]
|
|
53
|
+
pred_mask = (pred > 0.5)
|
|
54
|
+
|
|
55
|
+
if targets.ndim == preds.ndim:
|
|
56
|
+
gt_mask = (target_batch[idx_class] > 0.5)
|
|
57
|
+
else:
|
|
58
|
+
gt_mask = (target_indices == idx_class)
|
|
59
|
+
|
|
60
|
+
correct = torch.sum(pred_mask == gt_mask).item()
|
|
61
|
+
total = gt_mask.numel()
|
|
62
|
+
|
|
63
|
+
if total > 0:
|
|
64
|
+
current_acc = correct / total
|
|
65
|
+
metrics_to_return[idx_class] += current_acc
|
|
66
|
+
valid_batches_per_class[idx_class] += 1
|
|
67
|
+
|
|
68
|
+
final_metric = []
|
|
69
|
+
for i in range(num_classes):
|
|
70
|
+
if valid_batches_per_class[i] > 0:
|
|
71
|
+
final_metric.append(metrics_to_return[i] / valid_batches_per_class[i])
|
|
72
|
+
else:
|
|
73
|
+
final_metric.append(float('nan'))
|
|
74
|
+
|
|
75
|
+
return final_metric
|
|
76
|
+
|
|
77
|
+
def Precision3d(preds, labels, nb_classes):
|
|
78
|
+
pred_flatten = preds.flatten()
|
|
79
|
+
label_flatten = labels.flatten()
|
|
80
|
+
|
|
81
|
+
precision_tab = []
|
|
82
|
+
for idx_class in range(1, nb_classes+1):
|
|
83
|
+
TP = np.sum((pred_flatten == idx_class) & (label_flatten == idx_class))
|
|
84
|
+
FP = np.sum((pred_flatten == idx_class) & (label_flatten != idx_class))
|
|
85
|
+
if (TP + FP) == 0:
|
|
86
|
+
precision_tab.append(0.0)
|
|
87
|
+
else:
|
|
88
|
+
precision_tab.append(TP/(TP+FP))
|
|
89
|
+
|
|
90
|
+
return precision_tab
|
|
91
|
+
|
|
92
|
+
def Recall3d(preds, labels, nb_classes):
|
|
93
|
+
pred_flatten = preds.flatten()
|
|
94
|
+
label_flatten = labels.flatten()
|
|
95
|
+
|
|
96
|
+
precision_tab = []
|
|
97
|
+
for idx_class in range(1, nb_classes+1):
|
|
98
|
+
TP = np.sum((pred_flatten == idx_class) & (label_flatten == idx_class))
|
|
99
|
+
FN = np.sum((pred_flatten != idx_class) & (label_flatten == idx_class))
|
|
100
|
+
if (TP + FN) == 0:
|
|
101
|
+
precision_tab.append(0.0)
|
|
102
|
+
else:
|
|
103
|
+
precision_tab.append(TP/(TP+FN))
|
|
104
|
+
|
|
105
|
+
return precision_tab
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def F13d(preds, labels, nb_classes):
|
|
109
|
+
current_prec = Precision3d(preds, labels, nb_classes)
|
|
110
|
+
current_recall = Recall3d(preds, labels, nb_classes)
|
|
111
|
+
|
|
112
|
+
f1_scores = []
|
|
113
|
+
for idx_class in range(nb_classes):
|
|
114
|
+
if current_prec[idx_class] + current_recall[idx_class] == 0:
|
|
115
|
+
f1_scores.append(0.0)
|
|
116
|
+
else:
|
|
117
|
+
f1_scores.appennd(2* (current_prec[idx_class] * current_recall[idx_class])/ (current_prec[idx_class] + current_recall[idx_class]))
|
|
118
|
+
|
|
119
|
+
return f1_scores
|