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.
Files changed (30) hide show
  1. {openmlcore-1.1.0 → openmlcore-2.0.0}/PKG-INFO +6 -8
  2. {openmlcore-1.1.0 → openmlcore-2.0.0}/README.md +5 -7
  3. {openmlcore-1.1.0 → openmlcore-2.0.0}/pyproject.toml +1 -1
  4. openmlcore-2.0.0/src/openmlcore/base/data3d.py +174 -0
  5. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/loss/loss.py +67 -75
  6. openmlcore-2.0.0/src/openmlcore/loss/loss3d.py +92 -0
  7. openmlcore-2.0.0/src/openmlcore/metrics/metrics3d.py +119 -0
  8. openmlcore-2.0.0/src/openmlcore/models/MobileUnetR.py +329 -0
  9. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/base_model.py +54 -5
  10. openmlcore-2.0.0/src/openmlcore/models/lib/tnet.py +43 -0
  11. openmlcore-2.0.0/src/openmlcore/models/pointNetPP.py +209 -0
  12. openmlcore-2.0.0/src/openmlcore/models/pointnet.py +62 -0
  13. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/unetpp.py +1 -1
  14. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/PKG-INFO +6 -8
  15. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/SOURCES.txt +7 -0
  16. {openmlcore-1.1.0 → openmlcore-2.0.0}/tests/test_loss.py +6 -12
  17. {openmlcore-1.1.0 → openmlcore-2.0.0}/setup.cfg +0 -0
  18. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/__init__.py +0 -0
  19. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/base/data.py +0 -0
  20. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/metrics/metrics.py +0 -0
  21. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/UNets.py +0 -0
  22. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/attUnets.py +0 -0
  23. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/lib/attention.py +0 -0
  24. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/lib/convolutions.py +0 -0
  25. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore/models/segformer.py +0 -0
  26. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/dependency_links.txt +0 -0
  27. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/requires.txt +0 -0
  28. {openmlcore-1.1.0 → openmlcore-2.0.0}/src/openmlcore.egg-info/top_level.txt +0 -0
  29. {openmlcore-1.1.0 → openmlcore-2.0.0}/tests/test_base_model.py +0 -0
  30. {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: 1.1.0
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++, Mask R-CNN
28
+ - Classic & Deep Segmentation: U-Net, Attention U-Net, UNet++
29
29
 
30
- - Advanced Transformers: Swin Transformer, TransUNet, MobileUNetR
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 Hub Integration: Push and pull checkpoints effortlessly using native .safetensors formatting.
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++, Mask R-CNN
10
+ - Classic & Deep Segmentation: U-Net, Attention U-Net, UNet++
11
11
 
12
- - Advanced Transformers: Swin Transformer, TransUNet, MobileUNetR
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 Hub Integration: Push and pull checkpoints effortlessly using native .safetensors formatting.
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openmlcore"
7
- version = "1.1.0"
7
+ version = "2.0.0"
8
8
  authors = [
9
9
  { name="Doyez Sébastien", email="doyez.sebastien34090@gmail.com" },
10
10
  ]
@@ -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, index_ignore=255):
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
- self.index_ignore = index_ignore
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
- num_classes = outputs.shape[1]
87
- probs = F.softmax(outputs, dim=1)
88
- targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
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, index_ignore=255):
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.index_ignore = index_ignore
113
+ self.weights = weights
104
114
 
105
115
  def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
106
- num_classes = outputs.shape[1]
107
- targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
108
-
109
- bce_loss = F.binary_cross_entropy_with_logits(outputs, targets_one_hot, reduction='none')
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, index_ignore=255):
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
- targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
142
-
143
- error = probs - targets_one_hot
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