scribble-from-segmentation 0.0.1__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.
- scribble_from_segmentation-0.0.1/.gitignore +3 -0
- scribble_from_segmentation-0.0.1/PKG-INFO +104 -0
- scribble_from_segmentation-0.0.1/README.md +80 -0
- scribble_from_segmentation-0.0.1/pyproject.toml +46 -0
- scribble_from_segmentation-0.0.1/scribble_from_segmentation/scribble_from_segmentation.py +233 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scribble-from-segmentation
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Generate scribble annotations from segmentation masks
|
|
5
|
+
Author: Alex Senden
|
|
6
|
+
Maintainer: Alex Senden
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: computer-vision,image-annotation,image-processing,machine-learning,segmentation
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: opencv-python
|
|
21
|
+
Requires-Dist: pillow
|
|
22
|
+
Requires-Dist: scikit-image
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Scribble From Segmentation
|
|
26
|
+
|
|
27
|
+
Generate scribble annotations from segmentation masks using advanced image processing techniques.
|
|
28
|
+
|
|
29
|
+
## Overview
|
|
30
|
+
|
|
31
|
+
This tool converts segmentation masks into scribble annotations by applying morphological operations and skeletonization. It's useful for creating weak supervision labels or simplifying segmentation annotations for training purposes.
|
|
32
|
+
|
|
33
|
+
The tool:
|
|
34
|
+
|
|
35
|
+
- Processes segmentation masks and extracts individual classes
|
|
36
|
+
- Automatically detects and skips background classes
|
|
37
|
+
- Applies morphological operations to refine class boundaries
|
|
38
|
+
- Skeletonizes regions to create scribble-like annotations
|
|
39
|
+
- Preserves the original color information for each class
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Install from PyPI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install scribble-from-segmentation
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or install from source:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Use the command-line interface to process segmentation masks:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
scribble-from-segmentation --input_dir /path/to/segmentation/masks --output_dir /path/to/output/scribbles
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Arguments
|
|
64
|
+
|
|
65
|
+
- `--input_dir` (required): Path to directory containing segmentation mask images
|
|
66
|
+
- `--output_dir` (required): Path to directory where scribble annotations will be saved
|
|
67
|
+
|
|
68
|
+
### Supported Image Formats
|
|
69
|
+
|
|
70
|
+
- PNG
|
|
71
|
+
- JPG/JPEG
|
|
72
|
+
- GIF
|
|
73
|
+
- BMP
|
|
74
|
+
- TIF
|
|
75
|
+
|
|
76
|
+
## Python API
|
|
77
|
+
|
|
78
|
+
You can also use the tool directly in your Python code:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from scribble_from_segmentation import generate_scribbles_from_segmentations
|
|
82
|
+
|
|
83
|
+
# Generate scribbles from segmentation masks
|
|
84
|
+
generate_scribbles_from_segmentations(
|
|
85
|
+
input_dir="/path/to/segmentation/masks",
|
|
86
|
+
output_dir="/path/to/output/scribbles"
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Requirements
|
|
91
|
+
|
|
92
|
+
- Python >= 3.8
|
|
93
|
+
- numpy
|
|
94
|
+
- opencv-python
|
|
95
|
+
- Pillow
|
|
96
|
+
- scikit-image
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT License
|
|
101
|
+
|
|
102
|
+
## Author
|
|
103
|
+
|
|
104
|
+
Alex Senden
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Scribble From Segmentation
|
|
2
|
+
|
|
3
|
+
Generate scribble annotations from segmentation masks using advanced image processing techniques.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This tool converts segmentation masks into scribble annotations by applying morphological operations and skeletonization. It's useful for creating weak supervision labels or simplifying segmentation annotations for training purposes.
|
|
8
|
+
|
|
9
|
+
The tool:
|
|
10
|
+
|
|
11
|
+
- Processes segmentation masks and extracts individual classes
|
|
12
|
+
- Automatically detects and skips background classes
|
|
13
|
+
- Applies morphological operations to refine class boundaries
|
|
14
|
+
- Skeletonizes regions to create scribble-like annotations
|
|
15
|
+
- Preserves the original color information for each class
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Install from PyPI:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install scribble-from-segmentation
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install from source:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Use the command-line interface to process segmentation masks:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
scribble-from-segmentation --input_dir /path/to/segmentation/masks --output_dir /path/to/output/scribbles
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Arguments
|
|
40
|
+
|
|
41
|
+
- `--input_dir` (required): Path to directory containing segmentation mask images
|
|
42
|
+
- `--output_dir` (required): Path to directory where scribble annotations will be saved
|
|
43
|
+
|
|
44
|
+
### Supported Image Formats
|
|
45
|
+
|
|
46
|
+
- PNG
|
|
47
|
+
- JPG/JPEG
|
|
48
|
+
- GIF
|
|
49
|
+
- BMP
|
|
50
|
+
- TIF
|
|
51
|
+
|
|
52
|
+
## Python API
|
|
53
|
+
|
|
54
|
+
You can also use the tool directly in your Python code:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from scribble_from_segmentation import generate_scribbles_from_segmentations
|
|
58
|
+
|
|
59
|
+
# Generate scribbles from segmentation masks
|
|
60
|
+
generate_scribbles_from_segmentations(
|
|
61
|
+
input_dir="/path/to/segmentation/masks",
|
|
62
|
+
output_dir="/path/to/output/scribbles"
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- Python >= 3.8
|
|
69
|
+
- numpy
|
|
70
|
+
- opencv-python
|
|
71
|
+
- Pillow
|
|
72
|
+
- scikit-image
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT License
|
|
77
|
+
|
|
78
|
+
## Author
|
|
79
|
+
|
|
80
|
+
Alex Senden
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scribble-from-segmentation"
|
|
7
|
+
description = "Generate scribble annotations from segmentation masks"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [{ name = "Alex Senden" }]
|
|
12
|
+
maintainers = [{ name = "Alex Senden" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"segmentation",
|
|
15
|
+
"computer-vision",
|
|
16
|
+
"image-annotation",
|
|
17
|
+
"image-processing",
|
|
18
|
+
"machine-learning",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.8",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
30
|
+
]
|
|
31
|
+
dependencies = ["numpy", "opencv-python", "Pillow", "scikit-image"]
|
|
32
|
+
|
|
33
|
+
# Version is automatically provided by hatch-vcs
|
|
34
|
+
dynamic = ["version"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
scribble-from-segmentation = "scribble_from_segmentation.scribble_from_segmentation:main"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.version]
|
|
40
|
+
source = "vcs"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["scribble_from_segmentation/**", "README.md"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
include = ["scribble_from_segmentation/**"]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import cv2
|
|
3
|
+
import math
|
|
4
|
+
import numpy as np
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from PIL import Image
|
|
8
|
+
from skimage import measure, morphology
|
|
9
|
+
|
|
10
|
+
IMAGE_FILE_EXTENSIONS = (
|
|
11
|
+
".png",
|
|
12
|
+
".jpg",
|
|
13
|
+
".jpeg",
|
|
14
|
+
".gif",
|
|
15
|
+
".bmp",
|
|
16
|
+
".tif",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
BORDER_PADDING = 200
|
|
20
|
+
KERNEL_SIZE = 11
|
|
21
|
+
SMALL_KERNEL_SIZE = 7
|
|
22
|
+
DILATION_ITERATIONS = 5
|
|
23
|
+
BACKGROUND_THRESHOLD = 0.5
|
|
24
|
+
|
|
25
|
+
ELLIPSE_KERNEL = cv2.getStructuringElement(
|
|
26
|
+
cv2.MORPH_ELLIPSE, (KERNEL_SIZE, KERNEL_SIZE)
|
|
27
|
+
)
|
|
28
|
+
SMALL_ELLIPSE_KERNEL = cv2.getStructuringElement(
|
|
29
|
+
cv2.MORPH_ELLIPSE, (SMALL_KERNEL_SIZE, SMALL_KERNEL_SIZE)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_args():
|
|
34
|
+
parser = argparse.ArgumentParser(
|
|
35
|
+
description="Generate scribble annotations from segmentation masks."
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--input_dir",
|
|
39
|
+
type=str,
|
|
40
|
+
required=True,
|
|
41
|
+
help="The path of the input directory containing a segmentation mask to be reduced.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--output_dir",
|
|
45
|
+
type=str,
|
|
46
|
+
required=True,
|
|
47
|
+
help="The path of the directory for tiles to be saved in.",
|
|
48
|
+
)
|
|
49
|
+
return parser.parse_args()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_image(image, path):
|
|
53
|
+
Image.fromarray(image.astype(np.uint8)).save(path)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def discover_segmentation_classes(image):
|
|
57
|
+
return [list(colour) for colour in list(set(image.getdata()))]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def set_colour(colour_image, binary_image, colour):
|
|
61
|
+
np.copyto(colour_image, colour, where=binary_image[..., None])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def generate_comparison(image1, image2):
|
|
65
|
+
height, width = image1.shape
|
|
66
|
+
|
|
67
|
+
blue_arr = np.zeros((height, width, 3), dtype=np.uint8)
|
|
68
|
+
red_arr = np.zeros((height, width, 3), dtype=np.uint8)
|
|
69
|
+
|
|
70
|
+
blue = np.array([0, 0, 255], dtype=blue_arr.dtype)
|
|
71
|
+
red = np.array([255, 0, 0], dtype=red_arr.dtype)
|
|
72
|
+
|
|
73
|
+
set_colour(blue_arr, image1, blue)
|
|
74
|
+
set_colour(red_arr, image2, red)
|
|
75
|
+
|
|
76
|
+
return blue_arr + red_arr
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_image_paths(input_dir):
|
|
80
|
+
images = [
|
|
81
|
+
file for file in os.listdir(input_dir) if file.endswith(IMAGE_FILE_EXTENSIONS)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
# Ensure at least one image exists
|
|
85
|
+
if len(images) == 0:
|
|
86
|
+
raise Exception(f"ERROR: No files discovered in input directory {input_dir}")
|
|
87
|
+
|
|
88
|
+
return images
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_row_direction(boolean_image):
|
|
92
|
+
# Preprocess: dilate image to merge nearby objects
|
|
93
|
+
dilated = cv2.dilate(np.transpose(boolean_image), ELLIPSE_KERNEL, iterations=2)
|
|
94
|
+
|
|
95
|
+
# Get largest object from dilated image
|
|
96
|
+
labeled = measure.label(dilated, connectivity=2)
|
|
97
|
+
regions = measure.regionprops(labeled)
|
|
98
|
+
largest_region = max(regions, key=lambda r: r.area)
|
|
99
|
+
|
|
100
|
+
# Align to axis if near one
|
|
101
|
+
theta = largest_region.orientation
|
|
102
|
+
if -0.1 < theta and theta < 0.1:
|
|
103
|
+
theta = 0
|
|
104
|
+
elif theta < -1.5 or theta > 1.5:
|
|
105
|
+
theta = math.pi / 2.0
|
|
106
|
+
|
|
107
|
+
return theta
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def oriented_line_kernel(theta, size, thickness):
|
|
111
|
+
kernel = np.zeros((size, size), dtype=np.uint8)
|
|
112
|
+
|
|
113
|
+
# Center point (size should always be odd)
|
|
114
|
+
center = (size - 1) // 2
|
|
115
|
+
|
|
116
|
+
# Direction vector
|
|
117
|
+
dx = np.round(np.cos(theta), 3)
|
|
118
|
+
dy = np.round(np.sin(theta), 3)
|
|
119
|
+
|
|
120
|
+
# Large enough line endpoints in both directions
|
|
121
|
+
x0 = int(center - dx * size)
|
|
122
|
+
y0 = int(center - dy * size)
|
|
123
|
+
x1 = int(center + dx * size)
|
|
124
|
+
y1 = int(center + dy * size)
|
|
125
|
+
|
|
126
|
+
# Draw line on kernel
|
|
127
|
+
cv2.line(kernel, (x0, y0), (x1, y1), 1, thickness=thickness)
|
|
128
|
+
|
|
129
|
+
return kernel
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_convolutional_kernels(image):
|
|
133
|
+
row_direction_theta = get_row_direction(
|
|
134
|
+
image[BORDER_PADDING:-BORDER_PADDING, BORDER_PADDING:-BORDER_PADDING]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
thin_kernel = oriented_line_kernel(row_direction_theta, SMALL_KERNEL_SIZE, 1)
|
|
138
|
+
thick_kernel = oriented_line_kernel(row_direction_theta, KERNEL_SIZE, 2)
|
|
139
|
+
|
|
140
|
+
return thin_kernel, thick_kernel
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def generate_scribble_for_boolean(boolean_image):
|
|
144
|
+
ELLIPSE_KERNEL = cv2.getStructuringElement(
|
|
145
|
+
cv2.MORPH_ELLIPSE, (KERNEL_SIZE, KERNEL_SIZE)
|
|
146
|
+
)
|
|
147
|
+
thin_row_kernel, row_kernel = get_convolutional_kernels(boolean_image)
|
|
148
|
+
|
|
149
|
+
# Dilate mask to morph shapes together, produces less nubs in skeleton
|
|
150
|
+
dilated = cv2.dilate(boolean_image, row_kernel, iterations=DILATION_ITERATIONS)
|
|
151
|
+
skeleton = morphology.skeletonize(dilated).astype(np.uint8)
|
|
152
|
+
|
|
153
|
+
# Remove some extra nubs via repeated opening
|
|
154
|
+
denubbed = cv2.dilate(skeleton, ELLIPSE_KERNEL, iterations=2)
|
|
155
|
+
denubbed = cv2.morphologyEx(denubbed, cv2.MORPH_OPEN, row_kernel, iterations=4)
|
|
156
|
+
regrown = cv2.dilate(denubbed, ELLIPSE_KERNEL, iterations=1)
|
|
157
|
+
|
|
158
|
+
# Contract vertically, since row kernel overexpands vertically
|
|
159
|
+
regrown_contracted = cv2.erode(regrown, thin_row_kernel, iterations=8)
|
|
160
|
+
|
|
161
|
+
# Remorphology.skeletonize and dilate to smoothen and unify line width
|
|
162
|
+
second_skeleton = morphology.skeletonize(regrown_contracted).astype(np.uint8)
|
|
163
|
+
return cv2.dilate(second_skeleton, ELLIPSE_KERNEL, iterations=1)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def generate_scribbles_from_segmentations(input_dir: str, output_dir: str):
|
|
167
|
+
images = get_image_paths(input_dir)
|
|
168
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
169
|
+
|
|
170
|
+
for image_file_name in images:
|
|
171
|
+
image_file_path = os.path.join(input_dir, image_file_name)
|
|
172
|
+
print(f"INFO: Processing image {image_file_path}")
|
|
173
|
+
|
|
174
|
+
pil_image = Image.open(image_file_path).convert("RGB")
|
|
175
|
+
segmentation_classes = discover_segmentation_classes(pil_image)
|
|
176
|
+
|
|
177
|
+
# Pad image to allow kernels to operate "offscreen"
|
|
178
|
+
np_image = np.asarray(pil_image)
|
|
179
|
+
np_image = np.pad(
|
|
180
|
+
np_image,
|
|
181
|
+
(
|
|
182
|
+
(BORDER_PADDING, BORDER_PADDING),
|
|
183
|
+
(BORDER_PADDING, BORDER_PADDING),
|
|
184
|
+
(0, 0),
|
|
185
|
+
),
|
|
186
|
+
mode="edge",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
scribble_annotation = np.zeros(
|
|
190
|
+
(np_image.shape[0], np_image.shape[1], 3), dtype=np.uint8
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
for segmentation_class in segmentation_classes:
|
|
194
|
+
boolean_image = np.all(np_image == segmentation_class, axis=-1).astype(
|
|
195
|
+
np.uint8
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Check if class is background
|
|
199
|
+
if np.sum(boolean_image) / boolean_image.size > BACKGROUND_THRESHOLD:
|
|
200
|
+
print(
|
|
201
|
+
f"INFO: Class {segmentation_class} detected as background class. Skipping."
|
|
202
|
+
)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Generate scribble
|
|
206
|
+
class_scribble = generate_scribble_for_boolean(boolean_image)
|
|
207
|
+
|
|
208
|
+
# Add scribble to output map
|
|
209
|
+
set_colour(
|
|
210
|
+
colour_image=scribble_annotation,
|
|
211
|
+
binary_image=class_scribble.astype(bool),
|
|
212
|
+
colour=np.asarray(segmentation_class[:3], dtype=np.uint8),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Save resulting image
|
|
216
|
+
save_image(
|
|
217
|
+
scribble_annotation[
|
|
218
|
+
BORDER_PADDING:-BORDER_PADDING, BORDER_PADDING:-BORDER_PADDING
|
|
219
|
+
],
|
|
220
|
+
os.path.join(
|
|
221
|
+
output_dir,
|
|
222
|
+
f"{os.path.splitext(image_file_name)[0]}_scribble.png",
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main():
|
|
228
|
+
args = parse_args()
|
|
229
|
+
generate_scribbles_from_segmentations(args.input_dir, args.output_dir)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
main()
|