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.
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ local/
3
+ .vscode/
@@ -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()