Skip to content

Commit c24d659

Browse files
authored
Add support of holes to contours generation (#335)
* Add support of holes to contours generation * Fix logic in case of absence of nested contours * Cleanup python impl * Add cpp implementation * Unify contour str reprs * Fix np warning * Resolve some of corner-cases * Update refs * Update pre-commit config * Fix numpy issue * Update ref tests results * Retain old data type for Contour init args * Exclude nested contours from contour confidence calculation * Exclude nested contours from prob computation * Rename child_shapes for clarity * Unify calls of drawContours * Tests debug * Update ref results * Add numpy conversion to shape * Fix list of contours conversion
1 parent a9fb359 commit c24d659

File tree

9 files changed

+104
-37
lines changed

9 files changed

+104
-37
lines changed

.github/workflows/test_accuracy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- name: Run Python Test
3636
run: |
3737
source venv/bin/activate
38-
pytest --data=./data tests/python/accuracy/test_accuracy.py
38+
pytest -v --data=./data tests/python/accuracy/test_accuracy.py
3939
- name: Install CPP dependencies
4040
run: |
4141
sudo bash src/cpp/install_dependencies.sh

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ repos:
3030
- id: mypy
3131
additional_dependencies: [types-PyYAML, types-setuptools]
3232

33-
- repo: https://github.com/pre-commit/mirrors-prettier
34-
rev: v4.0.0-alpha.8
33+
- repo: https://github.com/rbubley/mirrors-prettier
34+
rev: v3.6.2
3535
hooks:
3636
- id: prettier
3737

src/cpp/models/include/models/results.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,11 @@ struct Contour {
315315
std::string label;
316316
float probability;
317317
std::vector<cv::Point> shape;
318+
std::vector<std::vector<cv::Point>> excluded_shapes;
318319

319320
friend std::ostream& operator<<(std::ostream& os, const Contour& contour) {
320321
return os << contour.label << ": " << std::fixed << std::setprecision(3) << contour.probability << ", "
321-
<< contour.shape.size();
322+
<< contour.shape.size() << ", " << contour.excluded_shapes.size();
322323
}
323324
};
324325

@@ -332,7 +333,7 @@ static inline std::vector<Contour> getContours(const std::vector<SegmentedObject
332333
if (contours.size() != 1) {
333334
throw std::runtime_error("findContours() must have returned only one contour");
334335
}
335-
combined_contours.push_back({obj.label, obj.confidence, contours[0]});
336+
combined_contours.push_back({obj.label, obj.confidence, contours[0], {}});
336337
}
337338
return combined_contours;
338339
}

src/cpp/models/src/segmentation_model.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,17 +298,31 @@ std::vector<Contour> SegmentationModel::getContours(const ImageResultWithSoftPre
298298
cv::Scalar(index, index, index),
299299
label_index_map);
300300
std::vector<std::vector<cv::Point>> contours;
301-
cv::findContours(label_index_map, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);
301+
std::vector<cv::Vec4i> hierarchy;
302+
cv::findContours(label_index_map, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_NONE);
302303

303304
std::string label = getLabelName(index - 1);
304305

305-
for (unsigned int i = 0; i < contours.size(); i++) {
306+
for (size_t i = 0; i < contours.size(); ++i) {
307+
if (hierarchy[i][3] >= 0) {
308+
continue;
309+
}
310+
306311
cv::Mat mask = cv::Mat::zeros(imageResult.resultImage.rows,
307312
imageResult.resultImage.cols,
308313
imageResult.resultImage.type());
309314
cv::drawContours(mask, contours, i, 255, -1);
315+
316+
std::vector<std::vector<cv::Point>> children;
317+
int next_child_idx = hierarchy[i][2];
318+
while (next_child_idx >= 0) {
319+
children.push_back(contours[next_child_idx]);
320+
cv::drawContours(mask, contours, next_child_idx, 0, -1);
321+
next_child_idx = hierarchy[next_child_idx][0];
322+
}
323+
310324
float probability = (float)cv::mean(current_label_soft_prediction, mask)[0];
311-
combined_contours.push_back({label, probability, contours[i]});
325+
combined_contours.push_back({label, probability, contours[i], children});
312326
}
313327
}
314328

src/python/model_api/models/result/segmentation.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,35 @@ def rotated_rects(self, value):
149149

150150

151151
class Contour:
152-
def __init__(self, label: str, probability: float, shape: list[tuple[int, int]]):
153-
self.shape = shape
152+
"""Represents a semantic segmentation mask as internals of a contour with "holes".
153+
Args:
154+
label (str): The label of the contour.
155+
probability (float): The probability associated with the contour.
156+
shape (np.ndarray | list[tuple[int, int]]): The shape of the contour. Shape is represented as a
157+
list of 2d points or an equivalent numpy array (N, 2).
158+
excluded_shapes (list[np.ndarray] | list[tuple[int, int]] | None, optional): Shapes of excluded contours.
159+
If empty, the main shape is simply connected. Otherwise, excluded_shapes
160+
represent "holes". Defaults to None.
161+
"""
162+
163+
def __init__(
164+
self,
165+
label: str,
166+
probability: float,
167+
shape: np.ndarray | list[tuple[int, int]],
168+
excluded_shapes: list[np.ndarray] | list[list[tuple[int, int]]] | None = None,
169+
):
170+
self.shape = np.array(shape)
154171
self.label = label
155172
self.probability = probability
173+
self.excluded_shapes = [np.array(x) for x in excluded_shapes] if excluded_shapes is not None else None
156174

157175
def __str__(self):
158-
return f"{self.label}: {self.probability:.3f}, {len(self.shape)}"
176+
num_children = len(self.excluded_shapes) if self.excluded_shapes is not None else 0
177+
return f"{self.label}: {self.probability:.3f}, {len(self.shape)}, {num_children}"
178+
179+
def __repr__(self):
180+
return self.__str__()
159181

160182

161183
class ImageResultWithSoftPrediction(Result):

src/python/model_api/models/segmentation.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def postprocess(self, outputs: dict, meta: dict) -> ImageResultWithSoftPredictio
186186
def get_contours(
187187
self,
188188
prediction: ImageResultWithSoftPrediction,
189-
) -> list:
189+
) -> list[Contour]:
190190
n_layers = prediction.soft_prediction.shape[2]
191191

192192
if n_layers == 1:
@@ -207,23 +207,30 @@ def get_contours(
207207
obj_group = prediction.resultImage == layer_index
208208
label_index_map = obj_group.astype(np.uint8) * 255
209209

210-
contours, _hierarchy = cv2.findContours(
210+
contours, hierarchy = cv2.findContours(
211211
label_index_map,
212-
cv2.RETR_EXTERNAL,
212+
cv2.RETR_CCOMP,
213213
cv2.CHAIN_APPROX_NONE,
214214
)
215+
if len(contours):
216+
hierarchy = hierarchy.squeeze(axis=0)
217+
218+
for i, contour in enumerate(contours):
219+
if hierarchy[i][3] >= 0:
220+
continue
215221

216-
for contour in contours:
217222
mask = np.zeros(prediction.resultImage.shape, dtype=np.uint8)
218-
cv2.drawContours(
219-
mask,
220-
np.asarray([contour]),
221-
contourIdx=-1,
222-
color=1,
223-
thickness=-1,
224-
)
223+
cv2.drawContours(mask, contours, contourIdx=i, color=1, thickness=-1)
224+
225+
children = []
226+
next_child_idx = hierarchy[i][2]
227+
while next_child_idx >= 0:
228+
children.append(contours[next_child_idx])
229+
cv2.drawContours(mask, contours, contourIdx=next_child_idx, color=0, thickness=-1)
230+
next_child_idx = hierarchy[next_child_idx][0]
231+
225232
probability = cv2.mean(current_label_soft_prediction, mask)[0]
226-
combined_contours.append(Contour(label, probability, contour))
233+
combined_contours.append(Contour(label, probability, contour, children))
227234

228235
return combined_contours
229236

src/python/model_api/tilers/instance_segmentation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def _merge_results(self, results, shape) -> InstanceSegmentationResult:
123123
labels = labels.astype(np.int32)
124124
resized_masks, label_names = [], []
125125
for mask, box, label_idx in zip(masks, bboxes, labels):
126-
label_names.append(self.model.labels[int(label_idx)])
126+
label_names.append(self.model.labels[int(label_idx.squeeze())])
127127
resized_masks.append(_segm_postprocess(box, mask, *shape[:-1]))
128128

129129
resized_masks = np.stack(resized_masks) if resized_masks else masks

0 commit comments

Comments
 (0)